Mercurial > libervia-backend
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 |