Implement a secure ICS protocol targeting LoRa Node151 microcontroller for controlling irrigation.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

332 lines
7.9 KiB

  1. import asyncio
  2. import functools
  3. import os
  4. import unittest
  5. from Strobe.Strobe import Strobe, KeccakF
  6. from Strobe.Strobe import AuthenticationFailed
  7. import lora_comms
  8. from lora_comms import make_pktbuf
  9. domain = b'com.funkthat.lora.irrigation.shared.v0.0.1'
  10. # Response to command will be the CMD and any arguments if needed.
  11. # The command is encoded as an unsigned byte
  12. CMD_TERMINATE = 1 # no args: terminate the sesssion, reply confirms
  13. # The follow commands are queue up, but will be acknoledged when queued
  14. CMD_WAITFOR = 2 # arg: (length): waits for length seconds
  15. CMD_RUNFOR = 3 # arg: (chan, length): turns on chan for length seconds
  16. class LORANode(object):
  17. '''Implement a LORANode initiator.'''
  18. def __init__(self, syncdatagram):
  19. self.sd = syncdatagram
  20. self.st = Strobe(domain, F=KeccakF(800))
  21. async def start(self):
  22. msg = self.st.send_enc(os.urandom(16) + b'reqreset') + \
  23. self.st.send_mac(8)
  24. resp = await self.sd.sendtillrecv(msg, 1)
  25. self.st.recv_enc(resp[:16])
  26. self.st.recv_mac(resp[16:])
  27. resp = await self.sd.sendtillrecv(
  28. self.st.send_enc(b'confirm') + self.st.send_mac(8), 1)
  29. pkt = self.st.recv_enc(resp[:9])
  30. self.st.recv_mac(resp[9:])
  31. if pkt != b'confirmed':
  32. raise RuntimeError
  33. @staticmethod
  34. def _encodeargs(*args):
  35. r = []
  36. for i in args:
  37. r.append(i.to_bytes(4, byteorder='little'))
  38. return b''.join(r)
  39. async def _sendcmd(self, cmd, *args):
  40. cmdbyte = cmd.to_bytes(1, byteorder='little')
  41. pkt = await self.sd.sendtillrecv(
  42. self.st.send_enc(cmdbyte +
  43. self._encodeargs(*args)) + self.st.send_mac(8), 1)
  44. resp = self.st.recv_enc(pkt[:-8])
  45. self.st.recv_mac(pkt[-8:])
  46. if resp[0:1] != cmdbyte:
  47. raise RuntimeError('response does not match, got: %s, expected: %s' % (repr(resp[0:1]), repr(cmdbyte)))
  48. async def waitfor(self, length):
  49. return await self._sendcmd(CMD_WAITFOR, length)
  50. async def runfor(self, chan, length):
  51. return await self._sendcmd(CMD_RUNFOR, chan, length)
  52. async def terminate(self):
  53. return await self._sendcmd(CMD_TERMINATE)
  54. class SyncDatagram(object):
  55. '''Base interface for a more simple synchronous interface.'''
  56. def __init__(self): #pragma: no cover
  57. pass
  58. async def recv(self, timeout=None): #pragma: no cover
  59. '''Receive a datagram. If timeout is not None, wait that many
  60. seconds, and if nothing is received in that time, raise an TimeoutError
  61. exception.'''
  62. raise NotImplementedError
  63. async def send(self, data): #pragma: no cover
  64. '''Send a datagram.'''
  65. raise NotImplementedError
  66. async def sendtillrecv(self, data, freq):
  67. '''Send the datagram in data, every freq seconds until a datagram
  68. is received. If timeout seconds happen w/o receiving a datagram,
  69. then raise an TimeoutError exception.'''
  70. while True:
  71. await self.send(data)
  72. try:
  73. return await self.recv(freq)
  74. except TimeoutError:
  75. pass
  76. class MockSyncDatagram(SyncDatagram):
  77. '''A testing version of SyncDatagram. Define a method runner which
  78. implements part of the sequence. In the function, await on either
  79. self.get, to wait for the other side to send something, or await
  80. self.put w/ data to send.'''
  81. def __init__(self):
  82. self.sendq = asyncio.Queue()
  83. self.recvq = asyncio.Queue()
  84. self.task = None
  85. self.task = asyncio.create_task(self.runner())
  86. self.get = self.sendq.get
  87. self.put = self.recvq.put
  88. async def drain(self):
  89. '''Wait for the runner thread to finish up.'''
  90. return await self.task
  91. async def runner(self): #pragma: no cover
  92. raise NotImplementedError
  93. async def recv(self, timeout=None):
  94. return await self.recvq.get()
  95. async def send(self, data):
  96. return await self.sendq.put(data)
  97. def __del__(self): #pragma: no cover
  98. if self.task is not None and not self.task.done():
  99. self.task.cancel()
  100. class TestSyncData(unittest.IsolatedAsyncioTestCase):
  101. async def test_syncsendtillrecv(self):
  102. class MySync(SyncDatagram):
  103. def __init__(self):
  104. self.sendq = []
  105. self.resp = [ TimeoutError(), b'a' ]
  106. async def recv(self, timeout=None):
  107. assert timeout == 1
  108. r = self.resp.pop(0)
  109. if isinstance(r, Exception):
  110. raise r
  111. return r
  112. async def send(self, data):
  113. self.sendq.append(data)
  114. ms = MySync()
  115. r = await ms.sendtillrecv(b'foo', 1)
  116. self.assertEqual(r, b'a')
  117. self.assertEqual(ms.sendq, [ b'foo', b'foo' ])
  118. def timeout(timeout):
  119. def timeout_wrapper(fun):
  120. @functools.wraps(fun)
  121. async def wrapper(*args, **kwargs):
  122. return await asyncio.wait_for(fun(*args, **kwargs),
  123. timeout)
  124. return wrapper
  125. return timeout_wrapper
  126. class TestLORANode(unittest.IsolatedAsyncioTestCase):
  127. @timeout(2)
  128. async def test_lora(self):
  129. class TestSD(MockSyncDatagram):
  130. async def runner(self):
  131. l = Strobe(domain, F=KeccakF(800))
  132. # start handshake
  133. r = await self.get()
  134. pkt = l.recv_enc(r[:-8])
  135. l.recv_mac(r[-8:])
  136. assert pkt.endswith(b'reqreset')
  137. await self.put(l.send_enc(os.urandom(16)) +
  138. l.send_mac(8))
  139. r = await self.get()
  140. c = l.recv_enc(r[:-8])
  141. l.recv_mac(r[-8:])
  142. assert c == b'confirm'
  143. await self.put(l.send_enc(b'confirmed') +
  144. l.send_mac(8))
  145. r = await self.get()
  146. cmd = l.recv_enc(r[:-8])
  147. l.recv_mac(r[-8:])
  148. assert cmd[0] == CMD_WAITFOR
  149. assert int.from_bytes(cmd[1:], byteorder='little') == 30
  150. await self.put(l.send_enc(cmd[0:1]) +
  151. l.send_mac(8))
  152. r = await self.get()
  153. cmd = l.recv_enc(r[:-8])
  154. l.recv_mac(r[-8:])
  155. assert cmd[0] == CMD_RUNFOR
  156. assert int.from_bytes(cmd[1:5], byteorder='little') == 1
  157. assert int.from_bytes(cmd[5:], byteorder='little') == 50
  158. await self.put(l.send_enc(cmd[0:1]) +
  159. l.send_mac(8))
  160. r = await self.get()
  161. cmd = l.recv_enc(r[:-8])
  162. l.recv_mac(r[-8:])
  163. assert cmd[0] == CMD_TERMINATE
  164. await self.put(l.send_enc(cmd[0:1]) +
  165. l.send_mac(8))
  166. tsd = TestSD()
  167. l = LORANode(tsd)
  168. await l.start()
  169. await l.waitfor(30)
  170. await l.runfor(1, 50)
  171. await l.terminate()
  172. await tsd.drain()
  173. # Make sure all messages have been processed
  174. self.assertTrue(tsd.sendq.empty())
  175. self.assertTrue(tsd.recvq.empty())
  176. @timeout(2)
  177. async def test_ccode(self):
  178. _self = self
  179. from ctypes import pointer, sizeof, c_uint8
  180. # seed the RNG
  181. prngseed = b'abc123'
  182. lora_comms.strobe_seed_prng((c_uint8 *
  183. len(prngseed))(*prngseed), len(prngseed))
  184. # Create the state for testing
  185. commstate = lora_comms.CommsState()
  186. # These are the expected messages and their arguments
  187. exptmsgs = [
  188. (CMD_WAITFOR, [ 30 ]),
  189. (CMD_RUNFOR, [ 1, 50 ]),
  190. (CMD_TERMINATE, [ ]),
  191. ]
  192. def procmsg(msg, outbuf):
  193. msgbuf = msg._from()
  194. #print('procmsg:', repr(msg), repr(msgbuf), repr(outbuf))
  195. cmd = msgbuf[0]
  196. args = [ int.from_bytes(msgbuf[x:x + 4],
  197. byteorder='little') for x in range(1, len(msgbuf),
  198. 4) ]
  199. if exptmsgs[0] == (cmd, args):
  200. exptmsgs.pop(0)
  201. outbuf[0].pkt[0] = cmd
  202. outbuf[0].pktlen = 1
  203. else: #pragma: no cover
  204. raise RuntimeError('cmd not found')
  205. # wrap the callback function
  206. cb = lora_comms.process_msgfunc_t(procmsg)
  207. class CCodeSD(MockSyncDatagram):
  208. async def runner(self):
  209. for expectlen in [ 24, 17, 9, 9, 9 ]:
  210. # get message
  211. gb = await self.get()
  212. r = make_pktbuf(gb)
  213. outbytes = bytearray(64)
  214. outbuf = make_pktbuf(outbytes)
  215. # process the test message
  216. lora_comms.comms_process(commstate, r,
  217. outbuf)
  218. # make sure the reply matches length
  219. _self.assertEqual(expectlen,
  220. outbuf.pktlen)
  221. # pass the reply back
  222. await self.put(outbytes[:outbuf.pktlen])
  223. # Initialize everything
  224. lora_comms.comms_init(commstate, cb)
  225. # Create test fixture
  226. tsd = CCodeSD()
  227. l = LORANode(tsd)
  228. # Send various messages
  229. await l.start()
  230. await l.waitfor(30)
  231. await l.runfor(1, 50)
  232. await l.terminate()
  233. await tsd.drain()
  234. # Make sure all messages have been processed
  235. self.assertTrue(tsd.sendq.empty())
  236. self.assertTrue(tsd.recvq.empty())
  237. # Make sure all the expected messages have been
  238. # processed.
  239. self.assertFalse(exptmsgs)