· 8 years ago · Jun 10, 2018, 02:12 PM
1"""
2pyaxo.py - a python implementation of the axolotl ratchet protocol.
3https://github.com/trevp/axolotl/wiki/newversion
4
5Symmetric encryption is done using the python-gnupg module.
6
7Copyright (C) 2014 by David R. Andersen <k0rx@RXcomm.net>
8
9This program is free software: you can redistribute it and/or modify
10it under the terms of the GNU General Public License as published by
11the Free Software Foundation, either version 3 of the License, or
12(at your option) any later version.
13
14This program is distributed in the hope that it will be useful,
15but WITHOUT ANY WARRANTY; without even the implied warranty of
16MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17GNU General Public License for more details.
18
19You should have received a copy of the GNU General Public License
20along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22For more information, see https://github.com/rxcomm/pyaxo
23"""
24
25import errno
26import os
27import sqlite3
28import sys
29import struct
30from collections import namedtuple
31from functools import wraps
32from getpass import getpass
33from threading import Lock
34from time import time
35
36import nacl.secret
37import nacl.utils
38from nacl.encoding import Base64Encoder
39from nacl.exceptions import CryptoError
40from nacl.hash import sha256
41from nacl.public import PrivateKey, PublicKey, Box
42from passlib.utils.pbkdf2 import pbkdf2
43
44
45ALICE_MODE = True
46BOB_MODE = False
47
48SALTS = {'RK': b'\x00',
49 'HK': {ALICE_MODE: b'\x01', BOB_MODE: b'\x02'},
50 'NHK': {ALICE_MODE: b'\x03', BOB_MODE: b'\x04'},
51 'CK': {ALICE_MODE: b'\x05', BOB_MODE: b'\x06'},
52 'CONVid': b'\x07'}
53
54HEADER_LEN = 84
55HEADER_PAD_NUM_LEN = 1
56HEADER_COUNT_NUM_LEN = 4
57
58
59def sync(f):
60 @wraps(f)
61 def synced_f(self, *args, **kwargs):
62 with self.lock:
63 return f(self, *args, **kwargs)
64 return synced_f
65
66
67class Axolotl(object):
68
69 def __init__(self, name, dbname='axolotl.db', dbpassphrase='', nonthreaded_sql=True):
70 self.name = name
71 self.dbname = dbname
72 self.nonthreaded_sql = nonthreaded_sql
73 if dbpassphrase is None:
74 self.dbpassphrase = None
75 elif dbpassphrase != '':
76 self.dbpassphrase = hash_(dbpassphrase)
77 else:
78 self.dbpassphrase = getpass('Database passphrase for '+ self.name + ': ').strip()
79 self.conversation = AxolotlConversation(self, keys=dict(), mode=None)
80 self.state['DHIs_priv'], self.state['DHIs'] = generate_keypair()
81 self.state['DHRs_priv'], self.state['DHRs'] = generate_keypair()
82 self.handshakeKey, self.handshakePKey = generate_keypair()
83 self.storeTime = 2*86400 # minimum time (seconds) to store missed ephemeral message keys
84 self.persistence = SqlitePersistence(self.dbname,
85 self.dbpassphrase,
86 self.storeTime,
87 self.nonthreaded_sql)
88
89 @property
90 def state(self):
91 return self.conversation.keys
92
93 @state.setter
94 def state(self, state):
95 self.conversation.keys = state
96
97 @property
98 def mode(self):
99 return self.conversation.mode
100
101 @mode.setter
102 def mode(self, mode):
103 self.conversation.mode = mode
104
105 @property
106 def db(self):
107 return self.persistence.db
108
109 @db.setter
110 def db(self, db):
111 self.persistence.db = db
112
113 def tripleDH(self, a, a0, B, B0):
114 if self.mode == None:
115 sys.exit(1)
116 return generate_3dh(a, a0, B, B0, self.mode)
117
118 def genDH(self, a, B):
119 return generate_dh(a, B)
120
121 def genKey(self):
122 return generate_keypair()
123
124 def initState(self, other_name, other_identityKey, other_handshakeKey,
125 other_ratchetKey, verify=True):
126 if verify:
127 print('Confirm ' + other_name + ' has identity key fingerprint:\n')
128 fingerprint = hash_(other_identityKey).encode('hex').upper()
129 fprint = ''
130 for i in range(0, len(fingerprint), 4):
131 fprint += fingerprint[i:i+2] + ':'
132 print(fprint[:-1] + '\n')
133 print('Be sure to verify this fingerprint with ' + other_name + \
134 ' by some out-of-band method!')
135 print('Otherwise, you may be subject to a Man-in-the-middle attack!\n')
136 ans = raw_input('Confirm? y/N: ').strip()
137 if ans != 'y':
138 print('Key fingerprint not confirmed - exiting...')
139 sys.exit()
140
141 self.conversation = self.init_conversation(other_name,
142 self.state['DHIs_priv'],
143 self.state['DHIs'],
144 self.handshakeKey,
145 other_identityKey,
146 other_handshakeKey,
147 self.state['DHRs_priv'],
148 self.state['DHRs'],
149 other_ratchetKey)
150
151 def init_conversation(self, other_name,
152 priv_identity_key, identity_key, priv_handshake_key,
153 other_identity_key, other_handshake_key,
154 priv_ratchet_key=None, ratchet_key=None,
155 other_ratchet_key=None, mode=None):
156 if mode is None:
157 if identity_key < other_identity_key:
158 mode = ALICE_MODE
159 else:
160 mode = BOB_MODE
161
162 mkey = generate_3dh(priv_identity_key, priv_handshake_key,
163 other_identity_key, other_handshake_key,
164 mode)
165
166 return self.create_conversation(other_name,
167 mkey,
168 mode,
169 priv_identity_key,
170 identity_key,
171 other_identity_key,
172 priv_ratchet_key,
173 ratchet_key,
174 other_ratchet_key)
175
176 def createState(self, other_name, mkey, mode=None, other_identityKey=None, other_ratchetKey=None):
177 if mode is not None:
178 self.mode = mode
179 else:
180 if self.mode is None: # mode not selected
181 sys.exit(1)
182
183 self.conversation = self.create_conversation(other_name,
184 mkey,
185 self.mode,
186 self.state['DHIs_priv'],
187 self.state['DHIs'],
188 other_identityKey,
189 self.state['DHRs_priv'],
190 self.state['DHRs'],
191 other_ratchetKey)
192
193 self.ratchetKey = False
194 self.ratchetPKey = False
195
196 def create_conversation(self, other_name, mkey, mode,
197 priv_identity_key, identity_key,
198 other_identity_key,
199 priv_ratchet_key=None, ratchet_key=None,
200 other_ratchet_key=None):
201 if mode is ALICE_MODE:
202 HKs = None
203 HKr = kdf(mkey, SALTS['HK'][BOB_MODE])
204 CKs = None
205 CKr = kdf(mkey, SALTS['CK'][BOB_MODE])
206 DHRs_priv = None
207 DHRs = None
208 DHRr = other_ratchet_key
209 Ns = 0
210 Nr = 0
211 PNs = 0
212 ratchet_flag = True
213 else: # bob mode
214 HKs = kdf(mkey, SALTS['HK'][BOB_MODE])
215 HKr = None
216 CKs = kdf(mkey, SALTS['CK'][BOB_MODE])
217 CKr = None
218 DHRs_priv = priv_ratchet_key
219 DHRs = ratchet_key
220 DHRr = None
221 Ns = 0
222 Nr = 0
223 PNs = 0
224 ratchet_flag = False
225 RK = kdf(mkey, SALTS['RK'])
226 NHKs = kdf(mkey, SALTS['NHK'][mode])
227 NHKr = kdf(mkey, SALTS['NHK'][not mode])
228 CONVid = kdf(mkey, SALTS['CONVid'])
229 DHIr = other_identity_key
230
231 keys = \
232 { 'name': self.name,
233 'other_name': other_name,
234 'RK': RK,
235 'HKs': HKs,
236 'HKr': HKr,
237 'NHKs': NHKs,
238 'NHKr': NHKr,
239 'CKs': CKs,
240 'CKr': CKr,
241 'DHIs_priv': priv_identity_key,
242 'DHIs': identity_key,
243 'DHIr': DHIr,
244 'DHRs_priv': DHRs_priv,
245 'DHRs': DHRs,
246 'DHRr': DHRr,
247 'CONVid': CONVid,
248 'Ns': Ns,
249 'Nr': Nr,
250 'PNs': PNs,
251 'ratchet_flag': ratchet_flag,
252 }
253
254 return AxolotlConversation(self, keys, mode)
255
256 def encrypt(self, plaintext):
257 return self.conversation.encrypt(plaintext)
258
259 def enc(self, key, plaintext):
260 return encrypt_symmetric(key, plaintext)
261
262 def dec(self, key, encrypted):
263 return decrypt_symmetric(key, encrypted)
264
265 def decrypt(self, msg):
266 return self.conversation.decrypt(msg)
267
268 def encrypt_file(self, filename):
269 self.conversation.encrypt_file(filename)
270
271 def decrypt_file(self, filename):
272 self.conversation.decrypt_file(filename)
273
274 def encrypt_pipe(self):
275 self.conversation.encrypt_pipe()
276
277 def decrypt_pipe(self):
278 self.conversation.decrypt_pipe()
279
280 def printKeys(self):
281 self.conversation.print_keys()
282
283 def saveState(self):
284 self.save_conversation(self.conversation)
285
286 def save_conversation(self, conversation):
287 self.persistence.save_conversation(conversation)
288
289 def loadState(self, name, other_name):
290 self.persistence.db = self.openDB()
291 self.conversation = self.load_conversation(other_name, name)
292 if self.conversation:
293 return
294 else:
295 return False
296
297 def load_conversation(self, other_name, name=None):
298 return self.persistence.load_conversation(self,
299 name or self.name,
300 other_name)
301
302 def delete_conversation(self, conversation):
303 return self.persistence.delete_conversation(conversation)
304
305 def get_other_names(self):
306 return self.persistence.get_other_names(self.name)
307
308 def openDB(self):
309 return self.persistence._open_db()
310
311 def writeDB(self):
312 self.persistence.write_db()
313
314 def printState(self):
315 self.conversation.print_state()
316
317
318class AxolotlConversation:
319 def __init__(self, axolotl, keys, mode, staged_hk_mk=None):
320 self._axolotl = axolotl
321 self.lock = Lock()
322 self.keys = keys
323 self.mode = mode
324 self.staged_hk_mk = staged_hk_mk or dict()
325 self.staged = False
326
327 self.handshake_key = None
328 self.handshake_pkey = None
329
330 @property
331 def name(self):
332 return self.keys['name']
333
334 @name.setter
335 def name(self, name):
336 self.keys['name'] = name
337
338 @property
339 def other_name(self):
340 return self.keys['other_name']
341
342 @other_name.setter
343 def other_name(self, other_name):
344 self.keys['other_name'] = other_name
345
346 @property
347 def id_(self):
348 return self.keys['CONVid']
349
350 @id_.setter
351 def id_(self, id_):
352 self.keys['CONVid'] = id_
353
354 @property
355 def ns(self):
356 return self.keys['Ns']
357
358 @ns.setter
359 def ns(self, ns):
360 self.keys['Ns'] = ns
361
362 @property
363 def nr(self):
364 return self.keys['Nr']
365
366 @nr.setter
367 def nr(self, nr):
368 self.keys['Nr'] = nr
369
370 @property
371 def pns(self):
372 return self.keys['PNs']
373
374 @pns.setter
375 def pns(self, pns):
376 self.keys['PNs'] = pns
377
378 @property
379 def ratchet_flag(self):
380 return self.keys['ratchet_flag']
381
382 @ratchet_flag.setter
383 def ratchet_flag(self, ratchet_flag):
384 self.keys['ratchet_flag'] = ratchet_flag
385
386 def _try_skipped_mk(self, msg, pad_length):
387 msg1 = msg[:HEADER_LEN-pad_length]
388 msg2 = msg[HEADER_LEN:]
389 for skipped_mk in self.staged_hk_mk.values():
390 try:
391 decrypt_symmetric(skipped_mk.hk, msg1)
392 body = decrypt_symmetric(skipped_mk.mk, msg2)
393 except CryptoError:
394 pass
395 else:
396 del self.staged_hk_mk[skipped_mk.mk]
397 return body
398 return None
399
400 def _stage_skipped_mk(self, hkr, nr, np, ckr):
401 timestamp = int(time())
402 ckp = ckr
403 for i in range(np - nr):
404 mk = hash_(ckp + '0')
405 ckp = hash_(ckp + '1')
406 self.staged_hk_mk[mk] = SkippedMessageKey(mk, hkr, timestamp)
407 self.staged = True
408 mk = hash_(ckp + '0')
409 ckp = hash_(ckp + '1')
410 return ckp, mk
411
412 @sync
413 def encrypt(self, plaintext):
414 if self.ratchet_flag:
415 self.keys['DHRs_priv'], self.keys['DHRs'] = generate_keypair()
416 self.keys['HKs'] = self.keys['NHKs']
417 self.keys['RK'] = hash_(self.keys['RK'] +
418 generate_dh(self.keys['DHRs_priv'], self.keys['DHRr']))
419 self.keys['NHKs'] = kdf(self.keys['RK'], SALTS['NHK'][self.mode])
420 self.keys['CKs'] = kdf(self.keys['RK'], SALTS['CK'][self.mode])
421 self.pns = self.ns
422 self.ns = 0
423 self.ratchet_flag = False
424 mk = hash_(self.keys['CKs'] + '0')
425 msg1 = encrypt_symmetric(
426 self.keys['HKs'],
427 struct.pack('>I', self.ns) + struct.pack('>I', self.pns) +
428 self.keys['DHRs'])
429 msg2 = encrypt_symmetric(mk, plaintext)
430 pad_length = HEADER_LEN - len(msg1)
431 pad = os.urandom(pad_length - HEADER_PAD_NUM_LEN) + chr(pad_length)
432 msg = msg1 + pad + msg2
433 self.ns += 1
434 self.keys['CKs'] = hash_(self.keys['CKs'] + '1')
435 return msg
436
437 @sync
438 def decrypt(self, msg):
439 pad = msg[HEADER_LEN-HEADER_PAD_NUM_LEN:HEADER_LEN]
440 pad_length = ord(pad)
441 msg1 = msg[:HEADER_LEN-pad_length]
442
443 body = self._try_skipped_mk(msg, pad_length)
444 if body and body != '':
445 return body
446
447 header = None
448 if self.keys['HKr']:
449 try:
450 header = decrypt_symmetric(self.keys['HKr'], msg1)
451 except CryptoError:
452 pass
453 if header and header != '':
454 Np = struct.unpack('>I', header[:HEADER_COUNT_NUM_LEN])[0]
455 CKp, mk = self._stage_skipped_mk(self.keys['HKr'], self.nr, Np, self.keys['CKr'])
456 try:
457 body = decrypt_symmetric(mk, msg[HEADER_LEN:])
458 except CryptoError:
459 print('Undecipherable message')
460 sys.exit(1)
461 else:
462 try:
463 header = decrypt_symmetric(self.keys['NHKr'], msg1)
464 except CryptoError:
465 pass
466 if self.ratchet_flag or not header or header == '':
467 print('Undecipherable message')
468 sys.exit(1)
469 Np = struct.unpack('>I', header[:HEADER_COUNT_NUM_LEN])[0]
470 PNp = struct.unpack('>I', header[HEADER_COUNT_NUM_LEN:HEADER_COUNT_NUM_LEN*2])[0]
471 DHRp = header[HEADER_COUNT_NUM_LEN*2:]
472 if self.keys['CKr']:
473 self._stage_skipped_mk(self.keys['HKr'], self.nr, PNp, self.keys['CKr'])
474 HKp = self.keys['NHKr']
475 RKp = hash_(self.keys['RK'] + generate_dh(self.keys['DHRs_priv'], DHRp))
476 NHKp = kdf(RKp, SALTS['NHK'][not self.mode])
477 CKp = kdf(RKp, SALTS['CK'][not self.mode])
478 CKp, mk = self._stage_skipped_mk(HKp, 0, Np, CKp)
479 try:
480 body = decrypt_symmetric(mk, msg[HEADER_LEN:])
481 except CryptoError:
482 pass
483 if not body or body == '':
484 print('Undecipherable message')
485 sys.exit(1)
486 self.keys['RK'] = RKp
487 self.keys['HKr'] = HKp
488 self.keys['NHKr'] = NHKp
489 self.keys['DHRr'] = DHRp
490 self.keys['DHRs_priv'] = None
491 self.keys['DHRs'] = None
492 self.ratchet_flag = True
493 self.nr = Np + 1
494 self.keys['CKr'] = CKp
495 return body
496
497 def encrypt_file(self, filename):
498 with open(filename, 'r') as f:
499 plaintext = f.read()
500 ciphertext = b2a(self.encrypt(plaintext)) + '\n'
501 with open(filename+'.asc', 'w') as f:
502 lines = [ciphertext[i:i+64] for i in xrange(0, len(ciphertext), 64)]
503 for line in lines:
504 f.write(line+'\n')
505
506 def decrypt_file(self, filename):
507 with open(filename, 'r') as f:
508 ciphertext = a2b(f.read())
509 plaintext = self.decrypt(ciphertext)
510 print(plaintext)
511
512 def encrypt_pipe(self):
513 plaintext = sys.stdin.read()
514 ciphertext = b2a(self.encrypt(plaintext)) + '\n'
515 sys.stdout.write(ciphertext)
516 sys.stdout.flush()
517
518 def decrypt_pipe(self):
519 ciphertext = a2b(sys.stdin.read())
520 plaintext = self.decrypt(ciphertext)
521 sys.stdout.write(plaintext)
522 sys.stdout.flush()
523
524 def save(self):
525 self._axolotl.save_conversation(self)
526
527 def delete(self):
528 self._axolotl.delete_conversation(self)
529
530 def print_keys(self):
531 print('Your Identity key is:\n' + b2a(self.keys['DHIs']) + '\n')
532 fingerprint = hash_(self.keys['DHIs']).encode('hex').upper()
533 fprint = ''
534 for i in range(0, len(fingerprint), 4):
535 fprint += fingerprint[i:i+2] + ':'
536 print('Your identity key fingerprint is: ')
537 print(fprint[:-1] + '\n')
538 print('Your Ratchet key is:\n' + b2a(self.keys['DHRs']) + '\n')
539 if self.handshake_key:
540 print('Your Handshake key is:\n' + b2a(self.handshake_pkey))
541 else:
542 print('Your Handshake key is not available')
543
544 def print_state(self):
545 print()
546 print('Warning: saving this data to disk is insecure!')
547 print()
548 for key in sorted(self.keys):
549 if 'priv' in key:
550 pass
551 else:
552 if self.keys[key] is None:
553 print(key + ': None')
554 elif type(self.keys[key]) is bool:
555 if self.keys[key]:
556 print(key + ': True')
557 else:
558 print(key + ': False')
559 elif type(self.keys[key]) is str:
560 try:
561 self.keys[key].decode('ascii')
562 print(key + ': ' + self.keys[key])
563 except UnicodeDecodeError:
564 print(key + ': ' + b2a(self.keys[key]))
565 else:
566 print(key + ': ' + str(self.keys[key]))
567 if self.mode is ALICE_MODE:
568 print('Mode: Alice')
569 else:
570 print('Mode: Bob')
571
572
573class SkippedMessageKey:
574 def __init__(self, mk, hk, timestamp):
575 self.mk = mk
576 self.hk = hk
577 self.timestamp = timestamp
578
579
580class SqlitePersistence(object):
581 def __init__(self, dbname, dbpassphrase, store_time, nonthreaded):
582 super(SqlitePersistence, self).__init__()
583 self.lock = Lock()
584 self.dbname = dbname
585 self.dbpassphrase = dbpassphrase
586 self.store_time = store_time
587 self.nonthreaded = nonthreaded
588
589 self.db = self._open_db()
590
591 def _open_db(self):
592 db = sqlite3.connect(':memory:', check_same_thread=self.nonthreaded)
593 db.row_factory = sqlite3.Row
594
595 with db:
596 try:
597 if self.dbpassphrase is not None:
598 with open(self.dbname, 'rb') as f:
599 crypt_sql = f.read()
600 try:
601 sql = decrypt_symmetric(self.dbpassphrase,
602 crypt_sql)
603 except CryptoError:
604 print('Bad passphrase!')
605 sys.exit(1)
606 else:
607 db.cursor().executescript(sql)
608 else:
609 with open(self.dbname, 'r') as f:
610 sql = f.read()
611 try:
612 db.cursor().executescript(sql)
613 except sqlite3.OperationalError:
614 print('Bad sql! Password problem - cannot create the database.')
615 sys.exit(1)
616 except IOError as e:
617 if e.errno == errno.ENOENT:
618 self._create_db(db)
619 else:
620 raise
621 else:
622 self._delete_expired_skipped_mk(db)
623 return db
624
625 def _create_db(self, db):
626 db.execute('''
627 CREATE TABLE IF NOT EXISTS
628 skipped_mk (
629 my_identity,
630 to_identity,
631 HKr TEXT,
632 mk TEXT,
633 timestamp INTEGER)''')
634 db.execute('''
635 CREATE UNIQUE INDEX IF NOT EXISTS
636 message_keys
637 ON
638 skipped_mk (mk)''')
639 db.execute('''
640 CREATE TABLE IF NOT EXISTS
641 conversations (
642 my_identity TEXT,
643 other_identity TEXT,
644 RK TEXT,
645 HKs TEXT,
646 HKr TEXT,
647 NHKs TEXT,
648 NHKr TEXT,
649 CKs TEXT,
650 CKr TEXT,
651 DHIs_priv TEXT,
652 DHIs TEXT,
653 DHIr TEXT,
654 DHRs_priv TEXT,
655 DHRs TEXT,
656 DHRr TEXT,
657 CONVid TEXT,
658 Ns INTEGER,
659 Nr INTEGER,
660 PNs INTEGER,
661 ratchet_flag INTEGER,
662 mode INTEGER)''')
663 db.execute('''
664 CREATE UNIQUE INDEX IF NOT EXISTS
665 conversation_route
666 ON
667 conversations (
668 my_identity,
669 other_identity)''')
670
671 def _delete_expired_skipped_mk(self, db):
672 timestamp = int(time())
673 rowtime = timestamp - self.store_time
674 db.execute('''
675 DELETE FROM
676 skipped_mk
677 WHERE
678 timestamp < ?''', (rowtime,))
679
680 def _commit_skipped_mk(self, conversation):
681 with self.db as db:
682 db.execute('''
683 DELETE FROM
684 skipped_mk
685 WHERE
686 my_identity = ? AND
687 to_identity = ?''', (
688 conversation.name,
689 conversation.other_name))
690 for skipped_mk in conversation.staged_hk_mk.values():
691 db.execute('''
692 INSERT INTO
693 skipped_mk (
694 my_identity,
695 to_identity,
696 HKr,
697 mk,
698 timestamp)
699 VALUES (?, ?, ?, ?, ?)''', (
700 conversation.name,
701 conversation.other_name,
702 b2a(skipped_mk.hk),
703 b2a(skipped_mk.mk),
704 skipped_mk.timestamp))
705
706 def _load_skipped_mk(self, name, other_name):
707 skipped_hk_mk = dict()
708 with self.db as db:
709 rows = db.execute('''
710 SELECT
711 *
712 FROM
713 skipped_mk
714 WHERE
715 my_identity = ? AND
716 to_identity = ?''', (name, other_name))
717 for row in rows:
718 mk = a2b(row['mk'])
719 skipped_hk_mk[mk] = SkippedMessageKey(mk,
720 hk=a2b(row['hkr']),
721 timestamp=row['timestamp'])
722 return skipped_hk_mk
723
724 def write_db(self):
725 with self.db as db:
726 sql = bytes('\n'.join(db.iterdump()))
727 if self.dbpassphrase is not None:
728 crypt_sql = encrypt_symmetric(self.dbpassphrase, sql)
729 with open(self.dbname, 'wb') as f:
730 f.write(crypt_sql)
731 else:
732 with open(self.dbname, 'w') as f:
733 f.write(sql)
734
735 @sync
736 def save_conversation(self, conversation):
737 HKs = 0 if conversation.keys['HKs'] is None else b2a(conversation.keys['HKs'])
738 HKr = 0 if conversation.keys['HKr'] is None else b2a(conversation.keys['HKr'])
739 CKs = 0 if conversation.keys['CKs'] is None else b2a(conversation.keys['CKs'])
740 CKr = 0 if conversation.keys['CKr'] is None else b2a(conversation.keys['CKr'])
741 DHIr = 0 if conversation.keys['DHIr'] is None else b2a(conversation.keys['DHIr'])
742 DHRs_priv = 0 if conversation.keys['DHRs_priv'] is None else b2a(conversation.keys['DHRs_priv'])
743 DHRs = 0 if conversation.keys['DHRs'] is None else b2a(conversation.keys['DHRs'])
744 DHRr = 0 if conversation.keys['DHRr'] is None else b2a(conversation.keys['DHRr'])
745 ratchet_flag = 1 if conversation.ratchet_flag else 0
746 mode = 1 if conversation.mode else 0
747 with self.db as db:
748 db.execute('''
749 REPLACE INTO
750 conversations (
751 my_identity,
752 other_identity,
753 RK,
754 HKS,
755 HKr,
756 NHKs,
757 NHKr,
758 CKs,
759 CKr,
760 DHIs_priv,
761 DHIs,
762 DHIr,
763 DHRs_priv,
764 DHRs,
765 DHRr,
766 CONVid,
767 Ns,
768 Nr,
769 PNs,
770 ratchet_flag,
771 mode)
772 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
773 ?, ?, ?)''', (
774 conversation.name,
775 conversation.other_name,
776 b2a(conversation.keys['RK']),
777 HKs,
778 HKr,
779 b2a(conversation.keys['NHKs']),
780 b2a(conversation.keys['NHKr']),
781 CKs,
782 CKr,
783 b2a(conversation.keys['DHIs_priv']),
784 b2a(conversation.keys['DHIs']),
785 DHIr,
786 DHRs_priv,
787 DHRs,
788 DHRr,
789 b2a(conversation.keys['CONVid']),
790 conversation.ns,
791 conversation.nr,
792 conversation.pns,
793 ratchet_flag,
794 mode))
795 self._commit_skipped_mk(conversation)
796 self.write_db()
797
798 @sync
799 def load_conversation(self, axolotl, name, other_name):
800 with self.db as db:
801 cur = db.cursor()
802 cur.execute('''
803 SELECT
804 *
805 FROM
806 conversations
807 WHERE
808 my_identity = ? AND
809 other_identity = ?''', (name, other_name))
810 row = cur.fetchone()
811 if row:
812 keys = \
813 { 'name': row['my_identity'],
814 'other_name': row['other_identity'],
815 'RK': a2b(row['rk']),
816 'NHKs': a2b(row['nhks']),
817 'NHKr': a2b(row['nhkr']),
818 'DHIs_priv': a2b(row['dhis_priv']),
819 'DHIs': a2b(row['dhis']),
820 'CONVid': a2b(row['convid']),
821 'Ns': row['ns'],
822 'Nr': row['nr'],
823 'PNs': row['pns'],
824 }
825 keys['HKs'] = None if row['hks'] == '0' else a2b(row['hks'])
826 keys['HKr'] = None if row['hkr'] == '0' else a2b(row['hkr'])
827 keys['CKs'] = None if row['cks'] == '0' else a2b(row['cks'])
828 keys['CKr'] = None if row['ckr'] == '0' else a2b(row['ckr'])
829 keys['DHIr'] = None if row['dhir'] == '0' else a2b(row['dhir'])
830 keys['DHRs_priv'] = None if row['dhrs_priv'] == '0' else a2b(row['dhrs_priv'])
831 keys['DHRs'] = None if row['dhrs'] == '0' else a2b(row['dhrs'])
832 keys['DHRr'] = None if row['dhrr'] == '0' else a2b(row['dhrr'])
833 ratchet_flag = row['ratchet_flag']
834 keys['ratchet_flag'] = True if ratchet_flag == 1 \
835 else False
836 mode = row['mode']
837 mode = True if mode == 1 else False
838
839 skipped_hk_mk = self._load_skipped_mk(name, other_name)
840
841 # exit at first match
842 return AxolotlConversation(axolotl, keys, mode, skipped_hk_mk)
843 else:
844 # if no matches
845 return None
846
847 @sync
848 def delete_conversation(self, conversation):
849 with self.db as db:
850 db.execute('''
851 DELETE FROM
852 skipped_mk
853 WHERE
854 to_identity = ?''', (conversation.other_name,))
855 db.execute('''
856 DELETE FROM
857 conversations
858 WHERE
859 other_identity = ?''', (conversation.other_name,))
860 self.write_db()
861
862 @sync
863 def get_other_names(self, name):
864 with self.db as db:
865 rows = db.execute('''
866 SELECT
867 other_identity
868 FROM
869 conversations
870 WHERE
871 my_identity = ?''', (name,))
872 return [row['other_identity'] for row in rows]
873
874
875def a2b(a):
876 return Base64Encoder.decode(a)
877
878
879def b2a(b):
880 return Base64Encoder.encode(b)
881
882
883def hash_(data):
884 return sha256(data).decode('hex')
885
886
887def kdf(secret, salt):
888 return pbkdf2(secret, salt, rounds=10, prf='hmac-sha256')
889
890
891Keypair = namedtuple('Keypair', 'priv pub')
892
893
894def generate_keypair():
895 privkey = PrivateKey.generate()
896 return Keypair(bytes(privkey), bytes(privkey.public_key))
897
898
899def generate_dh(a, b):
900 a = PrivateKey(a)
901 b = PublicKey(b)
902 return bytes(Box(a, b))
903
904
905def generate_3dh(a, a0, b, b0, mode=ALICE_MODE):
906 if mode is ALICE_MODE:
907 return hash_(generate_dh(a, b0) +
908 generate_dh(a0, b) +
909 generate_dh(a0, b0))
910 else:
911 return hash_(generate_dh(a0, b) +
912 generate_dh(a, b0) +
913 generate_dh(a0, b0))
914
915
916def encrypt_symmetric(key, plaintext):
917 nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE)
918 box = nacl.secret.SecretBox(key)
919 return bytes(box.encrypt(plaintext, nonce))
920
921
922def decrypt_symmetric(key, ciphertext):
923 box = nacl.secret.SecretBox(key)
924 return box.decrypt(ciphertext)