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.

otr.py 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. ## Copyright (C) 2008-2012 Kjell Braden <afflux@pentabarf.de>
  2. ## Copyright (C) 2019 Pavel R. <pd at narayana dot im>
  3. #
  4. # This file is part of Gajim OTR Plugin.
  5. #
  6. # Gajim OTR Plugin is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published
  8. # by the Free Software Foundation; version 3 only.
  9. #
  10. # This software 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 General Public License for more details.
  14. #
  15. # You can always obtain full license text at <http://www.gnu.org/licenses/>.
  16. import os, sys
  17. sys.path.append(os.path.dirname(__file__))
  18. from inspect import signature
  19. from gajim.common import const, app, helpers, configpaths
  20. from gajim.session import ChatControlSession
  21. from nbxmpp.protocol import Message, JID
  22. from otrplugin.potr import context, crypt, proto
  23. from otrplugin.keystore import Keystore
  24. # Prototype of OTR Channel (secure conversations between Gajim user (Alice) and Gajim peer (Bob)
  25. class OTRChannel(context.Context):
  26. # this method may be called self.sendMessage() when we need to send some data to our <peer> via XMPP
  27. def inject(self,msg,appdata=None):
  28. stanza = Message(to=self.peer, body=msg.decode(), typ='chat')
  29. stanza.setThread(appdata or ChatControlSession.generate_thread_id(None))
  30. self.user.stream.send_stanza(stanza)
  31. # this method called on channel state change
  32. def setState(self,state=0):
  33. if state and state != self.state:
  34. self.getCurrentTrust() is None and self.setCurrentTrust(0) != 0 and self.printl(OTR.TRUSTED[None].format(fprint=self.getCurrentKey())) # new fingerprint
  35. self.printl(OTR.STATUS[state].format(peer=self.peer,trust=OTR.TRUSTED[self.getCurrentTrust()],fprint=self.getCurrentKey())) # state is changed
  36. self.state = state
  37. # print some text to chat window
  38. def printl(self,line):
  39. println = self.user.getControl(self.peer) and self.user.getControl(self.peer).conv_textview.print_conversation_line
  40. println and println("OTR: "+line,kind='status',name='',tim='',**('jid' in signature(println).parameters and {'jid':None} or {}))
  41. @staticmethod
  42. def getPolicy(policy): return OTR.DEFAULT_POLICY.get(policy)
  43. # OTR instance for Gajim user (Alice)
  44. class OTR(context.Account):
  45. PROTO = ('XMPP', 1024)
  46. ENCRYPTION_NAME = ('OTR')
  47. DEFAULT_POLICY = {
  48. 'REQUIRE_ENCRYPTION': True,
  49. 'ALLOW_V1': False,
  50. 'ALLOW_V2': True,
  51. 'SEND_TAG': True,
  52. 'WHITESPACE_START_AKE': True,
  53. 'ERROR_START_AKE': True,
  54. }
  55. TRUSTED = {None:"new fingerprint received: *{fprint}*", 0:"untrusted", 1:"trusted", 2:"authenticated"}
  56. STATUS = {
  57. context.STATE_PLAINTEXT: "(re-)starting encrypted conversation with {peer}..",
  58. context.STATE_ENCRYPTED: "{trust} encrypted conversation started (fingerprint: {fprint})",
  59. context.STATE_FINISHED: "encrypted conversation with {peer} closed (fingerprint: {fprint})",
  60. context.UnencryptedMessage: "this message is *not encrypted*: {msg}",
  61. context.NotEncryptedError: "unable to process message (channel lost)",
  62. context.ErrorReceived: "received error message: {err}",
  63. crypt.InvalidParameterError: "unable to decrypt message (key/signature mismatch)",
  64. }
  65. def __init__(self,plugin,account):
  66. super(OTR,self).__init__(account,*OTR.PROTO)
  67. self.plugin = plugin
  68. self.log = plugin.log
  69. self.account = account
  70. self.stream = app.connections[account]
  71. self.jid = self.stream.get_own_jid()
  72. self.keystore = Keystore(os.path.join(configpaths.get('MY_DATA'), 'otr_' + self.jid.getStripped() + '.db'))
  73. self.loadTrusts()
  74. # get chat control
  75. def getControl(self,peer):
  76. ctl = app.interface.msg_win_mgr.get_control(peer.getStripped(),self.account)
  77. return ctl
  78. # get OTR context (encrypted dialog between Alice and Bob)
  79. def getContext(self,peer):
  80. peer in self.ctxs and self.ctxs[peer].state == context.STATE_FINISHED and self.ctxs.pop(peer).disconnect() # close dead channels
  81. self.ctxs[peer] = self.ctxs.get(peer) or OTRChannel(self,peer)
  82. return self.ctxs[peer]
  83. # load my private key
  84. def loadPrivkey(self):
  85. my = self.keystore.load(jid=self.jid)
  86. return (my and my.privatekey) and crypt.PK.parsePrivateKey(bytes.fromhex(my.privatekey))[0]
  87. # save my privatekey
  88. def savePrivkey(self):
  89. self.keystore.save(jid=self.jid,privatekey=self.getPrivkey().serializePrivateKey().hex())
  90. # load known fingerprints
  91. def loadTrusts(self):
  92. for peer in self.keystore.load(): self.setTrust(peer.jid,peer.fingerprint,peer.trust)
  93. # save known fingerprints
  94. def saveTrusts(self):
  95. for peer,fingerprints in self.trusts.items():
  96. for fingerprint,trust in fingerprints.items(): self.keystore.save(jid=peer,fingerprint=fingerprint,trust=trust)
  97. # decrypt message
  98. def decrypt(self,event,callback):
  99. peer = event.stanza.getFrom()
  100. channel, ctl = self.getContext(peer), self.getControl(peer)
  101. try:
  102. text, tlvs = channel.receiveMessage(event.msgtxt.encode(),appdata=event.stanza.getThread()) or b''
  103. except (context.UnencryptedMessage,context.NotEncryptedError,context.ErrorReceived,crypt.InvalidParameterError) as e:
  104. self.log.error("** got exception while decrypting message: %s" % e)
  105. channel.printl(OTR.STATUS[e].format(msg=event.msgtxt,err=e.args[0].error))
  106. else:
  107. event.msgtxt = text and text.decode() or ""
  108. event.encrypted = OTR.ENCRYPTION_NAME
  109. event.additional_data["encrypted"] = {"name":OTR.ENCRYPTION_NAME}
  110. callback(event)
  111. finally:
  112. if channel.mayRetransmit and channel.state and ctl: channel.mayRetransmit = ctl.send_message(channel.lastMessage.decode())
  113. # encrypt message
  114. def encrypt(self,event,callback):
  115. peer = event.msg_iq.getTo()
  116. channel, ctl = self.getContext(peer), event.control
  117. if event.xhtml: return ctl.send_message(event.message) # skip xhtml messages
  118. try:
  119. encrypted = channel.sendMessage(context.FRAGMENT_SEND_ALL_BUT_LAST,event.message.encode(),appdata=event.msg_iq.getThread()) or b''
  120. message = (encrypted != self.getDefaultQueryMessage(OTR.DEFAULT_POLICY.get)) and event.message or ""
  121. except context.NotEncryptedError as e:
  122. self.log.error("** got exception while encrypting message: %s" % e)
  123. channel.printl(peer,OTR.STATUS[e])
  124. else:
  125. event.msg_iq.setBody(encrypted.decode()) # encrypted data goes here
  126. event.message = message # message that will be displayed in our chat goes here
  127. event.encrypted, event.additional_data["encrypted"] = OTR.ENCRYPTION_NAME, {"name":OTR.ENCRYPTION_NAME} # some mandatory encryption flags
  128. callback(event)