diff options
Diffstat (limited to 'source/storage')
| -rw-r--r-- | source/storage/KeyManager.mc | 24 | ||||
| -rw-r--r-- | source/storage/Record.mc | 33 | ||||
| -rw-r--r-- | source/storage/Vault.mc | 91 |
3 files changed, 112 insertions, 36 deletions
diff --git a/source/storage/KeyManager.mc b/source/storage/KeyManager.mc index 2dd3712..ae2c5e8 100644 --- a/source/storage/KeyManager.mc +++ b/source/storage/KeyManager.mc @@ -81,17 +81,23 @@ class KeyManager { // entries while the watch is locked (docs/SECURITY.md). function fingerprint(entry as Entry) as ByteArray { deviceKeys(); - var input = Bytes.fromUtf8("OTPW-FP-1"); - input.add(entry.kind); - input.add(entry.type); + // The secret or contents go to the native HMAC as they are: a copy + // would need a byte-loop wipe (4 KB of contents, the watchdog). + var mac = new Cryptography.HashBasedMessageAuthenticationCode({ + :algorithm => Cryptography.HASH_SHA256, + :key => _fingerprintKey as ByteArray + }); + var prefix = Bytes.fromUtf8("OTPW-FP-1"); + prefix.add(entry.kind); + prefix.add(entry.type); + mac.update(prefix); var material = entry.kind == Entry.KIND_OTP ? entry.secret : entry.payload; - if (material != null) { - input.addAll(material); + if (material != null && material.size() > 0) { + mac.update(material); } - var mac = Hmac.nativeSha256(_fingerprintKey as ByteArray, input); - Bytes.wipe(input); - var out = mac.slice(0, FINGERPRINT_BYTES); - Bytes.wipe(mac); + var digest = mac.digest(); + var out = digest.slice(0, FINGERPRINT_BYTES); + Bytes.wipe(digest); return out; } diff --git a/source/storage/Record.mc b/source/storage/Record.mc index b26a4ba..2cfc046 100644 --- a/source/storage/Record.mc +++ b/source/storage/Record.mc @@ -63,12 +63,24 @@ module Record { // Seal an entry into record bytes. keyRef is KEY_DEVICE or KEY_MASTER and // must match `keys`. The plaintext TLV is wiped before returning. function seal(entry as Entry, keyRef as Number, keys as SealedBox.Keys) as ByteArray { + return sealWith(entry, keyRef, keys, null); + } + + // As seal(), but the plaintext TLV goes to `wiper` when given: wiping a + // large code's contents is a step of its own (the watchdog). + function sealWith(entry as Entry, keyRef as Number, keys as SealedBox.Keys, wiper as Jobs.Wiper?) as ByteArray { var header = encodeHeader(entry); var record = [VERSION, keyRef, (header.size() >> 8) & 0xFF, header.size() & 0xFF]b; record.addAll(header); var plain = encodeSecret(entry); - var box = SealedBox.seal(keys, aad(entry.id, record), plain); - Bytes.wipe(plain); + var leftovers = wiper != null ? [] as Array<ByteArray> : null; + var box = SealedBox.sealKeep(keys, aad(entry.id, record), plain, leftovers); + if (wiper != null) { + wiper.add(plain); + wiper.addAll(leftovers as Array<ByteArray>); + } else { + Bytes.wipe(plain); + } record.addAll(box); return record; } @@ -119,17 +131,30 @@ module Record { // keys, stored under another ID, or changed in any way. The decrypted // TLV is wiped; the caller must call entry.wipeSecrets() when done. function open(record as ByteArray, id as String, keys as SealedBox.Keys) as Entry? { + return openWith(record, id, keys, null); + } + + // As open(), but the decrypted TLV goes to `wiper` when given. + function openWith(record as ByteArray, id as String, keys as SealedBox.Keys, wiper as Jobs.Wiper?) as Entry? { var entry = decodeHeader(record); var end = headerEnd(record); if (entry == null || end == null || !entry.id.equals(id)) { return null; } - var plain = SealedBox.open(keys, aad(id, record.slice(0, end)), record.slice(end, record.size())); + var leftovers = wiper != null ? [] as Array<ByteArray> : null; + var plain = SealedBox.openKeep(keys, aad(id, record.slice(0, end)), record.slice(end, record.size()), leftovers); + if (wiper != null) { + wiper.addAll(leftovers as Array<ByteArray>); + } if (plain == null) { return null; } var map = Codec.decode(plain); - Bytes.wipe(plain); + if (wiper != null) { + wiper.add(plain); + } else { + Bytes.wipe(plain); + } if (map == null) { return null; } 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"); } |
