Skip to content

fix(security): gate cached private-key operations on device unlock - #1

Draft
mptfire wants to merge 3 commits into
masterfrom
sec/amb-003-cached-key-lock
Draft

mptfire wants to merge 3 commits into
masterfrom
sec/amb-003-cached-key-lock

Conversation

@mptfire

@mptfire mptfire commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Published as a draft for owner review. Local Android project validation remains blocked; do not merge until checks pass.

Finding: AMB-003 / Med. Branch: sec/amb-003-cached-key-lock. Commit: a7fb4867149c46d6ee3a3e1bc69292e84c66ad45.

Problem and threat model

After an account is cached while unlocked, a previously authorized local app or NIP-46 client can sign/decrypt after the device locks despite requireUnlockedDevice=true. Keystore gating of initial unwrap does not gate cached private bytes.

Source: Account.kt:73; LocalPreferences.kt:530. Shared with pinned upstream master; no fork-only regression.

Fix

Check the device-lock policy at Account private-key entry points and the backup derived-signer entry point.

User behavior and risk

When the existing option is enabled, cached-key operations reject while the device is locked. The default permits authorized background signing as before; this does not turn the separate UI PIN/biometric lock into a per-operation gate.

This is an application-level check at operation entry. It neither removes cached bytes from memory nor hardware-binds Schnorr operations, and cannot stop an operation that began before lock.

Tests and validation

Added regression coverage:

  • app/src/test/java/com/greenart7c3/nostrsigner/models/AccountCachedKeyLockTest.kt
  • app/src/test/java/com/greenart7c3/nostrsigner/service/DeviceUnlockPolicyTest.kt

AccountCachedKeyLockTest.kt and DeviceUnlockPolicyTest.kt were added. The pure policy was included in the 17-case portable policy harness; the Account/MockK Gradle test was not executed.

git diff --check passes. Patch applies cleanly to upstream 43bb743f6523676c942bba1c0790a80294f53a86.

Attempted ./gradlew ktlintCheck test lint :app:assembleFreeRelease --no-daemon: blocked before tasks, AGP 9.3.2 plugin resolution fails. GitHub CI status is tracked on this draft PR. Project lint/tests/build must pass before merging. For Android instrumentation, run the connected free-debug tests with a device once a build is available.

Migration and release notes

No storage migration. Existing requireUnlockedDevice preference is used.

No version bump. Repository changelogs are version-based; add accepted user-visible behavior to the next release notes at release preparation.

Prepared by Codex security review.
Original local review commit: c289f020c7ca50c5c3192f310cbf18e368e28bc4
@mptfire

mptfire commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

Reopening to trigger CI checks on the fork (Actions were not enabled when this draft was opened, so no checks have run yet).

@mptfire mptfire closed this Oct 13, 2026
@mptfire mptfire reopened this Oct 13, 2026
…test

- seedWords() returns the cached recovery mnemonic and was the one
  Account private-key entry point left ungated.
- The default keyAccessCheck read requireUnlockedDevice before async
  settings load landed, so the gate ran on a default-valued guess in the
  startup window; mirror WebDavService's check(isSettingsLoaded).
- AccountCachedKeyLockTest was missing io.mockk.Called for the
  wasNot Called verification and now also covers the seedWords gate.
@mptfire

mptfire commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

Fixup commit 53701fc6, pre-CI:

  • AccountCachedKeyLockTest was missing import io.mockk.Called for the wasNot Called verification (unresolved reference — the test could not compile).
  • seedWords() returns the cached recovery mnemonic and was the one Account private-key entry point left ungated; it now calls requirePrivateKeyAccess() and the test covers it.
  • The default keyAccessCheck read requireUnlockedDevice before async settings load landed, so the gate ran on a default-valued guess in the startup window; it now check(Amber.instance.isSettingsLoaded), mirroring WebDavService's fail-closed pattern in AMB-004.

Prepared by Codex security review. Preserve the existing security fix and
follow-up commits; add the missing validation gates without changing app code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant