"""Public Key Cipher
Implements a series of functions capable of encrypting and decrypting with `textbook RSA`_ public/private keypairs.
Attributes:
SYMBOLS (str): String with all characters to be encrypted/decrypted.
PUBLIC_KEY_PATH (str): String with absolute location of public key file.
PRIVATE_KEY_PATH (str): String with absolute location of private key file.
Note:
* https://www.nostarch.com/crackingcodes/ (BSD Licensed)
* The public and private keys are created by the
:py:mod:`CrackingCodes.Ch23.makePublicPrivateKeys` module.
* 'Textbook/Plain' RSA keys are not secure and should not be used to encrypt sensitive data.
.. _textbook RSA:
https://en.wikipedia.org/wiki/RSA_(cryptosystem)#Attacks_against_plain_RSA
"""
import sys, math
# The public and private keys for this program are created by
# the makePublicPrivateKeys.py program.
# This program must be run in the same folder as the key files.
SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890 !?.'
PUBLIC_KEY_PATH = "/home/jose/PycharmProjects/python-tutorials/pythontutorials/books/CrackingCodes/Ch23/al_sweigart_pubkey.txt"
PRIVATE_KEY_PATH= "/home/jose/PycharmProjects/python-tutorials/pythontutorials/books/CrackingCodes/Ch23/al_sweigart_privkey.txt"
[docs]def main():
# Runs a test that encrypts a message to a file or decrypts a message
# from a file.
filename = 'encrypted_file.txt' # The file to write to/read from.
mode = 'encrypt' # Set to either 'encrypt' or 'decrypt'.
if mode == 'encrypt':
message = 'Journalists belong in the gutter because that is where the ruling classes throw their guilty secrets. Gerald Priestland. The Founding Fathers gave the free press the protection it must have to bare the secrets of government and inform the people. Hugo Black.'
pubKeyFilename = PUBLIC_KEY_PATH
print('Encrypting and writing to %s...' % filename)
encryptedText = encryptAndWriteToFile(filename, pubKeyFilename, message)
print('Encrypted text:')
print(encryptedText)
elif mode == 'decrypt':
privKeyFilename = PRIVATE_KEY_PATH
print('Reading from %s and decrypting...' % filename)
decryptedText = readFromFileAndDecrypt(filename, privKeyFilename)
print('Decrypted text:')
print(decryptedText)
if mode == "encrypt":
privKeyFilename = PRIVATE_KEY_PATH
print("Decrypted text: ")
print(readFromFileAndDecrypt(filename, privKeyFilename))
[docs]def getBlocksFromText(message: str, blockSize: int) -> list:
"""Get blocks from text
Converts a string message to a list of block integers.
Args:
message: String containing message to convert into blocks of integers.
blockSize: Size of each block of integers.
Returns:
List with blocks of integers of the given size.
Note:
* If a character in the message is not in SYMBOLS, program exits with an error.
"""
for character in message:
if character not in SYMBOLS:
print('ERROR: The symbol set does not have the character %s' % character)
sys.exit()
blockInts = []
for blockStart in range(0, len(message), blockSize):
# Calculate the block integer for this block of text:
blockInt = 0
for i in range(blockStart, min(blockStart + blockSize, len(message))):
blockInt += (SYMBOLS.index(message[i])) * (len(SYMBOLS) ** (i % blockSize))
blockInts.append(blockInt)
return blockInts
[docs]def getTextFromBlocks(blockInts: list, messageLength: int, blockSize: int) -> str:
"""Get text from blocks
Converts a list of block integers to the original message string.
Args:
blockInts: List of block integers of specified size.
messageLength: Length of the original message.
blockSize: Bit size of each block of integers.
Returns:
Original message string before block integer conversion.
Note:
* The original message length is needed to properly convert the last block integer.
"""
message = []
for blockInt in blockInts:
blockMessage = []
for i in range(blockSize - 1, -1, -1):
if len(message) + i < messageLength:
# Decode the message string for the 128 (or whatever
# blockSize is set to) characters from this block integer:
charIndex = blockInt // (len(SYMBOLS) ** i)
blockInt = blockInt % (len(SYMBOLS) ** i)
blockMessage.insert(0, SYMBOLS[charIndex])
message.extend(blockMessage)
return ''.join(message)
[docs]def encryptMessage(message: str, key: tuple, blockSize: int) -> list:
"""Encrypt message
Converts the message string into a list of block integers, and then encrypts each block integer.
Args:
message: String containing message to encrypt with PUBLIC key.
key: Tuple with PUBLIC key used for encryption.
blockSize: Bit size of block integers (usually specified in the PUBLIC key file).
Returns:
List of block integers encrypted with PUBLIC key.
Note:
* Ensure to pass the PUBLIC key to encrypt.
"""
encryptedBlocks = []
n, e = key
for block in getBlocksFromText(message, blockSize):
# ciphertext = plaintext ^ e mod n
encryptedBlocks.append(pow(block, e, n))
return encryptedBlocks
[docs]def decryptMessage(encryptedBlocks: list, messageLength: int, key: tuple, blockSize: int) -> str:
"""Decrypt Message
Decrypts a list of encrypted block integers back to the original message string.
Args:
encryptedBlocks: List containing block integers encrypted with PUBLIC key.
messageLength: Length of the original message.
key: Tuple with PRIVATE key used to decryption.
blockSize: Bit size of block integers (usually specified in PRIVATE key file).
Returns:
Original message before block integer conversion and PUBLIC key encryption.
Notes:
* The original message length is required to properly decrypt the last block.
* Ensure to pass the PRIVATE key to decrypt.
"""
decryptedBlocks = []
n, d = key
for block in encryptedBlocks:
# plaintext = ciphertext ^ d mod n
decryptedBlocks.append(pow(block, d, n))
return getTextFromBlocks(decryptedBlocks, messageLength, blockSize)
[docs]def readKeyFile(keyFilename: str) -> tuple:
"""Read key from key file
Reads the given public/private key file and returns the key.
Args:
keyFilename: String containing absolute path to public/private key file.
Returns:
The key as a (n,e) or (n,d) tuple value.
"""
fo = open(keyFilename)
content = fo.read()
fo.close()
keySize, n, EorD = content.split(',')
return int(keySize), int(n), int(EorD)
[docs]def encryptAndWriteToFile(messageFilename: str, keyFilename: str, message: str, blockSize: int=None) -> str:
"""Encrypt and write to file
Using a key from a keyfile, encrypt the message and save it to a file.
Args:
messageFilename: String containing name of file to save encrypted message to.
keyFilename: String containing absolute file path of PUBLIC key file.
message: String containing message to encrypt and save.
blockSize: Bit size of blocks of integers used to convert and encrypt message (usually specified in
PUBLIC key file).
Returns:
Encrypted message string.
"""
keySize, n, e = readKeyFile(keyFilename)
if blockSize is None:
# If blockSize isn't given, set it to the largest size allowed by the key size and symbol set size.
blockSize = int(math.log(2 ** keySize, len(SYMBOLS)))
# Check that the key size is large enough for the block size:
if not (math.log(2 ** keySize, len(SYMBOLS)) >= blockSize):
sys.exit('ERROR: Block size is too large for the key and symbol set size. Did you specify the correct key file and encrypted file?')
# Encrypt the message:
encryptedBlocks = encryptMessage(message, (n, e), blockSize)
# Convert the large int values to one string value:
for i in range(len(encryptedBlocks)):
encryptedBlocks[i] = str(encryptedBlocks[i])
encryptedContent = ','.join(encryptedBlocks)
# Write out the encrypted string to the output file:
encryptedContent = '%s_%s_%s' % (len(message), blockSize, encryptedContent)
fo = open(messageFilename, 'w')
fo.write(encryptedContent)
fo.close()
# Also return the encrypted string:
return encryptedContent
[docs]def readFromFileAndDecrypt(messageFilename: str, keyFilename: str) -> str:
"""Read from file and decrypt
Using a key from a key file, read an encrypted message from a file and then decrypt it.
Args:
messageFilename: String containing name of file with encrypted message saved to it.
keyFilename: String containing absolute file path of PRIVATE key file.
Returns:
Decrypted message string.
Note:
* Checks block size in key file and exits with error if too large.
"""
keySize, n, d = readKeyFile(keyFilename)
# Read in the message length and the encrypted message from the file:
fo = open(messageFilename)
content = fo.read()
messageLength, blockSize, encryptedMessage = content.split('_')
messageLength = int(messageLength)
blockSize = int(blockSize)
# Check that key size is large enough for the block size:
if not (math.log(2 ** keySize, len(SYMBOLS)) >= blockSize):
sys.exit('ERROR: Block size is too large for the key and symbol set size. Did you specify the correct key file and encrypted file?')
# Convert the encrypted message into large int values:
encryptedBlocks = []
for block in encryptedMessage.split(','):
encryptedBlocks.append(int(block))
# Decrypt the large int values:
return decryptMessage(encryptedBlocks, messageLength, (n, d), blockSize)
# If publicKeyCipher.py is run (instead of imported as a module), call
# the main() function.
if __name__ == '__main__':
main()