1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
// 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.<i>" 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<Application.Storage.KeyType, Application.Storage.ValueType>);
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);
}
}
}
|