Skip to content

Commit 167eefe

Browse files
authored
Merge branch 'master' into pir-eye
2 parents 4cc425c + b27df91 commit 167eefe

File tree

3 files changed

+328
-2
lines changed

3 files changed

+328
-2
lines changed

Circuit_Playground_Bluefruit_NeoPixel_Controller/NeoPixel_Animator.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,13 @@
8383
if isinstance(packet, ColorPacket): # If the packet is color packet...
8484
if mode == 0: # And mode is 0...
8585
animations.color = packet.color # Update the animation to the color.
86-
print("Color:", packet.color)
86+
# Uncomment below to see the color tuple printed to the serial console.
87+
# print("Color:", packet.color)
8788
animation_color = packet.color # Keep track of the current color...
8889
elif mode == 1: # Because if mode is 1...
8990
animations.color = animation_color # Freeze the animation color.
90-
print("Color:", animation_color)
91+
# Uncomment below to see the color tuple printed to the serial console.
92+
# print("Color:", animation_color)
9193
elif isinstance(packet, ButtonPacket): # If the packet is a button packet...
9294
# Check to see if it's BUTTON_1 (which is being sent by the slide switch)
9395
if packet.button == ButtonPacket.BUTTON_1:

PyPortal_TOTP_Friend/code.py

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import time
2+
3+
import board
4+
import busio
5+
from digitalio import DigitalInOut
6+
import displayio
7+
import terminalio
8+
from simpleio import map_range
9+
import adafruit_hashlib as hashlib
10+
import adafruit_touchscreen
11+
from adafruit_button import Button
12+
from adafruit_progressbar import ProgressBar
13+
from adafruit_display_text.label import Label
14+
from adafruit_esp32spi import adafruit_esp32spi
15+
from adafruit_ntp import NTP
16+
from adafruit_pyportal import PyPortal
17+
18+
19+
# Background Color
20+
BACKGROUND = 0x0
21+
22+
# Button color
23+
BTN_COLOR = 0xFFFFFF
24+
25+
# Button text color
26+
BTN_TEXT_COLOR = 0x0
27+
28+
# Set to true if you never want to go to sleep!
29+
ALWAYS_ON = True
30+
31+
# How long to stay on if not in always_on mode
32+
ON_SECONDS = 60
33+
34+
# Get wifi details and more from a secrets.py file
35+
try:
36+
from secrets import secrets
37+
except ImportError:
38+
print("WiFi secrets are kept in secrets.py, please add them there!")
39+
raise
40+
41+
# Initialize PyPortal Display
42+
display = board.DISPLAY
43+
44+
WIDTH = board.DISPLAY.width
45+
HEIGHT = board.DISPLAY.height
46+
ts = adafruit_touchscreen.Touchscreen(board.TOUCH_XL, board.TOUCH_XR,
47+
board.TOUCH_YD, board.TOUCH_YU,
48+
calibration=(
49+
(5200, 59000),
50+
(5800, 57000)
51+
),
52+
size=(WIDTH, HEIGHT))
53+
54+
# Create a SHA1 Object
55+
SHA1 = hashlib.sha1
56+
57+
# PyPortal ESP32 AirLift Pins
58+
esp32_cs = DigitalInOut(board.ESP_CS)
59+
esp32_ready = DigitalInOut(board.ESP_BUSY)
60+
esp32_reset = DigitalInOut(board.ESP_RESET)
61+
62+
# Initialize PyPortal ESP32 AirLift
63+
spi = busio.SPI(board.SCK, board.MOSI, board.MISO)
64+
esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset)
65+
66+
def HMAC(k, m):
67+
"""# HMAC implementation, as hashlib/hmac wouldn't fit
68+
From https://en.wikipedia.org/wiki/Hash-based_message_authentication_code
69+
70+
"""
71+
SHA1_BLOCK_SIZE = 64
72+
KEY_BLOCK = k + (b'\0' * (SHA1_BLOCK_SIZE - len(k)))
73+
KEY_INNER = bytes((x ^ 0x36) for x in KEY_BLOCK)
74+
KEY_OUTER = bytes((x ^ 0x5C) for x in KEY_BLOCK)
75+
inner_message = KEY_INNER + m
76+
outer_message = KEY_OUTER + SHA1(inner_message).digest()
77+
return SHA1(outer_message)
78+
79+
def base32_decode(encoded):
80+
missing_padding = len(encoded) % 8
81+
if missing_padding != 0:
82+
encoded += '=' * (8 - missing_padding)
83+
encoded = encoded.upper()
84+
chunks = [encoded[i:i + 8] for i in range(0, len(encoded), 8)]
85+
86+
out = []
87+
for chunk in chunks:
88+
bits = 0
89+
bitbuff = 0
90+
for c in chunk:
91+
if 'A' <= c <= 'Z':
92+
n = ord(c) - ord('A')
93+
elif '2' <= c <= '7':
94+
n = ord(c) - ord('2') + 26
95+
elif n == '=':
96+
continue
97+
else:
98+
raise ValueError("Not base32")
99+
# 5 bits per 8 chars of base32
100+
bits += 5
101+
# shift down and add the current value
102+
bitbuff <<= 5
103+
bitbuff |= n
104+
# great! we have enough to extract a byte
105+
if bits >= 8:
106+
bits -= 8
107+
byte = bitbuff >> bits # grab top 8 bits
108+
bitbuff &= ~(0xFF << bits) # and clear them
109+
out.append(byte) # store what we got
110+
return out
111+
112+
def int_to_bytestring(int_val, padding=8):
113+
result = []
114+
while int_val != 0:
115+
result.insert(0, int_val & 0xFF)
116+
int_val >>= 8
117+
result = [0] * (padding - len(result)) + result
118+
return bytes(result)
119+
120+
121+
def generate_otp(int_input, secret_key, digits=6):
122+
""" HMAC -> OTP generator, pretty much same as
123+
https://github.com/pyotp/pyotp/blob/master/src/pyotp/otp.py
124+
125+
"""
126+
if int_input < 0:
127+
raise ValueError('input must be positive integer')
128+
hmac_hash = bytearray(
129+
HMAC(bytes(base32_decode(secret_key)),
130+
int_to_bytestring(int_input)).digest()
131+
)
132+
offset = hmac_hash[-1] & 0xf
133+
code = ((hmac_hash[offset] & 0x7f) << 24 |
134+
(hmac_hash[offset + 1] & 0xff) << 16 |
135+
(hmac_hash[offset + 2] & 0xff) << 8 |
136+
(hmac_hash[offset + 3] & 0xff))
137+
str_code = str(code % 10 ** digits)
138+
while len(str_code) < digits:
139+
str_code = '0' + str_code
140+
141+
return str_code
142+
143+
def display_otp_key(secret_name, secret_otp):
144+
"""Updates the displayio labels to display formatted OTP key and name.
145+
146+
"""
147+
# display the key's name
148+
label_title.text = secret_name
149+
# format and display the OTP
150+
label_secret.text = "{} {}".format(str(secret_otp)[0:3], str(secret_otp)[3:6])
151+
print("OTP Name: {}\nOTP Key: {}".format(secret_name, secret_otp))
152+
153+
print("===========================================")
154+
155+
# GFX Font
156+
font = terminalio.FONT
157+
158+
# Initialize new PyPortal object
159+
pyportal = PyPortal(esp=esp,
160+
external_spi=spi)
161+
162+
# Root DisplayIO
163+
root_group = displayio.Group(max_size=100)
164+
display.show(root_group)
165+
166+
BACKGROUND = BACKGROUND if isinstance(BACKGROUND, int) else 0x0
167+
bg_bitmap = displayio.Bitmap(display.width, display.height, 1)
168+
bg_palette = displayio.Palette(1)
169+
bg_palette[0] = BACKGROUND
170+
background = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette)
171+
172+
# Create a new DisplayIO group
173+
splash = displayio.Group(max_size=15)
174+
175+
splash.append(background)
176+
177+
key_group = displayio.Group(scale=5)
178+
# We'll use a default text placeholder for this label
179+
label_secret = Label(font, text="000 000")
180+
label_secret.x = (display.width // 2) // 13
181+
label_secret.y = 17
182+
key_group.append(label_secret)
183+
184+
label_title = Label(font, max_glyphs=14)
185+
label_title.text = " Loading.."
186+
label_title.x = 0
187+
label_title.y = 5
188+
key_group.append(label_title)
189+
190+
# append key_group to splash
191+
splash.append(key_group)
192+
193+
# Show the group
194+
display.show(splash)
195+
196+
print("Connecting to AP...")
197+
while not esp.is_connected:
198+
try:
199+
esp.connect_AP(secrets['ssid'], secrets['password'])
200+
except RuntimeError as e:
201+
print("Could not connect to AP, retrying: ", e)
202+
continue
203+
204+
print("Connected to ", secrets['ssid'])
205+
206+
# Initialize the NTP object
207+
ntp = NTP(esp)
208+
209+
# Fetch and set the microcontroller's current UTC time
210+
# keep retrying until a valid time is returned
211+
while not ntp.valid_time:
212+
ntp.set_time()
213+
print("Could not obtain NTP, re-fetching in 5 seconds...")
214+
time.sleep(5)
215+
216+
# Get the current time in seconds since Jan 1, 1970
217+
t = time.time()
218+
print("Seconds since Jan 1, 1970: {} seconds".format(t))
219+
220+
# Instead of using RTC which means converting back and forth
221+
# we'll just keep track of seconds-elapsed-since-NTP-call
222+
mono_time = int(time.monotonic())
223+
print("Monotonic time", mono_time)
224+
225+
# Add buttons to the interface
226+
assert len(secrets['totp_keys']) < 6, "This code can only display 5 keys at a time"
227+
228+
# generate buttons
229+
buttons = []
230+
231+
btn_x = 5
232+
for i in secrets['totp_keys']:
233+
button = Button(name=i[0], x=btn_x,
234+
y=175, width=60,
235+
height=60, label=i[0].strip(" "),
236+
label_font=font, label_color=BTN_TEXT_COLOR,
237+
fill_color=BTN_COLOR, style=Button.ROUNDRECT)
238+
buttons.append(button)
239+
# add padding btween buttons
240+
btn_x += 63
241+
242+
# append buttons to splash group
243+
for b in buttons:
244+
splash.append(b.group)
245+
246+
# refrsh timer label
247+
label_timer = Label(font, max_glyphs=2)
248+
label_timer.x = (display.width // 2) // 13
249+
label_timer.y = 15
250+
splash.append(label_timer)
251+
252+
# create a new progress bar
253+
progress_bar = ProgressBar(display.width//5, 125,
254+
200, 30, bar_color = 0xFFFFFF)
255+
256+
splash.append(progress_bar)
257+
258+
# how long to stay on if not in always_on mode
259+
countdown = ON_SECONDS
260+
261+
# current button state, defaults to first item in totp_keys
262+
current_button = secrets['totp_keys'][0][0]
263+
buttons[0].selected = True
264+
265+
while ALWAYS_ON or (countdown > 0):
266+
# Calculate current time based on NTP + monotonic
267+
unix_time = t - mono_time + int(time.monotonic())
268+
269+
# Update the key refresh timer
270+
timer = time.localtime(time.time()).tm_sec
271+
# timer resets on :00/:30
272+
if timer > 30:
273+
countdown = 60 - timer
274+
else:
275+
countdown = 30 - timer
276+
print('NTP Countdown: {}%'.format(countdown))
277+
# change the timer bar's color if text is about to refresh
278+
progress_bar.fill = 0xFFFFFF
279+
if countdown < 5:
280+
progress_bar.fill = 0xFF0000
281+
282+
# update the progress_bar with countdown
283+
countdown = map_range(countdown, 0, 30, 0.0, 1.0)
284+
progress_bar.progress = countdown
285+
286+
# poll the touchscreen
287+
p = ts.touch_point
288+
# if the touchscreen was pressed
289+
if p:
290+
for i, b in enumerate(buttons):
291+
if b.contains(p):
292+
b.selected = True
293+
for name, secret in secrets['totp_keys']:
294+
# check if button name is the same as a key name
295+
if b.name == name:
296+
current_button = name
297+
# Generate OTP
298+
otp = generate_otp(unix_time // 30, secret)
299+
display_otp_key(name, otp)
300+
else:
301+
b.selected = False
302+
else:
303+
for name, secret in secrets['totp_keys']:
304+
if current_button == name:
305+
# Generate OTP
306+
otp = generate_otp(unix_time // 30, secret)
307+
display_otp_key(name, otp)
308+
# We'll update every 1/4 second, we can hash very fast so its no biggie!
309+
countdown -= 0.25
310+
time.sleep(0.25)

PyPortal_TOTP_Friend/secrets.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# This file is where you keep secret settings, passwords, and tokens!
2+
# If you put them in the code you risk committing that info or sharing it
3+
4+
secrets = {
5+
'ssid' : 'yourwifissid',
6+
'password' : 'yourwifipassword',
7+
'timezone' : "America/New_York", # http://worldtimeapi.org/timezones
8+
# https://github.com/pyotp/pyotp example
9+
'totp_keys' : [("Discord ", "JBSWY3DPEHPK3PXP"),
10+
("Gmail", "JBSWY3DPEHPK3PZP"),
11+
("GitHub", "JBSWY5DZEHPK3PXP"),
12+
("Adafruit", "JBSWY6DZEHPK3PXP"),
13+
("Outlook", "JBSWY7DZEHPK3PXP")]
14+
}

0 commit comments

Comments
 (0)