OTP functionality implemented
This commit is contained in:
@@ -172,6 +172,16 @@ Exports the following functions:
|
|||||||
- `wget(url, filename='', useragent=None, hdrs={})`: download a file into a local path (returns `(status_code, filename)` tuple)
|
- `wget(url, filename='', useragent=None, hdrs={})`: download a file into a local path (returns `(status_code, filename)` tuple)
|
||||||
- `wput(url, file_path, field_name="file", method="POST", format="x-www-form-urlencoded", headers={}, useragent=None)`: upload a file from a local path in the `x-www-form-urlencoded` or JSON format (returns `(status_code, content)` tuple)
|
- `wput(url, file_path, field_name="file", method="POST", format="x-www-form-urlencoded", headers={}, useragent=None)`: upload a file from a local path in the `x-www-form-urlencoded` or JSON format (returns `(status_code, content)` tuple)
|
||||||
|
|
||||||
|
### `deck.otp`
|
||||||
|
|
||||||
|
A library that provides HOTP and TOTP one-time password implementations.
|
||||||
|
|
||||||
|
Exports the following functions:
|
||||||
|
|
||||||
|
- `hotp(key, counter, digits=6)`: generate a `digits`-long HOTP code from (bytes-only) key and counter value
|
||||||
|
- `totp(key, time_step, digits=6)`: generate a `digits`-long TOTP code from (bytes-only) key, using `time_step` as the basis (30 seconds is default and the most common)
|
||||||
|
- `get_epoch()`: a helper platform-agnostic function to get the current Unix timestamp in seconds
|
||||||
|
|
||||||
### `deck.llm`
|
### `deck.llm`
|
||||||
|
|
||||||
A helper library for interaction with remote LLM (large language model) endpoints that expose OpenAI-compatible API. As of now, it only exports a single class, `LLMChat`, to encapsulate all the interaction.
|
A helper library for interaction with remote LLM (large language model) endpoints that expose OpenAI-compatible API. As of now, it only exports a single class, `LLMChat`, to encapsulate all the interaction.
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ while True:
|
|||||||
status, res = runcode(__cmd)
|
status, res = runcode(__cmd)
|
||||||
if status != 1:
|
if status != 1:
|
||||||
__cmd = ""
|
__cmd = ""
|
||||||
|
if status == 0:
|
||||||
|
print("*ERROR* Exception:", str(res))
|
||||||
except Exception as __err:
|
except Exception as __err:
|
||||||
print("*ERROR* Exception:", str(__err))
|
print("*ERROR* Exception:", str(__err))
|
||||||
__cmd = ""
|
__cmd = ""
|
||||||
|
|||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
# deck.otp: HOTP/TOTP implementation for T-DeckARD
|
||||||
|
# Initially based on the utotp code
|
||||||
|
# Should be compatible with CircuitPython, MicroPython and CPython
|
||||||
|
# On CircuitPython, depends on adafruit_hashlib
|
||||||
|
# Usage: from deck.otp import hotp, totp
|
||||||
|
# Created by Luxferre in 2025, released into public domain
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ustruct as struct
|
||||||
|
except ImportError:
|
||||||
|
import struct
|
||||||
|
|
||||||
|
try:
|
||||||
|
from adafruit_hashlib import sha1
|
||||||
|
except ImportError:
|
||||||
|
from hashlib import sha1
|
||||||
|
|
||||||
|
FORCE_MPY = False
|
||||||
|
try:
|
||||||
|
from deck.time import datetime, timedelta
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
except ImportError:
|
||||||
|
from time import time
|
||||||
|
FORCE_MPY = True
|
||||||
|
|
||||||
|
class Sha1HMAC:
|
||||||
|
def __init__(self, key, msg=None):
|
||||||
|
def translate(d, t):
|
||||||
|
return bytes(t[x] for x in d)
|
||||||
|
_trans_5C = bytes((x ^ 0x5C) for x in range(256))
|
||||||
|
_trans_36 = bytes((x ^ 0x36) for x in range(256))
|
||||||
|
self.hasher = sha1
|
||||||
|
self.outer = self.hasher()
|
||||||
|
self.inner = self.hasher()
|
||||||
|
self.digest_size = 20 # // hashlib.sha1().digest_size
|
||||||
|
self.blocksize = 64
|
||||||
|
if len(key) > self.blocksize:
|
||||||
|
key = self.hasher(key).digest()
|
||||||
|
key = key + bytes(self.blocksize - len(key))
|
||||||
|
self.outer.update(translate(key, _trans_5C))
|
||||||
|
self.inner.update(translate(key, _trans_36))
|
||||||
|
if msg is not None:
|
||||||
|
self.update(msg)
|
||||||
|
|
||||||
|
def update(self, msg):
|
||||||
|
self.inner.update(msg)
|
||||||
|
|
||||||
|
def _current(self):
|
||||||
|
self.outer.update(self.inner.digest())
|
||||||
|
return self.outer
|
||||||
|
|
||||||
|
def digest(self):
|
||||||
|
return self._current().digest()
|
||||||
|
|
||||||
|
def get_epoch():
|
||||||
|
maybe_time = 0
|
||||||
|
if FORCE_MPY:
|
||||||
|
maybe_time = time()
|
||||||
|
if maybe_time < 946684801:
|
||||||
|
maybe_time += 946684801
|
||||||
|
else:
|
||||||
|
maybe_time = datetime.now()
|
||||||
|
td = timedelta(seconds=946684801)
|
||||||
|
ref_time = datetime(1970, 1, 1) + td
|
||||||
|
if maybe_time < ref_time:
|
||||||
|
maybe_time += td
|
||||||
|
try:
|
||||||
|
from deck.net import init_ntp
|
||||||
|
ntpobj = init_ntp()
|
||||||
|
maybe_time = int(str(ntpobj.utc_ns)[:-9])
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
timeval = int(maybe_time)
|
||||||
|
except:
|
||||||
|
timeval = int(maybe_time.timestamp())
|
||||||
|
return timeval
|
||||||
|
|
||||||
|
def hotp(key, counter, digits=6):
|
||||||
|
counter = struct.pack(">Q", counter)
|
||||||
|
mac = Sha1HMAC(key, counter).digest()
|
||||||
|
offset = mac[-1] & 0x0F
|
||||||
|
binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7FFFFFFF
|
||||||
|
code = str(binary)[-digits:]
|
||||||
|
return ((digits - len(code)) * "0") + code
|
||||||
|
|
||||||
|
def totp(key, time_step=30, digits=6):
|
||||||
|
if isinstance(key, str):
|
||||||
|
try:
|
||||||
|
key = b32decode(key)
|
||||||
|
except:
|
||||||
|
raise ValueError("Key must be b32 string or seed bytes")
|
||||||
|
return hotp(key, get_epoch() // time_step, digits)
|
||||||
|
|
||||||
|
_b32alphabet = b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
||||||
|
_b32tab = [v for v in _b32alphabet]
|
||||||
|
_b32rev = dict([(v, k) for k,v in enumerate(_b32tab)])
|
||||||
|
|
||||||
|
def unhexlify(data):
|
||||||
|
if len(data) % 2 != 0:
|
||||||
|
raise ValueError("Odd-length string")
|
||||||
|
return bytes([int(data[i : i + 2], 16) for i in range(0, len(data), 2)])
|
||||||
|
|
||||||
|
def b32encode(s):
|
||||||
|
if not isinstance(s, bytes_types):
|
||||||
|
raise TypeError("expected bytes, not %s" % s.__class__.__name__)
|
||||||
|
quanta, leftover = divmod(len(s), 5)
|
||||||
|
# Pad the last quantum with zero bits if necessary
|
||||||
|
if leftover:
|
||||||
|
s = s + bytes(5 - leftover) # Don't use += !
|
||||||
|
quanta += 1
|
||||||
|
encoded = bytearray()
|
||||||
|
for i in range(quanta):
|
||||||
|
c1, c2, c3 = struct.unpack("!HHB", s[i * 5 : (i + 1) * 5])
|
||||||
|
c2 += (c1 & 1) << 16
|
||||||
|
c3 += (c2 & 3) << 8
|
||||||
|
encoded += bytes(
|
||||||
|
[
|
||||||
|
_b32tab[c1 >> 11],
|
||||||
|
_b32tab[(c1 >> 6) & 0x1F],
|
||||||
|
_b32tab[(c1 >> 1) & 0x1F],
|
||||||
|
_b32tab[c2 >> 12],
|
||||||
|
_b32tab[(c2 >> 7) & 0x1F],
|
||||||
|
_b32tab[(c2 >> 2) & 0x1F],
|
||||||
|
_b32tab[c3 >> 5],
|
||||||
|
_b32tab[c3 & 0x1F],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if leftover == 1:
|
||||||
|
encoded = encoded[:-6] + b"======"
|
||||||
|
elif leftover == 2:
|
||||||
|
encoded = encoded[:-4] + b"===="
|
||||||
|
elif leftover == 3:
|
||||||
|
encoded = encoded[:-3] + b"==="
|
||||||
|
elif leftover == 4:
|
||||||
|
encoded = encoded[:-1] + b"="
|
||||||
|
return bytes(encoded)
|
||||||
|
|
||||||
|
def b32decode(s):
|
||||||
|
if isinstance(s, str):
|
||||||
|
s = s.encode()
|
||||||
|
quanta, leftover = divmod(len(s), 8)
|
||||||
|
if leftover:
|
||||||
|
raise ValueError("Incorrect padding")
|
||||||
|
s = s.upper()
|
||||||
|
padchars = s.find(b"=")
|
||||||
|
if padchars > 0:
|
||||||
|
padchars = len(s) - padchars
|
||||||
|
s = s[:-padchars]
|
||||||
|
else:
|
||||||
|
padchars = 0
|
||||||
|
# Now decode the full quanta
|
||||||
|
parts = []
|
||||||
|
acc = 0
|
||||||
|
shift = 35
|
||||||
|
for c in s:
|
||||||
|
val = _b32rev.get(c)
|
||||||
|
if val is None:
|
||||||
|
raise ValueError("Non-base32 digit found")
|
||||||
|
acc += _b32rev[c] << shift
|
||||||
|
shift -= 5
|
||||||
|
if shift < 0:
|
||||||
|
parts.append(unhexlify(bytes("%010x" % acc, "ascii")))
|
||||||
|
acc = 0
|
||||||
|
shift = 35
|
||||||
|
# Process the last, partial quanta
|
||||||
|
last = unhexlify(bytes("%010x" % acc, "ascii"))
|
||||||
|
if padchars == 0:
|
||||||
|
last = b"" # No characters
|
||||||
|
elif padchars == 1:
|
||||||
|
last = last[:-1]
|
||||||
|
elif padchars == 3:
|
||||||
|
last = last[:-2]
|
||||||
|
elif padchars == 4:
|
||||||
|
last = last[:-3]
|
||||||
|
elif padchars == 6:
|
||||||
|
last = last[:-4]
|
||||||
|
else:
|
||||||
|
raise ValueError("Incorrect padding")
|
||||||
|
parts.append(last)
|
||||||
|
return b"".join(parts)
|
||||||
+2
-2
@@ -3,6 +3,6 @@
|
|||||||
# Created by Luxferre in 2025, released into public domain
|
# Created by Luxferre in 2025, released into public domain
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from adafruit_datetime import datetime, date, time
|
from adafruit_datetime import datetime, date, time, timedelta
|
||||||
except:
|
except:
|
||||||
import datetime, date, time
|
from datetime import datetime, date, time, timedelta
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ adafruit_requests>=4.1.15
|
|||||||
adafruit_datetime>=1.4.4
|
adafruit_datetime>=1.4.4
|
||||||
adafruit_ntp>=3.3.5
|
adafruit_ntp>=3.3.5
|
||||||
adafruit_bus_device>=5.2.14
|
adafruit_bus_device>=5.2.14
|
||||||
|
adafruit_hashlib>=1.4.20
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from deck.time import *
|
|||||||
from deck.hwinfo import board_id, battery_v, cpu_f, REAL_HW
|
from deck.hwinfo import board_id, battery_v, cpu_f, REAL_HW
|
||||||
from deck.http import wget, wput, http_fetch, http_set_ua
|
from deck.http import wget, wput, http_fetch, http_set_ua
|
||||||
from deck.http import auth_header, url_escape, form_encode
|
from deck.http import auth_header, url_escape, form_encode
|
||||||
|
from deck.otp import totp, hotp, get_epoch
|
||||||
from deck import xlat
|
from deck import xlat
|
||||||
from app.ed import edit, view, edbuf, viewbuf, edurl, viewurl
|
from app.ed import edit, view, edbuf, viewbuf, edurl, viewurl
|
||||||
from app.blog import blogpost, blogpost_str, bloged
|
from app.blog import blogpost, blogpost_str, bloged
|
||||||
|
|||||||
Reference in New Issue
Block a user