Skip to content

Commit 253c54a

Browse files
author
brentru
committed
add code, up to network setup, ported to pyportal
1 parent 82dde52 commit 253c54a

2 files changed

Lines changed: 331 additions & 0 deletions

File tree

PyPortal_TOTP_Friend/code.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import time
2+
import board
3+
import busio
4+
from digitalio import DigitalInOut
5+
from adafruit_esp32spi import adafruit_esp32spi
6+
from adafruit_ntp import NTP
7+
import adafruit_hashlib as hashlib
8+
from adafruit_binascii import hexlify, unhexlify
9+
10+
# https://github.com/pyotp/pyotp example
11+
totp = [("Discord ", 'JBSWY3DPEHPK3PXP'),
12+
("Gmail ", 'abcdefghijklmnopqrstuvwxyz234567'),
13+
("Accounts", 'asfdkwefoaiwejfa323nfjkl')]
14+
ssid = 'my_wifi_ssid'
15+
password = 'my_wifi_password'
16+
17+
TEST = True # if you want to print out the tests the hashers
18+
ALWAYS_ON = False # Set to true if you never want to go to sleep!
19+
ON_SECONDS = 60 # how long to stay on if not in always_on mode
20+
21+
EPOCH_DELTA = 946684800 # seconds between year 2000 and year 1970
22+
SECS_DAY = 86400
23+
24+
# Create a SHA1 Object
25+
SHA1 = hashlib.sha1
26+
27+
if TEST:
28+
print("===========================================")
29+
sha1_output = hexlify(SHA1(b'hello world').digest())
30+
assert sha1_output == b"2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"
31+
32+
33+
# HMAC implementation, as hashlib/hmac wouldn't fit
34+
# From https://en.wikipedia.org/wiki/Hash-based_message_authentication_code
35+
def HMAC(k, m):
36+
SHA1_BLOCK_SIZE = 64
37+
KEY_BLOCK = k + (b'\0' * (SHA1_BLOCK_SIZE - len(k)))
38+
KEY_INNER = bytes((x ^ 0x36) for x in KEY_BLOCK)
39+
KEY_OUTER = bytes((x ^ 0x5C) for x in KEY_BLOCK)
40+
inner_message = KEY_INNER + m
41+
outer_message = KEY_OUTER + SHA1(inner_message).digest()
42+
return SHA1(outer_message)
43+
44+
45+
if TEST:
46+
KEY = b'abcd'
47+
MESSAGE = b'efgh'
48+
print("===========================================")
49+
hmac_out = hexlify(HMAC(KEY, MESSAGE).digest())
50+
assert hmac_out == b'e5dbcf9263188f9fce90df572afeb39b66b27198'
51+
52+
def base32_decode(encoded):
53+
missing_padding = len(encoded) % 8
54+
if missing_padding != 0:
55+
encoded += '=' * (8 - missing_padding)
56+
encoded = encoded.upper()
57+
chunks = [encoded[i:i + 8] for i in range(0, len(encoded), 8)]
58+
59+
out = []
60+
for chunk in chunks:
61+
bits = 0
62+
bitbuff = 0
63+
for c in chunk:
64+
if 'A' <= c <= 'Z':
65+
n = ord(c) - ord('A')
66+
elif '2' <= c <= '7':
67+
n = ord(c) - ord('2') + 26
68+
elif n == '=':
69+
continue
70+
else:
71+
raise ValueError("Not base32")
72+
# 5 bits per 8 chars of base32
73+
bits += 5
74+
# shift down and add the current value
75+
bitbuff <<= 5
76+
bitbuff |= n
77+
# great! we have enough to extract a byte
78+
if bits >= 8:
79+
bits -= 8
80+
byte = bitbuff >> bits # grab top 8 bits
81+
bitbuff &= ~(0xFF << bits) # and clear them
82+
out.append(byte) # store what we got
83+
return out
84+
85+
if TEST:
86+
print("===========================================")
87+
assert (bytes(base32_decode("IFSGCZTSOVUXIIJB")) == b'Adafruit!!')
88+
89+
def int_to_bytestring(i, padding=8):
90+
result = []
91+
while i != 0:
92+
result.insert(0, i & 0xFF)
93+
i >>= 8
94+
result = [0] * (padding - len(result)) + result
95+
return bytes(result)
96+
97+
98+
# HMAC -> OTP generator, pretty much same as
99+
# https://github.com/pyotp/pyotp/blob/master/src/pyotp/otp.py
100+
101+
102+
def generate_otp(int_input, secret_key, digits=6):
103+
if int_input < 0:
104+
raise ValueError('input must be positive integer')
105+
hmac_hash = bytearray(
106+
HMAC(bytes(base32_decode(secret_key)),
107+
int_to_bytestring(int_input)).digest()
108+
)
109+
offset = hmac_hash[-1] & 0xf
110+
code = ((hmac_hash[offset] & 0x7f) << 24 |
111+
(hmac_hash[offset + 1] & 0xff) << 16 |
112+
(hmac_hash[offset + 2] & 0xff) << 8 |
113+
(hmac_hash[offset + 3] & 0xff))
114+
str_code = str(code % 10 ** digits)
115+
while len(str_code) < digits:
116+
str_code = '0' + str_code
117+
118+
return str_code
119+
120+
print("===========================================")

