Security
How the vault is built, and what a breach would find.
Last updated 17 September 2026 · AssuredAuth, by CircleRoll LLC
This is the public description of how AssuredAuth protects the two-factor secrets, passwords and cards you keep in it. It is written so that a security-minded reader can check every claim against the app and against the published test vectors (available to any reviewer on request at hello@assuredauth.com). It says what is true and no more: nothing here claims to be beyond attack, and nothing is promised in percentages.
The short version: the vault exists in one place, your phone. Every value in it is encrypted with a key that the phone's security hardware holds and cannot export. We run no vault server, hold no copy and hold no key. A backup file leaves the phone only after it has been encrypted under a passphrase that is never stored anywhere.
Report a problem: hello@assuredauth.com. See "Reporting" at the end.
1. What is encrypted, with what
| Data | On the phone | Key |
|---|---|---|
| Two-factor secrets (TOTP/HOTP/Steam seeds) | On the phoneKeychain (iOS) / Android Keystore-backed store | KeyThe platform's hardware-bound key; the database row holds only a reference (secretRef), never the seed |
| Passwords (the password value, notes) | On the phoneSealed row: AES-256-GCM, a random 96-bit nonce per value | KeyA random 256-bit data key, itself wrapped by the hardware key in §2 |
| Cards (number, expiry, name on card, CVC) | On the phoneSame seal as passwords; the CVC is optional and is stored only if you enter it | KeySame data key |
| Metadata (service names, usernames, icons, sort order) | On the phoneThe app's database (SwiftData / Room) | KeyProtected by the operating system's file encryption; treated as non-secret |
Backup file (.authbackup) |
On the phoneWherever you put it: Files, iCloud Drive, Google Drive appDataFolder, a share sheet |
KeyA key derived from your passphrase (§3). Apple and Google receive the sealed file and nothing else |
| Watch / Wear companion | On the phoneThe watch's own Keychain / EncryptedSharedPreferences | KeyThe watch platform's keys; transferred over the platform's paired-device channel |
| Desktop extension traffic (one-time codes on request) | On the phoneNot stored; relayed as end-to-end encrypted envelopes | KeyAn X25519 key pair per pairing, private key in the Keychain / Keystore; the relay sees ciphertext only |
Diagnostics has no method that accepts a secret. No analytics event carries a vault field. No value from the vault is ever written to a log, in any build.
2. Where the keys live, on each platform
iOS.
The data key is wrapped by a key generated inside the Secure Enclave
(kSecAttrTokenIDSecureEnclave) with access control .privateKeyUsage + .userPresence,
accessibility …ThisDeviceOnly, in the app's shared Keychain access group so the AutoFill
extension can use the same key. The private key cannot be read out of the Enclave by the app, by
a backup, or by a debugger; the Enclave only agrees to use it after the user is present (Face ID,
Touch ID or the device passcode). ThisDeviceOnly means an iCloud or iTunes backup of the phone
does not carry the key; a restored phone starts with an empty vault and asks for your backup file
and passphrase. A phone with no passcode cannot gate a .userPresence key (the Enclave refuses
to create one), and the simulator takes the same path by design (its presence prompt is an alert
no test can answer): there the data key is a plain device-only Keychain item — still out of every
backup, still unreadable while the phone is locked, but with no second prompt — and the Security
screen says exactly that and suggests setting a passcode.
Android.
The data key is wrapped by an AndroidKeyStore AES-256 key, generated in StrongBox
when the device has one (otherwise the TEE), with setUserAuthenticationRequired(true) and a
30-second authentication validity after a biometric or device credential,
setUnlockedDeviceRequired(true), and setInvalidatedByBiometricEnrollment(true) left on. That
last flag is honest rather than load-bearing: Android applies it only to keys that demand a
biometric on every use, so on this 30-second key with the device credential allowed it changes
nothing — what keeps a stranger's finger out is that enrolling one needs the PIN, and that
removing the screen lock invalidates the key for good. Keystore keys are not exportable and are
excluded from Android's cloud backup. A domain an app names on its own views (webDomain) is
believed only from a known browser; from any other package it must also be one the system has
verified for that package (Digital Asset Links), since any app can claim any domain.
What this means for a stolen database file. The SQLite file and the sealed rows are ciphertext. The key that opens them lives in security hardware on the phone the file came from and cannot be copied with the file. There is nothing to guess offline: the data key is random, not derived from anything a person chose.
3. The backup file
Backups are the one thing that leaves the phone, so they are the one place an offline attack is possible in principle. The design makes that attack expensive and gives it as little to work with as possible:
- The passphrase is at least 12 characters (the sheet refuses fewer and shows a strength bar).
- No passphrase hint is stored anywhere, in the file or in the app.
- A fresh random 16-byte salt and 12-byte nonce for every file written.
- The whole body is AES-256-GCM, so a wrong passphrase or a damaged file is refused, never decrypted into garbage.
- The header (format, version, KDF parameters, salt) is bound into the GCM tag as additional authenticated data, so a file whose work factor has been edited down does not open.
The file is JSON and readable in any text editor; only ciphertext is opaque. Someone who finds
the file in five years, with the app gone, can see exactly how it was sealed and open it with
OpenSSL or any Argon2 library.
Format v1 (shipped in 1.0, stays readable)
{
"format": "authbackup",
"version": 1,
"kdf": "PBKDF2-HMAC-SHA256",
"rounds": 600000,
"salt": "<base64, 16 bytes>",
"cipher": "AES-256-GCM",
"nonce": "<base64, 12 bytes>",
"ciphertext": "<base64>",
"tag": "<base64, 16 bytes>"
}
| Step | Bytes |
|---|---|
key | PBKDF2-HMAC-SHA256(passphrase as UTF-8, salt, 600000 rounds, 32 bytes) |
aad | UTF-8("authbackup:1:600000:") ‖ salt (raw salt bytes, not base64) |
ciphertext, tag | AES-256-GCM(key, nonce, plaintext, aad); tag 16 bytes, stored separately |
plaintext | UTF-8 JSON, keys sorted: {"accounts":[…],"exportedAt":"…Z","passwords":[…],"version":1} |
Compatibility note: Android 1.0 wrote a variant of v1 — kdf in lower case, no tag key (the
16-byte tag appended to ciphertext), no additional data. Both platforms keep reading those files
(when tag is absent: split the last 16 bytes, open with empty AAD; compare kdf
case-insensitively) and both now write only the layout above.
Format v2 (current; the default for new backups)
{
"format": "authbackup",
"version": 2,
"kdf": "Argon2id",
"memoryKiB": 65536,
"passes": 3,
"lanes": 1,
"salt": "<base64, 16 bytes>",
"cipher": "AES-256-GCM",
"nonce": "<base64, 12 bytes>",
"ciphertext": "<base64>",
"tag": "<base64, 16 bytes>"
}
| Step | Bytes |
|---|---|
key | Argon2id(version 0x13, passphrase as UTF-8, salt, m = 65536 KiB, t = 3, p = 1, tagLength 32, no secret, no associated data) |
aad | UTF-8("authbackup:2:argon2id:65536:3:1:") ‖ salt |
ciphertext, tag | AES-256-GCM(key, nonce, plaintext, aad); tag 16 bytes, stored separately |
plaintext | UTF-8 JSON, keys sorted: {"accounts":[…],"cards":[…],"exportedAt":"…Z","passwords":[…],"version":2} |
Argon2id is the reference C implementation (phc-winner-argon2, CC0), vendored on both platforms.
64 MiB and 3 passes takes about a second on a current phone and makes each guess cost that memory
and time on any hardware — the property PBKDF2 lacks against GPU rigs, and the reason v2 exists.
A reader ignores keys it does not know, so a v1 reader given a v2 body simply never sees cards.
Proof
Three test-vector files (backup v1, backup v2 and Argon2id) are generated by a third implementation, independent of both apps (Node's OpenSSL for PBKDF2 and AES-GCM, a WebAssembly Argon2id checked at generation time against the reference implementation's published answers). Each holds the passphrase, salt, nonce, plaintext, derived key, AAD, expected ciphertext and tag, and the complete file. The unit tests on iOS and on Android read the same files and assert the same bytes — so a backup sealed on one platform is proven, not assumed, to open on the other, and an agreement of all three is not two copies of the same mistake. A companion tool opens a file a phone actually wrote with that same third implementation and checks the header against the rules above (exactly the format's keys, so no hint or name can be present; 16-byte salt, 12-byte nonce, 16-byte tag stored separately; version 2 at exactly 64 MiB / 3 / 1; the wrong passphrase refused); the automated iOS run writes one on the simulator and opens it that way, and searches the simulator's app container, App Group container and keychain database for every seeded password and card number as UTF-8 and UTF-16 (0 hits).
4. Threat model
| Situation | What they have | What they get |
|---|---|---|
| Lost or stolen phone, locked | What they haveThe device, the database file, the sealed rows | What they getCiphertext. The wrapping key is in the Secure Enclave / StrongBox and releases only after the device lock plus a fresh biometric or passcode. Wiping the phone remotely (Find My / Find My Device) destroys the key material with it. |
| Stolen backup file | What they haveThe .authbackup file from a cloud account, an email, a laptop |
What they getAn AES-256-GCM body under a key derived from a passphrase of at least 12 characters, with a per-file salt and Argon2id at 64 MiB × 3 passes (v2) or PBKDF2 at 600,000 rounds (v1). No hint, no username, no account list in the clear. Each guess costs the full derivation; there is no shortcut. A strong passphrase is the whole defense here, and the app says so on the sheet. |
| Stolen device with the screen unlocked | What they haveA phone in your hand, unlocked | What they getThe Codes list. The vault (passwords, cards) is behind the app lock and re-locks on the auto-lock timer (immediately / 1 min / 5 min); a decrypted value exists in memory only while its screen is showing it. Autofill and plain-text export ask for a fresh biometric even when the app is open. Turn on app lock with "Immediately" if this is the case you worry about. |
| Malicious app on the same phone | What they haveAnother app's sandbox, the clipboard, accessibility or overlay tricks | What they getIt cannot read our files (platform sandboxing) or use our key (the key is bound to our signing identity and to user presence). Copies clear from the clipboard after 30 seconds; on Android the copy is flagged sensitive and excluded from clipboard history. Vault screens set FLAG_SECURE on Android (no screenshots, no screen recording, blank in the switcher); iOS blanks the switcher thumbnail. Autofill on Android is served only to apps the system has verified against the saved domain (Digital Asset Links), so a look-alike app is not offered your password. |
| Phishing page | What they haveA site that looks like the one you saved | What they getNothing fills. Matching is by registrable domain (public suffix list, bundled and updated with the app): login.example.com and example.com match, example.com.evil.net and examp1e.com do not. The in-app browser fills only on the exact host the entry was saved for. iOS AutoFill adds Apple's associated-domains check. A password you must paste by hand into a look-alike is still a risk; the app cannot remove that, only avoid helping. |
| Our own servers | What they have— | What they getThere is no vault server. AssuredAuth has no accounts, no sign-in and no copy of your vault, so there is nothing of yours on our side to be stolen and cracked. Two things do run server-side, neither of which touches the vault: a relay mailbox for the desktop extension, which stores end-to-end encrypted envelopes (one-time codes, deleted when read; the relay never has a key), and the optional private connection's exit servers, which carry traffic and keep no access logs. |
| A subpoena or a government request | What they haveLegal process addressed to us | What they getNothing to hand over: we hold no vault, no key, no passphrase and no backup. We can be compelled to say that; we cannot be compelled to produce what does not exist. |
5. Clipboard, screenshots and memory
- Copy puts the value on the clipboard for 30 seconds, then clears it if it is still ours. On Android the clip is marked
EXTRA_IS_SENSITIVEso the system hides it from the clipboard preview and history; on iOS the pasteboard item is local-only (not Handoff'd to other devices). - Vault screens (Passwords, Password, Cards, Card, the passphrase sheets, plain-text export) set
FLAG_SECUREon Android. iOS has no equivalent flag; the app blanks its thumbnail in the app switcher and the values are behind an eye toggle by default. - Decrypted values live in memory only while a screen shows them and are dropped when it goes away or the app locks. The data key is unwrapped for a request and not cached across the lock.
- Plain-text export exists (so you can always leave) and is behind a confirm sheet and a fresh biometric, and its screen is
FLAG_SECUREtoo.
6. Autofill: what fills, and what does not
| Where | Passwords | Cards |
|---|---|---|
| iOS: Safari and apps (AutoFill extension) | PasswordsYes, after a fresh Face ID / Touch ID / passcode in the extension (LAContext), and only for the registrable domain the entry was saved for, with Apple's associated-domains check for apps |
CardsNo. Card autofill in Safari is Apple's alone; the card screen says "Fills in the AssuredAuth browser and with Copy" and promises nothing else |
| Android: apps and Chrome (AutofillService) | PasswordsYes, per-dataset authentication: the system shows BiometricPrompt before the fill; app package matched only when Digital Asset Links ties it to the saved domain |
CardsYes, the creditCardNumber, creditCardExpiration* and creditCardSecurityCode hints, same biometric gate |
| The AssuredAuth browser (both platforms) | PasswordsYes, exact host only, biometric before the fill | CardsYes, by field detection, biometric before the fill |
| Copy buttons | PasswordsBehind the app lock, 30-second clipboard | CardsNumber, expiry, CVC (same rules) |
Nothing fills on a look-alike host. Nothing fills without the biometric.
7. What never happens
No vault server. No analytics that carry a vault field. No plain-text export without a confirm sheet and a fresh biometric. No third-party SDK loaded on a vault screen. No screenshot of a vault screen. No passphrase hint. No logging of values, in any build.
8. What we cannot protect against
A passphrase weak enough to guess. A phone whose operating system is compromised at the root (a jailbroken or rooted device can lie to the app about everything above). A person who is watched while typing. Malware on the desktop you paste into. We say this so the list above is read for what it is: the things a design can remove, removed.
9. Review status
This design has been unit-tested against published known answers on both platforms and against its own cross-platform vectors. An external review is on the owner's list before the vault is advertised on the store listing; until it has happened, this document will say so here.
Reporting
If you believe you have found a weakness in anything described here, write to hello@assuredauth.com. Please include the platform, the app version and the steps; a proof that uses your own data is enough, and we ask that you do not test against other people's. We will acknowledge within a few days, fix what is real, and credit you here if you want to be credited.