Skip to content

Commit a637214

Browse files
committed
initial commit
0 parents  commit a637214

File tree

6 files changed

+535
-0
lines changed

6 files changed

+535
-0
lines changed

README.md

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
XMPP-OTR channel for Python
2+
===========================
3+
4+
This is a Python library for communicating with XMPP destinations using OTR
5+
([Off-the-Record Messaging](https://otr.cypherpunks.ca/)) encryption.
6+
7+
Features
8+
--------
9+
10+
* Your internet application can talk securely to you on your PC or smartphone
11+
using readily-available chat software with OTR support
12+
* OTRv2
13+
* Send to and receive from multiple destinations, with or without fingerprint
14+
verification
15+
* Pure python (no libotr dependency)
16+
17+
Example
18+
-------
19+
20+
import time
21+
from otrxmppchannel import OTRXMPPChannel
22+
from otrxmppchannel.connection import OTR_TRUSTED, OTR_UNTRUSTED,
23+
OTR_UNENCRYPTED, OTR_UNKNOWN
24+
25+
# Load the base64-encoded OTR DSA key. Constructing the object without
26+
# a key will generate one and provide it via ValueError exception.
27+
privkey = open('.otrprivkey', 'r').read()
28+
29+
class MyOTRChannel(OTRXMPPChannel):
30+
def on_receive(self, message, from_jid, otr_state):
31+
if otr_state == OTR_TRUSTED:
32+
state = 'trusted'
33+
elif otr_state == OTR_UNTRUSTED:
34+
state = 'UNTRUSTED!'
35+
elif otr_state == OTR_UNENCRYPTED:
36+
state = 'UNENCRYPTED!'
37+
else:
38+
state = 'UNKNOWN OTR STATUS!'
39+
print('received %s from %s (%s)' % (message, from_jid, state))
40+
41+
mychan = MyOTRXMPPChannel(
42+
'[email protected]/datadiode',
43+
'supersecret',
44+
[
45+
(
46+
47+
'33eb6b01c97ceba92bd6b5e3777189c43f8d6f03'
48+
),
49+
50+
],
51+
privkey
52+
)
53+
54+
mychan.send('') # Force OTR setup
55+
time.sleep(3) # Wait a bit for OTR setup to complete
56+
mychan.send('This message should be encrypted')
57+
58+
Notes
59+
-----
60+
61+
* XMPP invitations are not handled
62+
* It seems to take roughly 3 seconds to set up an OTR session. Messages sent
63+
before the session is ready may be lost.
64+
* The private key serialization format is specific to pure-python-otr.
65+
Conversions from other formats are not handled.
66+
67+
Dependencies
68+
------------
69+
70+
* [xmpppy](http://xmpppy.sourceforge.net/) (>= 0.4.1)
71+
* [pure-python-otr](https://github.com/afflux/pure-python-otr) (>= 1.0.0)
72+
73+
Author
74+
------
75+
76+
* [Mike Gogulski](mailto:[email protected]) - <https://github.com/mikegogulski>
77+
78+
Donations
79+
---------
80+
81+
If you found this software useful and would like to encourage its
82+
maintenance and further development, please consider making a donation to
83+
the Bitcoin address `1MWFhwdFVEhB3X4eVsm9WxwvAhaxQqNbJh`.
84+
85+
License
86+
-------
87+
88+
This is free and unencumbered public domain software. For more information,
89+
see <http://unlicense.org/> or the accompanying UNLICENSE file.

UNLICENSE

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
This is free and unencumbered software released into the public domain.
2+
3+
Anyone is free to copy, modify, publish, use, compile, sell, or
4+
distribute this software, either in source code form or as a compiled
5+
binary, for any purpose, commercial or non-commercial, and by any
6+
means.
7+
8+
In jurisdictions that recognize copyright laws, the author or authors
9+
of this software dedicate any and all copyright interest in the
10+
software to the public domain. We make this dedication for the benefit
11+
of the public at large and to the detriment of our heirs and
12+
successors. We intend this dedication to be an overt act of
13+
relinquishment in perpetuity of all present and future rights to this
14+
software under copyright law.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
OTHER DEALINGS IN THE SOFTWARE.
23+
24+
For more information, please refer to <http://unlicense.org/>

otrxmppchannel/__init__.py

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from connection import Connection
4+
5+
6+
class OTRXMPPChannel(object):
7+
"""
8+
OTR-XMPP communications channel
9+
-------------------------------
10+
Uses Off-the-Record Messaging for communications with XMPP destinations
11+
See https://otr.cypherpunks.ca/
12+
13+
Example::
14+
15+
import time
16+
from otrxmppchannel import OTRXMPPChannel
17+
from otrxmppchannel.connection import OTR_TRUSTED, OTR_UNTRUSTED,
18+
OTR_UNENCRYPTED, OTR_UNKNOWN
19+
20+
privkey = open('.otrprivkey', 'r').read()
21+
channel = OTRXMPPChannel(
22+
'[email protected]/datadiode',
23+
'supersecret',
24+
[
25+
(
26+
27+
'33eb6b01c97ceba92bd6b5e3777189c43f8d6f03'
28+
),
29+
30+
],
31+
privkey
32+
)
33+
34+
def my_receive(msg, from_jid, otr_state):
35+
state = ''
36+
if otr_state == OTR_TRUSTED:
37+
state = 'trusted'
38+
elif otr_state == OTR_UNTRUSTED:
39+
state = 'UNTRUSTED!'
40+
elif otr_state == OTR_UNENCRYPTED:
41+
state = 'UNENCRYPTED!'
42+
else:
43+
state = 'UNKNOWN OTR STATUS!'
44+
print('received %s from %s (%s)' % (msg, from_jid, state))
45+
46+
channel.send('') # set up OTR
47+
time.sleep(3)
48+
channel.send('This message should be encrypted')
49+
50+
NOTE: XMPP invitations are not handled
51+
NOTE: It seems to take roughly 3 seconds to set up an OTR session.
52+
Messages sent before the session is ready may be lost.
53+
54+
:param jid: source JID, e.g. '[email protected]'
55+
:param password: XMPP server password
56+
:param recipients: a single recipient JID, a tuple of
57+
*(jid, OTRFingerprint/None)*, or a list of same
58+
:param privkey: base64-encoded DSA private key for OTR. If *None* is
59+
passed, a new key will be generated and dumped via a *ValueError*
60+
exception.
61+
"""
62+
63+
def __init__(self, jid, password, recipients, privkey=None):
64+
usage = 'recipients can be a single recipient JID, a tuple of ' \
65+
'(jid, OTRFingerprint|None), or a list of same. Example: ' \
66+
'[\'[email protected]\', ' \
67+
'(\'[email protected]\', ' \
68+
'\'43d36b01c67deba92bd6b5e3711189c43f8d6f04\')].'
69+
# normalize recipients to a list of tuples
70+
if isinstance(recipients, str):
71+
self.recipients = [(recipients, None)]
72+
elif isinstance(recipients, tuple):
73+
self.recipients = [recipients]
74+
elif isinstance(recipients, list):
75+
self.recipients = recipients
76+
else:
77+
raise TypeError(usage)
78+
for i in range(0, len(self.recipients) - 1):
79+
if isinstance(self.recipients[i], str):
80+
self.recipients[i] = self.recipients[i], None
81+
elif isinstance(self.recipients[i], tuple):
82+
if len(self.recipients[i]) > 2 or len(self.recipients[i]) < 1:
83+
raise TypeError(usage)
84+
if len(self.recipients[i]) == 1:
85+
self.recipients[i] = self.recipients[i], None
86+
else:
87+
raise TypeError(usage)
88+
self.connection = Connection(jid, password, privkey, self.on_receive)
89+
self.connection.start()
90+
91+
def send(self, message):
92+
"""
93+
Send *message* to recipients
94+
:param message: message string
95+
"""
96+
for recipient in self.recipients:
97+
self.connection.send(message, recipient)
98+
99+
def on_receive(self, message, from_jid, otr_state):
100+
"""
101+
Override this method to create a custom message receipt handler. The
102+
handler provided simply discards received messages. Here is an
103+
example::
104+
105+
from connection import OTR_TRUSTED, OTR_UNTRUSTED, OTR_UNENCRYPTED
106+
107+
if otr_state == OTR_TRUSTED:
108+
state = 'trusted'
109+
elif otr_state == OTR_UNTRUSTED:
110+
state = 'UNTRUSTED!'
111+
elif otr_state == OTR_UNENCRYPTED:
112+
state = 'UNENCRYPTED!'
113+
else:
114+
state = 'UNKNOWN OTR STATUS!'
115+
print('received %s from %s (%s)' % (message, from_jid, state))
116+
117+
:param message: received message body
118+
:param from_jid: source JID of the message
119+
:param otr_state: an integer describing the state of the OTR
120+
relationship to the source JID (OTR_* constants defined in
121+
otrxmppchannel.connection)
122+
"""
123+
pass

otrxmppchannel/connection.py

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import xmpp
4+
import threading
5+
import time
6+
from Queue import Queue
7+
import potr
8+
from otrmodule import OTRAccount, OTRManager
9+
10+
OTR_TRUSTED = 0
11+
OTR_UNTRUSTED = 1
12+
OTR_UNENCRYPTED = 2
13+
OTR_UNKNOWN = 3
14+
15+
16+
def d(msg):
17+
print('---> %s' % msg)
18+
19+
20+
class AuthenticationError(Exception):
21+
pass
22+
23+
24+
class OTRXMPPMessage(object):
25+
def __init__(self, body, to_jid, fp=None):
26+
self.body = body
27+
self.to_jid = to_jid
28+
self.fp = fp
29+
30+
31+
class Connection(threading.Thread):
32+
def __init__(self, jid, password, pk, on_receive=None):
33+
threading.Thread.__init__(self)
34+
self.jid = xmpp.protocol.JID(jid)
35+
self.password = password
36+
self.on_receive = on_receive
37+
self.client = None
38+
self.q = Queue(maxsize=100)
39+
self.otr_account = OTRAccount(str(self.jid), pk)
40+
self.otr_manager = OTRManager(self.otr_account)
41+
42+
def run(self):
43+
while True:
44+
while not self.client or not self.client.isConnected():
45+
cl = xmpp.Client(
46+
self.jid.getDomain(),
47+
debug=[]
48+
)
49+
conntype = cl.connect()
50+
if not conntype:
51+
d('XMPP connect failed, retrying in 5 seconds')
52+
time.sleep(5)
53+
continue
54+
self.client = cl
55+
self.client.UnregisterDisconnectHandler(
56+
self.client.DisconnectHandler)
57+
auth = self.client.auth(self.jid.getNode(), self.password,
58+
self.jid.getResource())
59+
if not auth:
60+
d('XMPP authentication failed')
61+
raise AuthenticationError
62+
self.client.sendInitPresence(requestRoster=0)
63+
self.client.RegisterDisconnectHandler(self._on_disconnect)
64+
self.client.RegisterHandler('message', self._on_receive)
65+
while self.client.isConnected() and self.client.Process(0.1):
66+
if not self.q.empty():
67+
self._send(self.q.get())
68+
pass
69+
70+
def _on_disconnect(self):
71+
self.otr_manager.destroy_all_contexts()
72+
73+
def _on_receive(self, _, stanza):
74+
fromjid = stanza.getFrom()
75+
body = str(stanza.getBody())
76+
# d('stanza from %s: %s' % (fromjid, body))
77+
if stanza.getBody() is None:
78+
return
79+
fromjid = xmpp.protocol.JID(fromjid)
80+
fromjid.setResource(None)
81+
otrctx = self.otr_manager.get_context(self.client, str(fromjid))
82+
encrypted = True
83+
res = ()
84+
try:
85+
res = otrctx.receiveMessage(body)
86+
except potr.context.UnencryptedMessage:
87+
encrypted = False
88+
except potr.context.NotEncryptedError:
89+
# potr auto-responds saying we didn't expect an encrypted message
90+
return
91+
92+
msg = ''
93+
otr_state = OTR_UNKNOWN
94+
if not encrypted:
95+
if stanza['type'] in ('chat', 'normal'):
96+
msg = stanza.getBody()
97+
otr_state = OTR_UNENCRYPTED
98+
d('unencrypted message: %s' % msg)
99+
else:
100+
if res[0] is not None:
101+
msg = res[0]
102+
trust = otrctx.getCurrentTrust()
103+
if trust is None or trust == 'untrusted':
104+
otr_state = OTR_UNTRUSTED
105+
d('untrusted decrypted message: %s' % msg)
106+
else:
107+
otr_state = OTR_TRUSTED
108+
d('trusted decrypted message: %s' % msg)
109+
if msg is not None and msg != '':
110+
self.on_receive(msg, str(fromjid), otr_state)
111+
112+
def _send(self, msg):
113+
# d('_send(%s, %s, %s)' % (msg.body, msg.to_jid, msg.fp))
114+
otrctx = self.otr_manager.get_context(self.client, msg.to_jid, msg.fp)
115+
if otrctx.state == potr.context.STATE_ENCRYPTED:
116+
if otrctx.getCurrentTrust() == 'untrusted':
117+
d('fingerprint changed from %s to %s for %s!' % (
118+
msg.fp, otrctx.fp, msg.to_jid))
119+
self.client.send(
120+
xmpp.protocol.Message(
121+
msg.to_jid,
122+
'I would like to tell you something, but I '
123+
'don\'t trust your OTR fingerprint.'))
124+
else:
125+
otrctx.sendMessage(0, str(msg.body))
126+
else:
127+
# d('Initializing OTR with %s (message "%s")' % (
128+
# msg.to_jid, msg.body))
129+
self.client.send(
130+
xmpp.protocol.Message(
131+
msg.to_jid,
132+
otrctx.account.getDefaultQueryMessage(otrctx.getPolicy),
133+
typ='chat'))
134+
135+
def send(self, text, to_jid, fp=None):
136+
"""
137+
send a message to a JID, with an optional OTR fingerprint to verify
138+
:param text: message body
139+
:param to_jid: destination JID
140+
:param fp: expected OTR fingerprint from the destination JID
141+
:return:
142+
"""
143+
while self.q.full():
144+
d('queue full, discarding first entry')
145+
_ = self.q.get_nowait()
146+
if isinstance(to_jid, tuple):
147+
to_jid, fp = to_jid
148+
self.q.put_nowait(OTRXMPPMessage(text, to_jid, fp))

0 commit comments

Comments
 (0)