aboutsummaryrefslogtreecommitdiff
path: root/source/storage/Vault.mc
diff options
context:
space:
mode:
Diffstat (limited to 'source/storage/Vault.mc')
-rw-r--r--source/storage/Vault.mc91
1 files changed, 68 insertions, 23 deletions
diff --git a/source/storage/Vault.mc b/source/storage/Vault.mc
index 2890477..ef240e0 100644
--- a/source/storage/Vault.mc
+++ b/source/storage/Vault.mc
@@ -7,7 +7,7 @@
// header says locked; anything else is reported as damaged.
// * A locked entry added while the session is locked (or before a PIN
// exists) is sealed with the device key under "p.<id>" and hidden until
-// promotePending() moves it under the master key (README 7.7).
+// promotePending() / promoteNext() moves it under the master key (README 7.7).
// * Writes go record first, metadata second, so a crash between the two
// leaves at most an unreferenced record, never an index entry without
// its record. Deletes go the other way round.
@@ -108,12 +108,18 @@ class Vault {
// The full entry with its secret. Throws NOT_FOUND, LOCKED (locked entry
// while the session is locked) or DAMAGED. Call wipeSecrets() when done.
function open(id as String) as Entry {
+ return openWith(id, null);
+ }
+
+ // As open(), but the decrypted record TLV goes to `wiper` when given
+ // (Record.openWith): for a large code, wiping is a step of its own.
+ function openWith(id as String, wiper as Jobs.Wiper?) as Entry {
var record = recordFor(id);
var header = record != null ? Record.decodeHeader(record) : null;
if (record == null || header == null || !consistent(header, record)) {
throw new VaultError(VaultError.DAMAGED, "damaged entry " + id);
}
- var entry = Record.open(record, id, keysFor(header.locked));
+ var entry = Record.openWith(record, id, keysFor(header.locked), wiper);
if (entry == null) {
throw new VaultError(VaultError.DAMAGED, "damaged entry " + id);
}
@@ -138,7 +144,7 @@ class Vault {
entry.created = Clock.nowUnix(0);
}
if (entry.locked && !session.isUnlocked()) {
- write("p." + entry.id, sealChecked(entry, Record.KEY_DEVICE, keyManager.deviceKeys()));
+ write("p." + entry.id, sealChecked(entry, Record.KEY_DEVICE, keyManager.deviceKeys(), null));
_pending.add(entry.id);
saveMetaOrUndo("p." + entry.id, _pending, entry.id);
} else {
@@ -153,13 +159,19 @@ class Vault {
// entry, or locking one, needs an unlocked session. Throws NOT_FOUND,
// INVALID, LOCKED, LIMIT or FULL.
function update(entry as Entry) as Void {
+ updateWith(entry, null);
+ }
+
+ // As update(), but the plaintext TLV goes to `wiper` when given.
+ function updateWith(entry as Entry, wiper as Jobs.Wiper?) as Void {
var record = recordFor(entry.id);
checkStorable(entry);
entry.fingerprint = keyManager.fingerprint(entry);
if ((record != null && Record.keyRef(record) == Record.KEY_MASTER) || entry.locked) {
session.keys(); // throws LOCKED
}
- write("e." + entry.id, seal(entry));
+ write("e." + entry.id, sealChecked(entry, entry.locked ? Record.KEY_MASTER : Record.KEY_DEVICE, keysFor(entry.locked),
+ wiper));
_store.deleteValue("x." + entry.id); // the cached matrix may be stale
}
@@ -184,12 +196,21 @@ class Vault {
// key (device key, or the master key for locked entries) and bound to the
// ID. A full store just means no cache. Throws NOT_FOUND or LOCKED.
function saveCodeCache(id as String, data as ByteArray) as Void {
+ saveCodeCacheWith(id, data, null);
+ }
+
+ // As saveCodeCache(); the padded copy goes to `wiper` when given.
+ function saveCodeCacheWith(id as String, data as ByteArray, wiper as Jobs.Wiper?) as Void {
var keys = keysFor(isLockedRecord(id));
+ var leftovers = [] as Array<ByteArray>;
try {
- _store.setValue("x." + id, SealedBox.seal(keys, cacheAad(id), data));
+ _store.setValue("x." + id, SealedBox.sealKeep(keys, cacheAad(id), data, wiper != null ? leftovers : null));
} catch (e instanceof Lang.Exception) {
// Storage full: the code is simply encoded again next time.
}
+ if (wiper != null) {
+ wiper.addAll(leftovers);
+ }
}
// The cached matrix, or null when there is none or it doesn't verify.
@@ -257,30 +278,54 @@ class Vault {
// Move pending locked entries under the master key and into the list.
// Needs an unlocked session. Damaged pending items are dropped. Returns
- // the number of entries promoted.
+ // the number of entries promoted. All in one event: for tests and small
+ // queues; after a PIN the app promotes in steps (promoteNext, LockJob).
function promotePending() as Number {
- var keys = session.keys(); // throws LOCKED
+ session.keys(); // throws LOCKED
var promoted = 0;
while (_pending.size() > 0) {
- var id = _pending[0];
- var stored = _store.getValue("p." + id);
- var entry = stored instanceof ByteArray ? Record.open(stored, id, keyManager.deviceKeys()) : null;
- if (entry != null) {
- entry.locked = true;
- write("e." + id, sealChecked(entry, Record.KEY_MASTER, keys));
- entry.wipeSecrets();
- if (_ids.indexOf(id) < 0) {
- _ids.add(id);
- }
+ if (promoteNext(null) > 0) {
promoted++;
}
- _pending.remove(id);
- saveMeta();
- _store.deleteValue("p." + id);
}
return promoted;
}
+ // Promote the first pending entry (one record, then the metadata, so a
+ // crash in between at most promotes it again). Returns the record's
+ // size when it was promoted, -1 when it was damaged (dropped), 0 when
+ // none waits. The entry's secrets go to `wiper` when given (wiping a
+ // large code's contents is a step of its own), else are wiped here.
+ // Needs an unlocked session.
+ function promoteNext(wiper as Jobs.Wiper?) as Number {
+ var keys = session.keys(); // throws LOCKED
+ if (_pending.size() == 0) {
+ return 0;
+ }
+ var id = _pending[0];
+ var stored = _store.getValue("p." + id);
+ var entry = stored instanceof ByteArray ? Record.openWith(stored, id, keyManager.deviceKeys(), wiper) : null;
+ if (entry != null) {
+ entry.locked = true;
+ write("e." + id, sealChecked(entry, Record.KEY_MASTER, keys, wiper));
+ if (wiper != null) {
+ wiper.add(entry.secret);
+ wiper.add(entry.payload);
+ entry.secret = null;
+ entry.payload = null;
+ } else {
+ entry.wipeSecrets();
+ }
+ if (_ids.indexOf(id) < 0) {
+ _ids.add(id);
+ }
+ }
+ _pending.remove(id);
+ saveMeta();
+ _store.deleteValue("p." + id);
+ return entry != null ? (stored as ByteArray).size() : -1;
+ }
+
// ---- PIN and session -----------------------------------------------
function hasPin() as Boolean {
@@ -409,12 +454,12 @@ class Vault {
}
private function seal(entry as Entry) as ByteArray {
- return sealChecked(entry, entry.locked ? Record.KEY_MASTER : Record.KEY_DEVICE, keysFor(entry.locked));
+ return sealChecked(entry, entry.locked ? Record.KEY_MASTER : Record.KEY_DEVICE, keysFor(entry.locked), null);
}
// Seal and enforce the per-record size limit.
- private function sealChecked(entry as Entry, keyRef as Number, keys as SealedBox.Keys) as ByteArray {
- var record = Record.seal(entry, keyRef, keys);
+ private function sealChecked(entry as Entry, keyRef as Number, keys as SealedBox.Keys, wiper as Jobs.Wiper?) as ByteArray {
+ var record = Record.sealWith(entry, keyRef, keys, wiper);
if (record.size() > Record.MAX_BYTES) {
throw new VaultError(VaultError.LIMIT, "entry too big");
}