Surfaced during keep-android PR review of the nip55-caller-verification-android branch (pin keep@5fc0942, keep-android #to-link).
Summary
Nip55NonceStore in keep-mobile/src/nip55_caller.rs measures the 5-minute nonce freshness window with std::time::Instant (generate_at/consume_at, NONCE_EXPIRY at line 14). On Android, Instant maps to CLOCK_MONOTONIC, which is paused while the device is suspended (deep sleep).
The previous Kotlin implementation used SystemClock.elapsedRealtime() (CLOCK_BOOTTIME), which counts time spent in suspend. So the migration changed the effective freshness bound: a device that sleeps for more than 5 wall-clock minutes between a NIP-55 request notification being shown (nonce issued) and the user tapping to approve (nonce consumed) can still observe the nonce as Valid, widening the intended 5-minute window.
The current code documents this as an intentional tradeoff at nip55_caller.rs:164-167 (monotonic resists wall-clock manipulation; the Android second expiresAt - now > NONCE_EXPIRY clause was deliberately dropped). The two properties are in tension:
- Pro monotonic (current): a wall-clock change cannot move
expires_at. Wall-clock manipulation cannot extend or shorten the window.
- Con monotonic (this issue): suspend pauses the clock, so the real-world freshness window can exceed 5 minutes across deep sleep.
Impact
Low. The nonce is single-use (consume removes the entry unconditionally) and package-bound (the approval activity rejects a nonce whose bound package != the calling package). The 5-minute bound is defense-in-depth on an already-narrow path, not the primary control. No exploit is enabled by the widening on its own; an attacker would still need to intercept the nonce and impersonate the bound package.
Options
- Accept and document the relaxed wall-clock semantics (close as wontfix; update the comment to state the suspend-pause behavior explicitly so it is not mistaken for a strict 5-minute wall-clock bound).
- Use a boot-time clock for nonce expiry so the window holds across suspend.
Instant cannot express this portably; would need a platform-supplied CLOCK_BOOTTIME timestamp passed in (the consume_at/generate_at seams already take an injected now, so the store logic need not change, only the clock source the Android layer feeds it).
Test gap
consume_at/generate_at take an injected now, but there is no test asserting the intended wall-clock bound or documenting the relaxed suspend semantics. Whichever option is chosen, add a test (or a comment) pinning the decision.
Refs
keep-mobile/src/nip55_caller.rs:14 (NONCE_EXPIRY), :140-157 (generate_at), :164-177 (consume_at + tradeoff comment)
- keep-android
CallerVerificationStore.consumeNonce delegates here.
Surfaced during keep-android PR review of the nip55-caller-verification-android branch (pin keep@5fc0942, keep-android #to-link).
Summary
Nip55NonceStoreinkeep-mobile/src/nip55_caller.rsmeasures the 5-minute nonce freshness window withstd::time::Instant(generate_at/consume_at,NONCE_EXPIRYat line 14). On Android,Instantmaps toCLOCK_MONOTONIC, which is paused while the device is suspended (deep sleep).The previous Kotlin implementation used
SystemClock.elapsedRealtime()(CLOCK_BOOTTIME), which counts time spent in suspend. So the migration changed the effective freshness bound: a device that sleeps for more than 5 wall-clock minutes between a NIP-55 request notification being shown (nonce issued) and the user tapping to approve (nonce consumed) can still observe the nonce asValid, widening the intended 5-minute window.The current code documents this as an intentional tradeoff at
nip55_caller.rs:164-167(monotonic resists wall-clock manipulation; the Android secondexpiresAt - now > NONCE_EXPIRYclause was deliberately dropped). The two properties are in tension:expires_at. Wall-clock manipulation cannot extend or shorten the window.Impact
Low. The nonce is single-use (
consumeremoves the entry unconditionally) and package-bound (the approval activity rejects a nonce whose bound package != the calling package). The 5-minute bound is defense-in-depth on an already-narrow path, not the primary control. No exploit is enabled by the widening on its own; an attacker would still need to intercept the nonce and impersonate the bound package.Options
Instantcannot express this portably; would need a platform-suppliedCLOCK_BOOTTIMEtimestamp passed in (theconsume_at/generate_atseams already take an injectednow, so the store logic need not change, only the clock source the Android layer feeds it).Test gap
consume_at/generate_attake an injectednow, but there is no test asserting the intended wall-clock bound or documenting the relaxed suspend semantics. Whichever option is chosen, add a test (or a comment) pinning the decision.Refs
keep-mobile/src/nip55_caller.rs:14(NONCE_EXPIRY),:140-157(generate_at),:164-177(consume_at+ tradeoff comment)CallerVerificationStore.consumeNoncedelegates here.