From 75c7817694d4750458cba5583a74ed7cad3944cc Mon Sep 17 00:00:00 2001 From: caternuson Date: Mon, 19 Jul 2021 16:29:52 -0700 Subject: [PATCH] add Macropad 2FA TOTP code --- Macropad_2FA_TOTP/macropad_totp.py | 270 +++++++++++++++++++++ Macropad_2FA_TOTP/rtc_setter.py | 35 +++ Macropad_2FA_TOTP/secrcode_28.bdf | 370 +++++++++++++++++++++++++++++ Macropad_2FA_TOTP/secrets.py | 19 ++ 4 files changed, 694 insertions(+) create mode 100755 Macropad_2FA_TOTP/macropad_totp.py create mode 100755 Macropad_2FA_TOTP/rtc_setter.py create mode 100644 Macropad_2FA_TOTP/secrcode_28.bdf create mode 100755 Macropad_2FA_TOTP/secrets.py diff --git a/Macropad_2FA_TOTP/macropad_totp.py b/Macropad_2FA_TOTP/macropad_totp.py new file mode 100755 index 000000000..1b38744d2 --- /dev/null +++ b/Macropad_2FA_TOTP/macropad_totp.py @@ -0,0 +1,270 @@ +import time +# base hardware stuff +import board +import rtc +import keypad +import rotaryio +import neopixel +# crypto stuff +import adafruit_pcf8523 +import adafruit_hashlib as hashlib +# UI stuff +import displayio +import terminalio +from adafruit_bitmap_font import bitmap_font +from adafruit_display_text import label +from adafruit_progressbar.horizontalprogressbar import HorizontalProgressBar +# HID keyboard stuff +import usb_hid +from adafruit_hid.keyboard import Keyboard +from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS +from adafruit_hid.keycode import Keycode + +#--| User Config |-------------------------------------------------------- +UTC_OFFSET = -4 # time zone offset +USE_12HR = True # set 12/24 hour format +DISPLAY_TIMEOUT = 60 # screen saver timeout in seconds +DISPLAY_RATE = 1 # screen refresh rate +#------------------------------------------------------------------------- + +# TODO: remove this once this is resolved: +# https://github.com/adafruit/circuitpython/issues/4893 +# and this gets merged: +# https://github.com/adafruit/circuitpython/pull/4961 +EPOCH_OFFSET = 946684800 # delta from above issue thread + +# Get sekrets from a secrets.py file +try: + from secrets import secrets + totp_keys = secrets["totp_keys"] +except ImportError: + print("Secrets are kept in secrets.py, please add them there!") + raise +except KeyError: + print("TOTP info not found in secrets.py.") + raise + +# set board to use PCF8523 as its RTC +pcf = adafruit_pcf8523.PCF8523(board.I2C()) +rtc.set_time_source(pcf) + +#------------------------------------------------------------------------- +# H I D S E T U P +#------------------------------------------------------------------------- +time.sleep(1) # Sleep for a bit to avoid a race condition on some systems +keyboard = Keyboard(usb_hid.devices) +keyboard_layout = KeyboardLayoutUS(keyboard) # We're in the US :) + +#------------------------------------------------------------------------- +# D I S P L A Y S E T U P +#------------------------------------------------------------------------- +display = board.DISPLAY + +# Secret Code font by Matthew Welch +# http://www.squaregear.net/fonts/ +font = bitmap_font.load_font("/secrcode_28.bdf") + +name = label.Label(terminalio.FONT, text="?"*18, color=0xFFFFFF) +name.anchor_point = (0.0, 0.0) +name.anchored_position = (0, 0) + +code = label.Label(font, text="123456", color=0xFFFFFF) +code.anchor_point = (0.5, 0.0) +code.anchored_position = (display.width // 2, 15) + +rtc_date = label.Label(terminalio.FONT, text="2021/01/01") +rtc_date.anchor_point = (0.0, 0.5) +rtc_date.anchored_position = (0, 49) + +rtc_time = label.Label(terminalio.FONT, text="12:34:56 AM") +rtc_time.anchor_point = (0.0, 0.5) +rtc_time.anchored_position = (0, 59) + +progress_bar = HorizontalProgressBar((68, 46), (55, 17), bar_color=0xFFFFFF, min_value=0, max_value=30) + +splash = displayio.Group() +splash.append(name) +splash.append(code) +splash.append(rtc_date) +splash.append(rtc_time) +splash.append(progress_bar) + +display.show(splash) + +#------------------------------------------------------------------------- +# H E L P E R F U N C S +#------------------------------------------------------------------------- +def timebase(timetime): + return (timetime + EPOCH_OFFSET - (UTC_OFFSET*3600)) // 30 + +def compute_codes(timestamp): + codes = [] + for key in totp_keys: + if key: + codes.append(generate_otp(timestamp, key[1])) + else: + codes.append(None) + return codes + +def HMAC(k, m): + """# HMAC implementation, as hashlib/hmac wouldn't fit + From https://en.wikipedia.org/wiki/Hash-based_message_authentication_code + + """ + SHA1_BLOCK_SIZE = 64 + KEY_BLOCK = k + (b'\0' * (SHA1_BLOCK_SIZE - len(k))) + KEY_INNER = bytes((x ^ 0x36) for x in KEY_BLOCK) + KEY_OUTER = bytes((x ^ 0x5C) for x in KEY_BLOCK) + inner_message = KEY_INNER + m + outer_message = KEY_OUTER + hashlib.sha1(inner_message).digest() + return hashlib.sha1(outer_message) + +def base32_decode(encoded): + missing_padding = len(encoded) % 8 + if missing_padding != 0: + encoded += '=' * (8 - missing_padding) + encoded = encoded.upper() + chunks = [encoded[i:i + 8] for i in range(0, len(encoded), 8)] + + out = [] + for chunk in chunks: + bits = 0 + bitbuff = 0 + for c in chunk: + if 'A' <= c <= 'Z': + n = ord(c) - ord('A') + elif '2' <= c <= '7': + n = ord(c) - ord('2') + 26 + elif n == '=': + continue + else: + raise ValueError("Not base32") + # 5 bits per 8 chars of base32 + bits += 5 + # shift down and add the current value + bitbuff <<= 5 + bitbuff |= n + # great! we have enough to extract a byte + if bits >= 8: + bits -= 8 + byte = bitbuff >> bits # grab top 8 bits + bitbuff &= ~(0xFF << bits) # and clear them + out.append(byte) # store what we got + return out + +def int_to_bytestring(int_val, padding=8): + result = [] + while int_val != 0: + result.insert(0, int_val & 0xFF) + int_val >>= 8 + result = [0] * (padding - len(result)) + result + return bytes(result) + +def generate_otp(int_input, secret_key, digits=6): + """ HMAC -> OTP generator, pretty much same as + https://github.com/pyotp/pyotp/blob/master/src/pyotp/otp.py + + """ + if int_input < 0: + raise ValueError('input must be positive integer') + hmac_hash = bytearray( + HMAC(bytes(base32_decode(secret_key)), + int_to_bytestring(int_input)).digest() + ) + offset = hmac_hash[-1] & 0xf + code = ((hmac_hash[offset] & 0x7f) << 24 | + (hmac_hash[offset + 1] & 0xff) << 16 | + (hmac_hash[offset + 2] & 0xff) << 8 | + (hmac_hash[offset + 3] & 0xff)) + str_code = str(code % 10 ** digits) + while len(str_code) < digits: + str_code = '0' + str_code + + return str_code + +#------------------------------------------------------------------------- +# M A C R O P A D S E T U P +#------------------------------------------------------------------------- +key_pins = ( + board.KEY1, + board.KEY2, + board.KEY3, + board.KEY4, + board.KEY5, + board.KEY6, + board.KEY7, + board.KEY8, + board.KEY9, + board.KEY10, + board.KEY11, + board.KEY12, + board.BUTTON, +) + +keys = keypad.Keys(key_pins, value_when_pressed=False, pull=True) + +knob = rotaryio.IncrementalEncoder(board.ROTA, board.ROTB) + +pixels = neopixel.NeoPixel(board.NEOPIXEL, 12) +pixels.fill(0) + +###################################### +# MAIN +###################################### +awake = True +knob_pos = knob.position +current_key = key_pressed = 0 +last_compute = last_update = wake_up_time = time.time() +totp_codes = compute_codes(timebase(last_compute)) +while True: + now = time.time() + progress_bar.value = now % 30 + event = keys.events.get() + # wakeup if knob turned or button pressed + if knob.position != knob_pos or event: + if not awake: + last_update = 0 # force an update + awake = True + knob_pos = knob.position + wake_up_time = now + # handle key presses + if event: + if event.pressed: + key_pressed = event.key_number + # knob + if key_pressed == 12: + keyboard_layout.write(totp_codes[current_key]) + keyboard.send(Keycode.ENTER) + # keeb + elif key_pressed != current_key: + # is it a configured key? + if totp_keys[key_pressed]: + current_key = key_pressed + pixels.fill(0) + last_update = 0 # force an update + # update codes + if progress_bar.value < 0.5 and now - last_compute > 2: + totp_codes = compute_codes(timebase(now)) + last_compute = now + # update display + if now - last_update > DISPLAY_RATE and awake: + pixels[current_key] = totp_keys[current_key][2] + name.text = totp_keys[current_key][0][:18] + code.text = totp_codes[current_key] + tt = time.localtime() + if USE_12HR: + hour = tt.tm_hour % 12 + ampm = "AM" if tt.tm_hour < 12 else "PM" + else: + hour = tt.tm_hour + ampm = "" + rtc_date.text = "{:4}/{:2}/{:2}".format(tt.tm_year, tt.tm_mon, tt.tm_mday) + rtc_time.text = "{}:{:02}:{:02} {}".format(hour, tt.tm_min, tt.tm_sec, ampm) + last_update = now + splash.hidden = False + # go to sleep after inactivity + if awake and now - wake_up_time > DISPLAY_TIMEOUT: + awake = False + knob_pos = knob.position + pixels.fill(0) + splash.hidden = True diff --git a/Macropad_2FA_TOTP/rtc_setter.py b/Macropad_2FA_TOTP/rtc_setter.py new file mode 100755 index 000000000..e9473e8ba --- /dev/null +++ b/Macropad_2FA_TOTP/rtc_setter.py @@ -0,0 +1,35 @@ +import time +import board +import adafruit_pcf8523 + +pcf = adafruit_pcf8523.PCF8523(board.I2C()) + +# values to set +YEAR = 2021 +MON = 1 +DAY = 1 +HOUR = 12 +MIN = 23 +SEC = 42 + +print("Ready to set RTC to: {:4}/{:2}/{:2} {:2}:{:02}:{:02}".format(YEAR, + MON, + DAY, + HOUR, + MIN, + SEC)) +_ = input("Press ENTER to set.") + +pcf.datetime = time.struct_time((YEAR, MON, DAY, HOUR, MIN, SEC, 0, -1, -1)) + +print("SET!") + +while True: + now = pcf.datetime + print("{:4}/{:2}/{:2} {:2}:{:02}:{:02}".format(now.tm_year, + now.tm_mon, + now.tm_mday, + now.tm_hour, + now.tm_min, + now.tm_sec)) + time.sleep(1) \ No newline at end of file diff --git a/Macropad_2FA_TOTP/secrcode_28.bdf b/Macropad_2FA_TOTP/secrcode_28.bdf new file mode 100644 index 000000000..57ef3ef79 --- /dev/null +++ b/Macropad_2FA_TOTP/secrcode_28.bdf @@ -0,0 +1,370 @@ +STARTFONT 2.1 +COMMENT +COMMENT Converted from OpenType font "secrcode.ttf" by "otf2bdf 3.0". +COMMENT +FONT -FreeType-Secret Code-Medium-R-Normal--39-280-100-100-P-135-ISO10646-1 +SIZE 28 100 100 +FONTBOUNDINGBOX 17 27 0 0 +STARTPROPERTIES 19 +FOUNDRY "FreeType" +FAMILY_NAME "Secret Code" +WEIGHT_NAME "Medium" +SLANT "R" +SETWIDTH_NAME "Normal" +ADD_STYLE_NAME "" +PIXEL_SIZE 39 +POINT_SIZE 280 +RESOLUTION_X 100 +RESOLUTION_Y 100 +SPACING "P" +AVERAGE_WIDTH 135 +CHARSET_REGISTRY "ISO10646" +CHARSET_ENCODING "1" +FONT_ASCENT 29 +FONT_DESCENT 9 +COPYRIGHT "Copyright © 1998 by Matthew Welch. All Rights Reserved." +_OTF_FONTFILE "secrcode.ttf" +_OTF_PSNAME "SecretCode" +ENDPROPERTIES +CHARS 10 +STARTCHAR 0030 +ENCODING 48 +SWIDTH 540 0 +DWIDTH 21 0 +BBX 14 27 3 0 +BITMAP +0780 +1860 +2020 +2010 +4010 +4008 +4008 +4008 +8004 +8004 +8004 +8004 +8004 +8004 +8004 +8004 +8004 +8004 +8004 +4008 +4008 +4008 +4010 +2010 +2030 +1860 +0780 +ENDCHAR +STARTCHAR 0031 +ENCODING 49 +SWIDTH 540 0 +DWIDTH 21 0 +BBX 5 27 8 0 +BITMAP +08 +18 +68 +C8 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +08 +ENDCHAR +STARTCHAR 0032 +ENCODING 50 +SWIDTH 540 0 +DWIDTH 21 0 +BBX 14 27 3 0 +BITMAP +0FC0 +1020 +2010 +4008 +8004 +8004 +8004 +0004 +0004 +0008 +0008 +0010 +0030 +0020 +0040 +0080 +0100 +0100 +0200 +0400 +0800 +0800 +1000 +2000 +4000 +C000 +FFFC +ENDCHAR +STARTCHAR 0033 +ENCODING 51 +SWIDTH 540 0 +DWIDTH 21 0 +BBX 14 27 3 0 +BITMAP +0FC0 +1020 +2010 +4008 +8004 +8004 +8004 +0004 +0004 +000C +0008 +0010 +0060 +01C0 +0060 +0010 +0008 +000C +0004 +0004 +8004 +8004 +8004 +4008 +2010 +1020 +0FC0 +ENDCHAR +STARTCHAR 0034 +ENCODING 52 +SWIDTH 540 0 +DWIDTH 21 0 +BBX 13 27 4 0 +BITMAP +0040 +0040 +0040 +0040 +0840 +0840 +0840 +0840 +0840 +0840 +1040 +1040 +1040 +1040 +2040 +2040 +4040 +8040 +FFF8 +0040 +0040 +0040 +0040 +0040 +0040 +0040 +0040 +ENDCHAR +STARTCHAR 0035 +ENCODING 53 +SWIDTH 540 0 +DWIDTH 21 0 +BBX 14 27 3 0 +BITMAP +3FF0 +2000 +2000 +2000 +2000 +2000 +6000 +4000 +4000 +4780 +5860 +6010 +4010 +0008 +0008 +0004 +0004 +0004 +0004 +0004 +8004 +8008 +8008 +4010 +2010 +1060 +0F80 +ENDCHAR +STARTCHAR 0036 +ENCODING 54 +SWIDTH 540 0 +DWIDTH 21 0 +BBX 14 27 3 0 +BITMAP +07C0 +0830 +1008 +2000 +4000 +4000 +4000 +8000 +8000 +8F80 +9860 +A010 +E010 +C008 +C008 +8004 +8004 +8004 +8004 +8004 +8004 +4008 +4008 +6010 +2010 +1860 +0780 +ENDCHAR +STARTCHAR 0037 +ENCODING 55 +SWIDTH 540 0 +DWIDTH 21 0 +BBX 14 27 3 0 +BITMAP +FFFC +0004 +0008 +0008 +0010 +0010 +0020 +0020 +0040 +0040 +0080 +0080 +0100 +0100 +0200 +0200 +0400 +0400 +0800 +0800 +1000 +1000 +1000 +2000 +2000 +4000 +4000 +ENDCHAR +STARTCHAR 0038 +ENCODING 56 +SWIDTH 540 0 +DWIDTH 21 0 +BBX 14 27 3 0 +BITMAP +0FC0 +1020 +2010 +4008 +8004 +8004 +8004 +8004 +8004 +C00C +4008 +2010 +3860 +0FC0 +1860 +2010 +4008 +C00C +8004 +8004 +8004 +8004 +8004 +4008 +2010 +1020 +0FC0 +ENDCHAR +STARTCHAR 0039 +ENCODING 57 +SWIDTH 540 0 +DWIDTH 21 0 +BBX 14 27 3 0 +BITMAP +0780 +1860 +2010 +6010 +4008 +4008 +8004 +8004 +8004 +8004 +8004 +8004 +400C +400C +201C +2014 +1864 +07C4 +0008 +0008 +0008 +0008 +0010 +0010 +4020 +3040 +0F80 +ENDCHAR +ENDFONT diff --git a/Macropad_2FA_TOTP/secrets.py b/Macropad_2FA_TOTP/secrets.py new file mode 100755 index 000000000..547b81b02 --- /dev/null +++ b/Macropad_2FA_TOTP/secrets.py @@ -0,0 +1,19 @@ +# This file is where you keep secret settings, passwords, and tokens! +# If you put them in the code you risk committing that info or sharing it + +secrets = { + # tuples of name, sekret key, color + 'totp_keys' : [("Github", "JBSWY3DPEHPK3PXP", 0x8732A8), + ("Discord", "JBSWY3DPEHPK3PXQ", 0x32A89E), + ("Slack", "JBSWY5DZEHPK3PXR", 0xFC861E), + ("Basecamp", "JBSWY6DZEHPK3PXS", 0x55C24C), + ("Gmail", "JBSWY7DZEHPK3PXT", 0x3029FF), + None, + None, # must have 12 entires + None, # set None for unused keys + None, + ("Hello Kitty", "JBSWY7DZEHPK3PXU", 0xED164F), + None, + None, + ] + } \ No newline at end of file