summaryrefslogtreecommitdiff
path: root/source/storage/Codec.mc
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-19 15:27:37 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-19 15:27:37 +0200
commitdd5d0f68726fe56ee4d199867d414aea81905cc4 (patch)
tree1cb40b024ac13f454633198ad50cbc55f7283f39 /source/storage/Codec.mc
parent94181d58cf66c5b7b263fd435a344f1cd8872cff (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.mc101
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;
+ }
+}