comparison src/plugins/plugin_sec_otr.py @ 1095:ef7b7dd5c5db

plugin OTR: various improvments: - otr_data is now saved between sessions - private key is encrypted before saving - state change is detected (unencrypted/started/refreshed/finished) and (un)trusted (trusting is not implemented yet) - user feedback - fixed unencrypted message in an encrypted context warning - fixed inject msg encoding - minor refactoring
author Goffi <goffi@goffi.org>
date Thu, 26 Jun 2014 14:58:25 +0200
parents abcac1ac27a7
children 8def4a3f55c2
comparison
equal deleted inserted replaced
1094:4286a19e9e8a 1095:ef7b7dd5c5db
24 from sat.core.log import getLogger 24 from sat.core.log import getLogger
25 from sat.core import exceptions 25 from sat.core import exceptions
26 log = getLogger(__name__) 26 log = getLogger(__name__)
27 from twisted.words.protocols.jabber import jid 27 from twisted.words.protocols.jabber import jid
28 from twisted.python import failure 28 from twisted.python import failure
29 from twisted.internet import defer
29 import potr 30 import potr
31 from sat.memory import persistent
32
33 NS_OTR = "otr_plugin"
34 PRIVATE_KEY = "PRIVATE KEY"
30 35
31 DEFAULT_POLICY_FLAGS = { 36 DEFAULT_POLICY_FLAGS = {
32 'ALLOW_V1':False, 37 'ALLOW_V1':False,
33 'ALLOW_V2':True, 38 'ALLOW_V2':True,
34 'REQUIRE_ENCRYPTION':True, 39 'REQUIRE_ENCRYPTION':True,
44 "handler": "no", 49 "handler": "no",
45 "description": _("""Implementation of OTR""") 50 "description": _("""Implementation of OTR""")
46 } 51 }
47 52
48 53
49 PROTOCOL='xmpp'
50 MMS=1024
51
52
53 class Context(potr.context.Context): 54 class Context(potr.context.Context):
54 55
55 def __init__(self, host, account, peer): 56 def __init__(self, host, account, other_jid):
56 super(Context, self).__init__(account, peer) 57 super(Context, self).__init__(account, other_jid)
57 self.host = host 58 self.host = host
58 59
59 def getPolicy(self, key): 60 def getPolicy(self, key):
60 if key in DEFAULT_POLICY_FLAGS: 61 if key in DEFAULT_POLICY_FLAGS:
61 return DEFAULT_POLICY_FLAGS[key] 62 return DEFAULT_POLICY_FLAGS[key]
62 else: 63 else:
63 return False 64 return False
64 65
65 def inject(self, msg, appdata=None): 66 def inject(self, msg_str, appdata=None):
66 to_jid, profile = appdata 67 assert isinstance(self.peer, jid.JID)
67 assert isinstance(to_jid, jid.JID) 68 msg = msg_str.decode('utf-8')
68 client = self.host.getClient(profile) 69 client = self.user.client
69 log.debug('inject(%s, appdata=%s, to=%s)' % (msg, appdata, to_jid)) 70 log.debug(u'inject(%s, appdata=%s, to=%s)' % (msg, appdata, self.peer))
70 mess_data = {'message': msg, 71 mess_data = {'message': msg,
71 'type': 'chat', 72 'type': 'chat',
72 'from': client.jid, 73 'from': client.jid,
73 'to': to_jid, 74 'to': self.peer,
74 'subject': None, 75 'subject': None,
75 } 76 }
76 self.host.generateMessageXML(mess_data) 77 self.host.generateMessageXML(mess_data)
77 client.xmlstream.send(mess_data['xml']) 78 client.xmlstream.send(mess_data['xml'])
78 79
79 def setState(self, state): 80 def setState(self, state):
81 old_state = self.state
80 super(Context, self).setState(state) 82 super(Context, self).setState(state)
81 log.debug("setState: %s (self = %s)" % (state, self)) 83 log.debug(u"setState: %s (old_state=%s) " % (state, old_state))
82 # TODO: send signal to frontends, maybe a message feedback too 84
85 if state == potr.context.STATE_PLAINTEXT:
86 feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {'other_jid': self.peer.full()}
87 elif state == potr.context.STATE_ENCRYPTED:
88 try:
89 fingerprint, trusted = self.getCurrentTrust()
90 except TypeError:
91 trusted = False
92 trusted_str = _(u"trusted") if trusted else _(u"untrusted")
93
94 if old_state == potr.context.STATE_ENCRYPTED:
95 feedback = _(u"%(trusted)s OTR conversation with %(other_jid)s REFRESHED") % {'trusted': trusted_str, 'other_jid': self.peer.full()}
96 else:
97 feedback = _(u"%(trusted)s Encrypted OTR conversation started with %(other_jid)s") % {'trusted': trusted_str, 'other_jid': self.peer.full()}
98 elif state == potr.context.STATE_FINISHED:
99 feedback = _(u"OTR conversation with %(other_jid)s is FINISHED") % {'other_jid': self.peer.full()}
100 else:
101 log.error(_(u"Unknown OTR state"))
102 return
103
104 client = self.user.client
105 # FIXME: newMessage should manage system message, so they don't appear as coming from the contact
106 self.host.bridge.newMessage(client.jid.full(),
107 feedback,
108 mess_type="headline",
109 to_jid=self.peer.full(),
110 extra={},
111 profile=client.profile)
112 # TODO: send signal to frontends
83 113
84 114
85 class Account(potr.context.Account): 115 class Account(potr.context.Account):
86 116
87 def __init__(self, account_jid): 117 def __init__(self, host, client):
88 global PROTOCOL, MMS 118 log.debug(u"new account: %s" % client.jid)
89 assert isinstance(account_jid, jid.JID) 119 super(Account, self).__init__(client.jid, "xmpp", 1024)
90 log.debug("new account: %s" % account_jid) 120 self.host = host
91 super(Account, self).__init__(account_jid, PROTOCOL, MMS) 121 self.client = client
92 122
93 def loadPrivkey(self): 123 def loadPrivkey(self):
94 # TODO 124 log.debug(u"loadPrivkey")
95 log.debug("loadPrivkey") 125 return self.client.otr_priv_key
96 return None
97 126
98 def savePrivkey(self): 127 def savePrivkey(self):
99 # TODO 128 log.debug(u"savePrivkey")
100 log.debug("savePrivkey") 129 priv_key = self.getPrivkey().serializePrivateKey()
130 d = self.host.memory.encryptValue(priv_key, self.client.profile)
131 def save_encrypted_key(encrypted_priv_key):
132 self.client.otr_data[PRIVATE_KEY] = encrypted_priv_key
133 d.addCallback(save_encrypted_key)
101 134
102 135
103 class ContextManager(object): 136 class ContextManager(object):
104 137
105 def __init__(self, host, client): 138 def __init__(self, host, client):
106 self.host = host 139 self.host = host
107 self.account = Account(client.jid) 140 self.account = Account(host, client)
108 self.contexts = {} 141 self.contexts = {}
109 142
110 def startContext(self, other): 143 def startContext(self, other_jid):
111 assert isinstance(other, jid.JID) 144 assert isinstance(other_jid, jid.JID)
112 if not other in self.contexts: 145 context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid))
113 self.contexts[other] = Context(self.host, self.account, other) 146 return context
114 return self.contexts[other]
115 147
116 def getContextForUser(self, other): 148 def getContextForUser(self, other):
117 log.debug("getContextForUser [%s]" % other) 149 log.debug(u"getContextForUser [%s]" % other)
118 return self.startContext(other) 150 return self.startContext(other)
119 151
120 152
121 class OTR(object): 153 class OTR(object):
122 154
123 def __init__(self, host): 155 def __init__(self, host):
124 log.info(_("OTR plugin initialization")) 156 log.info(_(u"OTR plugin initialization"))
125 self.host = host 157 self.host = host
126 self.context_managers = {} 158 self.context_managers = {}
127 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) 159 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
128 host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000) 160 host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000)
129 161
162 @defer.inlineCallbacks
130 def profileConnected(self, profile): 163 def profileConnected(self, profile):
131 client = self.host.getClient(profile) 164 client = self.host.getClient(profile)
132 self.context_managers[profile] = ContextManager(self.host, client) 165 self.context_managers[profile] = ContextManager(self.host, client)
166 client.otr_data = persistent.PersistentBinaryDict(NS_OTR, profile)
167 yield client.otr_data.load()
168 encrypted_priv_key = client.otr_data.get(PRIVATE_KEY, None)
169 if encrypted_priv_key is not None:
170 priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, profile)
171 client.otr_priv_key = potr.crypt.PK.parsePrivateKey(priv_key)[0]
172 else:
173 client.otr_priv_key = None
133 174
134 def _receivedTreatment(self, data, profile): 175 def _receivedTreatment(self, data, profile):
135 from_jid = jid.JID(data['from']) 176 from_jid = jid.JID(data['from'])
136 log.debug("_receivedTreatment [from_jid = %s]" % from_jid) 177 log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid)
137 otrctx = self.context_managers[profile].getContextForUser(from_jid) 178 otrctx = self.context_managers[profile].getContextForUser(from_jid)
138
139 encrypted = True 179 encrypted = True
180
140 try: 181 try:
141 res = otrctx.receiveMessage(data['body'].encode('utf-8'), appdata=(from_jid, profile)) 182 res = otrctx.receiveMessage(data['body'].encode('utf-8'))
142 except potr.context.UnencryptedMessage: 183 except potr.context.UnencryptedMessage:
143 log.warning("Received unencrypted message in an encrypted context") 184 if otrctx.state == potr.context.STATE_ENCRYPTED:
144 # TODO: feedback to frontends (either message or popup) 185 log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': from_jid.full()})
186 client = self.host.getClient(profile)
187 self.host.bridge.newMessage(from_jid.full(),
188 _(u"WARNING: received unencrypted data in a supposedly encrypted context"),
189 mess_type="headline", # FIXME: add message type for internal informations
190 to_jid=client.jid.full(),
191 extra={},
192 profile=client.profile)
145 encrypted = False 193 encrypted = False
146 194
147 if encrypted == False: 195 if not encrypted:
148 return data 196 return data
149 else: 197 else:
150 if res[0] != None: 198 if res[0] != None:
151 # decrypted messages handling. 199 # decrypted messages handling.
152 # receiveMessage() will return a tuple, the first part of which will be the decrypted message 200 # receiveMessage() will return a tuple, the first part of which will be the decrypted message
163 to_jid = mess_data['to'] 211 to_jid = mess_data['to']
164 if mess_data['type'] != 'groupchat' and not to_jid.resource: 212 if mess_data['type'] != 'groupchat' and not to_jid.resource:
165 to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed 213 to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed
166 otrctx = self.context_managers[profile].getContextForUser(to_jid) 214 otrctx = self.context_managers[profile].getContextForUser(to_jid)
167 if mess_data['type'] != 'groupchat' and otrctx.state == potr.context.STATE_ENCRYPTED: 215 if mess_data['type'] != 'groupchat' and otrctx.state == potr.context.STATE_ENCRYPTED:
168 log.debug("encrypting message") 216 log.debug(u"encrypting message")
169 otrctx.sendMessage(0, mess_data['message'].encode('utf-8'), appdata=(to_jid, profile)) 217 otrctx.sendMessage(0, mess_data['message'].encode('utf-8'))
170 client = self.host.getClient(profile) 218 client = self.host.getClient(profile)
171 self.host.sendMessageToBridge(mess_data, client) 219 self.host.sendMessageToBridge(mess_data, client)
172 return False 220 return False
173 else: 221 else:
174 log.debug("sending message unencrypted") 222 log.debug(u"sending message unencrypted")
175 return True 223 return True
176 224