diff options
Diffstat (limited to 'source/storage')
| -rw-r--r-- | source/storage/Entry.mc | 3 | ||||
| -rw-r--r-- | source/storage/KeyManager.mc | 24 | ||||
| -rw-r--r-- | source/storage/Record.mc | 8 | ||||
| -rw-r--r-- | source/storage/Vault.mc | 19 |
4 files changed, 53 insertions, 1 deletions
diff --git a/source/storage/Entry.mc b/source/storage/Entry.mc index 57e0309..23c95c0 100644 --- a/source/storage/Entry.mc +++ b/source/storage/Entry.mc @@ -52,6 +52,9 @@ class Entry { var counter as Long = 0l; var created as Long = 0l; var namesEncrypted as Boolean = false; + // Keyed fingerprint of the type and secret (or payload), set by the + // vault: finds duplicates without decrypting (docs/SECURITY.md). + var fingerprint as ByteArray? = null; // Sensitive fields (encrypted at rest). var algorithm as Number = Hashes.SHA1; diff --git a/source/storage/KeyManager.mc b/source/storage/KeyManager.mc index ebb0a62..567288f 100644 --- a/source/storage/KeyManager.mc +++ b/source/storage/KeyManager.mc @@ -33,6 +33,9 @@ class KeyManager { private var _store as Persist.Backend; private var _deviceKeys as SealedBox.Keys? = null; + private var _fingerprintKey as ByteArray? = null; + + static const FINGERPRINT_BYTES = 8; function initialize(store as Persist.Backend) { _store = store; @@ -55,11 +58,32 @@ class KeyManager { _store.setValue(DEVICE_KEY, raw); } keys = new SealedBox.Keys(raw); + _fingerprintKey = Kdf.hkdf(raw, []b, Bytes.fromUtf8("OTPW fp v1"), KEY_BYTES); Bytes.wipe(raw); _deviceKeys = keys; return keys; } + // An 8-byte keyed fingerprint of an entry's kind, type and secret (or + // payload): equal for duplicates, meaningless without the device key. + // Stored in the readable header so imports can find duplicates of locked + // 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); + var material = entry.kind == Entry.KIND_OTP ? entry.secret : entry.payload; + if (material != null) { + input.addAll(material); + } + var mac = Hmac.nativeSha256(_fingerprintKey as ByteArray, input); + Bytes.wipe(input); + var out = mac.slice(0, FINGERPRINT_BYTES); + Bytes.wipe(mac); + return out; + } + // True when a PIN has been set. function hasPin() as Boolean { return _store.getValue(WRAP_KEY) != null; diff --git a/source/storage/Record.mc b/source/storage/Record.mc index 01c3c53..b26a4ba 100644 --- a/source/storage/Record.mc +++ b/source/storage/Record.mc @@ -6,7 +6,7 @@ // v (1 byte, = 1) | kr (1 byte) | hlen (2 bytes, big-endian) | header (hlen bytes) | box // // kr = 0: sealed with the device key, 1: sealed with the locked master key -// header = Codec TLV of the header fields (tags 1-12), readable without a key +// header = Codec TLV of the header fields (tags 1-13), readable without a key // box = SealedBox of the Codec TLV of the sensitive fields (tags 20-28) // aad = UTF-8 id || v || kr || hlen || header // @@ -47,6 +47,7 @@ module Record { const H_COUNTER = 10; const H_CREATED = 11; const H_NAMES_ENCRYPTED = 12; + const H_FINGERPRINT = 13; // Sensitive tags (inside the box). const S_ALGORITHM = 20; @@ -107,6 +108,7 @@ module Record { entry.counter = f.long(H_COUNTER); entry.created = f.long(H_CREATED); entry.namesEncrypted = f.flag(H_NAMES_ENCRYPTED); + entry.fingerprint = f.bytes(H_FINGERPRINT); if (!f.ok || !Entry.isValidId(entry.id) || entry.counter < 0) { return null; } @@ -170,6 +172,10 @@ module Record { w.addInt(H_COUNTER, entry.counter); w.addInt(H_CREATED, entry.created); w.addBool(H_NAMES_ENCRYPTED, entry.namesEncrypted); + var fingerprint = entry.fingerprint; + if (fingerprint != null) { + w.addBytes(H_FINGERPRINT, fingerprint); + } return w.bytes; } diff --git a/source/storage/Vault.mc b/source/storage/Vault.mc index 95f77c9..e77bbf2 100644 --- a/source/storage/Vault.mc +++ b/source/storage/Vault.mc @@ -51,6 +51,12 @@ class Vault { // ---- Reading ------------------------------------------------------- + // The underlying store (for the import buffer, which lives next to the + // entries and uses the same device key). + function storage() as Persist.Backend { + return _store; + } + // Entry IDs in display order (a copy). function ids() as Array<String> { return _ids.slice(0, null) as Array<String>; @@ -82,6 +88,17 @@ class Vault { return entry; } + // The header of a listed entry or of a pending one ("p.<id>"), or null + // when it doesn't exist or is damaged. Never throws. + function anyHeader(id as String) as Entry? { + var key = _ids.indexOf(id) >= 0 ? "e." + id : (_pending.indexOf(id) >= 0 ? "p." + id : null); + if (key == null) { + return null; + } + var stored = _store.getValue(key); + return stored instanceof ByteArray ? Record.decodeHeader(stored) : null; + } + // 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 { @@ -109,6 +126,7 @@ class Vault { throw new VaultError(VaultError.LIMIT, "at most " + MAX_ENTRIES + " entries"); } entry.id = newId(); + entry.fingerprint = keyManager.fingerprint(entry); if (entry.created == 0) { entry.created = Clock.nowUnix(0); } @@ -130,6 +148,7 @@ class Vault { function update(entry as Entry) 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 } |
