summaryrefslogtreecommitdiff
path: root/source/storage
diff options
context:
space:
mode:
Diffstat (limited to 'source/storage')
-rw-r--r--source/storage/KeyManager.mc24
-rw-r--r--source/storage/Record.mc33
-rw-r--r--source/storage/Vault.mc91
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");
}