Repository navigation
A security and feature release. Everything in v1.3.2, plus the changes below, including agent approvals, which let this phone approve or deny what a keep agent gateway holds for a human.
The bunker works through a proxy and never falls back to a direct connection
With a SOCKS proxy such as Orbot turned on, the bunker never started and went straight to an error. It now starts through the proxy, and a proxied start that fails never retries over a direct connection.
A stored proxy setting that could not be read was treated as "no proxy", so a Keystore fault on that record made the bunker and the relay connection connect directly and expose your IP address. Both now fail closed: the bunker stops with an error and the relay connection is refused. Only a proxy setting that reads cleanly as off connects directly.
Under a proxy, the bunker also looked up relay names with the phone's own resolver before starting. That sent the relay names to the local network in the clear. Names are now left for the proxy to resolve, and localhost and internal addresses are still refused. Adding a relay in the bunker screen still checks it with a local lookup, which a later release will change.
A full bunker app list refuses new apps instead of evicting one
When the list of paired bunker apps was full at 100, a new app pushed the oldest one off the list. The pushed-off app disappeared from the app, but its remembered approvals stayed live and the connect secret still let it back in. Someone holding the bunker URL could connect throwaway apps until one with remembered approvals was pushed off, leaving it able to sign without showing up anywhere.
A full list now refuses the new app and evicts nobody, even when it connects with the bunker secret. A nostrconnect:// request at the limit is refused with a message to revoke an app first. On a device that still holds more than 100 apps from versions before 1.0.5, the oldest ones past the limit are revoked at startup and pair again if they need to.
Revoking an app now replaces only the connect secret and keeps the bunker key, so your other apps keep their remembered approvals and do not pair again. An approval answered after its app was revoked is refused.
Co-signed Bitcoin spends show every output and are approved only in the app
When a peer asks this phone to co-sign a Bitcoin spend, the prompt now lists every output with its exact amount and address (or its script, marked as unrecognized, when the account has no wallet descriptor), then the fee and the total taken from the inputs. Nothing is shortened or labeled as change.
The notification for a spend shows only the output count, fee and total, and has no Approve button. A spend is approved in the app, behind a fresh strong biometric or device credential prompt every time, and without a secure screen lock it is refused. Only a Nostr event the core has verified keeps Approve on the notification, and that now needs the phone unlocked. A request the core cannot check at all carries a warning that the phone cannot see what it is signing and that it may be a Bitcoin spend.
The phone can now co-sign taproot key-path spends with the same prompt, which it refused before. The requester's signing round also stays open while you decide, up to a fixed limit, so a spend confirmed more than about 15 seconds after it arrives no longer fails.
Apps that sign through Keep are kept apart and bounded
An approval for an app's own NIP-55 sign request could also approve a peer's co-sign request for the same event, with no prompt. The two are now kept apart. Retries of your own approved request across co-signers no longer ask you again.
A background request from another app could hold a thread on both sides for minutes while it waited on co-signers. Keep now refuses it after 30 seconds, and each app may have one request in flight, so one app cannot lock every other app out. A response that arrives after the caller stopped waiting is never returned, and the activity log records it as abandoned.
An automatic approval refused for taking too long to log could still leave an "allow" row behind for a signature that was never returned. Such a row is now either never written or followed by an "abandoned" row with the same request id, and the "first time signing this kind" risk check counts only approvals from a log that verifies, so a refused or forged row cannot make a kind look familiar.
Per-app signing policy now belongs to each account. Settings from earlier versions are moved into the active account once, other accounts follow their own policy, and with no active account every app asks first.
Changing the date no longer shortens PIN lockouts
The PIN lockout level, which sets how long the next lockout lasts, dropped one step per day by the wall clock alone. Setting the date back, or far forward, reset it, so someone holding the unlocked phone could keep every lockout at the shortest step. The level now drops only by time that has really passed, and changing the date never lowers it faster.
Accounts whose key cannot be read are shown and set aside, not dropped
When the phone's secure storage stops decrypting an account's key, which a system update sometimes fixes, the app showed that account as simply gone. A card on Home now names each affected account, the other accounts keep working, and "Try again" reads the store again.
After a retry fails, the account can be set aside, which moves it out of the list without destroying anything, and only when the phone confirms the key can never be decrypted on this device. Set-aside accounts can be put back or discarded. Discarding destroys the key for good, asks for a warning and a fresh authentication, and then offers to restore from a backup or import. A discard that stops part way can be retried to finish. If the account list itself cannot be read, the app shows only "Try again" until it can.
With two accounts, one of them unreadable and no active account saved, the app picked the readable one and signed as an account you had not chosen. It now asks you to pick. A backup is refused while any account cannot be read, so it never leaves one out.
Settings that cannot be read get a way back
When the relay settings for the active account could not be read, the Relays card showed an empty list with no way out short of removing the account. It now shows an error and a "Reset relay settings" button, behind a strong biometric or the device credential. The reset puts back the default public relay, clears the profile relays and blocks the shares this phone does not hold, and it keeps the old settings if they read again in the meantime.
Adding a relay to the auth whitelist now says why it failed: the address is not usable, the list is full, or the save failed.
Some settings files first opened by an older build named their entries with a key derived from a value that is public. Each such file now moves to a fresh random key the next time it opens, when every entry in it can be accounted for.
Restoring a backup works again, and refused imports say why
Restoring a backup failed every time, and a phone with no account could not start a restore at all. Both now work. A restore never writes over an account that is still stored or one that cannot be read.
A refused import used to read "Import failed. Please try again.", which invites a retry that can never succeed. It now says when the key or that same share is already stored, or when another share of the group is. A key recovery whose share was already stored failed with a storage error, and now finishes, or says why it was refused.
Agent approvals
Keep can now pair with a keep agent gateway and act as its approver. Settings has a new "Agent approvals" card. Pairing scans the gateway's QR code and compares a fingerprint, and the gateway accepts only a release build on a locked device.
The screen shows the requests the gateway's policy holds for a human. Anything the agent wrote sits in its own labeled box. Approve, deny and unfreeze are each signed by a hardware key that needs a fresh strong biometric, and enrolling a new biometric invalidates it. "Freeze every agent" and "Freeze this agent" need no biometric and are never blocked by another action. Notifications only say that a request is waiting, with no request content and no actions, and the screen cannot be captured.
The pairing is saved in a way that a power loss cannot roll back, so a freeze is never lost. If the saved pairing is damaged, the app offers a reset behind your biometric or device lock, and keeps the pairing if it reads again.
keep moves to v0.14.0
keep.version moves to keep v0.14.0. The keep releases since v0.12.2 carry the approver core with its hardware key attestation, per-account app signing policy, the bunker app limit and revoke changes, the relay settings reset, named import refusals, setting aside accounts whose key is lost, the co-sign spend summaries and key-path spends, the signing round that waits while you decide, NIP-55 approvals kept apart from co-sign approvals, and a pre-approval that no longer skips the spend prompt.
Install
Download the APK for your device's architecture from the Assets below. Min SDK 33 (Android 13+).
keep-android-v1.4.0-arm64-v8a.apkfor physical deviceskeep-android-v1.4.0-x86_64.apkfor emulators and x86_64 hardware
Both per-ABI APKs reproduce bit-for-bit from source: the Reproducibility workflow rebuilds each one in a pinned container and compares it against the published artifact with apksigcopier compare, so a mismatch fails rather than passing quietly.
Verifying this release
Download SHA256SUMS, SHA256SUMS.asc, privkeyio-signing-key.asc and your APK into one folder:
gpg --import privkeyio-signing-key.asc
gpg --verify SHA256SUMS.asc SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS
Look for Good signature from "Kyle Santiago <kyle@privkey.io>" on key A47D99B6DB0D715D40C59A2023AE8A8EA7E24E38, and OK for your file. The warning that the key is not certified is expected, and means only that you have not marked it trusted yourself.
The APK carries its own signature, which is what Android checks on install and what F-Droid pins as AllowedAPKSigningKeys:
apksigner verify --print-certs keep-android-v1.4.0-arm64-v8a.apk
That must report certificate SHA-256 digest 3eb0e6f6f4e0962ebd717f84eea47428b73087b686c956633d6a9fcc4c854f50.
The git tag is signed with the same GPG key, so git verify-tag v1.4.0 checks the source this release was built from.