Want the threat model, parameter rationale, and every engineering detail?
Version: 1.0 Published: May 3, 2026 Applies to: ByteGuard iOS v2.3.x and later Contact: [email protected]
ByteGuard is a password manager that runs entirely on the user's Apple devices (iPhone / iPad / Mac). This whitepaper is for cryptographic auditors, independent security researchers, and high-assurance users. It documents the key hierarchy, encryption algorithms, device-level protection mechanisms, and threat model in full.
We follow Kerckhoffs's Principle: a cryptographic system's security must rely solely on key secrecy, not on algorithm or architecture obscurity. Everything disclosed in this document (algorithms, parameters, key hierarchy, storage locations) does not weaken real security — on the contrary, public disclosure is a prerequisite for audit and trust.
| Threat | Defense |
|---|---|
| Server breach / data leak | ByteGuard runs no backend servers; sync flows entirely through the user's own iCloud Private DB |
| Lost device | Field-level AES-256-GCM + Keychain kSecAttrAccessibleWhenUnlockedThisDeviceOnly |
| Network MITM | Master Password, Secret Key, and the plaintext Vault DEK never leave the device's RAM; uploads carry AES-256-GCM-encrypted field ciphertext plus the KEK-wrapped DEK (application-layer encryption, independent of CloudKit's own encryption) |
| Phishing logins | Passkey (FIDO2) private keys never leave the device |
| Weak Master Password brute force | Argon2id 64MB memory cost × 3 iterations (OWASP mobile recommendation) |
| GPU offline attack | Argon2id memory hardness is hostile to GPUs |
| Timing attack on password verify | Constant-time XOR-accumulate byte comparison |
| Memory residue | Vault DEK is overwritten three times (zero → random → zero) on lock / suspend / vault switch; Master Key / KEK exist only as locals inside the unlock function and are released by Swift ARC |
We make no claim about the following — any password manager that claims defense here is exaggerating:
These threats fall outside the security model of this whitepaper.
ByteGuard uses a five-tier key hierarchy with per-entry key isolation:
Master Password + Secret Key (128-bit) ──Argon2id──→ Master Key (32B)
│
├─HKDF "auth-v1"──→ Auth Hash (verify only)
│
└─HKDF "kek-v1"──→ KEK (32B)
│
↓ AES-GCM unwrap
Vault DEK (32B, vault-wide, randomly generated)
│
↓ HKDF "type-{T}-item-{ID}-v1"
Item Key (32B, per-entry)
│
↓ AES-256-GCM
Field ciphertext
| Input | Length | Source | Persistence |
|---|---|---|---|
| Master Password | user input | user input | never persisted |
| Secret Key | 128-bit (16B) | libsodium randomBytes at vault creation |
iOS Keychain (optional iCloud Keychain sync); also shown to the user as a 12-word BIP39 mnemonic for backup |
| Salt | 256-bit (32B) | libsodium randomBytes at vault creation |
SwiftData (vault metadata) |
Algorithm: Argon2id (libsodium implementation)
Input: Argon2id(password ‖ secretKey.base64, salt) → 32 bytes
Parameters:
Parameter rationale: OWASP Password Storage Cheat Sheet — mobile recommendation. Typical wall-clock on iPhone 14 Pro: 100~250 ms (imperceptible to user, costly to GPU attackers).
Security properties:
Both are derived from Master Key via HKDF-SHA256 (RFC 5869):
| Derived key | Purpose | HKDF info string | Output length |
|---|---|---|---|
| Auth Hash | Verify Master Password correctness | "vault-auth-v1" |
32B |
| KEK | Unwrap Vault DEK | "vault-kek-v1" |
32B |
Strict separation of duty:
Password verification uses constant-time XOR-accumulate comparison, eliminating timing attacks.
Generation: At vault creation, libsodium randomBytes produces a 32-byte random key. It is then wrapped with KEK using AES-256-GCM (the Wrapped DEK) and stored in SwiftData.
Why one DEK per vault, not per item? Persistence stores only one wrapped DEK (instead of N), so storage and sync overhead grows linearly but slowly with entry count. Per-entry isolation is delivered by the next layer (Item Key).
Master Password change is cheap:
Algorithm: HKDF-SHA256(Vault DEK, info="type-{T}-item-{ID}-v1") → 32B
T: data type (login / card / note / passkey / identity, ...)ID: entry's unique IDSecurity:
Algorithm: AES-256-GCM (CryptoKit implementation)
Encrypted per field: username, password, URL, custom fields, card number, CVV (session-memory only), notes, attachments — all sensitive fields.
Unique IV: CryptoKit auto-generates a unique 96-bit IV per encryption — identical plaintext never yields identical ciphertext.
Never persisted:
To support Face ID / Touch ID quick unlock, a copy of the Vault DEK is stored in iOS Keychain when the user enables biometric:
Access control (ACL):
kSecAttrAccessible = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
kSecAccessControlFlags = .biometryCurrentSet
Meaning:
WhenUnlockedThisDeviceOnly: Unreadable when device is locked; not synced via iCloud KeychainbiometryCurrentSet: Requires Face ID / Touch ID verification; if the user adds/removes/re-enrolls biometric data in system settings, iOS automatically deletes this entryiOS Keychain entries are encrypted by a device key inside Secure Enclave. When the app calls SecItemCopyMatching:
Key facts:
iOS's biometryCurrentSet semantic guarantees: the moment a user re-enrolls Face ID (even just adding an additional face), iOS automatically deletes the Keychain entry.
ByteGuard detects this change at launch via LAContext.evaluatedPolicyDomainState:
If biometryCurrentSet is unavailable (simulator, no enrolled biometrics), the DEK cache simply throws — and does not fall back to a plain Keychain entry without biometric protection.
Design rationale:
All user data lives in iOS SwiftData. ByteGuard does not rely on SwiftData's storage encryption — even if an attacker obtains the raw SwiftData file bytes, every sensitive field is already AES-256-GCM ciphertext.
iCloud sync is disabled by default; the user must explicitly enable it in Settings. Once enabled, SwiftData connects via ModelConfiguration(cloudKitDatabase: .private(...)) to CloudKit Private Database.
Plain facts about CloudKit encryption (to dispel a common misconception):
Key properties:
Seeing "the wrapped Vault DEK is uploaded to iCloud" naturally raises a concern: isn't putting the key in the cloud counter-intuitive? This section addresses that head-on.
Cross-device sync demands cryptographically: a user signing into a brand-new iPhone / iPad must be able to decrypt all previously stored fields. Without syncing the wrapped DEK, the new device can never obtain the DEK, and all ciphertext is effectively lost.
Envelope encryption (KEK wraps DEK) is the standard solution to this dilemma — it decouples "the user's master password" from "the data encryption key," letting the former live only in the user's head while the latter, once wrapped by a key derived from it, can sit anywhere (including a public server) without weakening security.
To recover the plaintext DEK from the wrapped DEK, an attacker must break through, in order:
wrapped DEK = AES-256-GCM(KEK, plaintext_DEK, IV)
↑
KEK required to decrypt
↑
KEK = HKDF-SHA256(Master Key, "vault-kek-v1")
↑
Master Key required
↑
Master Key = Argon2id(Master Password ‖ Secret Key, salt) · 64MB × 3 iter
↑
Three barriers must be cleared simultaneously:
① The user's Master Password (attacker doesn't know it)
② 128-bit Secret Key (user-specific; Apple cannot obtain it; never leaves the device in plaintext form)
③ Each guess requires a 64MB × 3 iter Argon2id computation
Assume a Master Password with only 60 bits of entropy (decent but not extreme), combined with the 128-bit Secret Key = 188 bits total.
Even if quantum computing matures and Argon2id is sped up by 10^15×, the search would still take 10^31 years — 10^21× the age of the universe.
Every mainstream password manager stores the wrapped master key (or equivalent) in the cloud:
| Vendor | Sync | Wrapped master key location | KDF |
|---|---|---|---|
| 1Password | Own cloud | Uploaded | PBKDF2 / protected by Account Password + Secret Key |
| Bitwarden | Own cloud | Uploaded | PBKDF2 / Argon2id (user-selectable) |
| Dashlane | Own cloud | Uploaded | Argon2id |
| Apple Keychain | iCloud Keychain E2EE | Uploaded (Apple ADP layer) | Apple-internal |
| ByteGuard | iCloud Private DB | Uploaded | Argon2id 64MB × 3 + 128-bit Secret Key |
ByteGuard's cryptographic strength is in the top tier of the industry, with key differentiators:
Uploading the wrapped DEK to iCloud does not weaken security at all — it is the core security premise of envelope encryption.
Security depends entirely on: ① Master Password strength ② the Secret Key never being leaked ③ Argon2id parameters. Whether CloudKit is breached, whether Apple can decrypt CloudKit, whether a network MITM intercepts ciphertext — all become irrelevant under this design.
iOS AutoFill runs in a separate process context that cannot directly access the main App's in-memory DEK. ByteGuard v2's design:
HKDF + vault id)biometryCurrentSet protectionBenefit: AutoFill never touches the master Vault DEK — principle of least privilege.
SecureMemory.zero(&buffer) performs a three-pass overwrite + memory barrier on sensitive bytes:
memset)arc4random_buf)memset)load(as: UInt8) prevents the compiler from optimizing the zeroing awayScope of zeroing (precise facts):
_currentDEK instance variable): explicitly overwritten via SecureMemory.zero in lockVault / clearAllKeychainDataunlockVault / performUnlockOperations. After the function returns, Swift ARC releases them. ARC release only zeros the reference count, not the bytes — bytes may linger in the heap until reallocated. This is a current implementation limitation.sodium_memzero internally after key derivation completesDefends against: compiler optimization, heap dumps (jailbreak/debugger scenarios), memory pages swapped to disk after app suspension. Does not defend against: live memory reads of Master Key / KEK during the brief window (millisecond~second range) before ARC releases them.
Password verification uses byte-wise XOR accumulation:
var result: UInt8 = 0
for i in 0..<computedHash.count {
result |= computedHash[i] ^ storedHash[i]
}
guard result == 0 else { throw .invalidPassword }
Ensures comparison time is independent of match position/count, eliminating timing side channels.
All randomness (Salt, Vault DEK, Secret Key, IV) comes from:
libsodium randomBytes.buf() (calls macOS / iOS arc4random_buf, ultimately backed by kernel CSPRNG)AES.GCM.seal() auto-generates IVs internallyWe never use Math.random(), Date(), PID, or other low-entropy sources.
The built-in password generator:
SecRandomCopyBytes (kernel CSPRNG)A zero-knowledge architecture means even we cannot do the following — by design, not as a limitation:
| Scenario | Status |
|---|---|
| Reset the Master Password | ❌ Cannot — there is no second decryption path |
| Recover a lost Secret Key | ❌ Cannot — we never held any Secret Key data |
| Decrypt user data for law enforcement | ❌ Cannot — server has no keys, no ciphertext |
| Insert backdoors in the encryption | ❌ Cannot — algorithms and parameters are fully public, independently auditable |
| Collect user passwords for risk control / training | ❌ We don't, and technically can't |
| Push ads or third-party SDKs | ❌ The app integrates no analytics, ad, or crash-reporting SDKs |
If a user loses both Master Password and Secret Key → data is permanently undecryptable. This is a hard design constraint, not a bug.
| Purpose | Algorithm | Implementation | Standard |
|---|---|---|---|
| Master Password KDF | Argon2id 64MB / 3 iter | libsodium | RFC 9106 |
| Key derivation | HKDF-SHA256 | CryptoKit | RFC 5869 |
| Symmetric encryption | AES-256-GCM | CryptoKit | NIST SP 800-38D |
| Password strength | zxcvbn port | internal | — |
| Breach detection | k-anonymity HIBP API | internal | HIBP API v3 |
| Mnemonic | BIP39 12 words (128-bit entropy) | internal | BIP-0039 |
| Passkey | WebAuthn / FIDO2 ECDSA P-256 | iOS AuthenticationServices | FIDO2 / CTAP 2.2 |
All HKDF info strings carry a -v1 suffix, enabling seamless future migration to v2 algorithms (e.g. SHA-3, AES-256-GCM-SIV). Every database migration path retains decryption capability for old ciphertext.
| Parameter | Value |
|---|---|
| Argon2id memory | 64 MB |
| Argon2id iterations | 3 |
| Argon2id output | 32 bytes |
| Salt length | 32 bytes |
| Secret Key length | 16 bytes (128-bit) |
| Master Key length | 32 bytes (256-bit) |
| KEK length | 32 bytes (256-bit) |
| Vault DEK length | 32 bytes (256-bit) |
| Item Key length | 32 bytes (256-bit) |
| AES-GCM IV length | 12 bytes (96-bit) |
| AES-GCM tag length | 16 bytes (128-bit) |
| Auth Hash length | 32 bytes |
| BIP39 mnemonic | 12 words |
| info | Purpose |
|---|---|
vault-auth-v1 |
Auth Hash derivation |
vault-kek-v1 |
KEK derivation |
type-{T}-item-{ID}-v1 |
Item Key derivation (by data type + entry ID) |
vault-dek-v1 |
(deprecated) legacy DEK derivation |
Version history
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2026-05-03 | Whitepaper v1.0 |
Feedback and vulnerability disclosure
Found a security issue? Email [email protected]. We commit to: