/ adafruit_tinylora / adafruit_tinylora_encryption.py
adafruit_tinylora_encryption.py
1 # AES-Python, Copyright (C) 2012 Bo Zhu http://about.bozhu.me 2 # 3 # Permission is hereby granted, free of charge, to any person obtaining a 4 # copy of this software and associated documentation files (the "Software"), 5 # to deal in the Software without restriction, including without limitation 6 # the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 # and/or sell copies of the Software, and to permit persons to whom the 8 # Software is furnished to do so, subject to the following conditions: 9 # 10 # The above copyright notice and this permission notice shall be included in 11 # all copies or substantial portions of the Software. 12 # 13 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 # DEALINGS IN THE SOFTWARE. 20 # 21 # Modified by Brent Rubell for Adafruit Industries 22 """ 23 `adafruit_tinylora_encryption.py` 24 ====================================================== 25 Required tinyLoRA Encryption Methods for AES and 26 Message Integrity checks. 27 * Author(s): adafruit 28 """ 29 # from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c 30 def xtime(col): 31 """xtime impl. for _mix_single_column() 32 """ 33 return (((col << 1) ^ 0x1B) & 0xFF) if (col & 0x80) else (col << 1) 34 35 36 # AES S-box 37 S_BOX = ( 38 b"c|w{\xf2ko\xc50\x01g+\xfe\xd7\xabv", 39 b"\xca\x82\xc9}\xfaYG\xf0\xad\xd4\xa2\xaf\x9c\xa4r\xc0", 40 b"\xb7\xfd\x93&6?\xf7\xcc4\xa5\xe5\xf1q\xd81\x15", 41 b"\x04\xc7#\xc3\x18\x96\x05\x9a\x07\x12\x80\xe2\xeb'\xb2u", 42 b"\t\x83,\x1a\x1bnZ\xa0R;\xd6\xb3)\xe3/\x84", 43 b"S\xd1\x00\xed \xfc\xb1[j\xcb\xbe9JLX\xcf", 44 b"\xd0\xef\xaa\xfbCM3\x85E\xf9\x02\x7fP<\x9f\xa8", 45 b"Q\xa3@\x8f\x92\x9d8\xf5\xbc\xb6\xda!\x10\xff\xf3\xd2", 46 b"\xcd\x0c\x13\xec_\x97D\x17\xc4\xa7~=d]\x19s", 47 b'`\x81O\xdc"*\x90\x88F\xee\xb8\x14\xde^\x0b\xdb', 48 b"\xe02:\nI\x06$\\\xc2\xd3\xacb\x91\x95\xe4y", 49 b"\xe7\xc87m\x8d\xd5N\xa9lV\xf4\xeaez\xae\x08", 50 b"\xbax%.\x1c\xa6\xb4\xc6\xe8\xddt\x1fK\xbd\x8b\x8a", 51 b"p>\xb5fH\x03\xf6\x0ea5W\xb9\x86\xc1\x1d\x9e", 52 b"\xe1\xf8\x98\x11i\xd9\x8e\x94\x9b\x1e\x87\xe9\xceU(\xdf", 53 b"\x8c\xa1\x89\r\xbf\xe6BhA\x99-\x0f\xb0T\xbb\x16", 54 ) 55 56 57 class AES: 58 """TinyLoRA AES Implementation 59 Functions in this implementation are from and/or derived from AES-Python 60 (https://github.com/bozhu/AES-Python) and TinyLoRa () 61 """ 62 63 def __init__(self, device_address, app_key, network_key, frame_counter): 64 self._app_key = app_key 65 self._device_address = device_address 66 self._network_key = network_key 67 self.frame_counter = frame_counter 68 69 def encrypt(self, aes_data): 70 """Performs AES Encryption routine with data. 71 :param bytearray data: Data to-be encrypted. 72 """ 73 self.encrypt_payload(aes_data) 74 return aes_data 75 76 def encrypt_payload(self, data): 77 """Encrypts data payload. 78 :param bytearray data: Data to-be-encrypted. 79 """ 80 block_a = bytearray(16) 81 # calculate required number of blocks 82 num_blocks = len(data) // 16 83 incomplete_block_size = len(data) % 16 84 if incomplete_block_size != 0: 85 num_blocks += 1 86 # k = data ptr 87 k = 0 88 i = 1 89 while i <= num_blocks: 90 block_a[0] = 0x01 91 block_a[1] = 0x00 92 block_a[2] = 0x00 93 block_a[3] = 0x00 94 block_a[4] = 0x00 95 block_a[5] = 0x00 96 # block from device_address, MSB first 97 block_a[6] = self._device_address[3] 98 block_a[7] = self._device_address[2] 99 block_a[8] = self._device_address[1] 100 block_a[9] = self._device_address[0] 101 # block from frame counter 102 block_a[10] = self.frame_counter & 0x00FF 103 block_a[11] = (self.frame_counter >> 8) & 0x00FF 104 block_a[12] = 0x00 105 block_a[13] = 0x00 106 block_a[14] = 0x00 107 block_a[15] = i 108 # calculate S 109 self._aes_encrypt(block_a, self._app_key) 110 # check for last block 111 if i != num_blocks: 112 for j in range(16): 113 data[k] ^= block_a[j] 114 k += 1 115 else: 116 if incomplete_block_size == 0: 117 incomplete_block_size = 16 118 for j in range(incomplete_block_size): 119 data[k] ^= block_a[j] 120 k += 1 121 i += 1 122 123 def _aes_encrypt(self, data, key): 124 """Performs 9 rounds of AES encryption on data per TinyLoRa spec. 125 NOTE: This is not an accurate aes_encrypt impl., tinylora performs 126 an additional key calculation after 9 rounds. 127 :param bytearray data: Data array. 128 :param bytearray key: Round Key Array. 129 """ 130 state = [ 131 ["0", "0", "0", "0"], 132 ["0", "0", "0", "0"], 133 ["0", "0", "0", "0"], 134 ["0", "0", "0", "0"], 135 ] 136 # Copy Data to State Array for manipulation 137 for col in range(4): 138 for row in range(4): 139 state[col][row] = data[row + (col << 2)] 140 # copy key to round_key 141 round_key = bytearray(key) 142 self._aes_add_round_key(round_key, state) 143 # encrypt data 9x 144 for round_nums in range(1, 10): 145 self._round_encrypt(state, round_key, round_nums) 146 self._aes_sub_bytes(state) 147 self._aes_shift_rows(state) 148 self._aes_calculate_key(10, round_key) 149 self._aes_add_round_key(round_key, state) 150 for row in range(4): 151 for col in range(4): 152 data[col + (row << 2)] = state[row][col] 153 154 def _round_encrypt(self, state, key, num_round): 155 """Performs one round of AES. 156 :param bytearray state: State array. 157 :param bytearray key: Round key array. 158 :param int round: AES round number. 159 """ 160 self._aes_sub_bytes(state) 161 self._aes_shift_rows(state) 162 self._aes_mix_columns(state) 163 self._aes_calculate_key(num_round, key) 164 self._aes_add_round_key(key, state) 165 166 def _aes_calculate_key(self, num_round, round_key): 167 """Performs round key calculation per TinyLoRa's spec. 168 :param int num_round: Round number 169 :param bytearray round_key: Round key array. 170 """ 171 tmp_arr = bytearray(4) 172 round_const = 0x01 173 # add round_const calculation 174 while num_round != 1: 175 b = round_const & 0x80 176 round_const <<= 1 177 round_const &= 0xFF 178 if b == 0x80: 179 round_const ^= 0x1B 180 num_round -= 1 181 # Calculate first temp 182 tmp_arr[0] = self._aes_sub_byte(round_key[12 + 1]) 183 tmp_arr[1] = self._aes_sub_byte(round_key[12 + 2]) 184 tmp_arr[2] = self._aes_sub_byte(round_key[12 + 3]) 185 tmp_arr[3] = self._aes_sub_byte(round_key[12 + 0]) 186 # XOR tmp_arr[0] wth round_const first 187 tmp_arr[0] ^= round_const 188 # then calculate new round key 189 for i in range(4): 190 for j in range(4): 191 round_key[j + (i << 2)] ^= tmp_arr[j] 192 tmp_arr[j] = round_key[j + (i << 2)] 193 194 @staticmethod 195 def _aes_add_round_key(round_key, state): 196 """AES AddRoundKey Step: Round_Key combined with the state. 197 :param bytearray round_key: Subkey for each round. 198 :param bytearray state: State array. 199 """ 200 for col in range(4): 201 for row in range(4): 202 state[col][row] ^= round_key[row + (col << 2)] 203 204 @staticmethod 205 def _aes_sub_byte(sub_byte): 206 """Sub-Byte Step: Used for returning specific byte 207 from the AES S_BOX. 208 :param byte sub_byte: byte to be replaced with S_BOX byte. 209 """ 210 row = (sub_byte >> 4) & 0x0F 211 col = sub_byte & 0x0F 212 return S_BOX[row][col] 213 214 def _aes_sub_bytes(self, state): 215 """AES SubBytes Step: Replace state arr. bytes w/sub-byte from S_BOX 216 :param bytearray s: State array. 217 """ 218 for col in range(4): 219 for row in range(4): 220 state[row][col] = self._aes_sub_byte(state[row][col]) 221 222 @staticmethod 223 def _mix_single_column(col): 224 """Mixes individual columns with state array 225 :param bytearray col: Column from statearray 226 """ 227 temp = col[0] ^ col[1] ^ col[2] ^ col[3] 228 col_zero = col[0] 229 col[0] ^= temp ^ xtime(col[0] ^ col[1]) 230 col[1] ^= temp ^ xtime(col[1] ^ col[2]) 231 col[2] ^= temp ^ xtime(col[2] ^ col[3]) 232 col[3] ^= temp ^ xtime(col[3] ^ col_zero) 233 234 def _aes_mix_columns(self, state): 235 """AES MixColumns Step: Multiplies each column of the state array with xtime. 236 :param bytearray state: State array. 237 """ 238 for i in range(4): 239 self._mix_single_column(state[i]) 240 241 @staticmethod 242 def _aes_shift_rows(arr): 243 """AES ShiftRows Step: State array's bytes shifted to the left. 244 :param bytearray state: State array. 245 """ 246 arr[0][1], arr[1][1], arr[2][1], arr[3][1] = ( 247 arr[1][1], 248 arr[2][1], 249 arr[3][1], 250 arr[0][1], 251 ) 252 arr[0][2], arr[1][2], arr[2][2], arr[3][2] = ( 253 arr[2][2], 254 arr[3][2], 255 arr[0][2], 256 arr[1][2], 257 ) 258 arr[0][3], arr[1][3], arr[2][3], arr[3][3] = ( 259 arr[3][3], 260 arr[0][3], 261 arr[1][3], 262 arr[2][3], 263 ) 264 265 def calculate_mic(self, lora_packet, lora_packet_length, mic): 266 """Calculates the validity of data messages, generates a message integrity check bytearray. 267 """ 268 block_b = bytearray(16) 269 key_k1 = bytearray(16) 270 key_k2 = bytearray(16) 271 old_data = bytearray(16) 272 new_data = bytearray(16) 273 block_b[0] = 0x49 274 block_b[6] = self._device_address[3] 275 block_b[7] = self._device_address[2] 276 block_b[8] = self._device_address[1] 277 block_b[9] = self._device_address[0] 278 block_b[10] = self.frame_counter & 0x00FF 279 block_b[11] = (self.frame_counter >> 8) & 0x00FF 280 block_b[15] = lora_packet_length 281 # calculate num. of blocks and blocksz of last block 282 num_blocks = lora_packet_length // 16 283 incomplete_block_size = lora_packet_length % 16 284 if incomplete_block_size != 0: 285 num_blocks += 1 286 # generate keys 287 self._mic_generate_keys(key_k1, key_k2) 288 # aes encryption on block_b 289 self._aes_encrypt(block_b, self._network_key) 290 # copy block_b to old_data 291 for i in range(16): 292 old_data[i] = block_b[i] 293 block_counter = 1 294 # calculate until n-1 packet blocks 295 k = 0 # ptr 296 while block_counter < num_blocks: 297 # copy data into array 298 for i in range(16): 299 new_data[i] = lora_packet[k] 300 k += 1 301 # XOR new_data with old_data 302 self._xor_data(new_data, old_data) 303 # aes encrypt new_data 304 self._aes_encrypt(new_data, self._network_key) 305 # copy new_data to old_data 306 for i in range(16): 307 old_data[i] = new_data[i] 308 # increase block_counter 309 block_counter += 1 310 # perform calculation on last block 311 if incomplete_block_size == 0: 312 for i in range(16): 313 new_data[i] = lora_packet[k] 314 k += 1 315 # xor with key 1 316 self._xor_data(new_data, key_k1) 317 # xor with old data 318 self._xor_data(new_data, old_data) 319 # aes routine 320 self._aes_encrypt(new_data, self._network_key) 321 else: 322 # copy the remaining data 323 for i in range(16): 324 if i < incomplete_block_size: 325 new_data[i] = lora_packet[k] 326 k += 1 327 if i == incomplete_block_size: 328 new_data[i] = 0x80 329 if i > incomplete_block_size: 330 new_data[i] = 0x00 331 # perform xor with key 2 332 self._xor_data(new_data, key_k2) 333 # perform xor with old data 334 self._xor_data(new_data, old_data) 335 self._aes_encrypt(new_data, self._network_key) 336 # load MIC[] with data 337 mic[0] = new_data[0] 338 mic[1] = new_data[1] 339 mic[2] = new_data[2] 340 mic[3] = new_data[3] 341 # return message integrity check array to calling method 342 return mic 343 344 def _mic_generate_keys(self, key_1, key_2): 345 # encrypt the 0's in k1 with network key 346 self._aes_encrypt(key_1, self._network_key) 347 # perform gen_key on key_1 348 # check if key_1's msb is 1 349 msb_key = (key_1[0] & 0x80) == 0x80 350 # shift k1 left 1b 351 self._shift_left(key_1) 352 # check if msb is 1 353 if msb_key: 354 key_1[15] ^= 0x87 355 # perform gen_key on key_2 356 # copy key_1 to key_2 357 key_2[0:16] = key_1[0:16] 358 msb_key = (key_2[0] & 0x80) == 0x80 359 self._shift_left(key_2) 360 # check if msb is 1 361 if msb_key: 362 key_2[15] ^= 0x87 363 364 @staticmethod 365 def _shift_left(data): 366 """ Shifts data bytearray left by 1 367 """ 368 for i in range(16): 369 if i < 15: 370 if (data[i + 1] & 0x80) == 0x80: 371 overflow = 1 372 else: 373 overflow = 0 374 else: 375 overflow = 0 376 # shift 1b left 377 data[i] = ((data[i] << 1) + overflow) & 0xFF 378 379 @staticmethod 380 def _xor_data(new_data, old_data): 381 """ XOR two data arrays 382 :param bytearray new_data: Calculated data. 383 :param bytearray old_data: data to be xor'd. 384 """ 385 for i in range(16): 386 new_data[i] ^= old_data[i]