# SPDX-FileCopyrightText: 2021 Carter Nelson for Adafruit Industries # # SPDX-License-Identifier: MIT import time # base hardware stuff import board import rtc import keypad import rotaryio import neopixel # crypto stuff from adafruit_pcf8523.pcf8523 import 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 #------------------------------------------------------------------------- # Get totp_keys from a totp_keys.py file try: from totp_keys import totp_keys except ImportError: print("TOTP info not found in totp_keys.py, please add them there!") raise # set board to use PCF8523 as its RTC i2c = board.I2C() # uses board.SCL and board.SDA # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller pcf = PCF8523(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.root_group = splash #------------------------------------------------------------------------- # H E L P E R F U N C S #------------------------------------------------------------------------- def timebase(timetime): return (timetime - (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 c == '=': 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 otp_code = ((hmac_hash[offset] & 0x7f) << 24 | (hmac_hash[offset + 1] & 0xff) << 16 | (hmac_hash[offset + 2] & 0xff) << 8 | (hmac_hash[offset + 3] & 0xff)) str_otp_code = str(otp_code % 10 ** digits) while len(str_otp_code) < digits: str_otp_code = '0' + str_otp_code return str_otp_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