Skip to content

Instantly share code, notes, and snippets.

@pugilist
Last active February 23, 2016 15:36
Show Gist options
  • Select an option

  • Save pugilist/18f9c5c7c173f93dcbd3 to your computer and use it in GitHub Desktop.

Select an option

Save pugilist/18f9c5c7c173f93dcbd3 to your computer and use it in GitHub Desktop.
cryptofunctions
#!/usr/bin/env python3.4
#
# Title: mth_275
#
# Author: Dave R.
#
# License: CC BY-NC-SA 3.0
# http://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_US
#
# Version:
# 2.0
#
# Description:
# This class contains a number of basic cryptographic functions
# It roughly follows the curriculum of the mth_257 course at
# Champlain College
#
# Usage:
# Import to use as a library.
#
#
# Requirements:
# python3.4
#
#
import string
from collections import OrderedDict
import copy
import binascii
class cipher(object):
def __init__(self):
self.available_charsets = {
'loweralpha':list(string.ascii_lowercase),
'upperalpha':list(string.ascii_uppercase),
'numeric':list(string.digits) }
self.charset = []
self.set_charset()
self.plainnumeric = list()
self.ciphernumeric = list()
self.plaintext = self._split_string(self.plaintext)
self.ciphertext = self._split_string(self.ciphertext)
#############################################
#
# Private methods shared by all ciphers
def _split_string(self,input_string):
# since we don't want to deal with strings, we'll split them up into lists of characters
return [ char for char in input_string ]
def _concat_list(self,input_list):
return ''.join(input_list)
def _encrypt(self):
# override these functions in sub-classes
pass
def _decrypt(self):
# override these functions in sub-classes
pass
def _abc_to_num(self,input_list):
#
# A list of strings to numbers
#
# Returns a list of numbers
#
digits=[]
for character in input_list:
if character.isalpha():
digits.append(self.charset.index(character))
return digits
def _num_to_abc(self,input_list):
#
# Takes a list of digits and translates to letters
#
# outputs a list of characters
#
abc=[]
for element in input_list:
abc.append(self.charset[element])
return abc
def _gcd(self,a,b):
#
# takes two ints as arguments
#
# returns the GCD
#
if b == 0:
return a
else:
self._gcd( b,a % b )
# Credit for _egcd and _modinv goes to https://en.wikibooks.org/wiki/Algorithm_Implementation/Mathematics/Extended_Euclidean_algorithm
def _egcd(self,a, b):
if a == 0:
return (b, 0, 1)
else:
g, y, x = self._egcd(b % a, a)
return (g, x - (b // a) * y, y)
def _modinv(self,a, m):
g, x, y = self._egcd(a, m)
if g != 1:
raise ValueError('[ ! ] Modular inverse does not exist, cannot continue.')
else:
return x % m
def tabular_to_cycle(self,tabular_cycle):
#
# This function takes a dictionary as an argument with the tabular key
# Ex.: tabular_to_cycle({'a':'d','b':'c','c':'a','d':'b','e':'e'})
# it returns a list of lists containing the disjointed cycles
# [['a', 'd', 'b', 'c'], ['e']]
#
# create a copy of our input variable so as not to modify the original data passed to the function.
tmp = copy.copy(tabular_cycle)
cycle= list()
num_cycles=0
while len(tmp) > 0:
(key,value) = tmp.popitem()
#print("[+] Starting key: %s" % key)
#print("[+] tmp: %s" % tmp)
cycle.append([])
cycle[num_cycles].append(key)
while True:
if key is not value:
cycle[num_cycles].append(value)
old_value = value
try:
value = tmp[value]
del tmp[old_value]
except:
break
if value == key:
break
num_cycles += 1
return cycle
def get_cycle_type(self,tabular_cycle):
cycle = self.tabular_to_cycle(tabular_cycle)
result = list()
for group in cycle:
result.append(len(group))
return result.sort()
#############################################
#
# Pointers to private methods which will be used for encryption / decryption
def encrypt(self):
# do not override this function.
if self.plaintext == '' or self.key == '':
raise ValueError("[ ! ] Input error, make sure to specify a key and a plaintext string")
else:
#return self._concat_list( self._encrypt() )
return self._encrypt()
def decrypt(self):
# do not override this function.
if self.ciphertext == '' or self.key == '':
raise ValueError("[ ! ] Input error, make sure to specify a key and a ciphertext string")
else:
#return self._concat_list( self._decrypt() )
return self._decrypt()
def set_charset(self,selection='loweralpha'):
#
# allows us to set the charset for our instance
#
# Pass the function a sinlge string (ex: loweralpha) to set the charset to loweralpha.
#
# You may also pass the function a list of strings (ex: ['loweralpha','upperalpha'] )
# to produce a list of both of them combined and set that as the charset.
#
#
# if only one charset is specified...
if type(selection) is str:
# if we don't recognise the input, throw an error
if selection not in self.available_charsets.keys():
print("Error, unknown input \'%s\' is not an available charset.\n Acceptable options are: %s") % (selection,self.available_charsets.keys())
else:
self.charset = self.available_charsets[selection]
elif type(selection) is list:
for element in selection:
if element not in self.available_charsets.keys():
print("Error, unknown input \'%s\' is not an available charset.\n Acceptable options are: %s") % (selection,self.available_charsets.keys())
else:
self.charset += self.available_charsets[element]
#############################################
#
# Common Cryptanalysis class functions shared by all ciphers.
def find_grams(self,min_length=2,max_length=None):
#
# Finds grams in our ciphertext
#
# min_length and max_length specifies the types of grams we are looking
# for.
#
# returns a dict of dicts with the following structure:
#
# { 'gram_length' : { 'gram': number_of_occurances } }
#
# NOTE: this method was ported to the new version of this library without resolving it's major limitation:
# It cannot find grams in numerical ciphertext, essentially, this only really works for mod 26 gram analysis.
# concatinate our ciphertext list into a string so this function works.
input_string = self._concat_list(self.ciphertext)
# if we didn't specify a max length, use the length of the string divided by 2. This will find all possible grams.
if max_length is None:
max_length = int(len(input_string)/2)
grams = {}
# do this so that max_length actually retuns grams of that length.
# otherwise max_length only produces grams _less than_ max_length
max_length += 1
#set up our dict of dicts for each gram length we're looking for
for i in range(min_length,max_length):
grams[i] = {}
for char in range(len(input_string)):
for gram_length in range(min_length,max_length):
#slice out text for a given length
gram = input_string[char:char+gram_length]
if gram in grams[gram_length].keys():
grams[gram_length][gram] += 1
else:
grams[gram_length][gram] = 1
# now clean up any grams that only show up once and return the dict...
for x in list(grams.keys()):
for y in list(grams[x].keys()):
if grams[x][y] == 1:
del grams[x][y]
if len(grams[x]) == 0:
del grams[x]
return grams
class sub_cipher(cipher):
def __init__(self, plaintext, ciphertext, key):
self.plaintext = plaintext
self.ciphertext = ciphertext
self.key = key
# call the parent class
super().__init__()
#############################################
#
# Common Cryptanalysis class functions shared by all substitution ciphers.
def _count_letters(self):
#
# count the occurences of each letter
# in the given string
#
# returns a dict with the following structure
#
# { 'letter': number_of_occurances }
#
count= {}
for character in self.ciphertext:
if character in count:
count[character] += 1
else:
count[character] = 1
return count
def _find_common_letters(self,limit=10):
letter_count = self._count_letters()
value_list = []
result = []
#grab the a lists of all of the values and make sure there's no duplicates.
for key,value in letter_count.items():
if value not in value_list:
value_list.append(value)
# reverse sort the list ( highest -> lowest )
value_list.sort(reverse=True)
for i in range(0,limit-1):
for key,value in letter_count.items():
if len(result) == 10:
break
elif value == value_list[i]:
result.append(key)
return result
def _count_letters_pct(self,accuracy=4):
#
# takes a string and an optional integer as args
#
# optional arg specifies decimal places to be displayed
#
# returns a dict similar to count_letters() except using percentage
#
count = self._count_letters()
str_len = len(self.ciphertext)
for key,val in count.items():
count[key] = round(float(val) / float(str_len),accuracy)
return count
def print_letter_count(self):
count = self._count_letters()
count_pct = self._count_letters_pct()
for character in self.charset:
try:
print("{0:<6s} {1:^6d} {2:>6f}".format(character,count[character],count_pct[character]))
except KeyError:
# if the letter does no occur in the ciphertext, catch this exception and print zeroes.
print("{0:<6s} {1:^6d} {2:>6f}".format(character,0,0))
class block_cipher(cipher):
def __init__(self):
# the key is probably going to be a matrix, here, so we'll need to derive the blocksize from the key dimensions... Right?
# call the parent class
#super().__init__()
pass
#############################################
#
# Common Cryptanalysis class functions shared by all block ciphers.
# nothing here yet :)
class cesar_cipher(sub_cipher):
def _encrypt(self):
self.ciphertext = self._cipher(self.plaintext,self.key)
return self.ciphertext
def _decrypt(self):
self.plaintext = self._cipher(self.ciphertext,self.key,False)
return self.plaintext
def _cipher(self, input_list,key,encrypt=True):
result = []
for character in input_list:
original_pos = self.charset.index(character)
if encrypt:
new_pos = (original_pos + key ) % len(self.charset)
else:
new_pos = (original_pos - key ) % len(self.charset)
result.append( self.charset[new_pos] )
return result
class affine(sub_cipher):
def __init__(self, plaintext, ciphertext, key,beta,mod):
self.beta = beta
self.mod = mod
super().__init__(plaintext,ciphertext,key)
def _encrypt(self):
self.plainnumeric = self._abc_to_num(self.plaintext)
return [ ((number * self.key) + self.beta) % self.mod for number in self.plainnumeric ]
def _decrypt(self):
# catch bad options nicely
try:
modinv = self._modinv(self.key,self.mod)
except ValueError as e:
print(e)
self.ciphernumeric = self._abc_to_num(self.ciphertext)
return [ self.key * ( number - self.beta ) % self.mod for number in self.ciphernumeric ]
class vigenere(sub_cipher):
def __init__(self, plaintext, ciphertext, key):
if not isinstance(key,str):
raise TypeError("[ ! ] Key must be a string.")
# call the parent class
super().__init__( plaintext, ciphertext, key )
self.key = self._split_string(self.key)
def _encrypt(self):
self.ciphertext = self._cipher()
return self.ciphertext
def _decrypt(self):
self.plaintext = self._cipher()
return self.plaintext
def _cipher(self,input_list,key,encrypt=True):
output_list = []
if encrypt:
numeric_list = self._abc_to_num(self.plaintext)
else:
numeric_list = self._abc_to_num(self.ciphertext)
numeric_key = self._abc_to_num(self.key)
key_len = len(self.key)
for i in range(0, len(numeric_list) ):
offset = numeric_key[ (i % key_len) ]
if encrypt:
output_list.append( (numeric_list[i] + key[offset] ) % len(self.charset) )
else:
output_list.append( (numeric_list[i] - key[offset] ) % len(self.charset) )
#############################################
#
# Common Cryptanalysis class functions.
def kasiski(self):
coincidences = self.kasiski_raw()
# print a header
print("{0:<9s} {1:<7s} {2:4<s}".format("offset","hits","graph"))
for (key,value) in coincidences.items():
#make it look purdy.
graph = ''
for i in range(0,value):
graph += "+"
print("{0:<9d} {1:<7d} {2:4<s}".format(key,value,graph))
def kasiski_raw(self):
#
# Takes a string as an argument
#
# iterates through all positions and retuns a dict of
# offsets and the number of coincidences for each.
#
# Data structure is as follows
# { 'offset': number_of_coincidences }
#
coincidences={}
starting_pos=0
str_len=len(self.ciphertext)
for i in range(0,str_len):
coincidences[i] = 0
for j in range(0,str_len):
# the offset we'll use to compare the two strings, note this is mod str_len so that the search "wraps" around the end of the string and continues at the begining
offset=(j+i) % str_len
if self.ciphertext[offset] == self.ciphertext[j]:
coincidences[i] += 1
return coincidences
class permutation(sub_cipher):
def __init__(self,plaintext,ciphertext,key):
if not isinstance(key,dict):
raise TypeError("[ ! ] Key must be a dictionary ( { plaintext : ciphertext } )")
super().__init__(plaintext,ciphertext,key)
def _cipher(self,input_list,highlight=False,hide_missing=False):
#
# This takes a string of ciphertext and a dictionary as arguments.
# The dictionary is formatted as follows { 'cyphertext value':'plaintext value' }
#
# Highlight makes the plaintext output uppercase to make it more readable. Note
# that this feature probably doesn't work if your charset is not loweralpha (oh well).
#
# hide_missing replaces the ciphertext with underscores to make it more readable
#
result = []
for char in input_list:
if char in key.keys():
if highlight:
result.append( key[char].upper() )
else:
result.append( key[char] )
else:
if hide_missing:
result.append("_")
else:
result.append(char)
return result
def _encrypt(self):
self.ciphertext = self._cipher(self.plaintext)
return self.ciphertext
def _decrypt(self):
self.plaintext = self._cipher(self.ciphertext)
return self.plaintext
def modify_key():
# maybe implement this setter function to allow for easy modification of the key variable.
pass
#############################################
#
# Common Cryptanalysis class functions.
#
# Fix me!
#
def print_bigram_matrix(self):
#
# Takes the cyphertext string and list of most common letters as an argument
#
# Prints a matrix for bigram analysis
#
matrix = self._get_bigram_matrix()
most_common_letters = self._find_common_letters()
#dump out our header
print(''.join('\t{}'.format(*k) for k in matrix.keys()))
print('----------'.join(''.format(*k) for k in matrix.keys()))
# this is probably more complicated than it needed to be...
# this prints out the table from the data returned by get_bigram_matrix()
for key in matrix.keys():
print(' |\n%s | ' % key,end="")
for subkey,value in matrix[key].items():
if subkey in most_common_letters:
print('\t%d' % value, end="")
print('\n |')
#
# Fix me!
#
def _get_bigram_matrix(self):
#
# Takes a string and a list as arguments, returns a big ugly dict of dicts indicating
# The bigram matrix values.
#
# Items in this datastructure can be called by their row column, like so:
#
# var['row']['col]
#
# grab our most common letters.
most_common_letters = self._find_common_letters()
# build our matrix using ordered dictionaries
matrix = OrderedDict( ( key, OrderedDict( (key2, 0) for key2 in most_common_letters )) for key in most_common_letters )
# populate our matrix with information given the common letters specified.
for element in most_common_letters:
#fix this...
for key,value in self._adjacent_letter_count(element).items():
matrix[element][key] = value[1]
if key in most_common_letters:
matrix[key][element] = value[0]
return matrix
#
# Fix me!
#
def _adjacent_letter_count(self,letter):
#
# Takes a string and a letter as arguments
#
# Returns a dict structured as follows
#
# { 'character': [ occurences_before, occurences_after ] }
#
#set up our results dict using the charaset of our chosing
result=dict((element,[0,0]) for element in self.charset)
# need to iterate by string position not letter, use range and len() instead
for i in range(0,len(self.ciphertext)):
if self.ciphertext[i] == letter:
result[self.ciphertext[i-1]][0] += 1
try:
result[self.ciphertext[i+1]][1] += 1
except IndexError:
pass
return result
class enigma(sub_cipher):
#
# This is incomplete. I'm lazy and might never complete it.
#
def __init__(self, message='', rotor_settings=dict(), kenngruppen='', plug_settings=dict(), reflector='b'):
# rotors
self.available_rotors = {
1: OrderedDict([('a', 'e'), ('b', 'k'), ('c', 'm'), ('d', 'f'), ('e', 'l'), ('f', 'g'), ('g', 'd'), ('h', 'q'), ('i', 'v'), ('j', 'z'), ('k', 'n'), ('l', 't'), ('m', 'o'), ('n', 'w'), ('o', 'y'), ('p', 'h'), ('q', 'x'), ('r', 'u'), ('s', 's'), ('t', 'p'), ('u', 'a'), ('v', 'i'), ('w', 'b'), ('x', 'r'), ('y', 'c'), ('z', 'j')]),
2: OrderedDict([('a', 'a'), ('b', 'j'), ('c', 'd'), ('d', 'k'), ('e', 's'), ('f', 'i'), ('g', 'r'), ('h', 'u'), ('i', 'x'), ('j', 'b'), ('k', 'l'), ('l', 'h'), ('m', 'w'), ('n', 't'), ('o', 'm'), ('p', 'c'), ('q', 'q'), ('r', 'g'), ('s', 'z'), ('t', 'n'), ('u', 'p'), ('v', 'y'), ('w', 'f'), ('x', 'v'), ('y', 'o'), ('z', 'e')]),
3: OrderedDict([('a', 'b'), ('b', 'd'), ('c', 'f'), ('d', 'h'), ('e', 'j'), ('f', 'l'), ('g', 'c'), ('h', 'p'), ('i', 'r'), ('j', 't'), ('k', 'x'), ('l', 'v'), ('m', 'z'), ('n', 'n'), ('o', 'y'), ('p', 'e'), ('q', 'i'), ('r', 'w'), ('s', 'g'), ('t', 'a'), ('u', 'k'), ('v', 'm'), ('w', 'u'), ('x', 's'), ('y', 'q'), ('z', 'o')]),
4: OrderedDict([('a', 'e'), ('b', 's'), ('c', 'o'), ('d', 'v'), ('e', 'p'), ('f', 'z'), ('g', 'j'), ('h', 'a'), ('i', 'y'), ('j', 'q'), ('k', 'u'), ('l', 'i'), ('m', 'r'), ('n', 'h'), ('o', 'x'), ('p', 'l'), ('q', 'n'), ('r', 'f'), ('s', 't'), ('t', 'g'), ('u', 'k'), ('v', 'd'), ('w', 'c'), ('x', 'm'), ('y', 'w'), ('z', 'b')]),
5: OrderedDict([('a', 'v'), ('b', 'z'), ('c', 'b'), ('d', 'r'), ('e', 'g'), ('f', 'i'), ('g', 't'), ('h', 'y'), ('i', 'u'), ('j', 'p'), ('k', 's'), ('l', 'd'), ('m', 'n'), ('n', 'h'), ('o', 'l'), ('p', 'x'), ('q', 'a'), ('r', 'w'), ('s', 'm'), ('t', 'j'), ('u', 'q'), ('v', 'o'), ('w', 'f'), ('x', 'e'), ('y', 'c'), ('z', 'k')])
}
self.available_reflectors = {
'b' : OrderedDict([('a', 'f'), ('b', 'v'), ('c', 'p'), ('d', 'j'), ('e', 'i'), ('f', 'a'), ('g', 'o'), ('h', 'y'), ('i', 'e'), ('j', 'd'), ('k', 'r'), ('l', 'z'), ('m', 'x'), ('n', 'w'), ('o', 'g'), ('p', 'c'), ('q', 't'), ('r', 'k'), ('s', 'u'), ('t', 'q'), ('u', 's'), ('v', 'b'), ('w', 'n'), ('x', 'm'), ('y', 'h'), ('z', 'l')])
}
self.plug_settings = plug_settings
self.message = message
self.rotor_settings = rotor_settings
self.kenngruppen = kenngruppen
self.reflector = reflector
#############################################
#
# Private methods for enigma class
# because the germans liked to start the alphabet with 1 instead of 0, we need to override these functions
def _abc_to_num(self,input_list):
#
# A list of strings to numbers
#
# Returns a list of numbers
#
digits=[]
for character in input_list:
if character.isalpha():
digits.append(self.charset.index(character))
return digits+1
def _num_to_abc(self,input_list):
#
# Takes a list of digits and translates to letters
#
# outputs a list of characters
#
abc=[]
for element in input_list:
abc.append(self.charset[element+1])
return abc
def _rearrange_rotor(self):
for rotor,setting in rotor_settings.items():
tipping_point = self._num_to_abc()
while rotor[ len(rotor)-1 ] != tipping_point:
#take an item off the front and put it at the end
tmp = rotor.popitem(last=False)
rotor[ tmp[0] ] = tmp[1]
def _plugboard(self,letter):
for key,value in self.rotor_settings.items():
if key == letter:
if value == '':
return letter
else:
return value
if value == letter:
return key
#
# TODO:
#
# -> write some shit for the message preamble
# -> write soem shit for the identifier
# -> write some shit for rotor permutations
# -> some shit about iterating the permutation shit.
#
# -> override _(en|de)cryption routines
# -> pretty print function
class des(sub_cipher):
def __init__(message='', key='', blocksize=64, rounds=16):
self.message = message
self.key = key
self.blocksize = blocksize
self.rounds = rounds
#define our IP too...
def _xor(a,b):
if len(a) != len(b):
raise ValueError("Error, both binary strings must be of the same length")
else:
result = ''
for char in range(0, len(a)):
result += str( int(a[char]) ^ int(b[char]) )
return result
class sdes(des):
def __init__(self, message=str(), key=str(), blocksize=8, rounds=3):
self.message = message
self.key = key
self.blocksize = blocksize
self.rounds = rounds
self.key_schedule_mask = {
1: [10, 4, 1, 5, 11],
2: [8, 9, 3, 7, 2]}
self.ip = None
self.ip_inv = None
#define our IP too...
def _shift_string(bin_string, shift, left=True):
if left:
return bin_string[shift:] + bin_string[0:shift]
else:
strlen = len(bin_string)
return bin_string[strlen-shift: strlen] + bin_string[: strlen-1]
def get_key_schedule(self):
result = dict()
for index,mask in self.key_schedule_mask.items():
result[index] = list()
for value in mask:
# since our place values start with 1, we need to adjust to make them work with the list index values
result[index].append(self.key[value-1])
return result
def _cipher():
result = str()
for i in range(0, len(self.plaintext), self.blocksize):
try:
chunk = self.plaintext[i : i+self.blocksize]
except IndexError:
chunk = self.plaintext[i:]
while len(chunk) < self.blocksize:
chunk += "0"
#call some function
# return some value
class rsa(sub_cipher):
def __init__(self,message=str(),p=int(),q=int(),pub_key=int(),priv_key=int()):
# mod = p * q
# 1 == (pub_key * priv_key ) % (p - 1) * ( q -1 )
pass
def _encrypt():
# plaintext ** public_key % mod n
pass
def _decypt():
# ciphertext ** private_key % mod n
pass
def _cipher(self,char, mod):
pass
'''
'some dumb string'.encode()
#then we can get the binary contents if we really want by doing this...
# there's gotta be a better way...
bin(int(str( string[character] )))
'''
#############################################
#
# Private Functions used by RSA
def _mod_exponentiation(self,base,exponent,mod):
expanded = self._bin_expand(exponent)
squares = self._successive_square(base=base, highest_value=exponent, mod=mod)
result = 1
for number in expanded:
result = ( result * squares[number] ) % mod
return result
def _successive_square(self, base, highest_value, mod, exponent=1, result=dict()):
if exponent == 1:
result[exponent] = base
exponent +=1
if exponent > highest_value:
return result
else:
#print("[+] Working with:")
#print(locals())
reduced_value = (base ** 2) % mod
result[exponent] = reduced_value
exponent *= 2
# if we have more work to do, recurse!
return self._successive_square(base=reduced_value, highest_value=highest_value, mod=mod, exponent=exponent, result=result)
def _bin_expand(self,value):
result = list()
expanded = bin(value)
# trim off the 0b prefix.
expanded = expanded[2:]
str_len = len(expanded)
for i in range(0,str_len):
if expanded[i] == '1':
result.append(2 ** (str_len-i-1))
return result
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment