Sign in with a private key (nsec / hex / ncryptsec) #8
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/nsec-login"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stacks on #7 (
feat/app-relays), which stacks on #6. Merge in that order.Closes the nsec half of
fa12577d.Why
Someone with an nsec in a password manager and no signer app currently has nowhere to go. Amber needs a second app; a bunker needs a running signer and a relay. This is the way in that needs neither.
What it is
Three pieces, split deliberately.
PrivateKeyInputclassifies the pasted text before any key material is touched — pure string work, no rust-nostr, no Android. Two reasons it is its own step:ncryptsecand stay away for annsec, and that is decided while the user is still typing.Keys.parseon an npub says the string is not a secret key, which reads as you typed it wrong and sends someone hunting for a typo that does not exist. Now it says it is a public key. Same for abunker://code, which is pointed at the row above.LocalKeySigneris the signer. It re-signs the JSON the app hands it rather than rebuilding the event from what it thinks the app meant — same seam the NIP-55 path uses, so a bug in our tag construction shows up here instead of being quietly corrected into something the app never produced.Storage is a sealed slot in
StashPreferencesunder its own keystore alias, not the bunker's. A NIP-46 client key can be revoked at the bunker and re-paired; this key is the identity, so dropping one login must not be able to make the other unopenable — in either direction. There is a test for exactly that.The order on screen
Amber → bunker → private key, and the private-key row carries a plain-words warning rather than a warning icon. Nostrord puts private key first. I think that is the wrong default for a beta app people are sideloading, but it is a one-line change and it is @laoc42's call.The honest cost
StashPreferencesused to document itself as "this process never holds a private key at all". After this it does. That comment is now corrected rather than left standing. The key is AES-GCM sealed by a keystore key (hardware-backed where the device has it), but it is on the phone, and anything that reads it is the user, permanently.Verification
Test keys are NIP-19's and NIP-49's own published vectors, so a pass says the app agrees with the specs rather than with itself. My first draft of the ncryptsec was recalled from memory and was wrong by one character; classification would have passed on it either way, which is why it needed checking against the spec rather than against the code.
The restart test asserts a publish, not a screen
a_stored_key_still_signs_after_a_restartshares a link and saves it, then reads the event off the relay. Reaching the library would have proved nothing: the app opens on its cache with no signer at all, so "not the login screen" is true even when nothing can sign. It also polls from inside the running Activity — finishing it first cancels the publish inviewModelScope, and an empty relay then reads exactly like a signer that was never rebuilt.Mutations
Each row patched one line, asserted the patch landed with
git diff --numstat, and ran the class that claims to cover it:PrivateKeyInputTestncryptsecstops asking for a passwordPrivateKeyInputTestPrivateKeyInputTestPrivateKeyInputTestcreated_atrebuilt instead of carriedLocalKeySignerE2ETestLocalKeySignerE2ETestLocalKeyStorageE2ETestLocalKeyStorageE2ETestPrivateKeyLoginE2ETestPrivateKeyLoginE2ETestPrivateKeyLoginE2ETestPrivateKeyLoginE2ETestOne row first reported
CAUGHT (does not compile). That is not a verdict — it means my harness conflated a build failure with a caught mutation. Re-run on its own it compiles fine and is genuinely caught (4 passed, 1 failed, and the failure is the alias test). Reported because a scripted trial name is not evidence.Not proven
ncryptsecwritten by another app. The vector under test is the spec's own, so the format is right, but nothing here has read a file produced by Amber or nsec.app.Harness notes for whoever is next
The login screen now scrolls, so the private-key row sits below the fold.
performClickdoes not scroll to its target, and five tests failed with the click silently doing nothing rather than with an error — which reads as "the button is broken".performScrollTo()first. Same forSign outon the settings screen.🤖 Generated with Claude Code
laoc referenced this pull request2026-08-06 10:32:57 +00:00