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