From dd5d0f68726fe56ee4d199867d414aea81905cc4 Mon Sep 17 00:00:00 2001 From: cirakg Date: Mon, 19 Oct 2026 15:27:37 +0200 Subject: feat(storage): add the encrypted vault, PIN keys, lockout, session and migrations Records are one ByteArray each: a readable TLV header plus an encrypt-then-MAC box whose aad binds the entry ID and the header. The device key seals unlocked entries and pending items; a random master key, wrapped under a PBKDF2 PIN key, seals locked ones. Wrong PINs are counted before the check, with delays on the trusted clock and an uptime fallback. Bytes.toUtf8 validates UTF-8 first, because the native conversion crashes on invalid input. Co-Authored-By: Claude Opus 5.5 --- source/util/Bytes.mc | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) (limited to 'source/util/Bytes.mc') diff --git a/source/util/Bytes.mc b/source/util/Bytes.mc index 1008fd2..a32475a 100644 --- a/source/util/Bytes.mc +++ b/source/util/Bytes.mc @@ -47,6 +47,66 @@ module Bytes { }) as ByteArray; } + // Decode UTF-8 bytes to a string. Empty input gives "". Malformed UTF-8 + // or a NUL byte gives null (callers treat that as damaged data). The + // bytes are checked first because convertEncodedString stops the app + // with an uncatchable system error on invalid UTF-8. + function toUtf8(data as ByteArray) as String? { + if (data.size() == 0) { + return ""; + } + if (!isValidUtf8(data)) { + return null; + } + return StringUtil.convertEncodedString(data, { + :fromRepresentation => StringUtil.REPRESENTATION_BYTE_ARRAY, + :toRepresentation => StringUtil.REPRESENTATION_STRING_PLAIN_TEXT, + :encoding => StringUtil.CHAR_ENCODING_UTF8 + }) as String; + } + + // True for well-formed UTF-8 (RFC 3629) without NUL bytes: no overlong + // forms, no surrogates (U+D800-DFFF), nothing above U+10FFFF. + function isValidUtf8(data as ByteArray) as Boolean { + var i = 0; + var n = data.size(); + while (i < n) { + var b = data[i]; + var more = 0; + var min = 0; + var cp = 0; + if (b == 0) { + return false; + } else if (b < 0x80) { + i++; + continue; + } else if (b >= 0xC2 && b <= 0xDF) { + more = 1; min = 0x80; cp = b & 0x1F; + } else if (b >= 0xE0 && b <= 0xEF) { + more = 2; min = 0x800; cp = b & 0x0F; + } else if (b >= 0xF0 && b <= 0xF4) { + more = 3; min = 0x10000; cp = b & 0x07; + } else { + return false; + } + if (i + more >= n) { + return false; + } + for (var k = 1; k <= more; k++) { + var c = data[i + k]; + if ((c & 0xC0) != 0x80) { + return false; + } + cp = (cp << 6) | (c & 0x3F); + } + if (cp < min || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) { + return false; + } + i += more + 1; + } + return true; + } + // A new array holding a followed by b; neither input is changed. function concat(a as ByteArray, b as ByteArray) as ByteArray { var out = new [0]b; -- cgit v1.2.3