diff options
Diffstat (limited to 'source/storage')
| -rw-r--r-- | source/storage/KeyManager.mc | 41 | ||||
| -rw-r--r-- | source/storage/Vault.mc | 10 |
2 files changed, 45 insertions, 6 deletions
diff --git a/source/storage/KeyManager.mc b/source/storage/KeyManager.mc index 9b2b3c6..2dd3712 100644 --- a/source/storage/KeyManager.mc +++ b/source/storage/KeyManager.mc @@ -15,6 +15,12 @@ // and iteration count are in the aad, so changing either fails the MAC. // Changing the PIN re-wraps the same LMK; entries aren't re-encrypted. // +// A new PIN has exactly PIN_DIGITS (4) digits and is stored with the +// marker "wl" = PIN_DIGITS next to the wrap (the wrap format is unchanged). +// A wrap without the marker is a legacy PIN of 4 to 10 digits from an +// earlier version: it still unlocks, and the PIN screen then asks for a new +// 4-digit PIN (isLegacyPin). +// // PBKDF2 runs as a PinJob (a Jobs.Job) because one derivation is calibrated // to about a second, far beyond one event's watchdog budget. @@ -28,8 +34,13 @@ class KeyManager { static const KEY_BYTES = 32; static const SALT_BYTES = 16; static const WRAP_LABEL = "OTPW-WRAP-1"; + // Every new PIN has exactly this many digits. + static const PIN_DIGITS = 4; + // Legacy PINs (set before the 4-digit rule) have 4 to 10 digits. static const MIN_PIN_DIGITS = 4; static const MAX_PIN_DIGITS = 10; + // The marker of a PIN set with PIN_DIGITS digits (see the header). + static const PIN_LENGTH_KEY = "wl"; private var _store as Persist.Backend; private var _deviceKeys as SealedBox.Keys? = null; @@ -113,12 +124,20 @@ class KeyManager { return _store.getValue(WRAP_KEY) != null; } + // True when the stored PIN predates the 4-digit rule (no marker), so it + // may have 4 to 10 digits and should be replaced by a 4-digit one. + function isLegacyPin() as Boolean { + var marker = _store.getValue(PIN_LENGTH_KEY); + return hasPin() && !(marker instanceof Number && marker == PIN_DIGITS); + } + // A fresh random master key (when the first PIN is created). function newMasterKey() as ByteArray { return Cryptography.randomBytes(KEY_BYTES); } - // True for 4 to 10 ASCII digits. + // True for 4 to 10 ASCII digits: any PIN that can unlock, legacy + // lengths included. static function isValidPin(pin as ByteArray) as Boolean { if (pin.size() < MIN_PIN_DIGITS || pin.size() > MAX_PIN_DIGITS) { return false; @@ -131,6 +150,11 @@ class KeyManager { return true; } + // True for exactly PIN_DIGITS ASCII digits: what a new PIN must be. + static function isValidNewPin(pin as ByteArray) as Boolean { + return pin.size() == PIN_DIGITS && isValidPin(pin); + } + // The derivation for checking `pin` against the stored wrap. Throws // NO_PIN when no PIN is set, DAMAGED when the wrap is malformed. function unlockJob(pin as ByteArray) as PinJob { @@ -149,8 +173,12 @@ class KeyManager { return new PinJob(pin, wrap.slice(0, SALT_BYTES), iterations); } - // The derivation for a new PIN, with a fresh salt. + // The derivation for a new PIN, with a fresh salt. Throws VaultError + // INVALID unless the PIN has exactly PIN_DIGITS digits. function newPinJob(pin as ByteArray, iterations as Number) as PinJob { + if (!isValidNewPin(pin)) { + throw new VaultError(VaultError.INVALID, "PIN: " + PIN_DIGITS + " digits"); + } if (iterations < Kdf.MIN_ITERATIONS || iterations > Kdf.MAX_ITERATIONS) { throw new Lang.InvalidValueException("PIN: iteration count out of range"); } @@ -177,7 +205,8 @@ class KeyManager { } // After a finished newPinJob: wrap masterKey under the new PIN and store - // it, replacing any previous wrap. Wipes the PIN key, not masterKey. + // it, replacing any previous wrap, and mark the PIN as a 4-digit one. + // Wipes the PIN key, not masterKey. function storeWrap(job as PinJob, masterKey as ByteArray) as Void { var keys = job.keys(); var wrap = Bytes.copy(job.salt); @@ -186,11 +215,15 @@ class KeyManager { keys.wipe(); job.wipe(); _store.setValue(WRAP_KEY, wrap); + // After the wrap: a crash in between leaves the PIN unmarked, which + // only means being asked for a new PIN once more. + _store.setValue(PIN_LENGTH_KEY, PIN_DIGITS); } // Forget the PIN (reset or erase). The master key is then gone for good. function deleteWrap() as Void { _store.deleteValue(WRAP_KEY); + _store.deleteValue(PIN_LENGTH_KEY); } private function wrapAad(job as PinJob) as ByteArray { @@ -211,7 +244,7 @@ class PinJob extends Jobs.Job { function initialize(pin as ByteArray, salt as ByteArray, iterations as Number) { Job.initialize(); if (!KeyManager.isValidPin(pin)) { - throw new Lang.InvalidValueException("PIN: 4 to 10 digits"); + throw new Lang.InvalidValueException("PIN: " + KeyManager.MIN_PIN_DIGITS + " to " + KeyManager.MAX_PIN_DIGITS + " digits"); } self.salt = salt; self.iterations = iterations; diff --git a/source/storage/Vault.mc b/source/storage/Vault.mc index fcd973f..2890477 100644 --- a/source/storage/Vault.mc +++ b/source/storage/Vault.mc @@ -287,6 +287,11 @@ class Vault { return keyManager.hasPin(); } + // True when the PIN predates the 4-digit rule (KeyManager.isLegacyPin). + function isLegacyPin() as Boolean { + return keyManager.isLegacyPin(); + } + // Start checking a PIN: counts the attempt first (README 7.6), then // returns the derivation to run. Throws NO_PIN, LOCKED_OUT or DAMAGED. function beginUnlock(pin as ByteArray) as PinJob { @@ -313,8 +318,9 @@ class Vault { return false; } - // Start setting a PIN: the first PIN, or a new one while unlocked. - // Throws LOCKED when a PIN exists and the session is locked. + // Start setting a PIN (exactly KeyManager.PIN_DIGITS digits): the first + // PIN, or a new one while unlocked. Throws LOCKED when a PIN exists and + // the session is locked, INVALID for any other length. function beginSetPin(pin as ByteArray, iterations as Number) as PinJob { if (keyManager.hasPin() && !session.isUnlocked()) { throw new VaultError(VaultError.LOCKED, "locked"); |
