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/Migration.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/Migration.mc')
| -rw-r--r-- | source/storage/Migration.mc | 89 |
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; + } +} |
