1055
|
1 #!/usr/bin/python |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 # SAT plugin for OTR encryption |
|
5 # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org) |
|
6 |
|
7 # This program is free software: you can redistribute it and/or modify |
|
8 # it under the terms of the GNU Affero General Public License as published by |
|
9 # the Free Software Foundation, either version 3 of the License, or |
|
10 # (at your option) any later version. |
|
11 |
|
12 # This program is distributed in the hope that it will be useful, |
|
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
15 # GNU Affero General Public License for more details. |
|
16 |
|
17 # You should have received a copy of the GNU Affero General Public License |
|
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
19 |
|
20 # XXX: thanks to Darrik L Mazey for his documentation (https://blog.darmasoft.net/2013/06/30/using-pure-python-otr.html) |
|
21 # this implentation is based on it |
|
22 |
|
23 from sat.core.i18n import _ |
|
24 from sat.core.log import getLogger |
|
25 from sat.core import exceptions |
|
26 log = getLogger(__name__) |
|
27 from twisted.words.protocols.jabber import jid |
|
28 from twisted.python import failure |
1095
|
29 from twisted.internet import defer |
1055
|
30 import potr |
1095
|
31 from sat.memory import persistent |
|
32 |
|
33 NS_OTR = "otr_plugin" |
|
34 PRIVATE_KEY = "PRIVATE KEY" |
1055
|
35 |
|
36 DEFAULT_POLICY_FLAGS = { |
|
37 'ALLOW_V1':False, |
|
38 'ALLOW_V2':True, |
|
39 'REQUIRE_ENCRYPTION':True, |
|
40 } |
|
41 |
|
42 PLUGIN_INFO = { |
|
43 "name": "OTR", |
|
44 "import_name": "OTR", |
|
45 "type": "SEC", |
|
46 "protocols": [], |
|
47 "dependencies": [], |
|
48 "main": "OTR", |
|
49 "handler": "no", |
|
50 "description": _("""Implementation of OTR""") |
|
51 } |
|
52 |
|
53 |
|
54 class Context(potr.context.Context): |
|
55 |
1095
|
56 def __init__(self, host, account, other_jid): |
|
57 super(Context, self).__init__(account, other_jid) |
1055
|
58 self.host = host |
|
59 |
|
60 def getPolicy(self, key): |
|
61 if key in DEFAULT_POLICY_FLAGS: |
|
62 return DEFAULT_POLICY_FLAGS[key] |
|
63 else: |
|
64 return False |
|
65 |
1095
|
66 def inject(self, msg_str, appdata=None): |
|
67 assert isinstance(self.peer, jid.JID) |
|
68 msg = msg_str.decode('utf-8') |
|
69 client = self.user.client |
|
70 log.debug(u'inject(%s, appdata=%s, to=%s)' % (msg, appdata, self.peer)) |
1055
|
71 mess_data = {'message': msg, |
|
72 'type': 'chat', |
|
73 'from': client.jid, |
1095
|
74 'to': self.peer, |
1055
|
75 'subject': None, |
|
76 } |
|
77 self.host.generateMessageXML(mess_data) |
|
78 client.xmlstream.send(mess_data['xml']) |
|
79 |
|
80 def setState(self, state): |
1095
|
81 old_state = self.state |
1055
|
82 super(Context, self).setState(state) |
1095
|
83 log.debug(u"setState: %s (old_state=%s) " % (state, old_state)) |
|
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 |
1055
|
113 |
|
114 |
|
115 class Account(potr.context.Account): |
|
116 |
1095
|
117 def __init__(self, host, client): |
|
118 log.debug(u"new account: %s" % client.jid) |
|
119 super(Account, self).__init__(client.jid, "xmpp", 1024) |
|
120 self.host = host |
|
121 self.client = client |
1055
|
122 |
|
123 def loadPrivkey(self): |
1095
|
124 log.debug(u"loadPrivkey") |
|
125 return self.client.otr_priv_key |
1055
|
126 |
|
127 def savePrivkey(self): |
1095
|
128 log.debug(u"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) |
1055
|
134 |
|
135 |
|
136 class ContextManager(object): |
|
137 |
|
138 def __init__(self, host, client): |
|
139 self.host = host |
1095
|
140 self.account = Account(host, client) |
1055
|
141 self.contexts = {} |
|
142 |
1095
|
143 def startContext(self, other_jid): |
|
144 assert isinstance(other_jid, jid.JID) |
|
145 context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid)) |
|
146 return context |
1055
|
147 |
|
148 def getContextForUser(self, other): |
1095
|
149 log.debug(u"getContextForUser [%s]" % other) |
1055
|
150 return self.startContext(other) |
|
151 |
|
152 |
|
153 class OTR(object): |
|
154 |
|
155 def __init__(self, host): |
1095
|
156 log.info(_(u"OTR plugin initialization")) |
1055
|
157 self.host = host |
|
158 self.context_managers = {} |
|
159 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) |
|
160 host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000) |
|
161 |
1095
|
162 @defer.inlineCallbacks |
1055
|
163 def profileConnected(self, profile): |
|
164 client = self.host.getClient(profile) |
|
165 self.context_managers[profile] = ContextManager(self.host, client) |
1095
|
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 |
1055
|
174 |
|
175 def _receivedTreatment(self, data, profile): |
|
176 from_jid = jid.JID(data['from']) |
1095
|
177 log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid) |
1055
|
178 otrctx = self.context_managers[profile].getContextForUser(from_jid) |
1095
|
179 encrypted = True |
1055
|
180 |
|
181 try: |
1095
|
182 res = otrctx.receiveMessage(data['body'].encode('utf-8')) |
1055
|
183 except potr.context.UnencryptedMessage: |
1095
|
184 if otrctx.state == potr.context.STATE_ENCRYPTED: |
|
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) |
1055
|
193 encrypted = False |
|
194 |
1095
|
195 if not encrypted: |
1055
|
196 return data |
|
197 else: |
|
198 if res[0] != None: |
|
199 # decrypted messages handling. |
|
200 # receiveMessage() will return a tuple, the first part of which will be the decrypted message |
|
201 data['body'] = res[0].decode('utf-8') |
|
202 raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history |
|
203 else: |
|
204 raise failure.Failure(exceptions.CancelError()) # no message at all (no history, no signal) |
|
205 |
|
206 def MessageReceivedTrigger(self, message, post_treat, profile): |
|
207 post_treat.addCallback(self._receivedTreatment, profile) |
|
208 return True |
|
209 |
|
210 def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): |
|
211 to_jid = mess_data['to'] |
|
212 if mess_data['type'] != 'groupchat' and not to_jid.resource: |
|
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 |
|
214 otrctx = self.context_managers[profile].getContextForUser(to_jid) |
|
215 if mess_data['type'] != 'groupchat' and otrctx.state == potr.context.STATE_ENCRYPTED: |
1095
|
216 log.debug(u"encrypting message") |
|
217 otrctx.sendMessage(0, mess_data['message'].encode('utf-8')) |
1055
|
218 client = self.host.getClient(profile) |
|
219 self.host.sendMessageToBridge(mess_data, client) |
|
220 return False |
|
221 else: |
1095
|
222 log.debug(u"sending message unencrypted") |
1055
|
223 return True |
|
224 |