From 13e39316edb230b037145d3fd9f0947f5dbd23b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juhani=20Krekel=C3=A4?= Date: Sun, 5 Jan 2020 13:44:57 +0200 Subject: [PATCH] Verify that the strings match --- kishib.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/kishib.py b/kishib.py index f60d57c..4505219 100644 --- a/kishib.py +++ b/kishib.py @@ -1,5 +1,7 @@ import base64 +import binascii import hashlib +import secrets import socket import sys @@ -118,16 +120,79 @@ def chunk(sliceable, length): for i in range(0, len(sliceable), length): yield sliceable[i:i + length] +def b32_encode(binary): + # Kishib uses a modified base32 compared to RFC 4648 + # * All letters are lowercase + # * 'i' is replaced with '8' and 'o' is replaced with '9' + encoded = base64.b32encode(binary).decode() + encoded = encoded.lower() + encoded = encoded.replace('i', '8').replace('o', '9') + return encoded + def format_hash(hash_bytes): - hash_base32 = base64.b32encode(hash_bytes).decode().lower().replace('=', '') + hash_base32 = b32_encode(hash_bytes) chunked_base32 = chunk(hash_base32, 4) return '-'.join(chunked_base32) +class HashFormatError(Exception): pass +class HashChecksumError(Exception): pass + +def parse_hash(auth_hash): + # Hash consists of 8 segments of four characters + segments = auth_hash.split('-') + if len(segments) != 8: + raise HashFormatError + if not all(len(i) == 4 for i in segments): + raise HashFormatError + + combined = ''.join(segments) + if 'i' in combined or 'o' in combined: + raise HashFormatError + + standard_base32 = combined.replace('8', 'i').replace('9', 'o').upper() + + try: + binary = base64.b32decode(standard_base32) + except binascii.Error as err: + raise HashFormatError from err + + truncated_hash = binary[:16] + hash_check = binary[16:] + + # Using secrets.compare_digest is not necessary, but I feel it is a + # good habit to avoid comparing hashes with variable-timed comparisons + if not secrets.compare_digest(sha512(truncated_hash)[:4], hash_check): + raise HashChecksumError + + return binary + def verify(client_pubkey, server_pubkey): own_hash = auth_hash(client_pubkey, server_pubkey) print('Authentication hash: %s' % format_hash(own_hash)) - # TODO: Actually verify + while True: + user_input = input('Do the hashes match (yes/no/[paste])? ') + + if user_input == 'no': + error('Could not transfer the keys') + + elif user_input == 'yes': + return + + else: + try: + other_hash = parse_hash(user_input) + except HashFormatError: + print('Expected \'yes\', \'no\' or a base32-encoded hash') + continue + except HashChecksumError: + print('Hash checksum check failed') + continue + + if secrets.compare_digest(own_hash, other_hash): + return + else: + error('Could not transfer the keys') def main(): # TODO: Actual agument parsing