aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-25 12:46:42 +0100
committercirakg <cirakg@cirakg.xyz>2026-10-25 12:46:42 +0100
commit096b5a4d17c85fa5f87db3e72364e9c3506d1e64 (patch)
tree9846544fe7d76d849df4a1ee2058f4847f674e7e /test
parente0384274bbeab999279bdc35bdddbd8a3a451aeb (diff)
feat(pin): exactly 4 digits, entered on the 4th digit
The keypad has no OK key; the 4th digit submits, so a tap that also arrives as START can no longer submit a partial PIN (the likely cause of "Wrong PIN" before the code opened). PINs set by earlier versions still unlock and are then replaced by a 4-digit one. Opening an entry ignores a second request from the same tap. A Require PIN sent from the phone while the watch is locked now applies at the next PIN instead of waiting for Confirm all. The linter skips .claude. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'test')
-rw-r--r--test/settings/ManageTest.mc40
-rw-r--r--test/storage/PinTest.mc62
-rw-r--r--test/storage/StorageSupport.mc21
3 files changed, 118 insertions, 5 deletions
diff --git a/test/settings/ManageTest.mc b/test/settings/ManageTest.mc
index 0adf785..1f050c5 100644
--- a/test/settings/ManageTest.mc
+++ b/test/settings/ManageTest.mc
@@ -243,7 +243,9 @@ function manageLocksEntries(logger as Test.Logger) as Boolean {
vault.lock();
s = ManageFixtures.send(vault, ids[1], ManageAction.LOCK);
SettingsFixtures.run(vault, s);
- var waits = !ManageFixtures.isLocked(vault, ids[1]) && ChangeQueue.count(vault) == 1;
+ var waits = !ManageFixtures.isLocked(vault, ids[1]) && ChangeQueue.count(vault) == 1
+ && SettingsFixtures.contains(logger, s, ManageForm.STATUS,
+ "Locks when you next enter your PIN on the watch: Require PIN: " + ids[1]);
// In lower case: the same form again would just be "Already applied".
s = ManageFixtures.send(vault, ids[0].toLower(), ManageAction.LOCK);
SettingsFixtures.run(vault, s);
@@ -253,6 +255,42 @@ function manageLocksEntries(logger as Test.Logger) as Boolean {
return now && waits && already && ManageFixtures.isLocked(vault, ids[1]);
}
+// Waiting "Require PIN" changes apply at the next PIN without "Confirm
+// all" (the PIN screen calls applyLocks); other changes keep waiting, and
+// the results of a following "Confirm all" list the locks too.
+(:test)
+function manageQueuedLocksApplyAtNextPin(logger as Test.Logger) as Boolean {
+ var vault = ManageFixtures.vault();
+ var ids = vault.ids();
+ var bank = ManageFixtures.addLocked(vault, "Bank");
+ vault.lock();
+ var s = ManageFixtures.send(vault, ids[0], ManageAction.LOCK);
+ SettingsFixtures.run(vault, s);
+ s = ManageFixtures.send(vault, bank, ManageAction.DELETE);
+ SettingsFixtures.run(vault, s);
+ s = ManageFixtures.send(vault, ids[1], ManageAction.LOCK);
+ SettingsFixtures.run(vault, s);
+ var counted = ChangeQueue.count(vault) == 3 && ChangeQueue.countLocks(vault) == 2;
+ ChangeQueue.applyLocks(vault); // still locked: nothing happens
+ var untouched = ChangeQueue.count(vault) == 3 && !ManageFixtures.isLocked(vault, ids[0]);
+ StorageFixtures.unlock(vault, StorageFixtures.PIN);
+ ChangeQueue.applyLocks(vault);
+ var left = ChangeQueue.descriptions(vault);
+ var applied = ManageFixtures.isLocked(vault, ids[0]) && ManageFixtures.isLocked(vault, ids[1])
+ && left.size() == 1 && left[0].equals("Delete: " + bank + " Bank (alice@example.com)")
+ && ChangeQueue.countLocks(vault) == 0 && vault.ids().indexOf(bank) >= 0;
+ var job = ManageFixtures.confirm(vault);
+ var reported = job.lines.size() == 3 && job.lines[0].find(ids[0] + " ") == 0
+ && job.lines[0].find("now requires the PIN") != null && SettingsFixtures.hasLine(job.lines, "Deleted " + bank);
+ if (!reported) {
+ logger.error(job.lines.toString());
+ }
+ // The lines are reported once.
+ var again = ManageFixtures.confirm(vault);
+ return counted && untouched && applied && reported && vault.ids().indexOf(bank) < 0
+ && again.lines.size() == 1 && again.lines[0].equals("No changes were waiting.");
+}
+
// Changes that arrive while the queue is being applied stay queued.
(:test)
function manageQueueKeepsLateArrivals(logger as Test.Logger) as Boolean {
diff --git a/test/storage/PinTest.mc b/test/storage/PinTest.mc
index 0212936..55adaa2 100644
--- a/test/storage/PinTest.mc
+++ b/test/storage/PinTest.mc
@@ -1,5 +1,5 @@
// Author: cirakg & claude ai
-// Summary: Tests for PINs: create, unlock, wrong PIN, the lockout schedule on a fake clock, erase, change, reset, auto-lock.
+// Summary: Tests for PINs: create, unlock, wrong PIN, 4-digit rule, legacy PINs, the lockout schedule on a fake clock, erase, change, reset, auto-lock.
import Toybox.Lang;
import Toybox.Test;
@@ -39,6 +39,64 @@ function pinRejectsMalformedPins(logger as Test.Logger) as Boolean {
return KeyManager.isValidPin(Bytes.fromUtf8("0000")) && KeyManager.isValidPin(Bytes.fromUtf8("0123456789"));
}
+// A new PIN has exactly KeyManager.PIN_DIGITS digits; other lengths are
+// refused before any derivation.
+(:test)
+function pinSetRequiresFourDigits(logger as Test.Logger) as Boolean {
+ var bad = ["123", "12345", "123456", "0123456789", "12a4"];
+ for (var i = 0; i < bad.size(); i++) {
+ if (KeyManager.isValidNewPin(Bytes.fromUtf8(bad[i]))) {
+ logger.error("accepted new PIN '" + bad[i] + "'");
+ return false;
+ }
+ var code = 0;
+ try {
+ new Vault(new MemoryBackend(), new FakeTime()).beginSetPin(StorageFixtures.pin(bad[i]), StorageFixtures.ITERATIONS);
+ } catch (err instanceof VaultError) {
+ code = err.code;
+ }
+ if (code != VaultError.INVALID) {
+ logger.error("set PIN '" + bad[i] + "' gave code " + code);
+ return false;
+ }
+ }
+ return KeyManager.isValidNewPin(Bytes.fromUtf8("0000")) && KeyManager.isValidNewPin(Bytes.fromUtf8("9876"));
+}
+
+// Setting or changing the PIN stores the 4-digit marker; forgetting the
+// PIN removes it.
+(:test)
+function pinMarkerIsSetBySetAndChange(logger as Test.Logger) as Boolean {
+ var store = new MemoryBackend();
+ var vault = new Vault(store, new FakeTime());
+ StorageFixtures.setPin(vault, StorageFixtures.PIN);
+ var afterSet = StorageFixtures.pinMarked(store) && !vault.isLegacyPin();
+ store.deleteValue(KeyManager.PIN_LENGTH_KEY);
+ var legacyWithout = vault.isLegacyPin();
+ StorageFixtures.setPin(vault, StorageFixtures.OTHER_PIN); // change while unlocked
+ var afterChange = StorageFixtures.pinMarked(store) && !vault.isLegacyPin();
+ vault.resetPin();
+ return afterSet && legacyWithout && afterChange && store.getValue(KeyManager.PIN_LENGTH_KEY) == null
+ && !vault.isLegacyPin();
+}
+
+// A 6-digit PIN from before the 4-digit rule still unlocks (and a wrong
+// one fails without an exception); a new 4-digit PIN then replaces it.
+(:test)
+function pinLegacySixDigitWrapStillUnlocks(logger as Test.Logger) as Boolean {
+ var store = new MemoryBackend();
+ StorageFixtures.setLegacyPin(store, StorageFixtures.LEGACY_PIN);
+ var vault = new Vault(store, new FakeTime());
+ var legacy = vault.hasPin() && vault.isLegacyPin();
+ var wrong = !StorageFixtures.unlock(vault, "2468101");
+ var right = StorageFixtures.unlock(vault, StorageFixtures.LEGACY_PIN) && vault.session.isUnlocked();
+ StorageFixtures.setPin(vault, StorageFixtures.PIN);
+ vault.lock();
+ var oldFails = !StorageFixtures.unlock(vault, StorageFixtures.LEGACY_PIN);
+ var newWorks = StorageFixtures.unlock(vault, StorageFixtures.PIN);
+ return legacy && wrong && right && !vault.isLegacyPin() && oldFails && newWorks;
+}
+
(:test)
function lockoutDelaySchedule(logger as Test.Logger) as Boolean {
var expected = [0, 0, 0, 0, 0, 30, 60, 120, 240, 480, 960, 1920, 3600, 3600, 3600];
@@ -170,7 +228,7 @@ function pinChangeKeepsEntries(logger as Test.Logger) as Boolean {
vault.lock();
var lockedCode = 0;
try {
- vault.beginSetPin(StorageFixtures.pin("999999"), StorageFixtures.ITERATIONS);
+ vault.beginSetPin(StorageFixtures.pin("9999"), StorageFixtures.ITERATIONS);
} catch (err instanceof VaultError) {
lockedCode = err.code;
}
diff --git a/test/storage/StorageSupport.mc b/test/storage/StorageSupport.mc
index 51a0c7c..5f2ead3 100644
--- a/test/storage/StorageSupport.mc
+++ b/test/storage/StorageSupport.mc
@@ -143,8 +143,9 @@ class FakeTime extends TimeSource {
(:test)
module StorageFixtures {
- const PIN = "246810";
- const OTHER_PIN = "135799";
+ const PIN = "2468"; // KeyManager.PIN_DIGITS digits, like every new PIN
+ const OTHER_PIN = "1357";
+ const LEGACY_PIN = "246810"; // a 6-digit PIN from before the 4-digit rule
const ITERATIONS = 2000; // Kdf.MIN_ITERATIONS: the cheapest allowed
// A fake TOTP entry (the secret is the RFC 4226 test key, not a real one).
@@ -205,6 +206,22 @@ module StorageFixtures {
vault.finishSetPin(job);
}
+ // Store a wrap the way versions before the 4-digit rule did: any 4 to
+ // 10 digits and no PIN-length marker. The session stays locked.
+ function setLegacyPin(store as MemoryBackend, text as String) as Void {
+ var keyManager = new KeyManager(store);
+ var job = new PinJob(pin(text), new [KeyManager.SALT_BYTES]b, ITERATIONS);
+ Jobs.runToCompletion(job, Kdf.ITERATIONS_PER_STEP);
+ keyManager.storeWrap(job, keyManager.newMasterKey());
+ store.deleteValue(KeyManager.PIN_LENGTH_KEY);
+ }
+
+ // True when the store holds the 4-digit PIN marker.
+ function pinMarked(store as MemoryBackend) as Boolean {
+ var marker = store.getValue(KeyManager.PIN_LENGTH_KEY);
+ return marker instanceof Number && marker == KeyManager.PIN_DIGITS;
+ }
+
// One unlock attempt; true when the PIN was right.
function unlock(vault as Vault, text as String) as Boolean {
var job = vault.beginUnlock(pin(text));