CC Safety Net
A coding agent CLI safety layer that blocks destructive git and filesystem commands and secret file access before execution.
Create a BIP39 recovery phrase from your email address and a secret sentence, on an offline phone. The same email and sentence give you the same phrase. Open source, for Android and iPhone.
SecuritySwift · KotlinGitHub @mondaylabsltdOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A recovery phrase you keep in your head. On an offline Android phone or iPhone, Keyper turns your email and a sentence only you know into a standard BIP39 recovery phrase. The same inputs always give the same phrase, so you do not need to write it down. Keyper stores nothing you type.
Coming to Google Play and the App Store for US$29.99, paid once. Building it yourself from this repository is free and gives the same app.
getkeyper.app · English · 简体中文
Android 8.1 (API 27) or later, or iOS 17 or later, with 4 GB of RAM or more. English and Chinese.
email ──── must be a valid address; lowercase A–Z ───────────────┐ salt
sentence ─ collapse extra spaces ─ HMAC-SHA256(domain) ─ Argon2id(1 GiB × 128) ─── K ─┬─ HMAC ─ 12 words
└─ HMAC ─ 24 words
A–Z, a–z, 0–9 and 18 symbols
! @ # $ % ^ & * ( ) . , ? - _ + = /. Case, symbols and the spaces between words count;
extra spaces (runs of spaces, spaces at either end) do not. There are no quotes, |, \,
: or ;, which are easy to mistype or misread. Write other languages in Latin
letters, for example Chinese in pinyin: Wo de di yi zhi mao jiao MiMi![email protected] and [email protected] are the same; [email protected] and [email protected]
are not. Phone numbers are not accepted. The exact rule is in SPEC.md §2.Domain.kt and
Domain.swift. It makes the same
sentence give different phrases in different deployments. It ships inside the app, so it
is public.The email-as-salt construction is the standard one (WarpWallet uses it too): the same sentence gives unrelated phrases for different people, and an attacker has to attack one person at a time. One email can go with several sentences, each giving its own phrase.
letters digits and symbols
q w e r t y u i o p 1 2 3 4 5 6 7 8 9 0
a s d f g h j k l ! @ # $ % ^ & * ( )
⇧ z x c v b n m ⌫ . , ? - _ + = / ⌫
123 [ space ] Done ABC [ space ] Done
0/O and 1/l/I apart. The email gets @ and . on the
bottom row and a Next key.The code and the domain constant are public: assume every attacker has them.
| An attacker has… | What still protects the phrase |
|---|---|
| your email | how hard your sentence is to guess: every guess costs 1 GiB × 128 passes |
| your sentence | only your email, which is usually not a secret |
| your email and your sentence | nothing: together they are the phrase |
Length does not make a sentence hard to guess. Famous lines, lyrics, proverbs and templates like "my cat is called X" are an attacker's first guesses and fall for a few dollars of electricity. Use details only you know: names, years, places, numbers, a symbol or two.
| Entropy of the sentence | 1000 top GPUs | Electricity alone |
|---|---|---|
| 30 bits | 7.5 hours | US$300 |
| 40 bits | 320 days | US$305,000 |
| 50 bits | 890 years | US$313 million |
| 60 bits | 910,000 years | US$320 billion |
The app estimates this while you type, using assumptions that favour the attacker (SPEC.md §4). Below US$100 million it warns you before generating, but it cannot recognise famous lines, so don't use one.
Do the recovery drill before you use the phrase. After generating, tap "First time? Do a recovery drill" and re-type both inputs from memory.
If the domain constant is lost or changed, no phrase can ever be regenerated. See Building and maintaining.
Other protections: leaving the app wipes the sentence and the phrase, and autofill is excluded. The QR code is the main way out; copying asks once per session whether to use it instead.
The offline gate checks every half second while the app is in front, without any permission:
getifaddrs) and that the cellular
radio is off. It deliberately avoids the Network framework: on iPhones sold in mainland China
that makes iOS ask whether the app may use wireless data, and a "no" would read as offline. It
cannot see Wi-Fi that is on but joined to no network, and reading Bluetooth would take a
permission, so it only reminds you to turn Bluetooth off. iOS lets no app open those settings,
so the steps are instructions.A copied phrase is meant to leave the clipboard after 60 seconds. Keyper never clears anything but its own copy: a newer clip from another app always survives.
| iPhone | Android 10+ | |
|---|---|---|
| Keyper open | cleared at 60 s by iOS | cleared at 60 s by Keyper |
| Keyper in the background | cleared at 60 s by iOS | cleared when you next open Keyper |
| Keyper closed, killed or force-stopped | cleared at 60 s by iOS | cleared when you next open Keyper |
On iPhone the copy carries a 60-second expiry that iOS enforces whatever Keyper does, and it
never reaches your other devices. On Android 10 and later an app in the background cannot see the
clipboard, so Keyper cannot tell its copy from a newer one and does not touch it until it is in
front again; if you never reopen Keyper, Android 13+ clears the clipboard after about an hour.
Android 8 and 9 still allow the check, so Keyper clears on time there. Tests:
app-android/backgroundtest and app-ios/KeyperTests/ClipboardTests.swift.
Going online and leaving the app end everything derived from the sentence and keep only the email; Clear all removes the email too. What that means in memory:
| Value | When | iPhone | Android |
|---|---|---|---|
| Key K, the drill fingerprint | going online, leaving, Clear all, a new derivation | overwritten with memset_s | overwritten through Wipe |
| P = HMAC(domain, sentence), the phrase's entropy, Argon2's scratch blocks | at the end of every derivation, also when cancelled | overwritten with memset_s | overwritten through Wipe |
| Argon2's 1 GiB working memory | the same | overwritten with memset_s, then unmapped | overwritten through Wipe, then unmapped |
| The sentence and the words as text | going online, leaving, Clear all | dropped, not overwritten | dropped, not overwritten |
memset_s is defined (C11 Annex K) so that the compiler must perform it even when
the memory is never read again; CI checks that the optimised Release binary links it
(app-ios/scripts/check-release.sh). It overwrites the buffer it is given: each sensitive array
is kept uniquely owned so that no copy-on-write copy is left elsewhere.Wipe writes the zeros and reads them back into a volatile field so the stores are
observable, but neither the JVM nor ART formally guarantees they survive every optimisation, and
ART's moving garbage collector may already have copied a heap array where no code can reach it.
The working memory is off-heap, so the collector never moves it, yet that alone does not make
its erasure certain. On Android, wiping is best-effort.The drill keeps no fast hash of the sentence: its fingerprint is HMAC-SHA256 keyed with K, over a
label and the length-prefixed normalised email and sentence, so it is useless without K, and lives
and dies with K. Tests: Argon2idTest/Argon2idTests (working memory zeroed on completion and on
cancellation), KeyperTest/KeyperTests (the fingerprint's vectors, keying and encoding).
The pre-freeze audit of the protocol and the Android app, with the attack scenarios, is in docs/security-audit.md. The Android and iOS release review dated 2026-11-02, including fixes and outstanding risks, is in docs/release-security-audit-2026-11-02.md (Chinese). To report a vulnerability, see SECURITY.md.
Keyper v1 · <Git revision>. A -dirty suffix means the build includes
uncommitted changes. This identifies the source revision; it does not authenticate a build.
Install only from the official store listings or build from source, and check your phrase
once with the recovery script before you use it.You need JDK 21 and the Android SDK (compileSdk 36).
cd app-android
./gradlew :core:test :app:testDebugUnitTest :app:lintDebug :app:assembleRelease
# app/build/outputs/apk/release/app-release.apk
Release signing reads app-android/keystore.properties (storeFile, storePassword,
keyAlias, keyPassword; git-ignored). Without it, release builds are left unsigned.
./gradlew :app:bundleRelease makes the bundle for Google Play.
You need Xcode 26 on a Mac.
cd app-ios/KeyperCore && swift test -c release # the protocol core, with SPEC's 1 GiB vectors
cd app-ios && xcodebuild -scheme Keyper -destination 'platform=iOS Simulator,name=iPhone 17' test
Open app-ios/Keyper.xcodeproj to run it on a phone, with your own team for signing, and set
the scheme's Run configuration to Release: Debug builds accept the testing flags below and can be
debugged. The core
is a Swift package that uses nothing but the system's CryptoKit; the app is SwiftUI, with UIKit
for the keyboard, the fields and the screen shield. The project uses synchronised folders, so a
new file needs no project edit. Debug builds also accept -KeyperSimulateOffline and
-KeyperAllowScreenshots for UI tests and -KeyperBench for timing on a phone; release builds
contain none of them.
The core tests run the SPEC vectors with the full 1 GiB Argon2id and take a few minutes.
KeyperTest.protocolV1IsFrozen (and its Swift twin) pin every
protocol constant, and the SPEC vectors pin the outputs. A change that alters any v1 phrase
fails the build. Both cores read the same fixtures in test-vectors/.Domain.kt and
Domain.swift alike (domain ID A248-7EAB). Tests pin the ID
and keep the two files equal. Keep offline copies of this repository and of every released
app: they all contain the constant.openssl rand -hex 32
before anyone uses your build, and update the domain-ID tests. Phrases from different
deployments are unrelated.You need a computer with Python 3.9 or later and a copy of this repository: the script reads the domain constant from it, so keep a copy offline. It takes about 25 seconds on a laptop.
python3 -m venv keyper-env
keyper-env/bin/pip install --require-hashes -r reference/requirements-recovery.txt
# Disconnect the computer from the network, then:
keyper-env/bin/python reference/keyper.py --domain-file app-android/app/src/main/kotlin/app/getkeyper/Domain.kt --words 12
On Windows, use keyper-env\Scripts\ instead of keyper-env/bin/. Before asking for anything, the
script prints domain id A248-7EAB (official Keyper); if it shows anything else, stop. It asks for
the sentence twice and never shows it. --self-test checks the script against SPEC's vectors.
| What | How | Result |
|---|---|---|
| Argon2id, BLAKE2b | RFC 9106 and RFC 7693 vectors; 48 random parameter sets against argon2-cffi (the RFC reference C code) | all equal |
| BIP39 | SHA-256 of the wordlist; Trezor's 24 official vectors | all equal |
| QR codes | module-by-module against segno for 40 versions × 8 masks; zxing-cpp decodes 42 real phrases and app screenshots | all equal, all decoded |
| Protocol | Kotlin against the Python reference (argon2-cffi + Trezor's mnemonic) on SPEC's 7 vectors; the app (emulator) against the Python reference with the real domain constant | equal |
| Email rule | 55 edge cases (test-vectors/email.tsv) in Kotlin, Swift and the Python reference | all equal |
| Freeze guards | protocol constants and the domain ID are pinned by tests | pass |
| The tests themselves | deliberately broken Argon2, QR masking and thread shutdown on cancel | each caught |
| Language switch | English and Chinese, after typing both inputs and generating | no restart; everything kept |
| Recovery drill | one wrong character, then the right input | mismatch reported; on a match the phrase is shown again |
| Large text | 24 words at 1.6× the system font size | every word shown in full |
| Keyboard | every character class typed on the app's keyboard in the emulator; taps in the gaps and along the edges; sliding; two-finger rollover with real multi-pointer events | as designed |
| Input isolation | system keyboard state while the fields have focus; text and key events injected over adb | the system keyboard never appears; injection blocked |
| Cancel and abort | Cancel, Home and turning Wi-Fi on during a derivation | CPU from 300%+ to 0 within 2 s, no crash |
| Offline gate | every switch, on emulators (API 36, 30) and a Redmi K40 (HyperOS) | input and generation only offline; Bluetooth "half-on" detected |
| Memory | 8 derivations in a row on an emulator with a 192 MB Java heap limit | memory released each time |
| Screenshots, permissions | screencap on the device; the merged manifest |
Argon2id, 1 GiB × 128 passes, 4 lanes: Redmi K40 (Snapdragon 870) release build 64.6 s, with no
throttling over a minute of full load; iPhone 11 (A13) about 72 s; desktop JVM (M2 Pro) about
22 s; Swift (M2 Pro) about 25 s; argon2-cffi (reference C code, M2 Pro) about 25 s. Reproduce with
./gradlew :core:test and ./gradlew :core:bench; on a phone, with a debug build:
adb shell 'CLASSPATH=$(pm path app.getkeyper.private | cut -d: -f2) app_process / app.getkeyper.DeviceBenchKt'.
reference/make_fixtures.py generates the test fixtures from the independent implementations
above.
zh/z or
ü as v/u). The drill catches mistakes today; it cannot catch memory drifting later.SPEC.md the protocol: v1, frozen; design decisions; test vectors
SECURITY.md how to report a vulnerability
docs/security-audit.md pre-freeze security audit, attack scenarios, residual risks
reference/keyper.py independent Python implementation (cross-checks, recovery without the app)
reference/requirements-recovery.txt what recovery installs, pinned with hashes
reference/make_fixtures.py regenerates the test fixtures from independent implementations
test-vectors/ fixtures both cores are tested against
app-android/core/ pure Kotlin/JVM: protocol, Argon2id, BLAKE2b, BIP39, QR codes, strength estimate
app-android/app/ Android UI: keyboard, offline gate, recovery drill; platform views only
app-ios/KeyperCore/ Swift package: the same core in Swift, on the system's CryptoKit
app-ios/Keyper/ iOS UI: SwiftUI, with UIKit for the keyboard, the fields and the screen shield
website/ getkeyper.app: SvelteKit, prerendered, on Cloudflare
store/ store listings, review notes, privacy answers, store graphics
Bug reports, translations and UI improvements are welcome. The protocol is frozen: no change may alter a v1 phrase. See CONTRIBUTING.md.
Copyright © 2026 MONDAY LABS LTD. Keyper is open source under the MIT License.
| black; 0 permissions |
| iOS core | the same fixtures and SPEC's 7 vectors in Swift (swift test -c release) | all equal |
| iOS app | typed key by key on the app's keyboard in the simulator, generated, compared word for word with the Python reference (real domain constant); QR code, drill, language switch, leaving the app, weak and short inputs, offline gate in UI tests | equal; as designed |
| iOS keyboard | two-finger rollover, sliding, gaps and edges, delete held while typing, in unit tests with several fingers | as designed |
| iOS texts | compact numbers against ICU itself (test-vectors/compact.tsv); strength texts against the Android app | equal |
| iOS on a phone | iPhone 11 (iOS 26.5): typed on the app's keyboard, generated, compared with the Python reference; a screenshot of the running app; background and back three times with the shield on; a clean install | equal; the screenshot shows only the page colour; no crash; no system prompt of any kind |
| iOS offline gate | iPhone 11 in airplane mode with Wi-Fi joined, then with Wi-Fi off, connected to the Mac by USB | blocks (screen checked), then opens; the USB link and Apple's device tunnel do not count |
| iOS permissions | the built Info.plist and the linked libraries | no usage descriptions, no background modes; system frameworks only |