OTR plugin for Gajim 1.0+
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.

context.py 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. # Copyright 2011-2012 Kjell Braden <afflux@pentabarf.de>
  2. #
  3. # This file is part of the python-potr library.
  4. #
  5. # python-potr is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU Lesser General Public License as published by
  7. # the Free Software Foundation; either version 3 of the License, or
  8. # any later version.
  9. #
  10. # python-potr is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU Lesser General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Lesser General Public License
  16. # along with this library. If not, see <http://www.gnu.org/licenses/>.
  17. # some python3 compatibilty
  18. from __future__ import unicode_literals
  19. try:
  20. type(basestring)
  21. except NameError:
  22. # all strings are unicode in python3k
  23. basestring = str
  24. unicode = str
  25. # callable is not available in python 3.0 and 3.1
  26. try:
  27. type(callable)
  28. except NameError:
  29. from collections import Callable
  30. def callable(x):
  31. return isinstance(x, Callable)
  32. import base64
  33. import logging
  34. import struct
  35. logger = logging.getLogger(__name__)
  36. from potr import crypt
  37. from potr import proto
  38. from potr import compatcrypto
  39. from time import time
  40. EXC_UNREADABLE_MESSAGE = 1
  41. EXC_FINISHED = 2
  42. HEARTBEAT_INTERVAL = 60
  43. STATE_PLAINTEXT = 0
  44. STATE_ENCRYPTED = 1
  45. STATE_FINISHED = 2
  46. FRAGMENT_SEND_ALL = 0
  47. FRAGMENT_SEND_ALL_BUT_FIRST = 1
  48. FRAGMENT_SEND_ALL_BUT_LAST = 2
  49. OFFER_NOTSENT = 0
  50. OFFER_SENT = 1
  51. OFFER_REJECTED = 2
  52. OFFER_ACCEPTED = 3
  53. class Context(object):
  54. def __init__(self, account, peername):
  55. self.user = account
  56. self.peer = peername
  57. self.policy = {}
  58. self.crypto = crypt.CryptEngine(self)
  59. self.tagOffer = OFFER_NOTSENT
  60. self.mayRetransmit = 0
  61. self.lastSend = 0
  62. self.lastMessage = None
  63. self.state = STATE_PLAINTEXT
  64. self.trustName = self.peer
  65. self.fragmentInfo = None
  66. self.fragment = None
  67. self.discardFragment()
  68. def getPolicy(self, key):
  69. raise NotImplementedError
  70. def inject(self, msg, appdata=None):
  71. raise NotImplementedError
  72. def policyOtrEnabled(self):
  73. return self.getPolicy('ALLOW_V2') or self.getPolicy('ALLOW_V1')
  74. def discardFragment(self):
  75. self.fragmentInfo = (0, 0)
  76. self.fragment = []
  77. def fragmentAccumulate(self, message):
  78. '''Accumulate a fragmented message. Returns None if the fragment is
  79. to be ignored, returns a string if the message is ready for further
  80. processing'''
  81. params = message.split(b',')
  82. if len(params) < 5 or not params[1].isdigit() or not params[2].isdigit():
  83. logger.warning('invalid formed fragmented message: %r', params)
  84. self.discardFragment()
  85. return message
  86. K, N = self.fragmentInfo
  87. try:
  88. k = int(params[1])
  89. n = int(params[2])
  90. except ValueError:
  91. logger.warning('invalid formed fragmented message: %r', params)
  92. self.discardFragment()
  93. return message
  94. fragData = params[3]
  95. logger.debug(params)
  96. if n >= k == 1:
  97. # first fragment
  98. self.discardFragment()
  99. self.fragmentInfo = (k, n)
  100. self.fragment.append(fragData)
  101. elif N == n >= k > 1 and k == K+1:
  102. # accumulate
  103. self.fragmentInfo = (k, n)
  104. self.fragment.append(fragData)
  105. else:
  106. # bad, discard
  107. self.discardFragment()
  108. logger.warning('invalid fragmented message: %r', params)
  109. return message
  110. if n == k > 0:
  111. assembled = b''.join(self.fragment)
  112. self.discardFragment()
  113. return assembled
  114. return None
  115. def removeFingerprint(self, fingerprint):
  116. self.user.removeFingerprint(self.trustName, fingerprint)
  117. def setTrust(self, fingerprint, trustLevel):
  118. ''' sets the trust level for the given fingerprint.
  119. trust is usually:
  120. - the empty string for known but untrusted keys
  121. - 'verified' for manually verified keys
  122. - 'smp' for smp-style verified keys '''
  123. self.user.setTrust(self.trustName, fingerprint, trustLevel)
  124. def getTrust(self, fingerprint, default=None):
  125. return self.user.getTrust(self.trustName, fingerprint, default)
  126. def setCurrentTrust(self, trustLevel):
  127. self.setTrust(self.crypto.theirPubkey.cfingerprint(), trustLevel)
  128. def getCurrentKey(self):
  129. return self.crypto.theirPubkey
  130. def getCurrentTrust(self):
  131. ''' returns a 2-tuple: first element is the current fingerprint,
  132. second is:
  133. - None if the key is unknown yet
  134. - a non-empty string if the key is trusted
  135. - an empty string if the key is untrusted '''
  136. if self.crypto.theirPubkey is None:
  137. return None
  138. return self.getTrust(self.crypto.theirPubkey.cfingerprint(), None)
  139. def receiveMessage(self, messageData, appdata=None):
  140. IGN = None, []
  141. if not self.policyOtrEnabled():
  142. raise NotOTRMessage(messageData)
  143. message = self.parse(messageData)
  144. if message is None:
  145. # nothing to see. move along.
  146. return IGN
  147. logger.debug(repr(message))
  148. if self.getPolicy('SEND_TAG'):
  149. if isinstance(message, basestring):
  150. # received a plaintext message without tag
  151. # we should not tag anymore
  152. self.tagOffer = OFFER_REJECTED
  153. else:
  154. # got something OTR-ish, cool!
  155. self.tagOffer = OFFER_ACCEPTED
  156. if isinstance(message, proto.Query):
  157. self.handleQuery(message, appdata=appdata)
  158. if isinstance(message, proto.TaggedPlaintext):
  159. # it's actually a plaintext message
  160. if self.state != STATE_PLAINTEXT or \
  161. self.getPolicy('REQUIRE_ENCRYPTION'):
  162. # but we don't want plaintexts
  163. raise UnencryptedMessage(message.msg)
  164. raise NotOTRMessage(message.msg)
  165. return IGN
  166. if isinstance(message, proto.AKEMessage):
  167. self.crypto.handleAKE(message, appdata=appdata)
  168. return IGN
  169. if isinstance(message, proto.DataMessage):
  170. ignore = message.flags & proto.MSGFLAGS_IGNORE_UNREADABLE
  171. if self.state != STATE_ENCRYPTED:
  172. self.sendInternal(proto.Error(
  173. 'You sent encrypted data, but I wasn\'t expecting it.'
  174. .encode('utf-8')), appdata=appdata)
  175. if ignore:
  176. return IGN
  177. raise NotEncryptedError(EXC_UNREADABLE_MESSAGE)
  178. try:
  179. plaintext, tlvs = self.crypto.handleDataMessage(message)
  180. self.processTLVs(tlvs, appdata=appdata)
  181. if plaintext and self.lastSend < time() - HEARTBEAT_INTERVAL:
  182. self.sendInternal(b'', appdata=appdata)
  183. return plaintext or None, tlvs
  184. except crypt.InvalidParameterError:
  185. if ignore:
  186. return IGN
  187. logger.exception('decryption failed')
  188. raise
  189. if isinstance(message, basestring):
  190. if self.state != STATE_PLAINTEXT or \
  191. self.getPolicy('REQUIRE_ENCRYPTION'):
  192. raise UnencryptedMessage(message)
  193. if isinstance(message, proto.Error):
  194. raise ErrorReceived(message)
  195. raise NotOTRMessage(messageData)
  196. def sendInternal(self, msg, tlvs=[], appdata=None):
  197. self.sendMessage(FRAGMENT_SEND_ALL, msg, tlvs=tlvs, appdata=appdata,
  198. flags=proto.MSGFLAGS_IGNORE_UNREADABLE)
  199. def sendMessage(self, sendPolicy, msg, flags=0, tlvs=[], appdata=None):
  200. if self.policyOtrEnabled():
  201. self.lastSend = time()
  202. if isinstance(msg, proto.OTRMessage):
  203. # we want to send a protocol message (probably internal)
  204. # so we don't need further protocol encryption
  205. # also we can't add TLVs to arbitrary protocol messages
  206. if tlvs:
  207. raise TypeError('can\'t add tlvs to protocol message')
  208. else:
  209. # we got plaintext to send. encrypt it
  210. msg = self.processOutgoingMessage(msg, flags, tlvs)
  211. if isinstance(msg, proto.OTRMessage) \
  212. and not isinstance(msg, proto.Query):
  213. # if it's a query message, it must not get fragmented
  214. return self.sendFragmented(bytes(msg), policy=sendPolicy, appdata=appdata)
  215. else:
  216. msg = bytes(msg)
  217. return msg
  218. def processOutgoingMessage(self, msg, flags, tlvs=[]):
  219. isQuery = self.parseExplicitQuery(msg) is not None
  220. if isQuery:
  221. return self.user.getDefaultQueryMessage(self.getPolicy)
  222. if self.state == STATE_PLAINTEXT:
  223. if self.getPolicy('REQUIRE_ENCRYPTION'):
  224. if not isQuery:
  225. self.lastMessage = msg
  226. self.lastSend = time()
  227. self.mayRetransmit = 2
  228. # TODO notify
  229. msg = self.user.getDefaultQueryMessage(self.getPolicy)
  230. return msg
  231. if self.getPolicy('SEND_TAG') and \
  232. self.tagOffer != OFFER_REJECTED and \
  233. self.shouldTagMessage(msg):
  234. self.tagOffer = OFFER_SENT
  235. versions = set()
  236. if self.getPolicy('ALLOW_V1'):
  237. versions.add(1)
  238. if self.getPolicy('ALLOW_V2'):
  239. versions.add(2)
  240. return proto.TaggedPlaintext(msg, versions)
  241. return msg
  242. if self.state == STATE_ENCRYPTED:
  243. msg = self.crypto.createDataMessage(msg, flags, tlvs)
  244. self.lastSend = time()
  245. return msg
  246. if self.state == STATE_FINISHED:
  247. raise NotEncryptedError(EXC_FINISHED)
  248. def disconnect(self, appdata=None):
  249. if self.state != STATE_FINISHED:
  250. self.sendInternal(b'', tlvs=[proto.DisconnectTLV()], appdata=appdata)
  251. self.setState(STATE_PLAINTEXT)
  252. self.crypto.finished()
  253. else:
  254. self.setState(STATE_PLAINTEXT)
  255. def setState(self, newstate):
  256. self.state = newstate
  257. def _wentEncrypted(self):
  258. self.setState(STATE_ENCRYPTED)
  259. def sendFragmented(self, msg, policy=FRAGMENT_SEND_ALL, appdata=None):
  260. mms = self.maxMessageSize(appdata)
  261. msgLen = len(msg)
  262. if mms != 0 and msgLen > mms:
  263. fms = mms - 19
  264. fragments = [ msg[i:i+fms] for i in range(0, msgLen, fms) ]
  265. fc = len(fragments)
  266. if fc > 65535:
  267. raise OverflowError('too many fragments')
  268. for fi in range(len(fragments)):
  269. ctr = unicode(fi+1) + ',' + unicode(fc) + ','
  270. fragments[fi] = b'?OTR,' + ctr.encode('ascii') \
  271. + fragments[fi] + b','
  272. if policy == FRAGMENT_SEND_ALL:
  273. for f in fragments:
  274. self.inject(f, appdata=appdata)
  275. return None
  276. elif policy == FRAGMENT_SEND_ALL_BUT_FIRST:
  277. for f in fragments[1:]:
  278. self.inject(f, appdata=appdata)
  279. return fragments[0]
  280. elif policy == FRAGMENT_SEND_ALL_BUT_LAST:
  281. for f in fragments[:-1]:
  282. self.inject(f, appdata=appdata)
  283. return fragments[-1]
  284. else:
  285. if policy == FRAGMENT_SEND_ALL:
  286. self.inject(msg, appdata=appdata)
  287. return None
  288. else:
  289. return msg
  290. def processTLVs(self, tlvs, appdata=None):
  291. for tlv in tlvs:
  292. if isinstance(tlv, proto.DisconnectTLV):
  293. logger.info('got disconnect tlv, forcing finished state')
  294. self.setState(STATE_FINISHED)
  295. self.crypto.finished()
  296. # TODO cleanup
  297. continue
  298. if isinstance(tlv, proto.SMPTLV):
  299. self.crypto.smpHandle(tlv, appdata=appdata)
  300. continue
  301. logger.info('got unhandled tlv: {0!r}'.format(tlv))
  302. def smpAbort(self, appdata=None):
  303. if self.state != STATE_ENCRYPTED:
  304. raise NotEncryptedError
  305. self.crypto.smpAbort(appdata=appdata)
  306. def smpIsValid(self):
  307. return self.crypto.smp and self.crypto.smp.prog != crypt.SMPPROG_CHEATED
  308. def smpIsSuccess(self):
  309. return self.crypto.smp.prog == crypt.SMPPROG_SUCCEEDED \
  310. if self.crypto.smp else None
  311. def smpGotSecret(self, secret, question=None, appdata=None):
  312. if self.state != STATE_ENCRYPTED:
  313. raise NotEncryptedError
  314. self.crypto.smpSecret(secret, question=question, appdata=appdata)
  315. def smpInit(self, secret, question=None, appdata=None):
  316. if self.state != STATE_ENCRYPTED:
  317. raise NotEncryptedError
  318. self.crypto.smp = None
  319. self.crypto.smpSecret(secret, question=question, appdata=appdata)
  320. def handleQuery(self, message, appdata=None):
  321. if 2 in message.versions and self.getPolicy('ALLOW_V2'):
  322. self.authStartV2(appdata=appdata)
  323. elif 1 in message.versions and self.getPolicy('ALLOW_V1'):
  324. self.authStartV1(appdata=appdata)
  325. def authStartV1(self, appdata=None):
  326. raise NotImplementedError()
  327. def authStartV2(self, appdata=None):
  328. self.crypto.startAKE(appdata=appdata)
  329. def parseExplicitQuery(self, message):
  330. otrTagPos = message.find(proto.OTRTAG)
  331. if otrTagPos == -1:
  332. return None
  333. indexBase = otrTagPos + len(proto.OTRTAG)
  334. if len(message) <= indexBase:
  335. return None
  336. compare = message[indexBase]
  337. hasq = compare == b'?'[0]
  338. hasv = compare == b'v'[0]
  339. if not hasq and not hasv:
  340. return None
  341. hasv |= len(message) > indexBase+1 and message[indexBase+1] == b'v'[0]
  342. if hasv:
  343. end = message.find(b'?', indexBase+1)
  344. else:
  345. end = indexBase+1
  346. return message[indexBase:end]
  347. def parse(self, message, nofragment=False):
  348. otrTagPos = message.find(proto.OTRTAG)
  349. if otrTagPos == -1:
  350. if proto.MESSAGE_TAG_BASE in message:
  351. return proto.TaggedPlaintext.parse(message)
  352. else:
  353. return message
  354. indexBase = otrTagPos + len(proto.OTRTAG)
  355. if len(message) <= indexBase:
  356. return message
  357. compare = message[indexBase]
  358. if nofragment is False and compare == b','[0]:
  359. message = self.fragmentAccumulate(message[indexBase:])
  360. if message is None:
  361. return None
  362. else:
  363. return self.parse(message, nofragment=True)
  364. else:
  365. self.discardFragment()
  366. queryPayload = self.parseExplicitQuery(message)
  367. if queryPayload is not None:
  368. return proto.Query.parse(queryPayload)
  369. if compare == b':'[0] and len(message) > indexBase + 4:
  370. try:
  371. infoTag = base64.b64decode(message[indexBase+1:indexBase+5])
  372. classInfo = struct.unpack(b'!HB', infoTag)
  373. cls = proto.messageClasses.get(classInfo, None)
  374. if cls is None:
  375. return message
  376. logger.debug('{user} got msg {typ!r}' \
  377. .format(user=self.user.name, typ=cls))
  378. return cls.parsePayload(message[indexBase+5:])
  379. except (TypeError, struct.error):
  380. logger.exception('could not parse OTR message %s', message)
  381. return message
  382. if message[indexBase:indexBase+7] == b' Error:':
  383. return proto.Error(message[indexBase+7:])
  384. return message
  385. def maxMessageSize(self, appdata=None):
  386. """Return the max message size for this context."""
  387. return self.user.maxMessageSize
  388. def getExtraKey(self, extraKeyAppId=None, extraKeyAppData=None, appdata=None):
  389. """ retrieves the generated extra symmetric key.
  390. if extraKeyAppId is set, notifies the chat partner about intended
  391. usage (additional application specific information can be supplied in
  392. extraKeyAppData).
  393. returns the 256 bit symmetric key """
  394. if self.state != STATE_ENCRYPTED:
  395. raise NotEncryptedError
  396. if extraKeyAppId is not None:
  397. tlvs = [proto.ExtraKeyTLV(extraKeyAppId, extraKeyAppData)]
  398. self.sendInternal(b'', tlvs=tlvs, appdata=appdata)
  399. return self.crypto.extraKey
  400. def shouldTagMessage(self, msg):
  401. """Hook to decide whether to tag a message based on its contents."""
  402. return True
  403. class Account(object):
  404. contextclass = Context
  405. def __init__(self, name, protocol, maxMessageSize, privkey=None):
  406. self.name = name
  407. self.privkey = privkey
  408. self.policy = {}
  409. self.protocol = protocol
  410. self.ctxs = {}
  411. self.trusts = {}
  412. self.maxMessageSize = maxMessageSize
  413. self.defaultQuery = '?OTRv{versions}?\nI would like to start ' \
  414. 'an Off-the-Record private conversation. However, you ' \
  415. 'do not have a plugin to support that.\nSee '\
  416. 'https://otr.cypherpunks.ca/ for more information.'
  417. def __repr__(self):
  418. return '<{cls}(name={name!r})>'.format(cls=self.__class__.__name__,
  419. name=self.name)
  420. def getPrivkey(self, autogen=True):
  421. if self.privkey is None:
  422. self.privkey = self.loadPrivkey()
  423. if self.privkey is None:
  424. if autogen is True:
  425. self.privkey = compatcrypto.generateDefaultKey()
  426. self.savePrivkey()
  427. else:
  428. raise LookupError
  429. return self.privkey
  430. def loadPrivkey(self):
  431. raise NotImplementedError
  432. def savePrivkey(self):
  433. raise NotImplementedError
  434. def saveTrusts(self):
  435. raise NotImplementedError
  436. def getContext(self, uid, newCtxCb=None):
  437. if uid not in self.ctxs:
  438. self.ctxs[uid] = self.contextclass(self, uid)
  439. if callable(newCtxCb):
  440. newCtxCb(self.ctxs[uid])
  441. return self.ctxs[uid]
  442. def getDefaultQueryMessage(self, policy):
  443. v = '2' if policy('ALLOW_V2') else ''
  444. msg = self.defaultQuery.format(versions=v)
  445. return msg.encode('ascii')
  446. def setTrust(self, key, fingerprint, trustLevel):
  447. if key not in self.trusts:
  448. self.trusts[key] = {}
  449. self.trusts[key][fingerprint] = trustLevel
  450. self.saveTrusts()
  451. def getTrust(self, key, fingerprint, default=None):
  452. if key not in self.trusts:
  453. return default
  454. return self.trusts[key].get(fingerprint, default)
  455. def removeFingerprint(self, key, fingerprint):
  456. if key in self.trusts and fingerprint in self.trusts[key]:
  457. del self.trusts[key][fingerprint]
  458. class NotEncryptedError(RuntimeError):
  459. pass
  460. class UnencryptedMessage(RuntimeError):
  461. pass
  462. class ErrorReceived(RuntimeError):
  463. pass
  464. class NotOTRMessage(RuntimeError):
  465. pass