Adafruit_Learning_System_Gu.../PyPortal_TOTP_Friend/code.py
Scott Shawcroft e0c283cf66
Use ESP32SPI directly instead of NTP
The NTP library is changing to support native sockets and this API
will break.
2022-05-16 14:10:46 -07:00

314 lines
9.2 KiB
Python

# SPDX-FileCopyrightText: 2017 Limor Fried for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import board
import busio
from digitalio import DigitalInOut
import displayio
import terminalio
from simpleio import map_range
import adafruit_hashlib as hashlib
import adafruit_touchscreen
from adafruit_button import Button
from adafruit_progressbar.progressbar import ProgressBar
from adafruit_display_text.label import Label
from adafruit_esp32spi import adafruit_esp32spi
from adafruit_pyportal import PyPortal
import rtc
# Background Color
BACKGROUND = 0x0
# Button color
BTN_COLOR = 0xFFFFFF
# Button text color
BTN_TEXT_COLOR = 0x0
# Set to true if you never want to go to sleep!
ALWAYS_ON = True
# How long to stay on if not in always_on mode
ON_SECONDS = 60
# Get wifi details and more from a secrets.py file
try:
from secrets import secrets
except ImportError:
print("WiFi secrets are kept in secrets.py, please add them there!")
raise
# Initialize PyPortal Display
display = board.DISPLAY
WIDTH = board.DISPLAY.width
HEIGHT = board.DISPLAY.height
ts = adafruit_touchscreen.Touchscreen(board.TOUCH_XL, board.TOUCH_XR,
board.TOUCH_YD, board.TOUCH_YU,
calibration=(
(5200, 59000),
(5800, 57000)
),
size=(WIDTH, HEIGHT))
# Create a SHA1 Object
SHA1 = hashlib.sha1
# PyPortal ESP32 AirLift Pins
esp32_cs = DigitalInOut(board.ESP_CS)
esp32_ready = DigitalInOut(board.ESP_BUSY)
esp32_reset = DigitalInOut(board.ESP_RESET)
# Initialize PyPortal ESP32 AirLift
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
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 + SHA1(inner_message).digest()
return 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
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
def display_otp_key(secret_name, secret_otp):
"""Updates the displayio labels to display formatted OTP key and name.
"""
# display the key's name
label_title.text = secret_name
# format and display the OTP
label_secret.text = "{} {}".format(str(secret_otp)[0:3], str(secret_otp)[3:6])
print("OTP Name: {}\nOTP Key: {}".format(secret_name, secret_otp))
print("===========================================")
# GFX Font
font = terminalio.FONT
# Initialize new PyPortal object
pyportal = PyPortal(esp=esp,
external_spi=spi)
# Root DisplayIO
root_group = displayio.Group()
display.show(root_group)
BACKGROUND = BACKGROUND if isinstance(BACKGROUND, int) else 0x0
bg_bitmap = displayio.Bitmap(display.width, display.height, 1)
bg_palette = displayio.Palette(1)
bg_palette[0] = BACKGROUND
background = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette)
# Create a new DisplayIO group
splash = displayio.Group()
splash.append(background)
key_group = displayio.Group(scale=5)
# We'll use a default text placeholder for this label
label_secret = Label(font, text="000 000")
label_secret.x = (display.width // 2) // 13
label_secret.y = 17
key_group.append(label_secret)
label_title = Label(font)
label_title.text = " Loading.."
label_title.x = 0
label_title.y = 5
key_group.append(label_title)
# append key_group to splash
splash.append(key_group)
# Show the group
display.show(splash)
print("Connecting to AP...")
while not esp.is_connected:
try:
esp.connect_AP(secrets['ssid'], secrets['password'])
except RuntimeError as e:
print("Could not connect to AP, retrying: ", e)
continue
print("Connected to ", secrets['ssid'])
# get_time will raise ValueError if the time isn't available yet so loop until
# it works.
now_utc = None
while now_utc is None:
try:
now_utc = time.localtime(esp.get_time()[0])
except ValueError:
pass
rtc.RTC().datetime = now_utc
# Get the current time in seconds since Jan 1, 1970
t = time.time()
print("Seconds since Jan 1, 1970: {} seconds".format(t))
# Instead of using RTC which means converting back and forth
# we'll just keep track of seconds-elapsed-since-NTP-call
mono_time = int(time.monotonic())
print("Monotonic time", mono_time)
# Add buttons to the interface
assert len(secrets['totp_keys']) < 6, "This code can only display 5 keys at a time"
# generate buttons
buttons = []
btn_x = 5
for i in secrets['totp_keys']:
button = Button(name=i[0], x=btn_x,
y=175, width=60,
height=60, label=i[0].strip(" "),
label_font=font, label_color=BTN_TEXT_COLOR,
fill_color=BTN_COLOR, style=Button.ROUNDRECT)
buttons.append(button)
# add padding btween buttons
btn_x += 63
# append buttons to splash group
for b in buttons:
splash.append(b)
# refrsh timer label
label_timer = Label(font)
label_timer.x = (display.width // 2) // 13
label_timer.y = 15
splash.append(label_timer)
# create a new progress bar
progress_bar = ProgressBar(display.width//5, 125,
200, 30, bar_color = 0xAAAAAA)
splash.append(progress_bar)
# how long to stay on if not in always_on mode
countdown = ON_SECONDS
# current button state, defaults to first item in totp_keys
current_button = secrets['totp_keys'][0][0]
buttons[0].selected = True
while ALWAYS_ON or (countdown > 0):
# Calculate current time based on NTP + monotonic
unix_time = t - mono_time + int(time.monotonic())
# Update the key refresh timer
timer = time.localtime(time.time()).tm_sec
# timer resets on :00/:30
if timer > 30:
countdown = 60 - timer
else:
countdown = 30 - timer
print('NTP Countdown: {}%'.format(countdown))
# change the timer bar's color if text is about to refresh
progress_bar.fill = 0xFFFFFF
if countdown < 5:
progress_bar.fill = 0xFF0000
# update the progress_bar with countdown
countdown = map_range(countdown, 0, 30, 0.0, 1.0)
progress_bar.progress = countdown
# poll the touchscreen
p = ts.touch_point
# if the touchscreen was pressed
if p:
for i, b in enumerate(buttons):
if b.contains(p):
b.selected = True
for name, secret in secrets['totp_keys']:
# check if button name is the same as a key name
if b.name == name:
current_button = name
# Generate OTP
otp = generate_otp(unix_time // 30, secret)
display_otp_key(name, otp)
else:
b.selected = False
else:
for name, secret in secrets['totp_keys']:
if current_button == name:
# Generate OTP
otp = generate_otp(unix_time // 30, secret)
display_otp_key(name, otp)
# We'll update every 1/4 second, we can hash very fast so its no biggie!
countdown -= 0.25
time.sleep(0.25)