Skip to content

Commit 3e3d1de

Browse files
authored
Merge pull request #1619 from caternuson/macropad_2fa
Add Macropad 2FA TOTP code
2 parents e69aaf2 + 75c7817 commit 3e3d1de

File tree

4 files changed

+694
-0
lines changed

4 files changed

+694
-0
lines changed

Macropad_2FA_TOTP/macropad_totp.py

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import time
2+
# base hardware stuff
3+
import board
4+
import rtc
5+
import keypad
6+
import rotaryio
7+
import neopixel
8+
# crypto stuff
9+
import adafruit_pcf8523
10+
import adafruit_hashlib as hashlib
11+
# UI stuff
12+
import displayio
13+
import terminalio
14+
from adafruit_bitmap_font import bitmap_font
15+
from adafruit_display_text import label
16+
from adafruit_progressbar.horizontalprogressbar import HorizontalProgressBar
17+
# HID keyboard stuff
18+
import usb_hid
19+
from adafruit_hid.keyboard import Keyboard
20+
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
21+
from adafruit_hid.keycode import Keycode
22+
23+
#--| User Config |--------------------------------------------------------
24+
UTC_OFFSET = -4 # time zone offset
25+
USE_12HR = True # set 12/24 hour format
26+
DISPLAY_TIMEOUT = 60 # screen saver timeout in seconds
27+
DISPLAY_RATE = 1 # screen refresh rate
28+
#-------------------------------------------------------------------------
29+
30+
# TODO: remove this once this is resolved:
31+
# https://github.com/adafruit/circuitpython/issues/4893
32+
# and this gets merged:
33+
# https://github.com/adafruit/circuitpython/pull/4961
34+
EPOCH_OFFSET = 946684800 # delta from above issue thread
35+
36+
# Get sekrets from a secrets.py file
37+
try:
38+
from secrets import secrets
39+
totp_keys = secrets["totp_keys"]
40+
except ImportError:
41+
print("Secrets are kept in secrets.py, please add them there!")
42+
raise
43+
except KeyError:
44+
print("TOTP info not found in secrets.py.")
45+
raise
46+
47+
# set board to use PCF8523 as its RTC
48+
pcf = adafruit_pcf8523.PCF8523(board.I2C())
49+
rtc.set_time_source(pcf)
50+
51+
#-------------------------------------------------------------------------
52+
# H I D S E T U P
53+
#-------------------------------------------------------------------------
54+
time.sleep(1) # Sleep for a bit to avoid a race condition on some systems
55+
keyboard = Keyboard(usb_hid.devices)
56+
keyboard_layout = KeyboardLayoutUS(keyboard) # We're in the US :)
57+
58+
#-------------------------------------------------------------------------
59+
# D I S P L A Y S E T U P
60+
#-------------------------------------------------------------------------
61+
display = board.DISPLAY
62+
63+
# Secret Code font by Matthew Welch
64+
# http://www.squaregear.net/fonts/
65+
font = bitmap_font.load_font("/secrcode_28.bdf")
66+
67+
name = label.Label(terminalio.FONT, text="?"*18, color=0xFFFFFF)
68+
name.anchor_point = (0.0, 0.0)
69+
name.anchored_position = (0, 0)
70+
71+
code = label.Label(font, text="123456", color=0xFFFFFF)
72+
code.anchor_point = (0.5, 0.0)
73+
code.anchored_position = (display.width // 2, 15)
74+
75+
rtc_date = label.Label(terminalio.FONT, text="2021/01/01")
76+
rtc_date.anchor_point = (0.0, 0.5)
77+
rtc_date.anchored_position = (0, 49)
78+
79+
rtc_time = label.Label(terminalio.FONT, text="12:34:56 AM")
80+
rtc_time.anchor_point = (0.0, 0.5)
81+
rtc_time.anchored_position = (0, 59)
82+
83+
progress_bar = HorizontalProgressBar((68, 46), (55, 17), bar_color=0xFFFFFF, min_value=0, max_value=30)
84+
85+
splash = displayio.Group()
86+
splash.append(name)
87+
splash.append(code)
88+
splash.append(rtc_date)
89+
splash.append(rtc_time)
90+
splash.append(progress_bar)
91+
92+
display.show(splash)
93+
94+
#-------------------------------------------------------------------------
95+
# H E L P E R F U N C S
96+
#-------------------------------------------------------------------------
97+
def timebase(timetime):
98+
return (timetime + EPOCH_OFFSET - (UTC_OFFSET*3600)) // 30
99+
100+
def compute_codes(timestamp):
101+
codes = []
102+
for key in totp_keys:
103+
if key:
104+
codes.append(generate_otp(timestamp, key[1]))
105+
else:
106+
codes.append(None)
107+
return codes
108+
109+
def HMAC(k, m):
110+
"""# HMAC implementation, as hashlib/hmac wouldn't fit
111+
From https://en.wikipedia.org/wiki/Hash-based_message_authentication_code
112+
113+
"""
114+
SHA1_BLOCK_SIZE = 64
115+
KEY_BLOCK = k + (b'\0' * (SHA1_BLOCK_SIZE - len(k)))
116+
KEY_INNER = bytes((x ^ 0x36) for x in KEY_BLOCK)
117+
KEY_OUTER = bytes((x ^ 0x5C) for x in KEY_BLOCK)
118+
inner_message = KEY_INNER + m
119+
outer_message = KEY_OUTER + hashlib.sha1(inner_message).digest()
120+
return hashlib.sha1(outer_message)
121+
122+
def base32_decode(encoded):
123+
missing_padding = len(encoded) % 8
124+
if missing_padding != 0:
125+
encoded += '=' * (8 - missing_padding)
126+
encoded = encoded.upper()
127+
chunks = [encoded[i:i + 8] for i in range(0, len(encoded), 8)]
128+
129+
out = []
130+
for chunk in chunks:
131+
bits = 0
132+
bitbuff = 0
133+
for c in chunk:
134+
if 'A' <= c <= 'Z':
135+
n = ord(c) - ord('A')
136+
elif '2' <= c <= '7':
137+
n = ord(c) - ord('2') + 26
138+
elif n == '=':
139+
continue
140+
else:
141+
raise ValueError("Not base32")
142+
# 5 bits per 8 chars of base32
143+
bits += 5
144+
# shift down and add the current value
145+
bitbuff <<= 5
146+
bitbuff |= n
147+
# great! we have enough to extract a byte
148+
if bits >= 8:
149+
bits -= 8
150+
byte = bitbuff >> bits # grab top 8 bits
151+
bitbuff &= ~(0xFF << bits) # and clear them
152+
out.append(byte) # store what we got
153+
return out
154+
155+
def int_to_bytestring(int_val, padding=8):
156+
result = []
157+
while int_val != 0:
158+
result.insert(0, int_val & 0xFF)
159+
int_val >>= 8
160+
result = [0] * (padding - len(result)) + result
161+
return bytes(result)
162+
163+
def generate_otp(int_input, secret_key, digits=6):
164+
""" HMAC -> OTP generator, pretty much same as
165+
https://github.com/pyotp/pyotp/blob/master/src/pyotp/otp.py
166+
167+
"""
168+
if int_input < 0:
169+
raise ValueError('input must be positive integer')
170+
hmac_hash = bytearray(
171+
HMAC(bytes(base32_decode(secret_key)),
172+
int_to_bytestring(int_input)).digest()
173+
)
174+
offset = hmac_hash[-1] & 0xf
175+
code = ((hmac_hash[offset] & 0x7f) << 24 |
176+
(hmac_hash[offset + 1] & 0xff) << 16 |
177+
(hmac_hash[offset + 2] & 0xff) << 8 |
178+
(hmac_hash[offset + 3] & 0xff))
179+
str_code = str(code % 10 ** digits)
180+
while len(str_code) < digits:
181+
str_code = '0' + str_code
182+
183+
return str_code
184+
185+
#-------------------------------------------------------------------------
186+
# M A C R O P A D S E T U P
187+
#-------------------------------------------------------------------------
188+
key_pins = (
189+
board.KEY1,
190+
board.KEY2,
191+
board.KEY3,
192+
board.KEY4,
193+
board.KEY5,
194+
board.KEY6,
195+
board.KEY7,
196+
board.KEY8,
197+
board.KEY9,
198+
board.KEY10,
199+
board.KEY11,
200+
board.KEY12,
201+
board.BUTTON,
202+
)
203+
204+
keys = keypad.Keys(key_pins, value_when_pressed=False, pull=True)
205+
206+
knob = rotaryio.IncrementalEncoder(board.ROTA, board.ROTB)
207+
208+
pixels = neopixel.NeoPixel(board.NEOPIXEL, 12)
209+
pixels.fill(0)
210+
211+
######################################
212+
# MAIN
213+
######################################
214+
awake = True
215+
knob_pos = knob.position
216+
current_key = key_pressed = 0
217+
last_compute = last_update = wake_up_time = time.time()
218+
totp_codes = compute_codes(timebase(last_compute))
219+
while True:
220+
now = time.time()
221+
progress_bar.value = now % 30
222+
event = keys.events.get()
223+
# wakeup if knob turned or button pressed
224+
if knob.position != knob_pos or event:
225+
if not awake:
226+
last_update = 0 # force an update
227+
awake = True
228+
knob_pos = knob.position
229+
wake_up_time = now
230+
# handle key presses
231+
if event:
232+
if event.pressed:
233+
key_pressed = event.key_number
234+
# knob
235+
if key_pressed == 12:
236+
keyboard_layout.write(totp_codes[current_key])
237+
keyboard.send(Keycode.ENTER)
238+
# keeb
239+
elif key_pressed != current_key:
240+
# is it a configured key?
241+
if totp_keys[key_pressed]:
242+
current_key = key_pressed
243+
pixels.fill(0)
244+
last_update = 0 # force an update
245+
# update codes
246+
if progress_bar.value < 0.5 and now - last_compute > 2:
247+
totp_codes = compute_codes(timebase(now))
248+
last_compute = now
249+
# update display
250+
if now - last_update > DISPLAY_RATE and awake:
251+
pixels[current_key] = totp_keys[current_key][2]
252+
name.text = totp_keys[current_key][0][:18]
253+
code.text = totp_codes[current_key]
254+
tt = time.localtime()
255+
if USE_12HR:
256+
hour = tt.tm_hour % 12
257+
ampm = "AM" if tt.tm_hour < 12 else "PM"
258+
else:
259+
hour = tt.tm_hour
260+
ampm = ""
261+
rtc_date.text = "{:4}/{:2}/{:2}".format(tt.tm_year, tt.tm_mon, tt.tm_mday)
262+
rtc_time.text = "{}:{:02}:{:02} {}".format(hour, tt.tm_min, tt.tm_sec, ampm)
263+
last_update = now
264+
splash.hidden = False
265+
# go to sleep after inactivity
266+
if awake and now - wake_up_time > DISPLAY_TIMEOUT:
267+
awake = False
268+
knob_pos = knob.position
269+
pixels.fill(0)
270+
splash.hidden = True

Macropad_2FA_TOTP/rtc_setter.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import time
2+
import board
3+
import adafruit_pcf8523
4+
5+
pcf = adafruit_pcf8523.PCF8523(board.I2C())
6+
7+
# values to set
8+
YEAR = 2021
9+
MON = 1
10+
DAY = 1
11+
HOUR = 12
12+
MIN = 23
13+
SEC = 42
14+
15+
print("Ready to set RTC to: {:4}/{:2}/{:2} {:2}:{:02}:{:02}".format(YEAR,
16+
MON,
17+
DAY,
18+
HOUR,
19+
MIN,
20+
SEC))
21+
_ = input("Press ENTER to set.")
22+
23+
pcf.datetime = time.struct_time((YEAR, MON, DAY, HOUR, MIN, SEC, 0, -1, -1))
24+
25+
print("SET!")
26+
27+
while True:
28+
now = pcf.datetime
29+
print("{:4}/{:2}/{:2} {:2}:{:02}:{:02}".format(now.tm_year,
30+
now.tm_mon,
31+
now.tm_mday,
32+
now.tm_hour,
33+
now.tm_min,
34+
now.tm_sec))
35+
time.sleep(1)

0 commit comments

Comments
 (0)