Skip to content

Commit

Permalink
Variable renamings, moving code around in a better order to be explai…
Browse files Browse the repository at this point in the history
…ned in the book, and simplifying code.
  • Loading branch information
asweigart committed Jan 29, 2013
1 parent 1b3897b commit dda1b18
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 138 deletions.
111 changes: 47 additions & 64 deletions simpleSubHacker.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
ciphertext word 'JHJHWDOV' (because all of these words have the pattern
'0.1.0.1.2.3.4.5')
In this program, a "map" or "letter mapping" is a dictionary where the
In this program, a "cipherletter mapping" is a dictionary where the
keys are single-letter strings (e.g. 'A', 'B', 'C', etc) and the values
are lists of single-letter strings that could possibly be the correct
decryption for the letter in the key. If the list is blank, this means
Expand All @@ -38,7 +38,7 @@
import wordPatterns

LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
nonLettersOrSpacePattern = re.compile('[^A-Z\s]')
nonLettersOrSpacePattern = re.compile('[^A-Za-z\s]')

def main():
message = 'Sy l nlx sr pyyacao l ylwj eiswi upar lulsxrj isr sxrjsxwjr, ia esmm rwctjsxsza sj wmpramh, lxo txmarr jia aqsoaxwa sr pqaceiamnsxu, ia esmm caytra jp famsaqa sj. Sy, px jia pjiac ilxo, ia sr pyyacao rpnajisxu eiswi lyypcor l calrpx ypc lwjsxu sx lwwpcolxwa jp isr sxrjsxwjr, ia esmm lwwabj sj aqax px jia rmsuijarj aqsoaxwa. Jia pcsusx py nhjir sr agbmlsxao sx jisr elh. -Facjclxo Ctrramm'
Expand All @@ -55,12 +55,12 @@ def main():
print(message)
print()
print('Copying hacked message to clipboard:')
hackedMessage = decryptWithLetterMapping(message, letterMapping)
hackedMessage = decryptWithCipherletterMapping(message, letterMapping)
pyperclip.copy(hackedMessage)
print(hackedMessage)


def getBlankMapping():
def getBlankCipherletterMapping():
# Returns a dict where the keys are uppercase single-letter strings
# and the values are blank lists.
# E.g. {'A': [], 'B': [], 'C': [], ...etc}
Expand All @@ -75,8 +75,8 @@ def getBlankMapping():


def addLettersToMapping(letterMapping, cipherWord, candidate):
# The letterMapping parameter is a "letter mapping" data structure
# that this function modifies.
# The letterMapping parameter is a "cipherletter mapping" data structure
# that the return value starts as a copy of.
# The cipherWord parameter is a string value of the ciphertext word.
# The candidate parameter is a possible English word that the
# cipherWord could decrypt to.
Expand All @@ -94,111 +94,94 @@ def addLettersToMapping(letterMapping, cipherWord, candidate):

def intersectMappings(mapA, mapB):
# To intersect two maps, create a blank map, and that add only the
# possible decryption letters if they exist in BOTH maps.
intersectedMapping = getBlankMapping()
# potential decryption letters if they exist in BOTH maps.
intersectedMapping = getBlankCipherletterMapping()
for letter in LETTERS:

# An empty list means "any letter is possible". In this case just
# copy the other map entirely.
if mapA[letter] == []:
intersectedMapping[letter] = copy.copy(mapB[letter])
intersectedMapping[letter] = copy.deepcopy(mapB[letter])
elif mapB[letter] == []:
intersectedMapping[letter] = copy.copy(mapA[letter])

intersectedMapping[letter] = copy.deepcopy(mapA[letter])
else:
# If a letter in mapA[letter] exists in mapB[letter], add
# that letter to intersectedMapping[letter].
for mappedLetter in mapA[letter]:
if mappedLetter in mapB[letter]:
intersectedMapping[letter].append(mappedLetter)

return intersectedMapping


