├── .gitignore ├── README.md ├── aes_gcm.py ├── requirements.txt ├── test.py └── test_gf_mul.sage /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.swp 3 | *~ 4 | .DS_Store 5 | check_gf_mul.py 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Packages 11 | *.egg 12 | *.egg-info 13 | dist 14 | build 15 | eggs 16 | parts 17 | bin 18 | var 19 | sdist 20 | develop-eggs 21 | .installed.cfg 22 | lib 23 | lib64 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Unit test / coverage reports 29 | .coverage 30 | .tox 31 | nosetests.xml 32 | 33 | # Translations 34 | *.mo 35 | 36 | # Mr Developer 37 | .mr.developer.cfg 38 | .project 39 | .pydevproject 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AES-GCM-Python 2 | 3 | A Python implementation of the authenticated encryption mode [Galois/Counter Mode (GCM)](http://en.wikipedia.org/wiki/Galois/Counter_Mode). 4 | 5 | Currently it supports only 128-bit AES and 96-bit nonce. 6 | 7 | ## Dependencies 8 | 9 | * [PyCrypto](https://github.com/dlitz/pycrypto) 10 | 11 | ## License 12 | 13 | Copyright (C) 2013 Bo Zhu http://about.bozhu.me 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a 16 | copy of this software and associated documentation files (the "Software"), 17 | to deal in the Software without restriction, including without limitation 18 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 19 | and/or sell copies of the Software, and to permit persons to whom the 20 | Software is furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 28 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 30 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 31 | DEALINGS IN THE SOFTWARE. 32 | -------------------------------------------------------------------------------- /aes_gcm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (C) 2013 Bo Zhu http://about.bozhu.me 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | """ 24 | 25 | from Crypto.Cipher import AES 26 | from Crypto.Util import Counter 27 | from Crypto.Util.number import long_to_bytes, bytes_to_long 28 | 29 | 30 | # GF(2^128) defined by 1 + a + a^2 + a^7 + a^128 31 | # Please note the MSB is x0 and LSB is x127 32 | def gf_2_128_mul(x, y): 33 | assert x < (1 << 128) 34 | assert y < (1 << 128) 35 | res = 0 36 | for i in range(127, -1, -1): 37 | res ^= x * ((y >> i) & 1) # branchless 38 | x = (x >> 1) ^ ((x & 1) * 0xE1000000000000000000000000000000) 39 | assert res < 1 << 128 40 | return res 41 | 42 | 43 | class InvalidInputException(Exception): 44 | def __init__(self, msg): 45 | self.msg = msg 46 | 47 | def __str__(self): 48 | return str(self.msg) 49 | 50 | 51 | class InvalidTagException(Exception): 52 | def __str__(self): 53 | return 'The authenticaiton tag is invalid.' 54 | 55 | 56 | # Galois/Counter Mode with AES-128 and 96-bit IV 57 | class AES_GCM: 58 | def __init__(self, master_key): 59 | self.change_key(master_key) 60 | 61 | def change_key(self, master_key): 62 | if master_key >= (1 << 128): 63 | raise InvalidInputException('Master key should be 128-bit') 64 | 65 | self.__master_key = long_to_bytes(master_key, 16) 66 | self.__aes_ecb = AES.new(self.__master_key, AES.MODE_ECB) 67 | self.__auth_key = bytes_to_long(self.__aes_ecb.encrypt(b'\x00' * 16)) 68 | 69 | # precompute the table for multiplication in finite field 70 | table = [] # for 8-bit 71 | for i in range(16): 72 | row = [] 73 | for j in range(256): 74 | row.append(gf_2_128_mul(self.__auth_key, j << (8 * i))) 75 | table.append(tuple(row)) 76 | self.__pre_table = tuple(table) 77 | 78 | self.prev_init_value = None # reset 79 | 80 | def __times_auth_key(self, val): 81 | res = 0 82 | for i in range(16): 83 | res ^= self.__pre_table[i][val & 0xFF] 84 | val >>= 8 85 | return res 86 | 87 | def __ghash(self, aad, txt): 88 | len_aad = len(aad) 89 | len_txt = len(txt) 90 | 91 | # padding 92 | if 0 == len_aad % 16: 93 | data = aad 94 | else: 95 | data = aad + b'\x00' * (16 - len_aad % 16) 96 | if 0 == len_txt % 16: 97 | data += txt 98 | else: 99 | data += txt + b'\x00' * (16 - len_txt % 16) 100 | 101 | tag = 0 102 | assert len(data) % 16 == 0 103 | for i in range(len(data) // 16): 104 | tag ^= bytes_to_long(data[i * 16: (i + 1) * 16]) 105 | tag = self.__times_auth_key(tag) 106 | # print 'X\t', hex(tag) 107 | tag ^= ((8 * len_aad) << 64) | (8 * len_txt) 108 | tag = self.__times_auth_key(tag) 109 | 110 | return tag 111 | 112 | def encrypt(self, init_value, plaintext, auth_data=b''): 113 | if init_value >= (1 << 96): 114 | raise InvalidInputException('IV should be 96-bit') 115 | # a naive checking for IV reuse 116 | if init_value == self.prev_init_value: 117 | raise InvalidInputException('IV must not be reused!') 118 | self.prev_init_value = init_value 119 | 120 | len_plaintext = len(plaintext) 121 | # len_auth_data = len(auth_data) 122 | 123 | if len_plaintext > 0: 124 | counter = Counter.new( 125 | nbits=32, 126 | prefix=long_to_bytes(init_value, 12), 127 | initial_value=2, # notice this 128 | allow_wraparound=False) 129 | aes_ctr = AES.new(self.__master_key, AES.MODE_CTR, counter=counter) 130 | 131 | if 0 != len_plaintext % 16: 132 | padded_plaintext = plaintext + \ 133 | b'\x00' * (16 - len_plaintext % 16) 134 | else: 135 | padded_plaintext = plaintext 136 | ciphertext = aes_ctr.encrypt(padded_plaintext)[:len_plaintext] 137 | 138 | else: 139 | ciphertext = b'' 140 | 141 | auth_tag = self.__ghash(auth_data, ciphertext) 142 | # print 'GHASH\t', hex(auth_tag) 143 | auth_tag ^= bytes_to_long(self.__aes_ecb.encrypt( 144 | long_to_bytes((init_value << 32) | 1, 16))) 145 | 146 | # assert len(ciphertext) == len(plaintext) 147 | assert auth_tag < (1 << 128) 148 | return ciphertext, auth_tag 149 | 150 | def decrypt(self, init_value, ciphertext, auth_tag, auth_data=b''): 151 | if init_value >= (1 << 96): 152 | raise InvalidInputException('IV should be 96-bit') 153 | if auth_tag >= (1 << 128): 154 | raise InvalidInputException('Tag should be 128-bit') 155 | 156 | if auth_tag != self.__ghash(auth_data, ciphertext) ^ \ 157 | bytes_to_long(self.__aes_ecb.encrypt( 158 | long_to_bytes((init_value << 32) | 1, 16))): 159 | raise InvalidTagException 160 | 161 | len_ciphertext = len(ciphertext) 162 | if len_ciphertext > 0: 163 | counter = Counter.new( 164 | nbits=32, 165 | prefix=long_to_bytes(init_value, 12), 166 | initial_value=2, 167 | allow_wraparound=True) 168 | aes_ctr = AES.new(self.__master_key, AES.MODE_CTR, counter=counter) 169 | 170 | if 0 != len_ciphertext % 16: 171 | padded_ciphertext = ciphertext + \ 172 | b'\x00' * (16 - len_ciphertext % 16) 173 | else: 174 | padded_ciphertext = ciphertext 175 | plaintext = aes_ctr.decrypt(padded_ciphertext)[:len_ciphertext] 176 | 177 | else: 178 | plaintext = b'' 179 | 180 | return plaintext 181 | 182 | 183 | if __name__ == '__main__': 184 | master_key = 0xfeffe9928665731c6d6a8f9467308308 185 | plaintext = b'\xd9\x31\x32\x25\xf8\x84\x06\xe5' + \ 186 | b'\xa5\x59\x09\xc5\xaf\xf5\x26\x9a' + \ 187 | b'\x86\xa7\xa9\x53\x15\x34\xf7\xda' + \ 188 | b'\x2e\x4c\x30\x3d\x8a\x31\x8a\x72' + \ 189 | b'\x1c\x3c\x0c\x95\x95\x68\x09\x53' + \ 190 | b'\x2f\xcf\x0e\x24\x49\xa6\xb5\x25' + \ 191 | b'\xb1\x6a\xed\xf5\xaa\x0d\xe6\x57' + \ 192 | b'\xba\x63\x7b\x39' 193 | auth_data = b'\xfe\xed\xfa\xce\xde\xad\xbe\xef' + \ 194 | b'\xfe\xed\xfa\xce\xde\xad\xbe\xef' + \ 195 | b'\xab\xad\xda\xd2' 196 | init_value = 0xcafebabefacedbaddecaf888 197 | ciphertext = b'\x42\x83\x1e\xc2\x21\x77\x74\x24' + \ 198 | b'\x4b\x72\x21\xb7\x84\xd0\xd4\x9c' + \ 199 | b'\xe3\xaa\x21\x2f\x2c\x02\xa4\xe0' + \ 200 | b'\x35\xc1\x7e\x23\x29\xac\xa1\x2e' + \ 201 | b'\x21\xd5\x14\xb2\x54\x66\x93\x1c' + \ 202 | b'\x7d\x8f\x6a\x5a\xac\x84\xaa\x05' + \ 203 | b'\x1b\xa3\x0b\x39\x6a\x0a\xac\x97' + \ 204 | b'\x3d\x58\xe0\x91' 205 | auth_tag = 0x5bc94fbc3221a5db94fae95ae7121a47 206 | 207 | print('plaintext:', hex(bytes_to_long(plaintext))) 208 | 209 | my_gcm = AES_GCM(master_key) 210 | encrypted, new_tag = my_gcm.encrypt(init_value, plaintext, auth_data) 211 | print('encrypted:', hex(bytes_to_long(encrypted))) 212 | print('auth tag: ', hex(new_tag)) 213 | 214 | try: 215 | decrypted = my_gcm.decrypt(init_value, encrypted, 216 | new_tag + 1, auth_data) 217 | except InvalidTagException: 218 | decrypted = my_gcm.decrypt(init_value, encrypted, new_tag, auth_data) 219 | print('decrypted:', hex(bytes_to_long(decrypted))) 220 | 221 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycrypto==2.6 2 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (C) 2013 Bo Zhu http://about.bozhu.me 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | """ 24 | 25 | from aes_gcm import AES_GCM 26 | from pprint import pprint 27 | from Crypto.Random.random import getrandbits 28 | from Crypto.Util.number import long_to_bytes 29 | 30 | test_cases = ({ 31 | 'master_key': 0x00000000000000000000000000000000, 32 | 'plaintext': b'', 33 | 'auth_data': b'', 34 | 'init_value': 0x000000000000000000000000, 35 | 'ciphertext': b'', 36 | 'auth_tag': 0x58e2fccefa7e3061367f1d57a4e7455a, 37 | }, { 38 | 'master_key': 0x00000000000000000000000000000000, 39 | 'plaintext': b'\x00\x00\x00\x00\x00\x00\x00\x00' + 40 | b'\x00\x00\x00\x00\x00\x00\x00\x00', 41 | 'auth_data': b'', 42 | 'init_value': 0x000000000000000000000000, 43 | 'ciphertext': b'\x03\x88\xda\xce\x60\xb6\xa3\x92' + 44 | b'\xf3\x28\xc2\xb9\x71\xb2\xfe\x78', 45 | 'auth_tag': 0xab6e47d42cec13bdf53a67b21257bddf, 46 | }, { 47 | 'master_key': 0xfeffe9928665731c6d6a8f9467308308, 48 | 'plaintext': b'\xd9\x31\x32\x25\xf8\x84\x06\xe5' + 49 | b'\xa5\x59\x09\xc5\xaf\xf5\x26\x9a' + 50 | b'\x86\xa7\xa9\x53\x15\x34\xf7\xda' + 51 | b'\x2e\x4c\x30\x3d\x8a\x31\x8a\x72' + 52 | b'\x1c\x3c\x0c\x95\x95\x68\x09\x53' + 53 | b'\x2f\xcf\x0e\x24\x49\xa6\xb5\x25' + 54 | b'\xb1\x6a\xed\xf5\xaa\x0d\xe6\x57' + 55 | b'\xba\x63\x7b\x39\x1a\xaf\xd2\x55', 56 | 'auth_data': b'', 57 | 'init_value': 0xcafebabefacedbaddecaf888, 58 | 'ciphertext': b'\x42\x83\x1e\xc2\x21\x77\x74\x24' + 59 | b'\x4b\x72\x21\xb7\x84\xd0\xd4\x9c' + 60 | b'\xe3\xaa\x21\x2f\x2c\x02\xa4\xe0' + 61 | b'\x35\xc1\x7e\x23\x29\xac\xa1\x2e' + 62 | b'\x21\xd5\x14\xb2\x54\x66\x93\x1c' + 63 | b'\x7d\x8f\x6a\x5a\xac\x84\xaa\x05' + 64 | b'\x1b\xa3\x0b\x39\x6a\x0a\xac\x97' + 65 | b'\x3d\x58\xe0\x91\x47\x3f\x59\x85', 66 | 'auth_tag': 0x4d5c2af327cd64a62cf35abd2ba6fab4, 67 | }, { 68 | 'master_key': 0xfeffe9928665731c6d6a8f9467308308, 69 | 'plaintext': b'\xd9\x31\x32\x25\xf8\x84\x06\xe5' + 70 | b'\xa5\x59\x09\xc5\xaf\xf5\x26\x9a' + 71 | b'\x86\xa7\xa9\x53\x15\x34\xf7\xda' + 72 | b'\x2e\x4c\x30\x3d\x8a\x31\x8a\x72' + 73 | b'\x1c\x3c\x0c\x95\x95\x68\x09\x53' + 74 | b'\x2f\xcf\x0e\x24\x49\xa6\xb5\x25' + 75 | b'\xb1\x6a\xed\xf5\xaa\x0d\xe6\x57' + 76 | b'\xba\x63\x7b\x39', 77 | 'auth_data': b'\xfe\xed\xfa\xce\xde\xad\xbe\xef' + 78 | b'\xfe\xed\xfa\xce\xde\xad\xbe\xef' + 79 | b'\xab\xad\xda\xd2', 80 | 'init_value': 0xcafebabefacedbaddecaf888, 81 | 'ciphertext': b'\x42\x83\x1e\xc2\x21\x77\x74\x24' + 82 | b'\x4b\x72\x21\xb7\x84\xd0\xd4\x9c' + 83 | b'\xe3\xaa\x21\x2f\x2c\x02\xa4\xe0' + 84 | b'\x35\xc1\x7e\x23\x29\xac\xa1\x2e' + 85 | b'\x21\xd5\x14\xb2\x54\x66\x93\x1c' + 86 | b'\x7d\x8f\x6a\x5a\xac\x84\xaa\x05' + 87 | b'\x1b\xa3\x0b\x39\x6a\x0a\xac\x97' + 88 | b'\x3d\x58\xe0\x91', 89 | 'auth_tag': 0x5bc94fbc3221a5db94fae95ae7121a47, 90 | }) 91 | 92 | 93 | if __name__ == '__main__': 94 | num_failures = 0 95 | 96 | for test_data in test_cases: 97 | test_gcm = AES_GCM(test_data['master_key']) 98 | encrypted, tag = test_gcm.encrypt( 99 | test_data['init_value'], 100 | test_data['plaintext'], 101 | test_data['auth_data'] 102 | ) 103 | 104 | states = [] 105 | tags = [] 106 | ivs = [] 107 | aads = [] 108 | 109 | # extra encryptions 110 | s = encrypted 111 | for i in range(1000): 112 | iv = getrandbits(96) 113 | a = long_to_bytes(getrandbits(1024)) 114 | s, t = test_gcm.encrypt(iv, s, a) 115 | states.append(s) 116 | tags.append(t) 117 | ivs.append(iv) 118 | aads.append(a) 119 | 120 | # extra decryptions 121 | for i in range(999, -1, -1): 122 | assert s == states[i] 123 | iv = ivs[i] 124 | t = tags[i] 125 | a = aads[i] 126 | s = test_gcm.decrypt(iv, s, t, a) 127 | encrypted = s 128 | 129 | decrypted = test_gcm.decrypt( 130 | test_data['init_value'], 131 | encrypted, 132 | tag, 133 | test_data['auth_data'] 134 | ) 135 | 136 | if encrypted != test_data['ciphertext'] or \ 137 | tag != test_data['auth_tag'] or \ 138 | decrypted != test_data['plaintext']: 139 | num_failures += 1 140 | print('This test case failed:') 141 | pprint(test_data) 142 | print() 143 | 144 | if num_failures == 0: 145 | print('All test cases passed!') 146 | else: 147 | print(num_failures, 'test cases failed in total.') 148 | -------------------------------------------------------------------------------- /test_gf_mul.sage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sage 2 | 3 | """ 4 | Copyright (C) 2013 Bo Zhu http://about.bozhu.me 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | """ 24 | 25 | BF. = GF(2)[] 26 | FF. = GF(2 ^ 128, modulus=X ^ 128 + X ^ 7 + X ^ 2 + X + 1) 27 | 28 | 29 | def int2ele(integer): 30 | res = 0 31 | for i in range(128): 32 | # rightmost bit is x127 33 | res += (integer & 1) * (A ^ (127 - i)) 34 | integer >>= 1 35 | return res 36 | 37 | 38 | def ele2int(element): 39 | integer = element.integer_representation() 40 | res = 0 41 | for i in range(128): 42 | res = (res << 1) + (integer & 1) 43 | integer >>= 1 44 | return res 45 | 46 | 47 | def gf_2_128_mul_correct(x1, x2): 48 | return ele2int(int2ele(x1) * int2ele(x2)) 49 | 50 | 51 | from aes_gcm import gf_2_128_mul as gf_2_128_mul_to_verify 52 | 53 | 54 | if __name__ == '__main__': 55 | from os import urandom 56 | from Crypto.Util.number import bytes_to_long 57 | 58 | for i in range(1000): 59 | x = bytes_to_long(urandom(16)) # 16 bytes 60 | h = bytes_to_long(urandom(16)) 61 | assert gf_2_128_mul_to_verify(x, h) == gf_2_128_mul_correct(x, h) 62 | 63 | print '1000 random test cases passed!' 64 | --------------------------------------------------------------------------------