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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
// Author: cirakg & claude ai
// Summary: Changes from the phone that wait for the PIN on the watch (README 7.9), sealed with the device key under "q".
//
// Anyone who controls the phone can send settings, so edits, new secrets,
// "Don't require PIN" and deletes of locked entries are not applied when
// they arrive: they wait here until someone confirms them with the PIN on
// the watch (or rejects them). Each item is one ManageAction sealed on its
// own with the device key (aad "OTPW-Q-1"), so a damaged item costs only
// itself. A new secret in an item is no more exposed than it was in the
// settings it came from, as with pending entries (README 7.7).
import Toybox.Application;
import Toybox.Lang;
module ChangeQueue {
const KEY = "q";
const MAX_ITEMS = 16;
const AAD = "OTPW-Q-1";
function count(vault as Vault) as Number {
var stored = vault.storage().getValue(KEY);
return stored instanceof Array ? stored.size() : 0;
}
// Append an action. Throws VaultError LIMIT (too many waiting) or FULL.
function add(vault as Vault, action as ManageAction) as Void {
var stored = vault.storage().getValue(KEY);
var items = stored instanceof Array ? stored as Array<Application.Storage.ValueType> : [] as Array<Application.Storage.ValueType>;
if (items.size() >= MAX_ITEMS) {
throw new VaultError(VaultError.LIMIT, MAX_ITEMS + " changes already wait for your PIN. Confirm or reject them on the watch first");
}
var plain = action.toBytes();
items.add(SealedBox.seal(vault.keyManager.deviceKeys(), Bytes.fromUtf8(AAD), plain));
Bytes.wipe(plain);
try {
vault.storage().setValue(KEY, items);
} catch (e instanceof Lang.Exception) {
throw new VaultError(VaultError.FULL, "storage full");
}
}
// The waiting actions in arrival order; items that don't verify are
// left out (and dropped by the next clear()).
function load(vault as Vault) as Array<ManageAction> {
var out = [] as Array<ManageAction>;
var stored = vault.storage().getValue(KEY);
if (!(stored instanceof Array)) {
return out;
}
var keys = vault.keyManager.deviceKeys();
var aad = Bytes.fromUtf8(AAD);
for (var i = 0; i < stored.size(); i++) {
var box = stored[i];
var plain = box instanceof ByteArray ? SealedBox.open(keys, aad, box) : null;
if (plain != null) {
var action = ManageAction.fromBytes(plain);
Bytes.wipe(plain);
if (action != null) {
out.add(action);
}
}
}
return out;
}
// Short descriptions for the confirmation screen ("Delete: K7F2 Bank").
function descriptions(vault as Vault) as Array<String> {
var actions = load(vault);
var out = [] as Array<String>;
for (var i = 0; i < actions.size(); i++) {
out.add(actions[i].describe());
actions[i].wipe();
}
return out;
}
function clear(vault as Vault) as Void {
vault.storage().deleteValue(KEY);
}
// Remove the first n items (the ones just applied), keeping any that
// arrived from the phone meanwhile.
function drop(vault as Vault, n as Number) as Void {
var stored = vault.storage().getValue(KEY);
if (!(stored instanceof Array) || stored.size() <= n) {
clear(vault);
return;
}
try {
vault.storage().setValue(KEY, stored.slice(n, null) as Array<Application.Storage.ValueType>);
} catch (e instanceof Lang.Exception) {
clear(vault); // can't happen when shrinking; never re-apply
}
}
}
// Applies every waiting change after the PIN was entered, one per step
// (each re-seals a record; a code may need its CodeCheck). The applied
// items leave the queue only at the end, so a crash midway loses nothing;
// applying one again is harmless (a deleted entry is then just missing).
class ChangeQueueJob extends Jobs.Job {
var lines as Array<String> = [] as Array<String>;
private var _vault as Vault;
private var _actions as Array<ManageAction>;
private var _loaded as Number = 0;
private var _index as Number = 0;
private var _run as ManageRun? = null;
function initialize(vault as Vault) {
Job.initialize();
_vault = vault;
var stored = vault.storage().getValue(ChangeQueue.KEY);
_loaded = stored instanceof Array ? stored.size() : 0;
_actions = ChangeQueue.load(vault);
}
function step(budget as Number) as Boolean {
var run = _run;
if (run == null) {
if (_index >= _actions.size()) {
ChangeQueue.drop(_vault, _loaded);
if (lines.size() == 0) {
lines.add("No changes were waiting.");
}
return true;
}
run = new ManageRun(_vault, _actions[_index], true);
_run = run;
}
if (run.step(budget)) {
var problem = run.failed ? "Not applied: " + _actions[_index].describe() + ". " : "";
lines.add(problem + run.message);
lines.addAll(run.notes);
_actions[_index].wipe();
_index++;
_run = null;
}
return false;
}
}
|