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.
 
 
 
 
 
 

430 lines
15 KiB

  1. """
  2. An example implementation of STROBE.
  3. The key tree may be patented. Also, it may be easy to violate other
  4. patents with this code, so be careful.
  5. Copyright (c) Mike Hamburg, Cryptography Research, 2015-2016.
  6. I will need to contact legal to get a license for this; in the mean time
  7. it is for example purposes only.
  8. """
  9. from .Keccak import KeccakF
  10. from .ControlWord import *
  11. from collections import namedtuple
  12. import base64
  13. import threading
  14. import itertools
  15. class StrobeException(Exception):
  16. def __init__(self,*args,**kwargs):
  17. Exception.__init__(self,*args,**kwargs)
  18. class AuthenticationFailed(StrobeException):
  19. def __init__(self,*args,**kwargs):
  20. StrobeException.__init__(self,*args,**kwargs)
  21. class ProtocolError(StrobeException):
  22. def __init__(self,*args,**kwargs):
  23. StrobeException.__init__(self,*args,**kwargs)
  24. def zeros():
  25. while True: yield 0
  26. class Strobe(object):
  27. """
  28. STROBE protocol framework
  29. """
  30. version = "v0.7"
  31. PAD = 0x04
  32. CSHAKE_PAD = 0x80
  33. def __init__(self,proto,dir=None,F=None,rate=None,steg=0,copy_from=None,over_rate=None,doInit=True,verbose=False):
  34. if copy_from is not None:
  35. self.F = copy_from.F
  36. self.rate = copy_from.rate
  37. self.proto = copy_from.proto
  38. self.off = copy_from.off
  39. self.prev_mark = copy_from.prev_mark
  40. self.dir = copy_from.dir
  41. self.st = bytearray(copy_from.st)
  42. self.steg = copy_from.steg
  43. self.over_rate = copy_from.over_rate
  44. self.verbose = verbose
  45. else:
  46. if F is None: F = KeccakF()
  47. if rate is None: rate = F.nbytes - 32 - 2
  48. self.F = F
  49. self.rate = rate
  50. self.proto = proto
  51. self.off = self.prev_mark = 0
  52. self.dir = dir
  53. self.steg = steg
  54. self.over_rate = rate + 2
  55. self.verbose = verbose
  56. if doInit: self.init(proto,over_rate)
  57. else: self.st = bytearray(self.F.nbytes)
  58. def __str__(self):
  59. if self.dir is None: dir = "None"
  60. elif self.dir == DIR_CLIENT: dir = "DIR_CLIENT"
  61. elif self.dir == DIR_SERVER: dir = "DIR_SERVER"
  62. return "%s(%s,dir=%s,F=%s)" % (
  63. self.__class__.__name__,self.proto,dir,self.F
  64. )
  65. def copy(self):
  66. return Strobe(proto=self.proto,copy_from=self)
  67. def init(self,proto,over_rate=None):
  68. """
  69. The initialization routine sets up the state in a way that is
  70. unique to this Strobe protocol. Unlike SHA-3, the protocol
  71. and rate are distinguished up front in the first call to the
  72. F-function.
  73. """
  74. self.st = bytearray(self.F.nbytes)
  75. # Initialize according to cSHAKE. TODO: check that this is correct
  76. aString = "STROBE " + self.__class__.version
  77. cShakeD = bytearray([1,self.over_rate,1,len(aString)]) + aString + bytearray([1,0])
  78. self.st[0:len(cShakeD)] = cShakeD
  79. self.st = self.F(self.st)
  80. self.duplex(FLAG_A|FLAG_M,proto)
  81. def _run_f(self):
  82. """
  83. Pad out blocks and run the sponge's F-function
  84. """
  85. self.st[self.off] ^= self.prev_mark
  86. self.st[self.off+1] ^= self.PAD
  87. self.st[self.over_rate-1] ^= self.CSHAKE_PAD
  88. # if self.verbose:
  89. # print "**** IN ****"
  90. # print "".join(("%02x" % b for b in self.st))
  91. self.st = self.F(self.st)
  92. # if self.verbose:
  93. # print "**** OU ****"
  94. # print "".join(("%02x" % b for b in self.st))
  95. self.off = self.prev_mark = 0
  96. def _set_mode(self, mode):
  97. """
  98. Put a delimiter in the hash state.
  99. """
  100. self.st[self.off] ^= self.prev_mark
  101. self.off += 1
  102. self.prev_mark = self.off
  103. if self.off >= self.rate: self._run_f()
  104. # Adjust the mode for initiator vs responder
  105. if mode & FLAG_T:
  106. if self.dir is None:
  107. self.dir = mode & FLAG_I
  108. mode ^= self.dir
  109. self.st[self.off] ^= mode
  110. self.off += 1
  111. if self.off >= self.rate or (mode & (FLAG_C | FLAG_K)):
  112. self._run_f()
  113. def duplex(self,op,data=None,length=None,as_iter=False):
  114. """
  115. The main STROBE duplex operation.
  116. """
  117. # steg support: if would send/recv in the clear, send/recv encrypted instead.
  118. if op & FLAG_T: op |= self.steg
  119. self._set_mode(op)
  120. (I,T,C,A,K) = (bool(op & f) for f in [FLAG_I,FLAG_T,FLAG_C,FLAG_A,FLAG_K])
  121. if isinstance(data,str): data = bytearray(data)
  122. # compute flags
  123. yield_anything = (A and I) or (T and not I)
  124. read_anything = (T and I) or (A and not I)
  125. verify_mac = (I,T,A) == (True,True,False)
  126. if data is None or not read_anything:
  127. if length is None: data = ()
  128. else: data = zeros()
  129. if length is not None:
  130. data = itertools.islice(data,length)
  131. if self.verbose: print("Duplex mode=0x%02x:\n " % op, end=' ')
  132. out = self._duplex_iter((I,T,A,C,K),data)
  133. if yield_anything:
  134. # Return the iterator
  135. if as_iter: return out
  136. return bytearray(out)
  137. elif verify_mac:
  138. # Asked to verify a MAC
  139. res = 0
  140. for o in out: res |= o
  141. if res: raise AuthenticationFailed()
  142. return ()
  143. else:
  144. # The data is not used
  145. for o in out: pass
  146. return ()
  147. def _duplex_iter(self, op, data):
  148. """
  149. Duplexing sponge construction, iterator-version.
  150. """
  151. (I,T,A,C,K) = op
  152. res = 0
  153. if C: s2o = 0x00FF
  154. else: s2o = 0
  155. s2s = 0xFFFF
  156. if T and not I: s2s ^= s2o
  157. if K:
  158. # The DPA-resistant key tree is a CRI design to mitigate differential
  159. # power analysis at a protocol level.
  160. if self.off != 0:
  161. # Since we call self.mark(C or K) above, this is only possible through
  162. # misuse of "more"
  163. raise Exception("Bug: user called keytree with off != 0")
  164. keytreebits = 2
  165. assert keytreebits > 0 and 8 % keytreebits == 0 and self.PAD << keytreebits < 256
  166. mask = (1<<keytreebits)-1
  167. s2o >>= 8-keytreebits
  168. s2s >>= 8-keytreebits
  169. for byte in data:
  170. for bpos in range(0,8,keytreebits):
  171. byte ^= (self.st[0] & s2o) << bpos
  172. self.st[0] &= s2s
  173. self.st[0] ^= (byte >> bpos) & mask
  174. self.st[1] ^= self.PAD<<keytreebits
  175. self.st[self.over_rate-1] ^= self.CSHAKE_PAD
  176. self.st = self.F(self.st)
  177. yield byte
  178. else:
  179. # Not the keytree
  180. for byte in data:
  181. if self.verbose: print("%02x" % byte, end=' ')
  182. byte ^= self.st[self.off] & s2o
  183. self.st[self.off] &= s2s
  184. self.st[self.off] ^= byte
  185. self.off += 1
  186. if self.off >= self.rate: self._run_f()
  187. yield byte
  188. if self.verbose: print()
  189. def begin_steg(self):
  190. """
  191. Begin steganography.
  192. """
  193. self.steg = FLAG_C
  194. @staticmethod
  195. def i2o_le(number,length):
  196. """
  197. Encode a non-negative integer to bytes, little-endian, of the given length.
  198. """
  199. if number < 0 or number >= 1 << (8*length):
  200. raise ProtocolError("Cannot encode number %d in %d bytes"
  201. % (number, length))
  202. return [ 0xFF & number >> (8*i)
  203. for i in range(length) ]
  204. @staticmethod
  205. def o2i_le(enc_number):
  206. """
  207. Decode a non-negative integer from bytes, little-endian.
  208. """
  209. return sum(( int(x)<<(8*i) for (i,x) in enumerate(enc_number) ))
  210. def outbound(self,cw,data=(),length=None,**kwargs):
  211. """
  212. Send or inject data with the given control-word.
  213. """
  214. if length is not None and data is not ():
  215. raise ProtocolError("Explicit length set with data")
  216. if cw.length_bytes == 0:
  217. encoded_length = ()
  218. if length is None: length = cw.length
  219. else:
  220. # determine the length
  221. if length is None: length = cw.length
  222. if length is None:
  223. try: length = len(data)
  224. except TypeError:
  225. data = bytearray(data)
  226. length = len(data)
  227. # encode it
  228. encoded_length = self.i2o_le(length,cw.length_bytes)
  229. cw_bytes = itertools.chain(cw.bytes, encoded_length)
  230. s1 = self.duplex(cw.cmode, cw_bytes)
  231. s2 = self.duplex(cw.dmode, data, length=length, **kwargs)
  232. return bytearray(s1) + bytearray(s2)
  233. def send(self,cw,*args,**kwargs):
  234. """
  235. Same as .outbound, but assert that mode includes actually sending
  236. data to the wire.
  237. (It is possible that no data will be sent if the length is 0.)
  238. """
  239. if not (cw.dmode | cw.cmode) & FLAG_T:
  240. raise ProtocolError(
  241. "Used .send on non-T control word; use .inject or .outbound instead"
  242. )
  243. return self.outbound(cw,*args,**kwargs)
  244. def inject(self,cw,*args,**kwargs):
  245. """
  246. Same as .outbound, but assert that the mode does not include
  247. sending data to the wire.
  248. """
  249. if (cw.dmode | cw.cmode) & FLAG_T:
  250. raise ProtocolError(
  251. "Used .inject on T control word; use .send or .outbound instead"
  252. )
  253. self.outbound(cw,*args,**kwargs)
  254. def recv_cw(self,data,possible_cws):
  255. """
  256. Receive data from a list of possible keywords.
  257. Return the keyword and length, or throw an error.
  258. """
  259. # create stream data
  260. cm = FLAG_I|FLAG_A|FLAG_T|FLAG_M
  261. stream = self.duplex(cm,data,as_iter=True)
  262. poss = list(possible_cws)
  263. i = 0
  264. dr = []
  265. def can_begin_with(cw,bs):
  266. if len(bs) > len(cw.bytes) + cw.length_bytes: return False
  267. lencmp = min(len(bs),len(cw.bytes))
  268. return bytearray(cw.bytes[0:lencmp]) == bytearray(bs[0:lencmp])
  269. while len(poss) > 1:
  270. b = next(stream)
  271. dr.append(b)
  272. poss = [cw for cw in poss if can_begin_with(cw,dr)]
  273. if len(poss) == 0:
  274. # oops, eliminated all possibilities
  275. raise ProtocolError("None of the expected CWs received")
  276. # read extra bytes to finish the control word
  277. cw = poss[0]
  278. extra = len(cw.bytes) + cw.length_bytes - len(dr)
  279. dr.extend(itertools.islice(stream,extra))
  280. if cw.length_bytes > 0:
  281. actual_length = self.o2i_le(dr[-cw.length_bytes:])
  282. # Sanity-check length
  283. if cw.length is not None and cw.length != actual_length:
  284. raise ProtocolError("Received length %d doesn't matched expected length %d"
  285. % (actual_length, cw.length))
  286. elif cw.min_length is not None and cw.min_length > actual_length:
  287. raise ProtocolError("Received length %d less than expected min-length %d"
  288. % (actual_length, cw.min_length))
  289. elif cw.max_length is not None and cw.max_length < actual_length:
  290. raise ProtocolError("Received length %d greater than expected max-length %d"
  291. % (actual_length, cw.max_length))
  292. return cw, actual_length
  293. else:
  294. return cw, cw.length
  295. def inbound_data(self,cw,data,**kwargs):
  296. """
  297. Take data from a connection.
  298. """
  299. mode = cw.dmode
  300. if mode & (FLAG_A | FLAG_T): mode |= FLAG_I
  301. return self.duplex(mode,data,**kwargs)
  302. def inbound(self,cws,data=(),length=None,return_cw=False):
  303. """
  304. Dual of outbound, except that you can pass multiple control words.
  305. """
  306. if isinstance(cws,ControlWord): cws = [cws]
  307. data = iter(data)
  308. if any((cw1.cmode & FLAG_T for cw1 in cws)):
  309. cw,length = self.recv_cw(data,cws)
  310. else:
  311. assert len(cws) == 1
  312. cw = cws[0]
  313. bytes = cw.bytes
  314. if length is None: length = cw.length
  315. if cw.length_bytes != 0:
  316. assert length is not None
  317. bytes = bytes + bytearray(self.i2o_le(length,cw.length_bytes))
  318. self.duplex(cw.cmode, bytes)
  319. # NB: This precludes use of a "PING" tag, where one party sends the tag
  320. # and the other party sends the data. So if you're going to do that,
  321. # you'll need to call recv_cw and outbound separately.
  322. idata = self.inbound_data(cw,data,length=length)
  323. if return_cw: return cw,idata
  324. else: return idata
  325. def recv(self,cws,*args,**kwargs):
  326. """
  327. Same as .inbound, but assert that mode includes actually receiving
  328. data to the wire.
  329. """
  330. if isinstance(cws,ControlWord): cws = [cws]
  331. if not all(((cw.dmode | cw.cmode) & FLAG_T for cw in cws)):
  332. raise ProtocolError(
  333. "Used .recv on non-T control word; use .extract or .inbound instead"
  334. )
  335. return self.inbound(cws,*args,**kwargs)
  336. def extract(self,cw,*args,**kwargs):
  337. """
  338. Same as .inbound, but assert that the mode does not include
  339. receiving data from the wire.
  340. """
  341. if (cw.dmode | cw.cmode) & FLAG_T:
  342. raise ProtocolError(
  343. "Used .extract on T control word; use .recv or .inbound instead"
  344. )
  345. return self.inbound(cw,*args,**kwargs)
  346. def send_siv(self,msg):
  347. post = self.copy()
  348. msg1 = self.outbound(SIV_PT_INNER,msg)
  349. mac1 = self.outbound(SIV_MAC_INNER)
  350. mac2 = post.outbound(SIV_MAC_OUTER,mac1)
  351. msg2 = post.outbound(APP_CIPHERTEXT,msg1)
  352. return itertools.chain(mac2, msg2)
  353. def recv_siv(self,msg):
  354. post = self.copy()
  355. msg = iter(msg)
  356. mac1 = post.inbound(SIV_MAC_OUTER,msg)
  357. msg1 = post.inbound(APP_CIPHERTEXT,msg)
  358. msg0 = self.inbound(SIV_PT_INNER,msg1)
  359. self.inbound(SIV_MAC_INNER,mac1)
  360. return msg0