#!/usr/bin/env python3

import hashlib
import re

def normalize_serial(serial):
    '''
    Given a human-read serial number, provide a consistent string.

    Collapse some similar-looking characters to reduce the risk of disagreement
    on the serial number between the person who reads the computer's serial
    number at provision time and the person who reads it after the loan has
    been paid off.

    This will, in theory, result in the same BIOS password for two distinct
    serial numbers. However, it's unlikely that a manufacturer will have two
    distinct, similar-looking serial numbers for the same reason we collapse
    them. And even more unlikely an attacker will obtain more than one computer
    in a set of similar-looking-serial-numbered computers to exploit this
    weakness. We assume it's far, far more likely that the two serial number
    reads described above will result in diverging reads.

    This assumes that serial numbers are limited to the regex below.
    '''

    # validate
    # --------
    pattern = re.compile(r'[0-9A-Za-z_ -]')
    unexpected_chars = pattern.sub('', serial)

    errmsg = "serial number has unexpected characters: '{}'".format(
            unexpected_chars)
    assert not unexpected_chars, errmsg

    # normalize
    # --------
    serial = serial.lower()

    # strip "spacing" characters in case users don't know how to describe the
    # characters, omit them, etc.
    serial = serial.replace('-', '')
    serial = serial.replace('_', '')
    serial = serial.replace(' ', '')

    # collapse similar-looking characters
    #
    # note: these substitutions are mostly/entirely to remove ambiguity between
    # the capital letters and the numbers but they're written in lower case
    # (after lowering the case) to make each pair more visually-distinct here
    #
    # as a rule, this transforms letter -> number
    serial = serial.replace('g', '6')
    serial = serial.replace('i', '1')
    serial = serial.replace('l', '1')
    serial = serial.replace('o', '0')
    serial = serial.replace('s', '5')
    serial = serial.replace('z', '2')

    return serial

def get_salt(salt_file):
    # randomly-generated salt so users can't trivially pass their serial number
    # through sha256 and read the digest
    with open(salt_file) as f:
        salt = f.read()
    return salt.strip()

def main(salt_file, serial_raw):
    # arbitrary choice that attempts to balance sufficiently-long vs. "easy to
    # input". Most likely, this will only need to be input two or fewer times in
    # the lifetime of the computer
    RESET_CODE_LENGTH = 16

    salt = get_salt(salt_file)
    serial = normalize_serial(serial_raw)
    hash_msg = hashlib.sha256()
    hash_msg.update(salt.encode())
    hash_msg.update(serial.encode())

    hash = ''.join(c for c in hash_msg.hexdigest())[:RESET_CODE_LENGTH]
    print(hash)

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(
            description='generate BIOS password for a given serial number')
    parser.add_argument('salt_file', metavar='SALT_FILE')
    parser.add_argument('serial_number', metavar='SERIAL_NUMBER')
    args = parser.parse_args()

    main(args.salt_file, args.serial_number)
