summaryrefslogtreecommitdiff
path: root/source
diff options
context:
space:
mode:
Diffstat (limited to 'source')
-rw-r--r--source/app/Fixtures.mc10
-rw-r--r--source/settings/ChangeQueue.mc76
-rw-r--r--source/settings/ManageForm.mc6
-rw-r--r--source/settings/SettingsProcessor.mc8
-rw-r--r--source/storage/KeyManager.mc41
-rw-r--r--source/storage/Vault.mc10
-rw-r--r--source/ui/Nav.mc20
-rw-r--r--source/ui/OtpView.mc11
-rw-r--r--source/ui/PinView.mc87
-rw-r--r--source/ui/QueueMenu.mc3
10 files changed, 234 insertions, 38 deletions
diff --git a/source/app/Fixtures.mc b/source/app/Fixtures.mc
index 7c21d9a..01b15a8 100644
--- a/source/app/Fixtures.mc
+++ b/source/app/Fixtures.mc
@@ -9,8 +9,9 @@
// FIXTURE_WRAP is that PIN's wrap of FIXTURE_MASTER_KEY, precomputed by
// tools/gen_fixture_wrap.py (fake salt, IV and key): deriving the PIN key on
// the watch in one event trips the watchdog, so the fixture stores the
-// result. Unlocking it with the app's own code also cross-checks the wrap
-// format against the Python implementation (test/storage/PinTest.mc).
+// result, with the 4-digit PIN marker (KeyManager.PIN_LENGTH_KEY).
+// Unlocking it with the app's own code also cross-checks the wrap format
+// against the Python implementation (test/storage/PinTest.mc).
import Toybox.Application;
import Toybox.Lang;
@@ -18,8 +19,8 @@ import Toybox.Lang;
(:debug)
module Fixtures {
- const FIXTURE_PIN = "123456";
- const FIXTURE_WRAP = "101112131415161718191a1b1c1d1e1f000007d001202122232425262728292a2b2c2d2e2f857b961d4854e06740f602627bfb952efaac5383e800768221bf3a7b292ee881e2ce2f76b9dd744b2ac32322473df351929bc6dfcbfc2941527ae8b90614a8a6a6bd0f72d33161bafd16b1039caa670c"; // lint: allow-literal (fake fixture)
+ const FIXTURE_PIN = "1234";
+ const FIXTURE_WRAP = "101112131415161718191a1b1c1d1e1f000007d001202122232425262728292a2b2c2d2e2f133549536635b7a7ae3add45a59c42953d4c666538390a99305164dba9c99961e7148e1c561b6cfc70cfde7f9e3e59179464ce65bf91dd08807ff8a52027d77c90c2b692ca22fe4cbd3fb20ffb440137"; // lint: allow-literal (fake fixture)
const FIXTURE_MASTER_KEY = "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"; // lint: allow-literal (fake fixture)
// Erase all app storage (UI-test build only).
@@ -33,6 +34,7 @@ module Fixtures {
function load(vault as Vault) as Void {
if (TestMode.ON && !vault.hasPin()) {
Application.Storage.setValue(KeyManager.WRAP_KEY, Bytes.fromHex(FIXTURE_WRAP));
+ Application.Storage.setValue(KeyManager.PIN_LENGTH_KEY, KeyManager.PIN_DIGITS);
vault.session.unlock(Bytes.fromHex(FIXTURE_MASTER_KEY));
}
var rfcKey = "12345678901234567890"; // lint: allow-literal (RFC 4226 test key)
diff --git a/source/settings/ChangeQueue.mc b/source/settings/ChangeQueue.mc
index 12f88f7..c5919f6 100644
--- a/source/settings/ChangeQueue.mc
+++ b/source/settings/ChangeQueue.mc
@@ -8,6 +8,11 @@
// own with the device key (aad "OTPW-Q-1"), so a damaged item costs only
// itself. A new secret in an item is no more exposed than it was in the
// settings it came from, as with pending entries (README 7.7).
+//
+// "Require PIN" waits here only while the watch is locked (it needs the
+// master key). It only adds protection, so applyLocks() applies it at the
+// next correct (or newly set) PIN, without "Confirm all", the way pending
+// entries are promoted.
import Toybox.Application;
import Toybox.Lang;
@@ -18,6 +23,10 @@ module ChangeQueue {
const MAX_ITEMS = 16;
const AAD = "OTPW-Q-1";
+ // What applyLocks() did at the last PIN, for the results of a "Confirm
+ // all" that follows it (ChangeQueueJob). Replaced at every PIN.
+ var _lockLines as Array<String> = [] as Array<String>;
+
function count(vault as Vault) as Number {
var stored = vault.storage().getValue(KEY);
return stored instanceof Array ? stored.size() : 0;
@@ -75,6 +84,71 @@ module ChangeQueue {
return out;
}
+ // The number of waiting "Require PIN" actions (applied at the next PIN).
+ function countLocks(vault as Vault) as Number {
+ var actions = load(vault);
+ var n = 0;
+ for (var i = 0; i < actions.size(); i++) {
+ if (actions[i].action == ManageAction.LOCK) {
+ n++;
+ }
+ actions[i].wipe();
+ }
+ return n;
+ }
+
+ // After a correct or new PIN (session unlocked): apply the waiting
+ // "Require PIN" actions and remove them from the queue; the other items
+ // keep waiting, in order. Each is one re-seal (ManageRun, no code
+ // check), like promotePending. One that fails (the entry is gone or
+ // already locked) is dropped too.
+ function applyLocks(vault as Vault) as Void {
+ _lockLines = [] as Array<String>;
+ var stored = vault.storage().getValue(KEY);
+ if (!(stored instanceof Array) || !vault.session.isUnlocked()) {
+ return;
+ }
+ var keys = vault.keyManager.deviceKeys();
+ var aad = Bytes.fromUtf8(AAD);
+ var kept = [] as Array<Application.Storage.ValueType>;
+ for (var i = 0; i < stored.size(); i++) {
+ var box = stored[i];
+ var plain = box instanceof ByteArray ? SealedBox.open(keys, aad, box) : null;
+ var action = plain != null ? ManageAction.fromBytes(plain) : null;
+ Bytes.wipe(plain);
+ if (action != null && action.action == ManageAction.LOCK) {
+ var run = new ManageRun(vault, action, true);
+ Jobs.runToCompletion(run, 1);
+ _lockLines.add(run.failed ? "Not applied: " + action.describe() + ". " + run.message : run.message);
+ } else {
+ if (action != null) {
+ action.wipe();
+ }
+ kept.add(box as Application.Storage.ValueType); // damaged items are dropped by the next clear()
+ }
+ }
+ if (kept.size() == stored.size()) {
+ return;
+ }
+ if (kept.size() == 0) {
+ clear(vault);
+ return;
+ }
+ try {
+ vault.storage().setValue(KEY, kept);
+ } catch (e instanceof Lang.Exception) {
+ // Can't happen when shrinking; if it did, the locks just apply
+ // again at the next PIN ("already requires the PIN").
+ }
+ }
+
+ // The lines of the last applyLocks(), once (then forgotten).
+ function takeLockLines() as Array<String> {
+ var lines = _lockLines;
+ _lockLines = [] as Array<String>;
+ return lines;
+ }
+
function clear(vault as Vault) as Void {
vault.storage().deleteValue(KEY);
}
@@ -115,6 +189,8 @@ class ChangeQueueJob extends Jobs.Job {
var stored = vault.storage().getValue(ChangeQueue.KEY);
_loaded = stored instanceof Array ? stored.size() : 0;
_actions = ChangeQueue.load(vault);
+ // "Require PIN" changes the PIN just applied (ChangeQueue.applyLocks).
+ lines.addAll(ChangeQueue.takeLockLines());
}
function step(budget as Number) as Boolean {
diff --git a/source/settings/ManageForm.mc b/source/settings/ManageForm.mc
index a075880..70d707f 100644
--- a/source/settings/ManageForm.mc
+++ b/source/settings/ManageForm.mc
@@ -8,7 +8,8 @@
// applies it, or queues it (ChangeQueue) when it touches a locked
// entry: edit, replace, counter, "Don't require PIN", the favourite
// mark and delete wait until the PIN is entered on the watch. "Require PIN" waits too while
-// the watch is locked (it needs the master key). Moving never waits:
+// the watch is locked (it needs the master key), but is applied at the
+// next PIN without confirming (ChangeQueue.applyLocks). Moving never waits:
// the order isn't protected and a position is absolute, so applying a
// move twice can't hit another entry.
// Replacing a code's contents runs CodeCheck with the entry's own options
@@ -383,7 +384,8 @@ class ManageRun extends Jobs.Job {
return fail(e.code == VaultError.LIMIT ? e.getErrorMessage() as String : "storage full");
}
queued = true;
- message = "Waiting for your PIN on the watch: " + _a.describe();
+ message = (_a.action == ManageAction.LOCK ? "Locks when you next enter your PIN on the watch: "
+ : "Waiting for your PIN on the watch: ") + _a.describe();
_a.wipe();
return true;
}
diff --git a/source/settings/SettingsProcessor.mc b/source/settings/SettingsProcessor.mc
index cf7684b..57fe411 100644
--- a/source/settings/SettingsProcessor.mc
+++ b/source/settings/SettingsProcessor.mc
@@ -65,11 +65,17 @@ class SettingsProcessor {
}
if (job.forms > 0) {
var lines = job.lines;
- var waiting = ChangeQueue.count(AppState.theVault());
+ // "Require PIN" items apply by themselves at the next PIN.
+ var vault = AppState.theVault();
+ var locks = ChangeQueue.countLocks(vault);
+ var waiting = ChangeQueue.count(vault) - locks;
if (waiting > 0) {
lines.add(waiting + (waiting == 1 ? " change waits" : " changes wait")
+ " for your PIN: menu, Phone changes.");
}
+ if (locks > 0) {
+ lines.add(locks + (locks == 1 ? " entry locks" : " entries lock") + " when you next enter your PIN.");
+ }
Results.save(lines);
WatchUi.pushView(new InfoView("Results", lines, "results"), new InfoDelegate(), WatchUi.SLIDE_UP);
Results.markSeen();
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");
diff --git a/source/ui/Nav.mc b/source/ui/Nav.mc
index 222386a..af08d4a 100644
--- a/source/ui/Nav.mc
+++ b/source/ui/Nav.mc
@@ -2,13 +2,31 @@
// Summary: Navigation between screens (README 13.2): opening entries, asking for the PIN first when needed, the menu.
import Toybox.Lang;
+import Toybox.System;
import Toybox.WatchUi;
module Nav {
+ // A second open request within this time is the same tap delivered
+ // twice (as START in onKey and as onTap) and is ignored.
+ const OPEN_DEBOUNCE_MS = 500;
+ var _lastOpenMs as Number? = null;
+
// Open an entry from the list: its detail screen, or the PIN screen first
- // when it is locked and the session is locked.
+ // when it is locked and the session is locked. Ignored while a PIN or
+ // detail screen is already on top, or right after another open, so one
+ // tap can't push two screens.
function openEntry(id as String) as Void {
+ var top = WatchUi.getCurrentView()[0];
+ if (top instanceof PinView || top instanceof OtpView || top instanceof CodeView) {
+ return;
+ }
+ var now = System.getTimer();
+ var last = _lastOpenMs;
+ if (last != null && now - last >= 0 && now - last < OPEN_DEBOUNCE_MS) {
+ return;
+ }
+ _lastOpenMs = now;
var vault = AppState.theVault();
var header = vault.header(id);
if (header == null) {
diff --git a/source/ui/OtpView.mc b/source/ui/OtpView.mc
index faf94d0..1c0c0bd 100644
--- a/source/ui/OtpView.mc
+++ b/source/ui/OtpView.mc
@@ -258,6 +258,11 @@ class OtpView extends WatchUi.View {
// Input on the OTP detail: START or a tap gives the next HOTP code; BACK closes.
class OtpDelegate extends WatchUi.BehaviorDelegate {
+ // One tap can arrive twice (as START and as a tap); a second "next
+ // code" within this time is ignored so the counter moves only once.
+ static const NEXT_DEBOUNCE_MS = 500;
+ private var _lastNextMs as Number? = null;
+
function initialize() {
BehaviorDelegate.initialize();
}
@@ -294,6 +299,12 @@ class OtpDelegate extends WatchUi.BehaviorDelegate {
if (v == null || !v.isHotp()) {
return;
}
+ var now = System.getTimer();
+ var last = _lastNextMs;
+ if (last != null && now - last >= 0 && now - last < NEXT_DEBOUNCE_MS) {
+ return;
+ }
+ _lastNextMs = now;
try {
AppState.theVault().setCounter(v.header.id, v.header.counter + 1);
} catch (e instanceof VaultError) {
diff --git a/source/ui/PinView.mc b/source/ui/PinView.mc
index 4502919..fa58bfe 100644
--- a/source/ui/PinView.mc
+++ b/source/ui/PinView.mc
@@ -2,11 +2,18 @@
// Summary: PIN entry for unlocking, creating and changing the PIN: touch keypad or button digit selector, lockout display.
//
// README 13.1 and 13.4:
-// * Touch screens get a 3x4 keypad (digits, delete, OK). Watches without a
-// touch screen get a digit selector: UP/DOWN change the digit, START
-// adds it (or confirms on "OK"), BACK deletes.
+// * A PIN has exactly 4 digits (KeyManager.PIN_DIGITS) and is submitted
+// as soon as the 4th is entered; there is no OK key.
+// * Touch screens get a keypad (1-9, delete, 0). The START button does
+// nothing there: a tap can also arrive as START, which used to submit a
+// PIN early. Watches without a touch screen get a digit selector:
+// UP/DOWN change the digit, START adds it, BACK deletes.
+// * A legacy PIN (4 to 10 digits, set before the 4-digit rule) is entered
+// the old way, with an OK key (and START on touch screens, "OK" in the
+// selector); after it unlocks, the screen asks for a new 4-digit PIN
+// and then continues to what was opened.
// * Entered digits show as dots. A wrong PIN vibrates and shows any wait.
-// * Creating or changing the PIN asks twice and warns below 6 digits.
+// * Creating or changing the PIN asks twice.
// * Key derivation (about 1 s) runs as a job on a timer, so the screen
// shows "Checking..." and the watchdog never trips.
// The typed digits are a ByteArray that is wiped after every attempt.
@@ -23,18 +30,19 @@ class PinView extends WatchUi.View {
static const MODE_CREATE = 1;
static const MODE_CHANGE = 2;
- // Selector positions 0-9 are digits; this one is "OK".
+ // Selector positions 0-9 are digits; this one is "OK" (legacy PINs only).
static const SELECT_OK = 10;
- // Keypad keys besides the digits.
+ // Keypad keys besides the digits (OK for legacy PINs only).
static const KEY_DELETE = -1;
static const KEY_OK = -2;
- static const RECOMMENDED_DIGITS = 6;
// Calibrate the PIN derivation to about this long on this watch.
static const DERIVATION_TARGET_MS = 1000;
static const CALIBRATION_SAMPLE = 200;
var mode as Number;
var busy as Boolean = false;
+ // Unlocking a legacy PIN: 4 to 10 digits, submitted with OK.
+ var legacy as Boolean = false;
private var _onSuccess as Method() as Void or Null;
private var _digits as ByteArray = []b;
@@ -57,6 +65,7 @@ class PinView extends WatchUi.View {
View.initialize();
mode = pinMode;
_onSuccess = onSuccess;
+ legacy = pinMode == MODE_UNLOCK && AppState.theVault().isLegacyPin();
}
function onShow() as Void {
@@ -85,7 +94,8 @@ class PinView extends WatchUi.View {
// ---- Input (called by PinDelegate) ----------------------------------
- // A keypad key: 0-9, KEY_DELETE or KEY_OK.
+ // A keypad key: 0-9, KEY_DELETE or KEY_OK (legacy PINs only). The
+ // PIN_DIGITS-th digit of a 4-digit PIN submits it.
function press(key as Number) as Void {
if (busy || waitSeconds() > 0) {
return;
@@ -93,17 +103,23 @@ class PinView extends WatchUi.View {
if (key == KEY_DELETE) {
deleteDigit();
} else if (key == KEY_OK) {
- submit();
- } else if (_digits.size() < KeyManager.MAX_PIN_DIGITS) {
+ if (legacy) {
+ submit();
+ }
+ } else if (_digits.size() < (legacy ? KeyManager.MAX_PIN_DIGITS : KeyManager.PIN_DIGITS)) {
_digits.add(0x30 + key);
_message = null;
+ if (!legacy && _digits.size() == KeyManager.PIN_DIGITS) {
+ submit();
+ }
}
WatchUi.requestUpdate();
}
- // Selector: UP/DOWN.
+ // Selector: UP/DOWN, over the digits (and "OK" for a legacy PIN).
function cycle(delta as Number) as Void {
- _selector = ((_selector + delta) % (SELECT_OK + 1) + SELECT_OK + 1) % (SELECT_OK + 1);
+ var positions = legacy ? SELECT_OK + 1 : SELECT_OK;
+ _selector = ((_selector + delta) % positions + positions) % positions;
WatchUi.requestUpdate();
}
@@ -146,19 +162,16 @@ class PinView extends WatchUi.View {
// ---- Checking and setting the PIN ------------------------------------
+ // A complete entry: check it (unlock) or keep it for the repeat (new PIN).
private function submit() as Void {
- if (!KeyManager.isValidPin(_digits)) {
- showMessage("4 to 10 digits", true);
- return;
+ if (!(legacy ? KeyManager.isValidPin(_digits) : KeyManager.isValidNewPin(_digits))) {
+ return; // a legacy OK with fewer than 4 digits
}
if (mode == MODE_UNLOCK) {
startUnlock();
} else if (_first == null) {
_first = _digits;
_digits = []b;
- if (_first.size() < RECOMMENDED_DIGITS) {
- showMessage("6+ digits are safer", false);
- }
} else if (Bytes.equal(_first as ByteArray, _digits)) {
startSetPin();
} else {
@@ -181,6 +194,7 @@ class PinView extends WatchUi.View {
clearDigits();
if (e.code == VaultError.NO_PIN) {
mode = MODE_CREATE;
+ legacy = false;
showMessage("Set a PIN first", false);
} else if (e.code != VaultError.LOCKED_OUT) {
showMessage("PIN data damaged", true);
@@ -201,7 +215,7 @@ class PinView extends WatchUi.View {
} catch (e instanceof VaultError) {
Bytes.wipe(_jobPin);
_jobPin = null;
- showMessage("Unlock first", true);
+ showMessage(e.code == VaultError.LOCKED ? "Unlock first" : KeyManager.PIN_DIGITS + " digits", true);
return;
}
startJob(method(:onSetDone));
@@ -225,8 +239,13 @@ class PinView extends WatchUi.View {
if (vault.finishUnlock(_job as PinJob)) {
_job = null;
vault.promotePending();
+ ChangeQueue.applyLocks(vault);
// Unlocked: "Encrypt names" can now reach locked entries.
AppState.processSettings();
+ if (legacy) {
+ askForNewPin();
+ return;
+ }
succeed();
return;
}
@@ -249,9 +268,23 @@ class PinView extends WatchUi.View {
vault.finishSetPin(_job as PinJob);
_job = null;
vault.promotePending();
+ ChangeQueue.applyLocks(vault);
succeed();
}
+ // After a legacy PIN unlocked: the same screen asks for a new 4-digit
+ // PIN, and onSuccess runs once that is set. BACK leaves it unlocked
+ // with the old PIN, which asks again at the next unlock.
+ private function askForNewPin() as Void {
+ AppState.activity();
+ legacy = false;
+ mode = MODE_CHANGE;
+ if (_selector == SELECT_OK) {
+ _selector = 0;
+ }
+ showMessage("Set a new " + KeyManager.PIN_DIGITS + "-digit PIN", false);
+ }
+
private function succeed() as Void {
AppState.activity();
var next = _onSuccess;
@@ -307,8 +340,8 @@ class PinView extends WatchUi.View {
2 * Ui.halfWidthAt(dc, y)), Graphics.TEXT_JUSTIFY_CENTER);
y += dc.getFontHeight(Graphics.FONT_SMALL) + 4;
- // Dots: entered digits filled, at least RECOMMENDED_DIGITS shown.
- var slots = _digits.size() > RECOMMENDED_DIGITS ? _digits.size() : RECOMMENDED_DIGITS;
+ // Dots: entered digits filled, PIN_DIGITS slots (more for a long legacy PIN).
+ var slots = _digits.size() > KeyManager.PIN_DIGITS ? _digits.size() : KeyManager.PIN_DIGITS;
var r = h / 60 > 3 ? h / 60 : 3;
var step = r * 3;
var x = cx - (slots - 1) * step / 2;
@@ -367,6 +400,9 @@ class PinView extends WatchUi.View {
_keys = [] as Array<Array<Number>>;
var log = "";
for (var i = 0; i < 12; i++) {
+ if (values[i] == KEY_OK && !legacy) {
+ continue; // no OK key: the 4th digit submits; the slot stays empty
+ }
var kx = left + (i % 3) * keyW;
var ky = top + (i / 3) * keyH;
_keys.add([kx, ky, keyW, keyH, values[i] as Number]);
@@ -468,11 +504,16 @@ class PinDelegate extends WatchUi.BehaviorDelegate {
return true;
}
+ // START on a touch screen submits only a legacy PIN. Otherwise it does
+ // nothing: one tap can arrive both here and in onTap, and submitting
+ // the digits typed so far was a wrong PIN.
function onKey(event as WatchUi.KeyEvent) as Boolean {
var v = view();
if (v != null && Ui.isTouch() && event.getKey() == WatchUi.KEY_ENTER) {
- v.markChanged();
- v.press(PinView.KEY_OK);
+ if (v.legacy) {
+ v.markChanged();
+ v.press(PinView.KEY_OK);
+ }
return true;
}
return false;
diff --git a/source/ui/QueueMenu.mc b/source/ui/QueueMenu.mc
index 880ca7c..3f9161d 100644
--- a/source/ui/QueueMenu.mc
+++ b/source/ui/QueueMenu.mc
@@ -4,7 +4,8 @@
// Confirming always asks for the PIN, even while the watch is unlocked:
// the point is that a person at the watch agrees, not that a key is
// available. (Before any PIN exists only "Require PIN" can wait here, and
-// confirming creates the PIN.) The changes are then applied by a
+// confirming creates the PIN. "Require PIN" items are applied by the PIN
+// screen itself, ChangeQueue.applyLocks, and listed in the results.) The changes are then applied by a
// ChangeQueueJob on a timer and the results screen shows what happened.
import Toybox.Lang;