diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-19 15:27:37 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-19 15:27:37 +0200 |
| commit | dd5d0f68726fe56ee4d199867d414aea81905cc4 (patch) | |
| tree | 1cb40b024ac13f454633198ad50cbc55f7283f39 /source/storage/Codec.mc | |
| parent | 94181d58cf66c5b7b263fd435a344f1cd8872cff (diff) | |
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 <noreply@anthropic.com>
Diffstat (limited to 'source/storage/Codec.mc')
| -rw-r--r-- | source/storage/Codec.mc | 101 |
1 files changed, 101 insertions, 0 deletions
diff --git a/source/storage/Codec.mc b/source/storage/Codec.mc new file mode 100644 index 0000000..469ba82 --- /dev/null +++ b/source/storage/Codec.mc @@ -0,0 +1,101 @@ +// Author: cirakg & claude ai +// Summary: Deterministic TLV byte encoding of record fields, so the same fields always give the same MAC input. +// +// A field is: tag (1 byte) | type (1 byte) | length (2 bytes, big-endian) | value. +// Types: bytes, UTF-8 text, 64-bit integer (8 bytes big-endian), boolean +// (1 byte). Fields are written in the order the caller gives, and Record.mc +// always gives them in tag order, so encoding is canonical. +// +// decode() is strict: a truncated field, an unknown type, invalid UTF-8 or a +// duplicate tag gives null, never a partial result. + +import Toybox.Lang; + +module Codec { + + const TYPE_BYTES = 1; + const TYPE_TEXT = 2; + const TYPE_INT = 3; + const TYPE_BOOL = 4; + const MAX_VALUE_BYTES = 65535; + + // Builds an encoding field by field. + class Writer { + var bytes as ByteArray = []b; + + function initialize() { + } + + function header(tag as Number, type as Number, length as Number) as Void { + if (length > MAX_VALUE_BYTES) { + throw new Lang.InvalidValueException("Codec: field too long"); + } + bytes.add(tag); + bytes.add(type); + bytes.add((length >> 8) & 0xFF); + bytes.add(length & 0xFF); + } + + function addBytes(tag as Number, value as ByteArray) as Void { + header(tag, TYPE_BYTES, value.size()); + bytes.addAll(value); + } + + function addText(tag as Number, value as String) as Void { + var utf8 = Bytes.fromUtf8(value); + header(tag, TYPE_TEXT, utf8.size()); + bytes.addAll(utf8); + } + + function addInt(tag as Number, value as Number or Long) as Void { + header(tag, TYPE_INT, 8); + bytes.addAll(Bytes.uint64BigEndian(value.toLong())); + } + + function addBool(tag as Number, value as Boolean) as Void { + header(tag, TYPE_BOOL, 1); + bytes.add(value ? 1 : 0); + } + } + + // Tag -> value (ByteArray, String, Long or Boolean), or null when the + // encoding is malformed. + function decode(data as ByteArray) as Dictionary<Number, ByteArray or String or Long or Boolean>? { + var fields = {} as Dictionary<Number, ByteArray or String or Long or Boolean>; + var i = 0; + while (i < data.size()) { + if (i + 4 > data.size()) { + return null; + } + var tag = data[i]; + var type = data[i + 1]; + var length = (data[i + 2] << 8) | data[i + 3]; + i += 4; + if (i + length > data.size() || fields.hasKey(tag)) { + return null; + } + var value = data.slice(i, i + length); + i += length; + if (type == TYPE_BYTES) { + fields[tag] = value; + } else if (type == TYPE_TEXT) { + var text = Bytes.toUtf8(value); + if (text == null) { + return null; + } + fields[tag] = text; + } else if (type == TYPE_INT && length == 8) { + var v = 0l; + for (var k = 0; k < 8; k++) { + v = (v << 8) | value[k].toLong(); + } + fields[tag] = v; + } else if (type == TYPE_BOOL && length == 1 && value[0] <= 1) { + fields[tag] = value[0] == 1; + } else { + return null; + } + } + return fields; + } +} |