def removeSolvedLettersFromMapping(letterMapping):
# Cipher letters in the mapping that map to only one letter are
# "solved" and can be removed from the other letters.
# For example, if 'A' maps to possible letters ['M', 'N'], and 'B'
# For example, if 'A' maps to potential letters ['M', 'N'], and 'B'
# maps to ['N'], then we know that 'B' must map to 'N', so we can
# remove 'N' from the list of what 'A' could map to. So 'A' then maps
# to ['M']. Note that now that 'A' maps to only one letter, we can
# remove 'M' from the list of possible mappings for every other
# letter. (This is why there is a loop that keeps reducing the map.)
previousSolvedLetters = []
solvedLetters = None
while previousSolvedLetters != solvedLetters:
# This loop will break when solvedLetters is not changed by the
# reduction process inside this loop (meaning it is the same
# as previousSolvedLetters).
previousSolvedLetters = solvedLetters
solvedLetters = []
letterMapping = copy.deepcopy(letterMapping)
loopAgain = True
while loopAgain:
# First assume that we will not loop again:
loopAgain = False

# solvedLetters will be a list of English letters that have one
# solvedLetters will be a list of uppercase letters that have one
# and only one possible mapping in letterMapping
for letter in LETTERS:
if len(letterMapping[letter]) == 1:
solvedLetters.append(letterMapping[letter][0])
solvedLetters = []
for cipherletter in LETTERS:
if len(letterMapping[cipherletter]) == 1:
solvedLetters.append(letterMapping[cipherletter][0])

# If a letter is solved, than it cannot possibly be a possible
# If a letter is solved, than it cannot possibly be a potential
# decryption letter for a different ciphertext letter, so we
# should remove it.
for letter in LETTERS:
# should remove it from those other lists.
for cipherletter in LETTERS:
for s in solvedLetters:
if len(letterMapping[letter]) != 1 and s in letterMapping[letter]:
letterMapping[letter].remove(s)

# With a letter removed, it's possible that we may have reduced
# other ciphertext letters to one and only one solution, so keep
# looping until previousSolvedLetters == solvedLetters. At that
# point, we'll know we can't remove any more letters.
if len(letterMapping[cipherletter]) != 1 and s in letterMapping[cipherletter]:
letterMapping[cipherletter].remove(s)
if len(letterMapping[cipherletter]) == 1:
# A new letter is now solved, so loop again.
loopAgain = True
return letterMapping


def hackSimpleSub(message):
letterMapping = getBlankMapping()

# allCandidates is a dict with keys of a single ciphertext word, and
# values of the possible word patterns.
# e.g. allCandidates == {'PYYACAO': ['alleged', 'ammeter', ...etc],
# 'EISWI': ['aerie', 'aging', 'algol', ...etc],
# 'LULSXRJ': ['abalone', 'abashed', ...etc],
# ...etc }
allCandidates = {}
interMapping = getBlankCipherletterMapping()
message = nonLettersOrSpacePattern.sub('', message.upper()).split()
for cipherWord in message:
if cipherWord in allCandidates:
continue # we've already done this word, so continue
pattern = makeWordPatterns.getWordPattern(cipherWord)
if pattern not in wordPatterns.allPatterns:
continue
allCandidates[cipherWord] = copy.copy(wordPatterns.allPatterns[pattern])

for cipherWord in allCandidates.keys():
# Get a new mapping for each ciphertext word.
newMap = getBlankMapping()

# Create a map that has all the letters' possible candidate
# decryptions.
for candidate in allCandidates[cipherWord]:
# Get a new cipherletter mapping for each ciphertext word.
newMap = getBlankCipherletterMapping()

wordPattern = makeWordPatterns.getWordPattern(cipherWord)
if wordPattern not in wordPatterns.allPatterns:
continue # This word was not in our dictionary, so continue.

# Add the letters of each candidate to the mapping.
for candidate in wordPatterns.allPatterns[wordPattern]:
newMap = addLettersToMapping(newMap, cipherWord, candidate)

# Intersect this new map with the existing map.
letterMapping = intersectMappings(letterMapping, newMap)
# Intersect the new mapping with the existing intersected mapping.
interMapping = intersectMappings(interMapping, newMap)

# Remove any solved letters from the other possible mappings.
letterMapping = removeSolvedLettersFromMapping(letterMapping)
# Remove any solved letters from the other lists.
interMapping = removeSolvedLettersFromMapping(interMapping)

return letterMapping
return interMapping


