diff options
Diffstat (limited to 'source/ui')
| -rw-r--r-- | source/ui/Nav.mc | 20 | ||||
| -rw-r--r-- | source/ui/OtpView.mc | 11 | ||||
| -rw-r--r-- | source/ui/PinView.mc | 87 | ||||
| -rw-r--r-- | source/ui/QueueMenu.mc | 3 |
4 files changed, 96 insertions, 25 deletions
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; |
