summaryrefslogtreecommitdiff
path: root/source/storage/Migration.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/Migration.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/Migration.mc')
-rw-r--r--source/storage/Migration.mc89
1 files changed, 89 insertions, 0 deletions
diff --git a/source/storage/Migration.mc b/source/storage/Migration.mc
new file mode 100644
index 0000000..e41d3fb
--- /dev/null
+++ b/source/storage/Migration.mc
@@ -0,0 +1,89 @@
+// Author: cirakg & claude ai
+// Summary: Storage schema versions: create the metadata on first start, migrate old schemas, refuse newer ones.
+//
+// The metadata "m" is a dictionary:
+// "schema" data version (CURRENT)
+// "ids" entry IDs in display order
+// "pending" IDs of pending locked entries (README 7.7)
+//
+// Rules (README 6.3):
+// * Each migration is one small, idempotent function from version k to
+// k + 1, run in order at start-up.
+// * A migration that rewrites records writes each new record under a
+// temporary key and switches the index to it before deleting the old
+// one, so a crash or power loss mid-migration loses nothing. The only
+// migration so far (0 to 1) changes just the metadata, in one write.
+// * Data from a newer schema (after a downgrade) is never modified.
+//
+// Version 0 never shipped: it is a fixture layout ("m" = {"order" => ids})
+// that exists to test the migration machinery before a real migration is
+// needed.
+
+import Toybox.Application;
+import Toybox.Lang;
+
+module Migration {
+
+ const CURRENT = 1;
+ const META_KEY = "m";
+
+ // The metadata at the CURRENT schema, migrating or creating it as needed.
+ // Throws VaultError NEWER_SCHEMA or DAMAGED.
+ function run(store as Persist.Backend) as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType> {
+ var stored = store.getValue(META_KEY);
+ if (stored == null) {
+ var fresh = {"schema" => CURRENT, "ids" => [], "pending" => []} as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>;
+ store.setValue(META_KEY, fresh);
+ return fresh;
+ }
+ if (!(stored instanceof Dictionary)) {
+ throw new VaultError(VaultError.DAMAGED, "metadata damaged");
+ }
+ var meta = stored as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>;
+ var schema = meta.hasKey("schema") ? meta["schema"] : 0;
+ if (!(schema instanceof Number) || schema < 0) {
+ throw new VaultError(VaultError.DAMAGED, "metadata damaged");
+ }
+ if (schema > CURRENT) {
+ throw new VaultError(VaultError.NEWER_SCHEMA, "data from a newer version");
+ }
+ if (schema == 0) {
+ meta = v0ToV1(meta);
+ store.setValue(META_KEY, meta);
+ }
+ if (!isValidV1(meta)) {
+ throw new VaultError(VaultError.DAMAGED, "metadata damaged");
+ }
+ return meta;
+ }
+
+ // Version 0 to 1: "order" becomes "ids", and "pending" is added.
+ function v0ToV1(meta as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>) as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType> {
+ var order = meta["order"];
+ return {
+ "schema" => 1,
+ "ids" => order instanceof Array ? order : [],
+ "pending" => []
+ } as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>;
+ }
+
+ // True when "ids" and "pending" are arrays of distinct valid IDs.
+ function isValidV1(meta as Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>) as Boolean {
+ var seen = {} as Dictionary<String, Boolean>;
+ var lists = [meta["ids"], meta["pending"]];
+ for (var l = 0; l < lists.size(); l++) {
+ var list = lists[l];
+ if (!(list instanceof Array)) {
+ return false;
+ }
+ for (var i = 0; i < list.size(); i++) {
+ var id = list[i];
+ if (!(id instanceof String) || !Entry.isValidId(id) || seen.hasKey(id)) {
+ return false;
+ }
+ seen[id] = true;
+ }
+ }
+ return true;
+ }
+}