├── .gitignore ├── LICENSE ├── examples └── elgamal.py ├── README.md └── groups.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Matthew Gray 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/elgamal.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Here are the steps of the El Gamal encryption algorithm. Note, this algorithm is not intended for realistic security purposes as presented. 3 | 4 | Bob does the following once. 5 | (a) Chooses a group G 6 | (b) Choose g ∈ G 7 | (c) Choose a natural number k 8 | (d) Compute h = g^k 9 | (e) Tell the whole world: G, g, h. 10 | Alice does the following whenever she wants to send a message to Bob. 11 | (a) Encode the message as m ∈ G using an international standard. 12 | (b) Choose a natural number s. 13 | (c) Encrypt m into c1 = g^s and c2 = h^s * m. 14 | (d) Sends to Bob (using insecure channel): c1, c2. 15 | Bob does the following upon receiving series of c1, c2 from Alice. 16 | (a) Decrypt c1 and c2 into m0 = c^(−k) * c2. 17 | (b) Decode m0 using the international standard. 18 | ''' 19 | 20 | #import parent dir 21 | #https://stackoverflow.com/questions/714063/importing-modules-from-parent-folder 22 | import os,sys,inspect 23 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 24 | 25 | from groups import * 26 | 27 | def power(G,e,n): 28 | ''' 29 | G: group 30 | e: element 31 | n: exponent 32 | ''' 33 | result = G.identity() 34 | for i in range(0,n): 35 | result = G.compose(result, e) 36 | return result 37 | 38 | def initialization(G, g, k): 39 | ''' 40 | G: group to use for encryption 41 | g: element of group to use 42 | k: natural number 43 | ''' 44 | 45 | h = power(G, g, k) 46 | return (G, g, h) 47 | 48 | def encrypt(init, msg, encode, s): 49 | ''' 50 | init: results from initialization 51 | msg: string msg to encode 52 | encode: function taking one param (msg) 53 | s: natural number 54 | ''' 55 | 56 | G = init[0] 57 | g = init[1] 58 | h = init[2] 59 | 60 | m = encode(msg) 61 | c1 = power(G, g, s) 62 | c2 = G.compose(power(G, h, s), m) 63 | return (c1, c2) 64 | 65 | def decrypt(G, c1, c2, k): 66 | ''' 67 | G: group 68 | c1: part 1 of encrypted data 69 | c2: part 2 of encrypted data 70 | k: private key 71 | ''' 72 | 73 | return G.compose(power(G, G.inverse(c1), k), c2) 74 | 75 | G = U(7) 76 | k = 9 77 | g = 3 78 | init = initialization(G, g, k) 79 | msg = 2 #"secret" message 80 | encode = lambda x: x 81 | s = 5 82 | encrypted_data = encrypt(init, msg, encode, s) 83 | decrypted = decrypt(G, encrypted_data[0], encrypted_data[1], k) 84 | 85 | print("The original message is {0}".format(msg)) 86 | print("Part 1 (c1) of the encrypted data is {0}".format(encrypted_data[0])) 87 | print("Part 2 (c2) of the encrypted data is {0}".format(encrypted_data[1])) 88 | print("The decrypted message is (should equal the original) {0}".format(decrypted)) 89 | 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # About 3 | 4 | This project was used as a tool to familiarize myself with very basic group theory (for abstract algebra/MA407@ncsu) and python (for programming languages and modeling/CSC495-002@ncsu) during the 2018 spring semester. It's far from a complete exploration of either, but it was a fun project and helped me understand some concepts. If you'd like to contribute I'll add some things in the issues section that could be worked on for practice with groups, python, and open-source in general. 5 | 6 | To run this file, simply open a terminal/cmd instance, change to the directory it's stored at, and type `python groups.py`. 7 | 8 | To learn more about group theory, check out [this wonderful video on YouTube](https://www.youtube.com/watch?v=g7L_r6zw4-c) or give it a google. 9 | 10 | # Examples and Usage 11 | 12 | A group requires a set of elements and a binary operation over that set of elements, so those two fields are required in the initialization of a group object. I also added in a parameter that maps elements to a "pretty" string representation so that when printing the Cayley table (multiplication table of all elements in a group) it looks nice. Based on that, here's an example of constructing a basic group, the integers under addition mod n. 13 | 14 | ```python 15 | def Z(n): 16 | #creates a list of all elements in the group 17 | elements = [e for e in range(0,n)] 18 | #creates the binary operation over the group, addition mod n 19 | operation = lambda e1, e2: ((e2 + e1) % n) 20 | #creates the mapping of group elements to a printable value, in this case just the element 21 | mapping = {e : e for e in elements} 22 | #constructs the group object and returns it 23 | return Group(elements, operation, mapping) 24 | 25 | #prints the string representation of the group, the Cayley table 26 | print(Z(5)) 27 | ``` 28 | 29 | # Contributing 30 | 31 | I have no clear goal in mind for this project, I'm just working on it because I find group theory cool and interesting. It also doubles as a good tool to familiarize myself with Python. If you'd like to contribute, feel free to check out the [issues](https://github.com/grayma/python-algebra-groups/issues) tab which lists what ideas could be implemented in the project. 32 | 33 | # Notes 34 | 35 | This is mostly for educational purposes, I'm far from an expert in Python or group theory, so be mindful of the following: 36 | 37 | * Something could be incorrect or greatly improved. Feel free to file an issue, work on it, and submit a pull request. 38 | * This is not meant to be a full abstract algebra framework. It may be fleshed out over time, but if you're interested in a full implementation, then you should check out [Sage](http://doc.sagemath.org/html/en/constructions/groups.html). -------------------------------------------------------------------------------- /groups.py: -------------------------------------------------------------------------------- 1 | from math import gcd 2 | 3 | class Group: 4 | def __init__(self, elements, operation, table_name_map): 5 | """ 6 | elements: elements of the group 7 | operation: operation on group 8 | table_name_map: map for cayley table to prettify any complex objects in groups for printing 9 | """ 10 | self.elements = elements 11 | self.operation = operation 12 | self.table_name_map = table_name_map 13 | self.id = None #placeholder to find group identity later 14 | 15 | def compose(self, e1, e2): 16 | """ 17 | e1: element 1 of composition 18 | e2: element 2 of composition 19 | 20 | returns the composition of e1 and e2. read "e1 o e2" 21 | """ 22 | if (e1 not in self.elements) or (e2 not in self.elements): 23 | raise Exception('Elements not in group.') 24 | value = self.operation(e1, e2) 25 | if value not in self.elements: 26 | raise Exception('Closure does not hold for {0} + {1} = {2}.'.format(e1,e2,value)) 27 | return value 28 | 29 | def identity(self): 30 | """ 31 | Gets the identity of the group 32 | """ 33 | if self.id: 34 | return self.id #if already found return 35 | for element in self.elements: 36 | for test_element in self.elements: 37 | if not (self.compose(element, test_element) == test_element): 38 | break 39 | self.id = element 40 | return element 41 | #shouldn't reach 42 | raise Exception('No identity, not a group') 43 | 44 | def inverse(self, e): 45 | """ 46 | e: element to find inverse of 47 | 48 | finds the inverse of an element `e` 49 | """ 50 | for element in self.elements: 51 | if self.compose(e, element) == self.identity(): 52 | return element 53 | #shouldn't reach 54 | raise Exception('No inverse for {0}, not a group'.format(e)) 55 | 56 | def __repr__(self): 57 | longest = max(len(str(v)) for k,v in self.table_name_map.items()) + 2 58 | #makes an element into a guarenteed len cell 59 | def cell(data,filler=' '): 60 | #thanks https://stackoverflow.com/questions/5676646/how-can-i-fill-out-a-python-string-with-spaces 61 | return '{message:{fill}{align}{width}}'.format( 62 | message=data, 63 | fill=filler, 64 | align='^', 65 | width=longest 66 | ) 67 | #maps elements to pretty values and strs 68 | def data_cell(e): 69 | return cell(str(self.table_name_map[e])) 70 | vert_sepr = '|' #vertical bar separator 71 | def hori_sepr(char): #horizontal row separator 72 | return cell('',filler=char) 73 | def row_sepr(char): #row separator 74 | return (hori_sepr(char) + vert_sepr * 2) + ((hori_sepr(char) + vert_sepr) * len(self.elements)) + '\n' 75 | 76 | cayley = cell('o') + vert_sepr * 2 77 | #headers 78 | for header in self.elements: 79 | cayley += data_cell(header) + vert_sepr 80 | cayley += '\n' + row_sepr('=') 81 | 82 | #table 83 | for row in self.elements: 84 | line = (data_cell(row) + vert_sepr * 2) 85 | for column in self.elements: 86 | line += data_cell(self.compose(row,column)) + vert_sepr 87 | cayley += line + '\n' + row_sepr('-') 88 | return cayley 89 | 90 | 91 | def coprime(a,b): 92 | return gcd(a,b) == 1 93 | def U(n): 94 | elements = [e for e in range(1,n) if coprime(e,n)] 95 | return Group(elements, lambda e1, e2: ((e2 * e1) % n), {e : e for e in elements}) 96 | 97 | 98 | def Z(n): 99 | elements = [e for e in range(0,n)] 100 | return Group(elements, lambda e1, e2: ((e2 + e1) % n), {e : e for e in elements}) 101 | 102 | if __name__ == '__main__': 103 | ########### 104 | #group Z_n# 105 | ########### 106 | print("Z_5") 107 | print(Z(5)) 108 | 109 | ########### 110 | #group U_n# 111 | ########### 112 | print("U_8") 113 | print(U(8)) 114 | 115 | ########### 116 | #group S_3# 117 | ########### 118 | p0 = ((1,2,3),(1,2,3)) 119 | p1 = ((1,2,3),(2,3,1)) 120 | p2 = ((1,2,3),(3,1,2)) 121 | p3 = ((1,2,3),(1,3,2)) 122 | p4 = ((1,2,3),(3,2,1)) 123 | p5 = ((1,2,3),(2,1,3)) 124 | p = (p0,p1,p2,p3,p4,p5) 125 | name_map = { p[i] : "p{0}".format(i) for i in range(0,len(p)) } 126 | #operation on S_3 127 | def map(e1,e2): 128 | return ((1,2,3), tuple( e1[1][e1[0].index(i)] for i in e2[1] ) ) 129 | S3 = Group(p, map, name_map) 130 | print("S_3") 131 | print(S3) 132 | print("Identity: {0}".format(S3.identity())) 133 | print("Inverse of p4: {0}".format(name_map[S3.inverse(p4)])) 134 | 135 | --------------------------------------------------------------------------------