diff options
Diffstat (limited to 'source/storage/Vault.mc')
| -rw-r--r-- | source/storage/Vault.mc | 91 |
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"); } |
