From 377e56176439c9c6dedd11f312754c5fcaa46504 Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Wed, 11 Sep 2019 04:31:44 +0000 Subject: [PATCH] Support multi-bot secure key exchange --- README.md | 30 +++++++++++++++++------ bwb/common.py | 67 +++++++++++++++++++++++++++++++++++++++++---------- setup.py | 2 +- 3 files changed, 78 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 24bb216..ffb7cf5 100644 --- a/README.md +++ b/README.md @@ -16,21 +16,37 @@ from bwb.molly import bwb ### Handshaking -On boot up, send `000000handshake [secret]` to BWB chat: +Boot up: ```text -secret = bwb.init() -await client.send_message(BWB, '000000handshake ' + secret) +client.send_message(BOT_WITH_BOT, '000000init ' + bwb.init()) ``` -When you see a `000000handshake [secret data]`: +On `000000init [data]`: ```text -bwb.init(secret_data) -await client.send_message(BWB, bwb.wrap('🤝')) +client.send_message(BOT_WITH_BOT, '000000handshake ' + bwb.handshake(data)) ``` -When you see and authed '🤝', reply with *unauthed* '🤝'. +On `000000handshake [data]`: + +```text +client.send_message(BOT_WITH_BOT, bwb.wrap('secret ' + bwb.secret(data))) +bwb.set_otp(bwb.init_secret) +``` + +On _OTP authed_ `123456secret [data]`: + +```text +bwb.set_secret(data) +client.send_message(BOT_WITH_BOT, bwb.wrap('🤝')) +``` + +On _OTP authed_ `123456🤝`: + +```text +client.send_message(BOT_WITH_BOT, '🤝') +``` ### Interaction diff --git a/bwb/common.py b/bwb/common.py index 2c22253..b83b0f7 100644 --- a/bwb/common.py +++ b/bwb/common.py @@ -13,20 +13,27 @@ from cryptography.hazmat.primitives.ciphers.algorithms import AES from cryptography.hazmat.primitives.ciphers.modes import CBC from cryptography.hazmat.primitives.padding import PKCS7 +from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat +from cryptography.hazmat.primitives.asymmetric.ec import ECDH +from cryptography.hazmat.primitives.kdf.hkdf import HKDF + OTP_LOOKAHEAD = 25 class common: def __init__(self): + self.init_secret = '' + self.key = None + self.master_pub = None self.otp = None - self.secret = '' + self.otp_secret = '' self.otp_count = 0 - def init(self, secret=None): - secret = secret or ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(12)) - self.otp = HOTP(secret.encode(), 6, SHA1(), backend=default_backend(), enforce_key_length=False) - self.secret = secret + def set_otp(self, otp_secret): + self.otp = HOTP(otp_secret.encode(), 6, SHA1(), backend=default_backend(), enforce_key_length=False) + self.otp_secret = otp_secret self.otp_count = 0 - return secret + return True def get_otp(self, target): if not self.otp: @@ -64,7 +71,7 @@ class common: return False def to_b58(self, text): - return 'l' + base58.b58encode(text.encode()).decode() + return 'l' + base58.b58encode(text).decode() def from_b58(self, text): if not text.startswith('l'): @@ -74,8 +81,10 @@ class common: except BaseException as e: return False - def enc(self, text): - if not self.secret: + def enc(self, text, key=None): + if not key: + key = self.otp_secret.encode() + if not key: raise RuntimeError('bwb not init yet') padder = PKCS7(AES.block_size).padder() @@ -83,7 +92,7 @@ class common: padded += padder.finalize() digest = Hash(SHA256(), default_backend()) - digest.update(self.secret.encode()) + digest.update(key) key = digest.finalize() iv = os.urandom(4) @@ -94,8 +103,10 @@ class common: return 'I' + base58.b58encode(iv + ct).decode() - def dec(self, ciphertext): - if not self.secret: + def dec(self, ciphertext, key=None): + if not key: + key = self.otp_secret.encode() + if not key: return False # so we can run every message through if not ciphertext.startswith('I'): return False @@ -106,7 +117,7 @@ class common: ct = ciphertext[4:] digest = Hash(SHA256(), default_backend()) - digest.update(self.secret.encode()) + digest.update(key) key = digest.finalize() cipher = Cipher(AES(key), CBC(iv * 4), default_backend()) @@ -119,6 +130,7 @@ class common: return unpadded.decode() except BaseException as e: + print(e) return False def wrap(self, text, target=None, b58=False, enc=False): @@ -149,3 +161,32 @@ class common: return True else: return False + + def get_pub(self): + self.key = X25519PrivateKey.generate() + pub_key = self.key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + return base58.b58encode(pub_key).decode() + + def init(self): + self.init_secret = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(14)) + return self.get_pub() + + def handshake(self, text): + self.master_pub = X25519PublicKey.from_public_bytes(base58.b58decode(text)) + slave_pub = self.get_pub() + self.set_otp(slave_pub) + return slave_pub + + def secret(self, text): + if not self.init_secret: return + self.set_otp(text) + slave_pub = X25519PublicKey.from_public_bytes(base58.b58decode(text)) + shared_key = self.key.exchange(slave_pub) + derived_key = HKDF(SHA256(), length=32, salt=b'Qot.', info=None, backend=default_backend()).derive(shared_key) + return self.enc(self.init_secret, derived_key) + + def set_secret(self, text): + shared_key = self.key.exchange(self.master_pub) + derived_key = HKDF(SHA256(), length=32, salt=b'Qot.', info=None, backend=default_backend()).derive(shared_key) + otp_secret = self.dec(text, derived_key) + return self.set_otp(otp_secret) diff --git a/setup.py b/setup.py index c97d295..a2426a2 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with io.open('README.md', encoding='utf-8') as fh: long_description = fh.read() setuptools.setup(name='bwb', - version='1.1.0', + version='2.0.0', description='bwb', long_description=long_description, long_description_content_type='text/markdown',