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 |
|
29 import potr |
|
30 |
|
31 DEFAULT_POLICY_FLAGS = { |
|
32 'ALLOW_V1':False, |
|
33 'ALLOW_V2':True, |
|
34 'REQUIRE_ENCRYPTION':True, |
|
35 } |
|
36 |
|
37 PLUGIN_INFO = { |
|
38 "name": "OTR", |
|
39 "import_name": "OTR", |
|
40 "type": "SEC", |
|
41 "protocols": [], |
|
42 "dependencies": [], |
|
43 "main": "OTR", |
|
44 "handler": "no", |
|
45 "description": _("""Implementation of OTR""") |
|
46 } |
|
47 |
|
48 |
|
49 PROTOCOL='xmpp' |
|
50 MMS=1024 |
|
51 |
|
52 |
|
53 class Context(potr.context.Context): |
|
54 |
|
55 def __init__(self, host, account, peer): |
|
56 super(Context, self).__init__(account, peer) |
|
57 self.host = host |
|
58 |
|
59 def getPolicy(self, key): |
|
60 if key in DEFAULT_POLICY_FLAGS: |
|
61 return DEFAULT_POLICY_FLAGS[key] |
|
62 else: |
|
63 return False |
|
64 |
|
65 def inject(self, msg, appdata=None): |
|
66 to_jid, profile = appdata |
|
67 assert isinstance(to_jid, jid.JID) |
|
68 client = self.host.getClient(profile) |
|
69 log.debug('inject(%s, appdata=%s, to=%s)' % (msg, appdata, to_jid)) |
|
70 mess_data = {'message': msg, |
|
71 'type': 'chat', |
|
72 'from': client.jid, |
|
73 'to': to_jid, |
|
74 'subject': None, |
|
75 } |
|
76 self.host.generateMessageXML(mess_data) |
|
77 client.xmlstream.send(mess_data['xml']) |
|
78 |
|
79 def setState(self, state): |
|
80 super(Context, self).setState(state) |
|
81 log.debug("setState: %s (self = %s)" % (state, self)) |
|
82 # TODO: send signal to frontends, maybe a message feedback too |
|
83 |
|
84 |
|
85 class Account(potr.context.Account): |
|
86 |
|
87 def __init__(self, account_jid): |
|
88 global PROTOCOL, MMS |
|
89 assert isinstance(account_jid, jid.JID) |
|
90 log.debug("new account: %s" % account_jid) |
|
91 super(Account, self).__init__(account_jid, PROTOCOL, MMS) |
|
92 |
|
93 def loadPrivkey(self): |
|
94 # TODO |
|
95 log.debug("loadPrivkey") |
|
96 return None |
|
97 |
|
98 def savePrivkey(self): |
|
99 # TODO |
|
100 log.debug("savePrivkey") |
|
101 |
|
102 |
|
103 class ContextManager(object): |
|
104 |
|
105 def __init__(self, host, client): |
|
106 self.host = host |
|
107 self.account = Account(client.jid) |
|
108 self.contexts = {} |
|
109 |
|
110 def startContext(self, other): |
|
111 assert isinstance(other, jid.JID) |
|
112 if not other in self.contexts: |
|
113 self.contexts[other] = Context(self.host, self.account, other) |
|
114 return self.contexts[other] |
|
115 |
|
116 def getContextForUser(self, other): |
|
117 log.debug("getContextForUser [%s]" % other) |
|
118 return self.startContext(other) |
|
119 |
|
120 |
|
121 class OTR(object): |
|
122 |
|
123 def __init__(self, host): |
|
124 log.info(_("OTR plugin initialization")) |
|
125 self.host = host |
|
126 self.context_managers = {} |
|
127 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) |
|
128 host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000) |
|
129 |
|
130 def profileConnected(self, profile): |
|
131 client = self.host.getClient(profile) |
|
132 self.context_managers[profile] = ContextManager(self.host, client) |
|
133 |
|
134 def _receivedTreatment(self, data, profile): |
|
135 from_jid = jid.JID(data['from']) |
|
136 log.debug("_receivedTreatment [from_jid = %s]" % from_jid) |
|
137 otrctx = self.context_managers[profile].getContextForUser(from_jid) |
|
138 |
|
139 encrypted = True |
|
140 try: |
|
141 res = otrctx.receiveMessage(data['body'].encode('utf-8'), appdata=(from_jid, profile)) |
|
142 except potr.context.UnencryptedMessage: |
|
143 log.warning("Received unencrypted message in an encrypted context") |
|
144 # TODO: feedback to frontends (either message or popup) |
|
145 encrypted = False |
|
146 |
|
147 if encrypted == False: |
|
148 return data |
|
149 else: |
|
150 if res[0] != None: |
|
151 # decrypted messages handling. |
|
152 # receiveMessage() will return a tuple, the first part of which will be the decrypted message |
|
153 data['body'] = res[0].decode('utf-8') |
|
154 raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history |
|
155 else: |
|
156 raise failure.Failure(exceptions.CancelError()) # no message at all (no history, no signal) |
|
157 |
|
158 def MessageReceivedTrigger(self, message, post_treat, profile): |
|
159 post_treat.addCallback(self._receivedTreatment, profile) |
|
160 return True |
|
161 |
|
162 def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): |
|
163 to_jid = mess_data['to'] |
|
164 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 |
|
166 otrctx = self.context_managers[profile].getContextForUser(to_jid) |
|
167 if mess_data['type'] != 'groupchat' and otrctx.state == potr.context.STATE_ENCRYPTED: |
|
168 log.debug("encrypting message") |
|
169 otrctx.sendMessage(0, mess_data['message'].encode('utf-8'), appdata=(to_jid, profile)) |
|
170 client = self.host.getClient(profile) |
|
171 self.host.sendMessageToBridge(mess_data, client) |
|
172 return False |
|
173 else: |
|
174 log.debug("sending message unencrypted") |
|
175 return True |
|
176 |