// Author: cirakg & claude ai // Summary: Keeps the text of an unfinished multi-part import between syncs, sealed with the device key, capped and expiring. // // README 11.2: when pasted JSON is still incomplete, the watch keeps what it // got and waits for the next part. The text is sealed (SealedBox, device // key, aad "OTPW-IMP-1"), because it holds secrets, and split into chunks // below Application.Storage's 8 KB per value: // "imp" {"n" => chunk count, "t" => unix time saved} // "imp." chunk i of the sealed box // Chunks are written before "imp", and "imp" is deleted first, so a torn // write leaves no readable half-buffer. Buffers older than MAX_AGE_S are // discarded. import Toybox.Application; import Toybox.Lang; module ImportBuffer { const META_KEY = "imp"; const CHUNK_BYTES = 7000; const MAX_TEXT_BYTES = 32768; const MAX_AGE_S = 86400; const AAD = "OTPW-IMP-1"; // The waiting text, or "" when there is none (or it expired or is damaged, // in which case it is deleted). function load(store as Persist.Backend, keys as SealedBox.Keys, now as Long) as String { var meta = store.getValue(META_KEY); if (!(meta instanceof Dictionary)) { return ""; } var n = meta["n"]; var t = meta["t"]; if (!(n instanceof Number) || !(t instanceof Long) || n < 1 || n > 16 || now - t > MAX_AGE_S || now < t) { clear(store); return ""; } var box = []b; for (var i = 0; i < n; i++) { var chunk = store.getValue(META_KEY + "." + i); if (!(chunk instanceof ByteArray)) { clear(store); return ""; } box.addAll(chunk); } var plain = SealedBox.open(keys, Bytes.fromUtf8(AAD), box); var text = plain != null ? Bytes.toUtf8(plain) : null; Bytes.wipe(plain); if (text == null) { clear(store); return ""; } return text; } // Save text as the waiting import. Returns false (and saves nothing) // when it is over MAX_TEXT_BYTES. function save(store as Persist.Backend, keys as SealedBox.Keys, text as String, now as Long) as Boolean { var plain = Bytes.fromUtf8(text); if (plain.size() > MAX_TEXT_BYTES) { Bytes.wipe(plain); return false; } clear(store); var box = SealedBox.seal(keys, Bytes.fromUtf8(AAD), plain); Bytes.wipe(plain); var n = 0; for (var at = 0; at < box.size(); at += CHUNK_BYTES) { store.setValue(META_KEY + "." + n, box.slice(at, at + CHUNK_BYTES < box.size() ? at + CHUNK_BYTES : box.size())); n++; } store.setValue(META_KEY, {"n" => n, "t" => now} as Dictionary); return true; } function hasPending(store as Persist.Backend) as Boolean { return store.getValue(META_KEY) != null; } // Delete the waiting import (after it completed, failed or was cancelled). function clear(store as Persist.Backend) as Void { var meta = store.getValue(META_KEY); store.deleteValue(META_KEY); var n = meta instanceof Dictionary && meta["n"] instanceof Number ? meta["n"] as Number : 16; for (var i = 0; i < n && i < 16; i++) { store.deleteValue(META_KEY + "." + i); } } }