Support multi-bot secure key exchange

This commit is contained in:
Tanner Collin 2019-09-11 04:31:44 +00:00
parent b95c6593c8
commit 377e561764
3 changed files with 78 additions and 21 deletions

View File

@ -16,21 +16,37 @@ from bwb.molly import bwb
### Handshaking ### Handshaking
On boot up, send `000000handshake [secret]` to BWB chat: Boot up:
```text ```text
secret = bwb.init() client.send_message(BOT_WITH_BOT, '000000init ' + bwb.init())
await client.send_message(BWB, '000000handshake ' + secret)
``` ```
When you see a `000000handshake [secret data]`: On `000000init [data]`:
```text ```text
bwb.init(secret_data) client.send_message(BOT_WITH_BOT, '000000handshake ' + bwb.handshake(data))
await client.send_message(BWB, bwb.wrap('🤝'))
``` ```
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 ### Interaction

View File

@ -13,20 +13,27 @@ from cryptography.hazmat.primitives.ciphers.algorithms import AES
from cryptography.hazmat.primitives.ciphers.modes import CBC from cryptography.hazmat.primitives.ciphers.modes import CBC
from cryptography.hazmat.primitives.padding import PKCS7 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 OTP_LOOKAHEAD = 25
class common: class common:
def __init__(self): def __init__(self):
self.init_secret = ''
self.key = None
self.master_pub = None
self.otp = None self.otp = None
self.secret = '' self.otp_secret = ''
self.otp_count = 0 self.otp_count = 0
def init(self, secret=None): def set_otp(self, otp_secret):
secret = secret or ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(12)) self.otp = HOTP(otp_secret.encode(), 6, SHA1(), backend=default_backend(), enforce_key_length=False)
self.otp = HOTP(secret.encode(), 6, SHA1(), backend=default_backend(), enforce_key_length=False) self.otp_secret = otp_secret
self.secret = secret
self.otp_count = 0 self.otp_count = 0
return secret return True
def get_otp(self, target): def get_otp(self, target):
if not self.otp: if not self.otp:
@ -64,7 +71,7 @@ class common:
return False return False
def to_b58(self, text): def to_b58(self, text):
return 'l' + base58.b58encode(text.encode()).decode() return 'l' + base58.b58encode(text).decode()
def from_b58(self, text): def from_b58(self, text):
if not text.startswith('l'): if not text.startswith('l'):
@ -74,8 +81,10 @@ class common:
except BaseException as e: except BaseException as e:
return False return False
def enc(self, text): def enc(self, text, key=None):
if not self.secret: if not key:
key = self.otp_secret.encode()
if not key:
raise RuntimeError('bwb not init yet') raise RuntimeError('bwb not init yet')
padder = PKCS7(AES.block_size).padder() padder = PKCS7(AES.block_size).padder()
@ -83,7 +92,7 @@ class common:
padded += padder.finalize() padded += padder.finalize()
digest = Hash(SHA256(), default_backend()) digest = Hash(SHA256(), default_backend())
digest.update(self.secret.encode()) digest.update(key)
key = digest.finalize() key = digest.finalize()
iv = os.urandom(4) iv = os.urandom(4)
@ -94,8 +103,10 @@ class common:
return 'I' + base58.b58encode(iv + ct).decode() return 'I' + base58.b58encode(iv + ct).decode()
def dec(self, ciphertext): def dec(self, ciphertext, key=None):
if not self.secret: if not key:
key = self.otp_secret.encode()
if not key:
return False # so we can run every message through return False # so we can run every message through
if not ciphertext.startswith('I'): if not ciphertext.startswith('I'):
return False return False
@ -106,7 +117,7 @@ class common:
ct = ciphertext[4:] ct = ciphertext[4:]
digest = Hash(SHA256(), default_backend()) digest = Hash(SHA256(), default_backend())
digest.update(self.secret.encode()) digest.update(key)
key = digest.finalize() key = digest.finalize()
cipher = Cipher(AES(key), CBC(iv * 4), default_backend()) cipher = Cipher(AES(key), CBC(iv * 4), default_backend())
@ -119,6 +130,7 @@ class common:
return unpadded.decode() return unpadded.decode()
except BaseException as e: except BaseException as e:
print(e)
return False return False
def wrap(self, text, target=None, b58=False, enc=False): def wrap(self, text, target=None, b58=False, enc=False):
@ -149,3 +161,32 @@ class common:
return True return True
else: else:
return False 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)

View File

@ -5,7 +5,7 @@ with io.open('README.md', encoding='utf-8') as fh:
long_description = fh.read() long_description = fh.read()
setuptools.setup(name='bwb', setuptools.setup(name='bwb',
version='1.1.0', version='2.0.0',
description='bwb', description='bwb',
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',