PyPortal_TOTP_Friend/limor_code.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import time
2+
3+
import adafruit_ssd1306
4+
import bitbangio as io
5+
import board
6+
import network
7+
import ntptime
8+
import ubinascii
9+
import uhashlib
10+
11+
# pylint: disable=broad-except
12+
13+
# https://github.com/pyotp/pyotp example
14+
totp = [("Discord ", 'JBSWY3DPEHPK3PXP'),
15+
("Gmail ", 'abcdefghijklmnopqrstuvwxyz234567'),
16+
("Accounts", 'asfdkwefoaiwejfa323nfjkl')]
17+
ssid = 'my_wifi_ssid'
18+
password = 'my_wifi_password'
19+
20+
TEST = False # if you want to print out the tests the hashers
21+
ALWAYS_ON = False # Set to true if you never want to go to sleep!
22+
ON_SECONDS = 60 # how long to stay on if not in always_on mode
23+
24+
i2c = io.I2C(board.SCL, board.SDA)
25+
oled = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c)
26+
27+
# Gimme a welcome screen!
28+
oled.fill(0)
29+
oled.text('CircuitPython', 0, 0)
30+
oled.text('PyTOTP Pal!', 0, 10)
31+
oled.text(' <3 adafruit <3 ', 0, 20)
32+
oled.show()
33+
time.sleep(0.25)
34+
35+
EPOCH_DELTA = 946684800 # seconds between year 2000 and year 1970
36+
SECS_DAY = 86400
37+
38+
SHA1 = uhashlib.sha1
39+
40+
if TEST:
41+
print("===========================================")
42+
print("SHA1 test: ", ubinascii.hexlify(SHA1(b'hello world').digest()))
43+
# should be 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
44+
45+
46+
# HMAC implementation, as hashlib/hmac wouldn't fit
47+
# From https://en.wikipedia.org/wiki/Hash-based_message_authentication_code
48+
def HMAC(k, m):
49+
SHA1_BLOCK_SIZE = 64
50+
KEY_BLOCK = k + (b'\0' * (SHA1_BLOCK_SIZE - len(k)))
51+
KEY_INNER = bytes((x ^ 0x36) for x in KEY_BLOCK)
52+
KEY_OUTER = bytes((x ^ 0x5C) for x in KEY_BLOCK)
53+
inner_message = KEY_INNER + m
54+
outer_message = KEY_OUTER + SHA1(inner_message).digest()
55+
return SHA1(outer_message)
56+
57+
58+
if TEST:
59+
KEY = b'abcd'
60+
MESSAGE = b'efgh'
61+
print("===========================================")
62+
print("HMAC test: ", ubinascii.hexlify(HMAC(KEY, MESSAGE).digest()))
63+
# should be e5dbcf9263188f9fce90df572afeb39b66b27198
64+
65+
66+
# Base32 decoder, since base64 lib wouldnt fit
67+
68+
def base32_decode(encoded):
69+
missing_padding = len(encoded) % 8
70+
if missing_padding != 0:
71+
encoded += '=' * (8 - missing_padding)
72+
encoded = encoded.upper()
73+
chunks = [encoded[i:i + 8] for i in range(0, len(encoded), 8)]
74+
75+
out = []
76+
for chunk in chunks:
77+
bits = 0
78+
bitbuff = 0
79+
for c in chunk:
80+
if 'A' <= c <= 'Z':
81+
n = ord(c) - ord('A')
82+
elif '2' <= c <= '7':
83+
n = ord(c) - ord('2') + 26
84+
elif n == '=':
85+
continue
86+
else:
87+
raise ValueError("Not base32")
88+
# 5 bits per 8 chars of base32
89+
bits += 5
90+
# shift down and add the current value
91+
bitbuff <<= 5
92+
bitbuff |= n
93+
# great! we have enough to extract a byte
94+
if bits >= 8:
95+
bits -= 8
96+
byte = bitbuff >> bits # grab top 8 bits
97+
bitbuff &= ~(0xFF << bits) # and clear them
98+
out.append(byte) # store what we got
99+
return out
100+
101+
102+
if TEST:
103+
print("===========================================")
104+
print("Base32 test: ", bytes(base32_decode("IFSGCZTSOVUXIIJB")))
105+
# should be "Adafruit!!"
106+
107+
108+
# Turns an integer into a padded-with-0x0 bytestr
109+
110+
111+
def int_to_bytestring(i, padding=8):
112+
result = []
113+
while i != 0:
114+
result.insert(0, i & 0xFF)
115+
i >>= 8
116+
result = [0] * (padding - len(result)) + result
117+
return bytes(result)
118+
119+
120+
# HMAC -> OTP generator, pretty much same as
121+
# https://github.com/pyotp/pyotp/blob/master/src/pyotp/otp.py
122+
123+
124+
def generate_otp(int_input, secret_key, digits=6):
125+
if int_input < 0:
126+
raise ValueError('input must be positive integer')
127+
hmac_hash = bytearray(
128+
HMAC(bytes(base32_decode(secret_key)),
129+
int_to_bytestring(int_input)).digest()
130+
)
131+
offset = hmac_hash[-1] & 0xf
132+
code = ((hmac_hash[offset] & 0x7f) << 24 |
133+
(hmac_hash[offset + 1] & 0xff) << 16 |
134+
(hmac_hash[offset + 2] & 0xff) << 8 |
135+
(hmac_hash[offset + 3] & 0xff))
136+
str_code = str(code % 10 ** digits)
137+
while len(str_code) < digits:
138+
str_code = '0' + str_code
139+
140+
return str_code
141+
142+
143+
print("===========================================")
144+
145+
# Set up networking
146+
sta_if = network.WLAN(network.STA_IF)
147+
148+
oled.fill(0)
149+
oled.text('Connecting to', 0, 0)
150+
oled.text(ssid, 0, 10)
151+
oled.show()
152+
153+
if not sta_if.isconnected():
154+
print("Connecting to SSID", ssid)
155+
sta_if.active(True)
156+
sta_if.connect(ssid, password)
157+
while not sta_if.isconnected():
158+
pass
159+
print("Connected! IP = ", sta_if.ifconfig()[0])
160+
161+
# Done! Let them know we made it
162+
oled.text("IP: " + sta_if.ifconfig()[0], 0, 20)
163+
oled.show()
164+
time.sleep(0.25)
165+
166+
# Get the latest time from NTP
167+
t = None
168+
while not t:
169+
try:
170+
t = ntptime.time()
171+
except Exception:
172+
pass
173+
time.sleep(0.1)
174+
175+
# NTP time is seconds-since-2000
176+
print("NTP time: ", t)
177+
178+
# But we need Unix time, which is seconds-since-1970
179+
t += EPOCH_DELTA
180+
print("Unix time: ", t)
181+
182+
# Instead of using RTC which means converting back and forth
183+
# we'll just keep track of seconds-elapsed-since-NTP-call
184+
mono_time = int(time.monotonic())
185+
print("Monotonic time", mono_time)
186+
187+
countdown = ON_SECONDS # how long to stay on if not in always_on mode
188+
while ALWAYS_ON or (countdown > 0):
189+
# Calculate current time based on NTP + monotonic
190+
unix_time = t - mono_time + int(time.monotonic())
191+
print("Unix time: ", unix_time)
192+
193+
# Clear the screen
194+
oled.fill(0)
195+
y = 0
196+
# We can do up to 3 per line on the Feather OLED
197+
for name, secret in totp:
198+
otp = generate_otp(unix_time // 30, secret)
199+
print(name + " OTP output: ", otp) # serial debugging output
200+
oled.text(name + ": " + str(otp), 0, y) # display name & OTP on OLED
201+
y += 10 # Go to next line on OLED
202+
# Display a little bar that 'counts down' how many seconds you have left
203+
oled.framebuf.line(0, 31, 128 - (unix_time % 30) * 4, 31, True)
204+
oled.show()
205+
# We'll update every 1/4 second, we can hash very fast so its no biggie!
206+
countdown -= 0.25
207+
time.sleep(0.25)
208+
209+
# All these hashes will be lost in time(), like tears in rain. Time to die
210+
oled.fill(0)
211+
oled.show()

0 commit comments

Comments
 (0)