implemente password hash with nonce

This commit is contained in:
2026-06-29 21:47:51 +02:00
parent 6cecf89def
commit 7b9446056f
2 changed files with 105 additions and 18 deletions

1
crypto-js.min.js vendored Normal file

File diff suppressed because one or more lines are too long

114
main.py
View File

@@ -1,5 +1,9 @@
import os
import time import time
import network import network
import utime
import ubinascii
import uhashlib
import uasyncio as asyncio import uasyncio as asyncio
from machine import Pin from machine import Pin
@@ -10,23 +14,88 @@ computer = Pin(2, Pin.OUT)
wlan = network.WLAN(network.STA_IF) wlan = network.WLAN(network.STA_IF)
_nonce = ""
_nonce_ts = 0
NONCE_TTL_MS = 5 * 60 * 1000
HTML = """\ HTML = """\
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head><title>Pico W Computer Management</title></head> <head><title>Pico W</title></head>
<body> <body>
<h1>Pico W - Computer Management</h1> <h1>Pico W - Computer Management</h1>
%s %s
<form method="POST" action="/computer"> <form id="form" method="POST" action="/computer">
<input type="password" name="pwd" placeholder="Password" required> <input type="hidden" id="nonce" name="nonce" value="%s">
<button type="submit">Turn On/Off</button> <input type="hidden" id="hmac" name="hmac">
<input type="password" id="pwd" placeholder="Password" required>
<button type="submit">Turn On/Off</button>
</form> </form>
<script src="/crypto-js.min.js"></script>
<script>
document.getElementById('form').addEventListener('submit', async function(event) {
event.preventDefault();
const pwd = document.getElementById('pwd').value;
const nonce = document.getElementById('nonce').value;
const hmac = CryptoJS.HmacSHA256(nonce, pwd).toString(CryptoJS.enc.Hex);
document.getElementById('hmac').value = hmac;
document.getElementById('pwd').value = '';
this.submit();
});
</script>
</body> </body>
</html> </html>
""" """
MSG_WRONG = "<p><strong>Wrong password.</strong></p>" MSG_WRONG = "<p><strong>Wrong password.</strong></p>"
MSG_OK = "<p>Computer turned on/off</p>" MSG_OK = "<p>Signal sent to computer.</p>"
MSG_EXPIRED = "<p><strong>Session expired, please reload the page.</strong></p>"
def _new_nonce() -> str:
global _nonce, _nonce_ts
_nonce = ubinascii.hexlify(os.urandom(16)).decode()
_nonce_ts = utime.ticks_ms()
return _nonce
def _nonce_valid(received: str) -> bool:
expired = utime.ticks_diff(utime.ticks_ms(), _nonce_ts) > NONCE_TTL_MS
return (not expired) and (_nonce != "") and (received == _nonce)
def _invalidate_nonce():
global _nonce, _nonce_ts
_nonce = ""
_nonce_ts = 0
def hmac_sha256(key: str, message: str) -> str:
BLOCK = 64
k = key.encode() if isinstance(key, str) else key
m = message.encode() if isinstance(message, str) else message
if len(k) > BLOCK:
k = uhashlib.sha256(k).digest()
k = k + b'\x00' * (BLOCK - len(k))
ipad = bytes(b ^ 0x36 for b in k)
opad = bytes(b ^ 0x5c for b in k)
ih = uhashlib.sha256()
ih.update(ipad)
ih.update(m)
oh = uhashlib.sha256()
oh.update(opad)
oh.update(ih.digest())
return ubinascii.hexlify(oh.digest()).decode()
def ct_equal(a: str, b: str) -> bool:
if len(a) != len(b):
return False
result = 0
for x, y in zip(a.encode(), b.encode()):
result |= x ^ y
return result == 0
def url_decode(s): def url_decode(s):
s = s.replace("+", " ") s = s.replace("+", " ")
@@ -41,7 +110,6 @@ def url_decode(s):
i += 1 i += 1
return "".join(out) return "".join(out)
def parse_form(body): def parse_form(body):
params = {} params = {}
for pair in body.split("&"): for pair in body.split("&"):
@@ -74,7 +142,6 @@ def _do_connect():
print("Connection failed (status=%d)" % wlan.status()) print("Connection failed (status=%d)" % wlan.status())
return False return False
def connect_wifi(): def connect_wifi():
while True: while True:
print("Connecting to Wi-Fi…") print("Connecting to Wi-Fi…")
@@ -83,7 +150,6 @@ def connect_wifi():
wlan.disconnect() wlan.disconnect()
time.sleep(3) time.sleep(3)
async def wifi_watchdog(): async def wifi_watchdog():
while True: while True:
await asyncio.sleep(5) await asyncio.sleep(5)
@@ -117,19 +183,41 @@ async def serve_client(reader, writer):
content_length = int(header.split(b":")[1].strip()) content_length = int(header.split(b":")[1].strip())
message = "" message = ""
if method == "GET" and path == "/crypto-js.min.js":
with open("crypto-js.min.js", "rb") as f:
js = f.read()
writer.write(
"HTTP/1.0 200 OK\r\n"
"Content-Type: application/javascript\r\n"
"Content-Length: %d\r\n\r\n" % len(js)
)
writer.write(js)
await writer.drain()
return
if method == "POST" and path == "/computer" and content_length > 0: if method == "POST" and path == "/computer" and content_length > 0:
body = await reader.read(content_length) body = await reader.read(content_length)
params = parse_form(body.decode()) params = parse_form(body.decode())
pwd = params.get("pwd", "")
if pwd != PASSWORD: received_nonce = params.get("nonce", "")
message = MSG_WRONG received_hmac = params.get("hmac", "")
if not _nonce_valid(received_nonce):
message = MSG_EXPIRED
else: else:
expected = hmac_sha256(PASSWORD, received_nonce)
_invalidate_nonce()
if ct_equal(expected, received_hmac):
asyncio.create_task(toggleComputer()) asyncio.create_task(toggleComputer())
message = MSG_OK message = MSG_OK
else:
message = MSG_WRONG
nonce = _new_nonce()
writer.write("HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n") writer.write("HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n")
writer.write(HTML % message) writer.write(HTML % (message, nonce))
await writer.drain() await writer.drain()
except OSError as e: except OSError as e:
@@ -137,7 +225,6 @@ async def serve_client(reader, writer):
finally: finally:
await writer.wait_closed() await writer.wait_closed()
async def main(): async def main():
print("Connecting to network…") print("Connecting to network…")
connect_wifi() connect_wifi()
@@ -149,7 +236,6 @@ async def main():
while True: while True:
await asyncio.sleep(60) await asyncio.sleep(60)
try: try:
asyncio.run(main()) asyncio.run(main())
finally: finally: