/ 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]