add Macropad 2FA TOTP code

This commit is contained in:
caternuson 2021-07-19 16:29:52 -07:00
parent e69aaf2780
commit 75c7817694
4 changed files with 694 additions and 0 deletions

View file

@ -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

35
Macropad_2FA_TOTP/rtc_setter.py Executable file
View file

@ -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)

View file

@ -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

19
Macropad_2FA_TOTP/secrets.py Executable file
View file

@ -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,
]
}