/ 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()