aboutsummaryrefslogtreecommitdiff
path: root/source/storage
diff options
context:
space:
mode:
Diffstat (limited to 'source/storage')
-rw-r--r--source/storage/Entry.mc3
-rw-r--r--source/storage/KeyManager.mc24
-rw-r--r--source/storage/Record.mc8
-rw-r--r--source/storage/Vault.mc19
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
}