def decryptWithLetterMapping(ciphertext, letterMapping):
def decryptWithCipherletterMapping(ciphertext, letterMapping):
# Return a string of the ciphertext decrypted with the letter mapping,
# with any ambiguous decrypted letters replaced with a _ underscore.

# First create a simple sub key from the letterMapping mapping.
key = ['x'] * len(LETTERS)
for letter in LETTERS:
if len(letterMapping[letter]) == 1:
# If only one possible letter mapping, add it to the key.
# If there's only one letter, add it to the key.
keyIndex = LETTERS.find(letterMapping[letter][0])
key[keyIndex] = letter
else:
Expand Down
140 changes: 68 additions & 72 deletions vigenereCipher.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,69 @@
# Vigenere Cipher (Polyalphabetic Substitution Cipher)
# http://inventwithpython.com/hacking (BSD Licensed)

import pyperclip

LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

def main():
# This text can be copy/pasted from http://invpy.com/vigenereCipher.py
message = """Alan Mathison Turing was a British mathematician, logician, cryptanalyst, and computer scientist. He was highly influential in the development of computer science, providing a formalisation of the concepts of "algorithm" and "computation" with the Turing machine. Turing is widely considered to be the father of computer science and artificial intelligence. During World War II, Turing worked for the Government Code and Cypher School (GCCS) at Bletchley Park, Britain's codebreaking centre. For a time he was head of Hut 8, the section responsible for German naval cryptanalysis. He devised a number of techniques for breaking German ciphers, including the method of the bombe, an electromechanical machine that could find settings for the Enigma machine. After the war he worked at the National Physical Laboratory, where he created one of the first designs for a stored-program computer, the ACE. In 1948 Turing joined Max Newman's Computing Laboratory at Manchester University, where he assisted in the development of the Manchester computers and became interested in mathematical biology. He wrote a paper on the chemical basis of morphogenesis, and predicted oscillating chemical reactions such as the Belousov-Zhabotinsky reaction, which were first observed in the 1960s. Turing's homosexuality resulted in a criminal prosecution in 1952, when homosexual acts were still illegal in the United Kingdom. He accepted treatment with female hormones (chemical castration) as an alternative to prison. Turing died in 1954, just over two weeks before his 42nd birthday, from cyanide poisoning. An inquest determined that his death was suicide; his mother and some others believed his death was accidental. On 10 September 2009, following an Internet campaign, British Prime Minister Gordon Brown made an official public apology on behalf of the British government for "the appalling way he was treated." As of May 2012 a private member's bill was before the House of Lords which would grant Turing a statutory pardon if enacted."""
key = 'ASIMOV'
mode = 'encrypt' # set to 'encrypt' or 'decrypt'

if mode == 'encrypt':
translated = encryptMessage(key, message)
elif mode == 'decrypt':
translated = decryptMessage(key, message)

print('%sed message:' % (mode.title()))
print(translated)
pyperclip.copy(translated)
print()
print('The message has been copied to the clipboard.')


def encryptMessage(key, message):
return translateMessage(key, message, 'encrypt')


def decryptMessage(key, message):
return translateMessage(key, message, 'decrypt')


def translateMessage(key, message, mode):
translated = [] # stores the encrypted/decrypted message string

keyIndex = 0
key = key.upper()

for symbol in message: # loop through each character in message
num = LETTERS.find(symbol.upper())
if num != -1:
if mode == 'encrypt':
num += LETTERS.find(key[keyIndex]) # add if encrypting
elif mode == 'decrypt':
num -= LETTERS.find(key[keyIndex]) # subtract if decrypting

# handle the potential wrap around
if num >= len(LETTERS):
num -= len(LETTERS)
elif num < 0:
num += len(LETTERS)

# add the encrypted/decrypted symbol to the end of translated.
if symbol.isupper():
translated.append(LETTERS[num])
elif symbol.islower():
translated.append(LETTERS[num].lower())

keyIndex += 1
if keyIndex == len(key):
keyIndex = 0
else:
# The symbol was not in LETTERS, so add it to translated as is.
translated.append(symbol)

return ''.join(translated)


# If vigenereCipher.py is run (instead of imported as a module) call
# the main() function.
if __name__ == '__main__':
# Vigenere Cipher (Polyalphabetic Substitution Cipher)
# http://inventwithpython.com/hacking (BSD Licensed)

import pyperclip

LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

def main():
# This text can be copy/pasted from http://invpy.com/vigenereCipher.py
myMessage = """Alan Mathison Turing was a British mathematician, logician, cryptanalyst, and computer scientist. He was highly influential in the development of computer science, providing a formalisation of the concepts of "algorithm" and "computation" with the Turing machine. Turing is widely considered to be the father of computer science and artificial intelligence. During World War II, Turing worked for the Government Code and Cypher School (GCCS) at Bletchley Park, Britain's codebreaking centre. For a time he was head of Hut 8, the section responsible for German naval cryptanalysis. He devised a number of techniques for breaking German ciphers, including the method of the bombe, an electromechanical machine that could find settings for the Enigma machine. After the war he worked at the National Physical Laboratory, where he created one of the first designs for a stored-program computer, the ACE. In 1948 Turing joined Max Newman's Computing Laboratory at Manchester University, where he assisted in the development of the Manchester computers and became interested in mathematical biology. He wrote a paper on the chemical basis of morphogenesis, and predicted oscillating chemical reactions such as the Belousov-Zhabotinsky reaction, which were first observed in the 1960s. Turing's homosexuality resulted in a criminal prosecution in 1952, when homosexual acts were still illegal in the United Kingdom. He accepted treatment with female hormones (chemical castration) as an alternative to prison. Turing died in 1954, just over two weeks before his 42nd birthday, from cyanide poisoning. An inquest determined that his death was suicide; his mother and some others believed his death was accidental. On 10 September 2009, following an Internet campaign, British Prime Minister Gordon Brown made an official public apology on behalf of the British government for "the appalling way he was treated." As of May 2012 a private member's bill was before the House of Lords which would grant Turing a statutory pardon if enacted."""
myKey = 'ASIMOV'
myMode = 'encrypt' # set to 'encrypt' or 'decrypt'

if myMode == 'encrypt':
translated = encryptMessage(myKey, myMessage)
elif myMode == 'decrypt':
translated = decryptMessage(myKey, myMessage)

print('%sed message:' % (myMode.title()))
print(translated)
pyperclip.copy(translated)
print()
print('The message has been copied to the clipboard.')


def encryptMessage(key, message):
return translateMessage(key, message, 'encrypt')


def decryptMessage(key, message):
return translateMessage(key, message, 'decrypt')


def translateMessage(key, message, mode):
translated = [] # stores the encrypted/decrypted message string

keyIndex = 0
key = key.upper()

for symbol in message: # loop through each character in message
num = LETTERS.find(symbol.upper())
if num != -1: # -1 means symbol.upper() was not found in LETTERS
if mode == 'encrypt':
num += LETTERS.find(key[keyIndex]) # add if encrypting
elif mode == 'decrypt':
num -= LETTERS.find(key[keyIndex]) # subtract if decrypting

num %= len(LETTERS) # handle the potential wrap around

# add the encrypted/decrypted symbol to the end of translated.
if symbol.isupper():
translated.append(LETTERS[num])
elif symbol.islower():
translated.append(LETTERS[num].lower())

keyIndex += 1 # move to the next letter in the key
if keyIndex == len(key):
keyIndex = 0
else:
# The symbol was not in LETTERS, so add it to translated as is.
translated.append(symbol)

return ''.join(translated)


# If vigenereCipher.py is run (instead of imported as a module) call
# the main() function.
if __name__ == '__main__':
main()
4 changes: 2 additions & 2 deletions vigenereHacker.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def getFactors(num):
# If we've calculated the factors before, they'll be in FACTOR_CACHE.
# In that case, just return a copy of the list of factors.
if num in FACTOR_CACHE:
return copy.copy(FACTOR_CACHE[num])
return copy.deepcopy(FACTOR_CACHE[num])

factors = [] # the list of factors found

Expand All @@ -81,7 +81,7 @@ def getFactors(num):

FACTOR_CACHE[num] = factors # add thist list to FACTOR_CACHE

return copy.copy(factors) # return a copy of this list of factors
return copy.deepcopy(factors) # return a copy of this list of factors


def getMostCommonFactors(seqFactors):
Expand Down

0 comments on commit dda1b18

Please sign in to comment.