diff --git a/README.md b/README.md new file mode 100644 index 0000000..18a381c --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +A Python implementation of the Playfair cipher + +The Playfair cipher was created by Charles Wheatstone and popularised by Lord Playfair (hence the name). +Playfair is a classic (means not electronic) symmetric cipher (the same key is used for encryption and decryption) +Using a password like "MONARCHY" we get a 5x5 matrix by using at first the distinct letters in our password +and then the rest of the alphabet. + +| | 1 | 2 | 3 | 4 | 5 | +|---|---|---|---|---|---| +|1|M|O|N|A|R| +|2|C|H|Y|B|D| +|3|E|F|G|I|K| +|4|L|P|Q|S|T| +|5|U|V|W|X|Z| + +Usually I and J are considered the same letter. +After we split the text in digraphs, if a digraph is the same letter twice, then a double_padding letter is appended. +If the plaintext has an odd number of letters, then an end_padding letter is appended. + +If the 2 letters are in the same row, each one is encrypted to the one to its right. +If the 2 letters are in the same collumn, each one is encrypted to the one beneth it. +Else, each letter is encrypted to the letter in the same row, but in the other's collumn + +Thus: +EE becomes EX.. and is encpypted to IU.. + +HD is encrypted to YC + +GW is encrypted to QN + +This is a rudimentary implementation of the Playfair cipher in python. +Though not much, it gets the job done, it is beautiful, and scores 9.79/10 in pylint + +Modified a warm july evening the day before my graph theory exam. diff --git a/playfair.py b/playfair.py index 9c79531..3fbaaa5 100644 --- a/playfair.py +++ b/playfair.py @@ -1,224 +1,275 @@ -# Title: Python Playfair -# Version: 1.0 -# Date: 2011-02-11 -# Description: A Python implementation of the Playfair cipher. -# Author: Joel Verhagen -# Website: http://www.joelverhagen.com -# Licensing: Do whatever the heck you want with it. Golly, you don't even need to credit me if you don't want. Just don't say you originally wrote it. That would just make me sad. +"""Title: Python Playfair +Version: 1.1 +Date: 2011-02-11 +Description: A Python implementation of the Playfair cipher. +Author: Joel Verhagen, Konstantinos Ameranis +Website: http://www.joelverhagen.com +Licensing: Do whatever the heck you want with it. Golly, you don't + even need to credit me if you don't want. Just don't say you + originally wrote it. That would just make me sad. +""" + import re + class PlayfairError(Exception): - def __init__(self, message): - print message - -class Playfair: - # omissionRule determines which omission rule you want to use (go figure). See the list at the beginning of the constructor - # doublePadding determines what letter you would like to use to pad a digraph that is double letters - # endPadding determines what letter you would like to use to pad the end of an input containing an odd number of letters - def __init__(self, omissionRule = 0, doublePadding = 'X', endPadding = 'X'): - omissionRules = [ - 'Merge J into I', - 'Omit Q', - 'Merge I into J', - ] - if omissionRule >= 0 and omissionRule < len(omissionRules): - self.omissionRule = omissionRule - else: - raise PlayfairError('Possible omission rule values are between 0 and ' + (len(omissionRules) - 1) + '.') - - # start with a blank password - self.grid = self.generateGrid('') - - # make sure the input for the double padding character is valid - if len(doublePadding) != 1: - raise PlayfairError('The double padding must be a single character.') - elif not self.isAlphabet(doublePadding): - raise PlayfairError('The double padding must be a letter of the alphabet.') - elif doublePadding not in self.grid: - raise PlayfairError('The double padding character must not be omitted by the omission rule.') - else: - self.doublePadding = doublePadding.upper() - - # make sure the input for the end padding character is valid - if len(endPadding) != 1: - raise PlayfairError('The end padding must be a single character.') - elif not self.isAlphabet(endPadding): - raise PlayfairError('The end padding must be a letter of the alphabet.') - elif endPadding not in self.grid: - raise PlayfairError('The end padding character must not be omitted by the omission rule.') - else: - self.endPadding = endPadding.upper() - - # returns None if the letter should be discarded, else returns the converted letter - def convertLetter(self, letter): - if self.omissionRule == 0: - if letter == 'J': - letter = 'I' - return letter - elif self.omissionRule == 1: - if letter == 'Q': - letter = None - return letter - elif self.omissionRule == 2: - if letter == 'I': - letter = 'J' - return letter - else: - raise PlayfairError('The omission rule provided has not been configured properly.') - - # returns the alphabet used by the cipher (takes into account the omission rule) - def getAlphabet(self): - fullAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - alphabet = '' - - for letter in fullAlphabet: - letter = self.convertLetter(letter) - if letter is not None and letter not in alphabet: - alphabet += letter - - return alphabet - - # generates the 25 character grid based on the omission rule and the given password - def generateGrid(self, password): - grid = '' - alphabet = self.getAlphabet() - - for letter in password: - if letter not in grid and letter in alphabet: - grid += letter - - for letter in alphabet: - if letter not in grid: - grid += letter - - return grid - - # splits the text input into digraphs - def generateDigraphs(self, input): - input = self.toAlphabet(input).upper() - inputFixed = '' - - for i in range(len(input)): - letter = self.convertLetter(input[i]) - if letter is not None: - inputFixed += letter - - digraphs = [] - - counter = 0 - while counter < len(inputFixed): - digraph = '' - if counter + 1 == len(inputFixed): # we have reached the end of the inputFixed - digraph = inputFixed[counter] + self.endPadding - digraphs.append(digraph) - break - elif inputFixed[counter] != inputFixed[counter + 1]: # we just need to create a normal digraph - digraph = inputFixed[counter] + inputFixed[counter + 1] - digraphs.append(digraph) - counter += 2 - else: # we have a double letter digraph, so we add the double padding - digraph = inputFixed[counter] + self.doublePadding - digraphs.append(digraph) - counter += 1 - - return digraphs - - # encrypts a digraph using the defined grid - def encryptDigraph(self, input): - if len(input) != 2: - raise PlayfairError('The digraph that is going to be encrypted must be exactly 2 characters long.') - elif not self.isUpper(input): - raise PlayfairError('The digraph that is going to be encrypted must contain only uppercase letters of the alphabet.') - - firstLetter = input[0] - secondLetter = input[1] - - firstLetterPosition = self.grid.find(firstLetter) - secondLetterPosition = self.grid.find(secondLetter) - - firstLetterCoordinates = (firstLetterPosition % 5, firstLetterPosition / 5) - secondLetterCoordinates = (secondLetterPosition % 5, secondLetterPosition / 5) - - if firstLetterCoordinates[0] == secondLetterCoordinates[0]: # letters are in the same column - firstEncrypted = self.grid[(((firstLetterCoordinates[1] + 1) % 5) * 5) + firstLetterCoordinates[0]] - secondEncrypted = self.grid[(((secondLetterCoordinates[1] + 1) % 5) * 5) + secondLetterCoordinates[0]] - elif firstLetterCoordinates[1] == secondLetterCoordinates[1]: # letters are in the same row - firstEncrypted = self.grid[(firstLetterCoordinates[1] * 5) + ((firstLetterCoordinates[0] + 1) % 5)] - secondEncrypted = self.grid[(secondLetterCoordinates[1] * 5) + ((secondLetterCoordinates[0] + 1) % 5)] - else: # letters are not in the same row or column, i.e. they form a rectangle - firstEncrypted = self.grid[(firstLetterCoordinates[1] * 5) + secondLetterCoordinates[0]] - secondEncrypted = self.grid[(secondLetterCoordinates[1] * 5) + firstLetterCoordinates[0]] - - return firstEncrypted+secondEncrypted - - # decrypts a digraph using the defined grid - def decryptDigraph(self, input): - if len(input) != 2: - raise PlayfairError('The digraph that is going to be encrypted must be exactly 2 characters long.') - elif not self.isUpper(input): - raise PlayfairError('The digraph that is going to be encrypted must contain only uppercase letters of the alphabet.') - - firstEncrypted = input[0] - secondEncrypted = input[1] - - firstEncryptedPosition = self.grid.find(firstEncrypted) - secondEncryptedPosition = self.grid.find(secondEncrypted) - - firstEncryptedCoordinates = (firstEncryptedPosition % 5, firstEncryptedPosition / 5) - secondEncryptedCoordinates = (secondEncryptedPosition % 5, secondEncryptedPosition / 5) - - if firstEncryptedCoordinates[0] == secondEncryptedCoordinates[0]: # letters are in the same column - firstLetter = self.grid[(((firstEncryptedCoordinates[1] - 1) % 5) * 5) + firstEncryptedCoordinates[0]] - secondLetter = self.grid[(((secondEncryptedCoordinates[1] - 1) % 5) * 5) + secondEncryptedCoordinates[0]] - elif firstEncryptedCoordinates[1] == secondEncryptedCoordinates[1]: # letters are in the same row - firstLetter = self.grid[(firstEncryptedCoordinates[1] * 5) + ((firstEncryptedCoordinates[0] - 1) % 5)] - secondLetter = self.grid[(secondEncryptedCoordinates[1] * 5) + ((secondEncryptedCoordinates[0] - 1) % 5)] - else: # letters are not in the same row or column, i.e. they form a rectangle - firstLetter = self.grid[(firstEncryptedCoordinates[1] * 5) + secondEncryptedCoordinates[0]] - secondLetter = self.grid[(secondEncryptedCoordinates[1] * 5) + firstEncryptedCoordinates[0]] - - return firstLetter+secondLetter - - # encrypts text input - def encrypt(self, input): - digraphs = self.generateDigraphs(input) - encryptedDigraphs = [] - - for digraph in digraphs: - encryptedDigraphs.append(self.encryptDigraph(digraph)) - - return ''.join(encryptedDigraphs) - - # decrypts text input - def decrypt(self, input): - digraphs = self.generateDigraphs(input) - - decryptedDigraphs = [] - - for digraph in digraphs: - decryptedDigraphs.append(self.decryptDigraph(digraph)) - - return ''.join(decryptedDigraphs) - - # sets the password for upcoming encryptions and decryptions - def setPassword(self, password): - password = self.toAlphabet(password).upper() - - self.grid = self.generateGrid(password) - - # strips out all non-alphabetical characters from the input - def toAlphabet(self, input): - return re.sub('[^A-Za-z]', '', input) - - # tests whether the string only contains alphabetical characters - def isAlphabet(self, input): - if re.search('[^A-Za-z]', input): - return False - return True - - # tests whether the string only contains uppercase alphabetical characters - def isUpper(self, input): - if re.search('[^A-Z]', input): - return False - return True \ No newline at end of file + """Playfair Exception Class""" + def __init__(self, message): + super(PlayfairError, self).__init__(message) + print message + + +class Playfair(object): + """Playfair Cipher Class + Public Api: + Playfair(password[, ommision_rule, double_padding, end_padding]) + set_password(password) + encrypt(text) + decrypt(text) + + If no password is set, throws an error + """ + omission_rules = [ + 'Merge J into I', + 'Omit Q', + 'Merge I into J', + ] + + def __init__(self, password, omission_rule=0, double_padding='X', + end_padding='X'): + """omission_rule determines which omission rule you want to use + (go figure). See the list at the beginning of the constructor + double_padding determines what letter you would like to use to pad + a digraph that is double letters + end_padding determines what letter you would like to use to pad + the end of an text containing an odd number of letters""" + if omission_rule >= 0 and omission_rule < len(self.omission_rules): + self.omission_rule = omission_rule + else: + raise PlayfairError('omission_rule values must be between 0 and ' + + (len(self.omission_rules) - 1)) + + self.grid = '' + self.password = '' + self.set_password(password) + + self.double_padding = self._check_padding(double_padding, 'double') + + self.end_padding = self._check_padding(end_padding, 'end') + + def _check_padding(self, padding, which_pad): + """make sure the text for the padding character is valid""" + if len(padding) != 1: + raise PlayfairError('The ' + which_pad + ' padding \ +must be a single character.') + elif not self._is_alphabet(padding): + raise PlayfairError('The ' + which_pad + ' padding must \ +be a letter of the alphabet.') + padding = padding.upper() + if padding not in self.grid: + raise PlayfairError('The ' + which_pad + ' padding character \ +must not be omitted by the omission rule.') + return padding + + def _convert_letter(self, letter): + """returns None if the letter should be discarded, + else returns the converted letter""" + if self.omission_rule == 0: + if letter == 'J': + letter = 'I' + return letter + elif self.omission_rule == 1: + if letter == 'Q': + letter = None + return letter + elif self.omission_rule == 2: + if letter == 'I': + letter = 'J' + return letter + else: + raise PlayfairError('The omission rule provided has not \ +been configured properly.') + + def _get_alphabet(self): + """returns the alphabet used by the cipher + (takes into account the omission rule)""" + full_alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + alphabet = '' + + for letter in full_alphabet: + letter = self._convert_letter(letter) + if letter is not None and letter not in alphabet: + alphabet += letter + + return alphabet + + def _generate_grid(self): + """generates the 25 character grid based on the omission rule + and the given password""" + if self.password is None: + raise PlayfairError("""No password set. Do not use this function. +Instead use set_password(password)""") + grid = '' + alphabet = self._get_alphabet() + + for letter in self.password: + if letter not in grid and letter in alphabet: + grid += letter + + for letter in alphabet: + if letter not in grid: + grid += letter + + return grid + + def _generate_digraphs(self, text): + """splits the text text into digraphs""" + text = self._to_alphabet(text).upper() + text_fixed = '' + + for i in text: + letter = self._convert_letter(i) + if letter is not None: + text_fixed += letter + + counter = 0 + while counter < len(text_fixed): + if counter + 1 == len(text_fixed): + # we have reached the end of the text_fixed + yield text_fixed[counter] + self.end_padding + break + elif text_fixed[counter] != text_fixed[counter + 1]: + # we just need to create a normal digraph + yield text_fixed[counter] + text_fixed[counter + 1] + counter += 2 + else: + # we have a double letter digraph, so we add the double padding + yield text_fixed[counter] + self.double_padding + counter += 1 + + def _encrypt_digraph(self, text): + """encrypts a digraph using the defined grid""" + if len(text) != 2: + raise PlayfairError('The digraph to be encrypted must \ +be exactly 2 characters long.') + elif not self._is_upper(text): + raise PlayfairError('The digraph to be encrypted must contain \ +only uppercase letters of the alphabet.') + + first_letter = text[0] + second_letter = text[1] + + first_letter_pos = self.grid.find(first_letter) + second_letter_pos = self.grid.find(second_letter) + + first_letter_coords = (first_letter_pos % 5, first_letter_pos / 5) + second_letter_coords = (second_letter_pos % 5, second_letter_pos / 5) + + if first_letter_coords[0] == second_letter_coords[0]: + # letters are in the same column + first_encrypted = self.grid[ + (first_letter_coords[1] + 1) % 5 * 5 + first_letter_coords[0]] + second_encrypted = self.grid[ + (second_letter_coords[1] + 1) % 5 * 5 + second_letter_coords[0]] + + elif first_letter_coords[1] == second_letter_coords[1]: + # letters are in the same row + first_encrypted = self.grid[ + first_letter_coords[1] * 5 + (first_letter_coords[0] + 1) % 5] + second_encrypted = self.grid[ + second_letter_coords[1] * 5 + (second_letter_coords[0] + 1) % 5] + else: + # letters are not in the same row or column + first_encrypted = self.grid[ + first_letter_coords[1] * 5 + second_letter_coords[0]] + second_encrypted = self.grid[ + second_letter_coords[1] * 5 + first_letter_coords[0]] + + return first_encrypted+second_encrypted + + def _decrypt_digraph(self, text): + """decrypts a digraph using the defined grid""" + if len(text) != 2: + raise PlayfairError('The digraph to be encrypted \ +must be exactly 2 characters long.') + elif not self._is_upper(text): + raise PlayfairError('The digraph to be encrypted must contain \ +only uppercase letters of the alphabet.') + + first_encrypted = text[0] + second_encrypted = text[1] + + first_encrypted_pos = self.grid.find(first_encrypted) + second_encrypted_pos = self.grid.find(second_encrypted) + + first_encrypted_coords = \ + (first_encrypted_pos % 5, first_encrypted_pos / 5) + second_encrypted_coords = \ + (second_encrypted_pos % 5, second_encrypted_pos / 5) + + if first_encrypted_coords[0] == second_encrypted_coords[0]: + # letters are in the same column + first_letter = self.grid[ + (first_encrypted_coords[1] - 1) % 5 * 5 + + first_encrypted_coords[0]] + second_letter = self.grid[ + (second_encrypted_coords[1] - 1) % 5 * 5 + + second_encrypted_coords[0]] + elif first_encrypted_coords[1] == second_encrypted_coords[1]: + # letters are in the same row + first_letter = self.grid[ + first_encrypted_coords[1] * 5 + + (first_encrypted_coords[0] - 1) % 5] + second_letter = self.grid[ + second_encrypted_coords[1] * 5 + + (second_encrypted_coords[0] - 1) % 5] + else: + # letters are not in the same row or column + first_letter = self.grid[ + first_encrypted_coords[1] * 5 + second_encrypted_coords[0]] + second_letter = self.grid[ + second_encrypted_coords[1] * 5 + first_encrypted_coords[0]] + + return first_letter+second_letter + + def _to_alphabet(self, text): + """strips out all non-alphabetical characters from the text""" + return re.sub('[^A-Za-z]', '', text) + + def _is_alphabet(self, text): + """tests whether the string only contains alphabetical characters""" + return not re.search('[^A-Za-z]', text) + + def _is_upper(self, text): + """tests whether the string contains only \ + uppercase alphabetical characters""" + return not re.search('[^A-Z]', text) + + def encrypt(self, text): + """encrypts text""" + if self.grid is None: + raise PlayfairError("No password has been specified") + encrypted_digraphs = [] + + for digraph in self._generate_digraphs(text): + encrypted_digraphs.append(self._encrypt_digraph(digraph)) + + return ''.join(encrypted_digraphs) + + def decrypt(self, text): + """decrypts text""" + if self.grid is None: + raise PlayfairError("No password has been specified") + decrypted_digraphs = [] + + for digraph in self._generate_digraphs(text): + decrypted_digraphs.append(self._decrypt_digraph(digraph)) + + return ''.join(decrypted_digraphs) + + def set_password(self, password): + """sets the password for upcoming encryptions and decryptions""" + password = self._to_alphabet(password).upper() + self.password = password + self.grid = self._generate_grid()