From: mar77i Date: Sat, 27 Jun 2026 07:21:10 +0000 (+0200) Subject: allow creating secrets.h from OTP DUMP output X-Git-Url: https://git.mar77i.info/?a=commitdiff_plain;h=5ad936d454da8a55e82685b78daf01db63395418;p=otp_nano allow creating secrets.h from OTP DUMP output --- diff --git a/decrypt.c b/decrypt.c index bf47543..7c6b9e4 100644 --- a/decrypt.c +++ b/decrypt.c @@ -93,7 +93,7 @@ uint8_t password_is_ready(void) { return decrypt_buffers.status > 0; } -int8_t list_secrets(void) { +void list_secrets(void) { size_t i; uint8_t c, colon_found = 0; for (i = 0; i < sizeof secret; i++) { @@ -106,7 +106,30 @@ int8_t list_secrets(void) { UART_write_byte(c); } decrypt_buffers.status = 2; - return 0; +} + +void dump_secrets(void) { + size_t i; + uint8_t b; + UART_write_string("{\"iv\":\""); + for (i = 0; i < sizeof iv; i++) { + b = pgm_read_byte(iv.b + i); + UART_write_byte("0123456789abcdef"[b >> 4]); + UART_write_byte("0123456789abcdef"[b & 15]); + } + UART_write_string("\",\n\"pw_mac\":\""); + for (i = 0; i < sizeof pw_mac; i++) { + b = pgm_read_byte(pw_mac.b + i); + UART_write_byte("0123456789abcdef"[b >> 4]); + UART_write_byte("0123456789abcdef"[b & 15]); + } + UART_write_string("\",\n\"secret\":\""); + for (i = 0; i < sizeof secret; i++) { + b = pgm_read_byte(secret + i); + UART_write_byte("0123456789abcdef"[b >> 4]); + UART_write_byte("0123456789abcdef"[b & 15]); + } + UART_write_string("\"}\n"); } static inline int8_t get_secret_and_totp_line( @@ -120,8 +143,10 @@ static inline int8_t get_secret_and_totp_line( ssize_t len; uint8_t *ptr, i, decimal[7]; ptr = line + colon_offset; - if ((len = from_b32_inplace(ptr)) <= 0) + if ((len = from_b32_inplace(ptr)) <= 0) { + UART_write_string("ERROR:INVALIDB32\n"); return -1; + } sha1_hmac(ptr, len, time_msg, 8, &mac); i = mac.b[sizeof mac - 1] & 0xf; UART_write_string_partial(line, colon_offset); diff --git a/decrypt.h b/decrypt.h index 39a3f56..a09cb76 100644 --- a/decrypt.h +++ b/decrypt.h @@ -8,7 +8,8 @@ uint8_t init_decrypt_buffers(uint8_t *password, uint8_t password_len); uint8_t password_is_ready(void); -int8_t list_secrets(void); +void list_secrets(void); +void dump_secrets(void); int8_t get_secrets_and_totp(uint8_t time_msg[8], uint8_t *search); void clear_decrypt_buffers(void); diff --git a/encoding.h b/encoding.h index b01ac1e..d140501 100644 --- a/encoding.h +++ b/encoding.h @@ -4,14 +4,11 @@ #ifndef ENCODING_H #define ENCODING_H -#include +#include #include -#include typedef int16_t ssize_t; -#include "uart.h" - static inline char from_hex8(char c) { if (c >= '0' && c <= '9') return c - '0'; @@ -86,10 +83,8 @@ static inline ssize_t from_b32_inplace(char *s) { for (; (val = b32_lut[s[pos]]) != 0xfd; pos++) { if (val == 0xfe) continue; - else if (val == 0) { - UART_write_string("ERROR:INVALIDB32\n"); + else if (val == 0) return -1; - } val &= 31; bits_left -= 5; if (bits_left < 0) { diff --git a/main.c b/main.c index 6c35c17..076ce52 100644 --- a/main.c +++ b/main.c @@ -2,7 +2,6 @@ // main.c #include -#include #include #include "decrypt.h" @@ -85,9 +84,13 @@ int8_t process_payload(uint8_t *payload) { payload += 4; if (!password_is_ready()) return -1; - if (strcmp(payload, "LS") == 0) - return list_secrets(); - else if (strncmp(payload, "GET ", 4) == 0) + if (strcmp(payload, "LS") == 0) { + list_secrets(); + return 0; + } else if (strcmp(payload, "DUMP") == 0) { + dump_secrets(); + return 0; + } else if (strncmp(payload, "GET ", 4) == 0) return process_otp_get(payload + 4); } return -1; diff --git a/manage_secrets.py b/manage_secrets.py index 5320c3a..e32d2af 100755 --- a/manage_secrets.py +++ b/manage_secrets.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import hmac +import json +import sys from argparse import ArgumentParser from getpass import getpass from hashlib import sha1 @@ -29,6 +31,9 @@ class CryptoManager: self.password = password self.indices = self.init_indices(self.get_hash(self.password, self.iv)) + def pw_mac(self): + return self.get_hash(self.iv, self.password) + def init_indices(self, first_key) -> list[int]: indices = list(range(20)) pick_accu = 0 @@ -55,24 +60,22 @@ class CryptoManager: opad_ba[i] ^= c return sha1(ipad_ba), sha1(opad_ba) - def scramble(self, data): - bio = BytesIO() + def scramble(self, data, fh=None): + if fh is None: + fh = BytesIO() round_key = self.iv ipad_template, opad_template = self.prepare_ipad_opad() for i in range(0, len(data), 10): - #derived = self.get_hash( - # self.password, round_key := self.advance_round_key(round_key) - #) round_key = self.advance_round_key(round_key) ipad, opad = ipad_template.copy(), opad_template.copy() ipad.update(round_key) opad.update(ipad.digest()) block = data[i:i + 10] - bio.write(bytes(a ^ b for a, b in zip(block, opad.digest()[:len(block)]))) - return bio.getvalue() + fh.write(bytes(a ^ b for a, b in zip(block, opad.digest()[:len(block)]))) + return fh -class HeaderReader: +class HeaderCodec: def __init__(self): self.iv_bio = BytesIO() self.pw_mac_bio = BytesIO() @@ -114,13 +117,16 @@ class HeaderReader: if done: self.current_bio = None - def parse(self, fh): + @classmethod + def parse(cls, fh): + header = cls() for line in fh: line = line.strip() - if self.current_bio is None: - self.find_header(line) + if header.current_bio is None: + header.find_header(line) else: - self.add_bytes(line) + header.add_bytes(line) + return header @property def iv(self): @@ -140,103 +146,117 @@ class HeaderReader: assert len(secret) > 0 return secret - -def cleanup_secret(infh): - return infh.read() - bio = BytesIO() - for line in infh: - line = line.strip() - if not line: - continue - assert len(line) < 79 and b":" in line - bio.write(line) - bio.write(b"\n") - return bio.getvalue() - - -class HeaderWriter: @staticmethod def indent(i): return " " * i - def __init__(self, fh, password): - self.fh = fh - self.iv = token_bytes(20) - self.password = password - - def print_as_const_sha_buf(self, varname, data): - print(f"static const struct sha1_buf {varname} PROGMEM = {{", file=self.fh) - print(f"{self.indent(4)}{{\n{self.indent(8)}", end="", file=self.fh) + @classmethod + def print_as_const_sha_buf(cls, fh, varname, data): + print(f"static const struct sha1_buf {varname} PROGMEM = {{", file=fh) + print(f"{cls.indent(4)}{{\n{cls.indent(8)}", end="", file=fh) last = len(data) - 1 for i, b in enumerate(data): - print(f"0x{b:02x}", end=",", file=self.fh) + print(f"0x{b:02x}", end=",", file=fh) if i == last: - print(f"\n{self.indent(4)}}}", file=self.fh) + print(f"\n{cls.indent(4)}}}", file=fh) elif i % 12 == 11: - print(f"\n{self.indent(8)}", end="", file=self.fh) + print(f"\n{cls.indent(8)}", end="", file=fh) else: - print(end=" ", file=self.fh) - print("};\n", file=self.fh) + print(end=" ", file=fh) + print("};\n", file=fh) - def print_as_const(self, varname, data): - print(f"static const uint8_t {varname}[] PROGMEM = {{", file=self.fh) - print(f"{self.indent(4)}", end="", file=self.fh) + @classmethod + def print_as_const(cls, fh, varname, data): + print(f"static const uint8_t {varname}[] PROGMEM = {{", file=fh) + print(f"{cls.indent(4)}", end="", file=fh) last = len(data) - 1 for i, b in enumerate(data): - print(f"0x{b:02x}", end=",", file=self.fh) + print(f"0x{b:02x}", end=",", file=fh) if i == last: - print(file=self.fh) + print(file=fh) elif i % 12 == 11: - print(f"\n{self.indent(4)}", end="", file=self.fh) + print(f"\n{cls.indent(4)}", end="", file=fh) else: - print(end=" ", file=self.fh) - print("};\n", file=self.fh) - - def write(self, secret): - print("\n// secrets.h\n", file=self.fh) - print("#ifndef SECRETS_H", file=self.fh) - print("#define SECRETS_H\n", file=self.fh) - print("#include ", file=self.fh) - print("#include \n", file=self.fh) - print('#include "sha1.h"\n', file=self.fh) - self.print_as_const_sha_buf("iv", self.iv) - self.print_as_const_sha_buf( - "pw_mac", CryptoManager.get_hash(self.iv, self.password) - ) - self.print_as_const( - "secret", - CryptoManager(self.iv, self.password).scramble(secret), - ) - print("#endif // SECRETS_H", file=self.fh) + print(end=" ", file=fh) + print("};\n", file=fh) + + @classmethod + def write(cls, fh, iv, pw_mac, secret): + print("\n// secrets.h\n", file=fh) + print("#ifndef SECRETS_H", file=fh) + print("#define SECRETS_H\n", file=fh) + print("#include ", file=fh) + print("#include \n", file=fh) + print('#include "sha1.h"\n', file=fh) + cls.print_as_const_sha_buf(fh, "iv", iv) + cls.print_as_const_sha_buf(fh, "pw_mac", pw_mac) + cls.print_as_const(fh, "secret", secret) + print("#endif // SECRETS_H", file=fh) + + +def cleanup_secret(infh): + bio = BytesIO() + for line in infh: + line = line.strip() + if not line: + continue + assert len(line) < 79 and b":" in line + bio.write(line) + bio.write(b"\n") + return bio.getvalue() def encrypt_secrets(project_dir, infile, new_password): password = getpass().encode() secrets_h = project_dir / "secrets.h" if not new_password and secrets_h.exists(): - header = HeaderReader() with secrets_h.open("rt") as fh: - header.parse(fh) + header = HeaderCodec.parse(fh) assert CryptoManager.get_hash(header.iv, password) == header.pw_mac else: other = getpass("Confirm password: ").encode() assert password == other with Path(infile).open("rb") as infh, secrets_h.open("wt") as outfh: - HeaderWriter(outfh, password).write(cleanup_secret(infh)) + crypto_manager = CryptoManager(token_bytes(20), password) + HeaderCodec.write( + outfh, + crypto_manager.iv, + crypto_manager.pw_mac(), + crypto_manager.scramble(cleanup_secret(infh)).getvalue(), + ) def decrypt_secrets(project_dir, outfile): password = getpass().encode() secrets_h = project_dir / "secrets.h" assert secrets_h.exists() - header = HeaderReader() with secrets_h.open("rt") as fh: - header.parse(fh) + header = HeaderCodec.parse(fh) assert CryptoManager.get_hash(header.iv, password) == header.pw_mac - with Path(outfile).open("wb") as fh: - fh.write(CryptoManager(header.iv, password).scramble(header.secret)) + outfile_path = Path(outfile) + assert outfile_path.absolute() != secrets_h.absolute(), ( + "Don't decrypt into secrets.h" + ) + with outfile_path.open("wb") as fh: + CryptoManager(header.iv, password).scramble(header.secret, fh) -def main(): +def from_dump(project_dir, outfile): + print("reading dump > ", end="") + sys.stdout.flush() + data = json.load(sys.stdin) + iv = bytes.fromhex(data["iv"]) + pw_mac = bytes.fromhex(data["pw_mac"]) + secret = bytes.fromhex(data["secret"]) + print("done") + if outfile is None: + outfile = project_dir / "secrets.h" + else: + outfile = Path(outfile) + with outfile.open("wt") as fh: + HeaderCodec.write(fh, iv, pw_mac, secret) + + +def parse_args(): ap = ArgumentParser() sp = ap.add_subparsers(dest="action", required=True, help="Actions") ep = sp.add_parser("encrypt") @@ -244,12 +264,20 @@ def main(): ep.add_argument("--new-password", action="store_true") dp = sp.add_parser("decrypt") dp.add_argument("outfile") - args = ap.parse_args() + mp = sp.add_parser("from_dump") + mp.add_argument("outfile", nargs="?", default=None) + return ap.parse_args() + + +def main(): + args = parse_args() project_dir = Path(__file__).parent.absolute() if args.action == "encrypt": encrypt_secrets(project_dir, args.infile, args.new_password) elif args.action == "decrypt": decrypt_secrets(project_dir, args.outfile) + elif args.action == "from_dump": + from_dump(project_dir, args.outfile) if __name__ == "__main__": diff --git a/repl.py b/repl.py index 288bed9..349eafb 100755 --- a/repl.py +++ b/repl.py @@ -3,6 +3,7 @@ import os import sys from getpass import getpass +from io import BytesIO from termios import ( B115200, CS8, @@ -78,37 +79,46 @@ class ReplManager: def print_reply(self, line): try: - response = line.decode('ascii').strip() + response = line.decode('ascii') except UnicodeDecodeError: print("decoding failed.") response = str(line) print(response) def await_some_reply(self, wait_for_ok=False, timeout=0.3): - ba = bytearray() + bio = BytesIO() stop_time = time() + timeout - done = False - while not done: + while time() < stop_time: sleep(0.1) t = time() - try: - received = os.read(self.fd, 256) - except BlockingIOError: - received = b"" - done |= t >= stop_time - if not received or done: + r = 0 + while True: + try: + received = os.read(self.fd, 4096) + except BlockingIOError: + received = b"" + if not received: + break + r += len(received) + bio.write(received) + stop_time = t + (timeout if wait_for_ok else 0.3) + if r == 0: continue - ba += received - stop_time = t + (timeout if wait_for_ok else 0.3) - while b"\n" in ba: - line, rest = ba.split(b"\n", 1) + value = bio.getvalue() + bio.seek(0) + bio.truncate() + while b"\n" in value: + line, value = value.split(b"\n", 1) self.print_reply(line) - ba = rest if wait_for_ok and line in (b"OK", b"NOK", b"EYB"): - done = True + stop_time = t break - if len(ba) > 0: - self.print_reply(ba) + bio.write(value) + value = bio.getvalue() + bio.seek(0) + bio.truncate() + if len(value) > 0: + self.print_reply(value) def __exit__(self, exc_type, exc_val, exc_tb): if self.last_cmd != "BYE": diff --git a/sha1.c b/sha1.c index eab857f..5982b7d 100644 --- a/sha1.c +++ b/sha1.c @@ -1,7 +1,6 @@ // sha1.c -#include #include "sha1.h" #define ARRAY_LENGTH(arr) (sizeof (arr) / sizeof (arr)[0])