Technical deep dive into ArmoryHub's security implementation.
Architecture decisions, cryptographic choices, and UX tradeoffs explained.
SecurityCoordinator (Facade Pattern) βββ BiometricAuthManager β βββ LAContext for Face ID/Touch ID β βββ Lock timeout management β βββ isAuthenticated state β βββ PINManager β βββ PIN validation (PBKDF2-SHA256, 310K iterations) β βββ Master key storage (encrypted in Keychain) β βββ Lockout protection (5 attempts = 30s timeout) β βββ currentSessionMasterKey (in-memory only) β βββ EncryptionManager β βββ EncryptionStateManager (race prevention) β βββ Field-level encryption (AES-256-GCM) β βββ Batch encrypt/decrypt operations β βββ iCloud encryption flag management β βββ KeyTransferManager βββ QR code generation (temporary vs backup) βββ QR code validation (expiry, one-time use) βββ Rate limiting (5 attempts per QR) βββ Import/export key data Supporting Components: βββ KeychainWrapper (kSecAttrAccessibleWhenUnlockedThisDeviceOnly) βββ PBKDF2 (CommonCrypto wrapper) βββ EncryptionState (state machine enum) βββ SecurityTypes (PINLength, LockTimeout, AuthenticationError)
Originally, the master key was generated when users set up their PINβeven if they never enabled encryption. This meant PIN-only users had unnecessary cryptographic material in their Keychain.
We moved master key generation to happen ONLY when encryption is first enabled. PIN setup now just creates a PIN hash for authentication.
We could decrypt data only in memory (never write plaintext to disk), but this would severely impact performance and UX. Core Data expects persistent storage.
Data is decrypted to plaintext in Core Data while app is unlocked, then re-encrypted when backgrounded. This is the same approach used by Signal, 1Password, and most secure apps.
Background/foreground transitions are asynchronous. Without coordination, the app could start decrypting data while encryption is still in progress, causing data corruption.
Implemented EncryptionStateManager with atomic state transitions (.decrypted, .encrypting, .encrypted, .decrypting). Only one transition allowed at a time, enforced by NSLock.
With encryption on, the master key stays in memory while app is unlocked. The 'Never' timeout would keep data decrypted and key in memory indefinitelyβa security risk.
Automatically hide 'Never' option in auto-lock picker when encryption is enabled. Programmatic attempts to set 'Never' are silently changed to 'Immediately'.
Key derivation function choice impacts both security (brute-force resistance) and UX (unlock speed). We follow OWASP 2023 recommendations.
PBKDF2-HMAC-SHA256 with 310,000 iterations. Takes ~100ms on iPhone 15 Pro, ~200ms on iPhone 12. Bcrypt and Argon2 considered but require third-party dependencies.
We could encrypt the entire SQLite database file with native Core Data encryption, but this is all-or-nothing (either everything is encrypted or nothing is).
Field-level encryption using AES-256-GCM. Each string/binary field encrypted individually. Dates and numeric values remain plaintext due to Core Data type constraints.
Field Encryption
Authenticated encryption with automatic nonce generation via Apple's CryptoKit. GCM mode provides both confidentiality and authenticity (prevents tampering). Approved by NSA for TOP SECRET data.
ChaCha20-Poly1305 (similar security, better on devices without AES hardware), AES-CBC (no authentication)
Native CryptoKit support, hardware acceleration on all Apple devices, authenticated encryption, battle-tested
PIN-to-Key Derivation
Stretches 6-digit PIN (20 bits entropy) into 256-bit key (makes brute force computationally infeasible). 310,000 iterations takes ~100ms, making each PIN attempt expensive.
Argon2id (more resistant to GPU/ASIC attacks), bcrypt (memory-hard), scrypt (memory-hard)
Native CommonCrypto support (no dependencies), FIPS-approved, OWASP 2023 compliant, fast enough for good UX
Salt Generation
Cryptographically secure random number generation backed by /dev/random. Generates unique 32-byte salts for each PIN and master key.
CryptoKit.SymmetricKey (similar), Swift's random APIs (NOT cryptographically secure)
Industry standard for iOS, guaranteed entropy, hardware-backed on modern devices
Sensitive Data Storage
Hardware-backed secure storage with Secure Enclave protection. Data encrypted with device-unique keys. kSecAttrAccessibleWhenUnlockedThisDeviceOnly prevents iCloud Keychain sync.
UserDefaults (INSECURE), File encryption (complex), CloudKit (keys would syncβbad)
Purpose-built for this, hardware protection, well-documented, industry standard
Users must understand two concepts: PIN (locks app) vs Encryption (protects data)
Gives users flexibility to choose security level. Free users get PIN for app lock. Pro users can add encryption for data protection. Many users want app lock without the complexity/risk of encryption.
Force encryption when PIN is enabled (simpler, but removes choice)
Separate features with clear UI explanations
More initial complexity, but users appreciate the choice and flexibility
No performance impactβapp is fast and responsive
Decrypting on-demand would add latency to every data access. Users expect instant scrolling and search. Banking apps (Chase, BofA) use same approach.
In-memory-only decryption (slower, more secure)
Plaintext while unlocked, encrypted when backgrounded
Fast app with forced encryption on background (best of both worlds)
Users wait ~1-2 seconds after PIN entry for large inventories
Decrypting 1000s of text fields takes time. We show 'Unlocking Your Data...' blocker during this time so users understand what's happening.
Lazy decryption (decrypt each field on accessβeven slower)
Batch decrypt on unlock with progress indicator
Slight delay on unlock, but instant access once decrypted
Users cannot choose 4-digit or 8-digit PINs
4 digits = 10,000 combinations (too weak). 6 digits = 1 million combinations (industry standard). 8+ digits = annoying to enter. We standardized on 6 for security/UX balance.
Configurable length (4-12 digits)
Fixed 6-digit PIN
Simpler UX, industry-standard security, muscle memory consistency
Users must manually scan QR codes between devices (less convenient)
Syncing encryption keys via iCloud Keychain would be easier BUT: (1) Keys would exist in Apple's servers (zero-knowledge broken), (2) Government could subpoena iCloud backups and get keys, (3) iCloud breach would expose keys. QR transfer keeps keys offline.
iCloud Keychain sync for encryption keys (more convenient, less secure)
Offline QR code transfer only
True zero-knowledge architecture, more setup friction, better security
What we protect against and what we don't. Understanding the threat model helps you choose the right security level.
With PIN or biometric enabled, thief cannot open app. With encryption enabled, even if they bypass app lock (jailbreak), data is encrypted garbage without your PIN.
With encryption enabled, AES-256-GCM encrypted data cannot be decrypted without your PIN. Forensic tools cannot break modern encryption. You may be legally compelled to provide PIN (5th Amendment protections vary by jurisdiction).
Your encryption keys are stored in device Keychain onlyβnever synced to iCloud. Even if Apple's iCloud is breached, attackers get encrypted blobs without keys. Data is useless.
While app is unlocked, master key is in memory and data is decrypted in database. Debugger attachment or memory dump would expose both. Auto-lock timeout provides mitigation by forcing periodic encryption.
iOS encrypted backups (and iCloud backups if encryption enabled) contain AES-256-GCM encrypted data. Without your PIN, backups are useless. Keys are in Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly (survives encrypted backup restore).
If attacker tricks you into giving them your PIN, they can decrypt your data. We cannot prevent user error. Education (in-app warnings) is our only defense.
Backup QR codes contain your encrypted master key. If attacker gets both the QR AND your PIN, they can decrypt all data. Solution: Change PIN if QR is compromised (regenerates key encryption).
Jailbreak gives root access, potentially allowing Keychain extraction. Secure Enclave still provides hardware protection, but sophisticated attackers may succeed. Recommendation: Don't use encryption on jailbroken devices.
PIN validation, master key management, lockout protection
Data encryption/decryption, state management, batch operations
QR code generation/import, multi-device key synchronization
We believe in transparency. Here are the limitations of our security implementation:
6 digits = 1 million combinations = ~20 bits of entropy. This is industry-standard but not high-entropy. PBKDF2's 310,000 iterations slow down brute force, but a determined attacker with GPU cluster could try millions of PINs offline (if they steal backup QR code).
Mitigation:
Use random 6 digits (not birth dates). Generate backup QR and store securely. Consider changing PIN periodically.
Data is decrypted to SQLite database while app is unlocked. Memory dump or debugger attachment during this window would expose plaintext. File system snapshots capture plaintext.
Mitigation:
Auto-lock with short timeout (1-5 minutes). 'Lock Now' button for instant encryption. Don't leave app unlocked unattended.
Master key is stored encrypted in Keychain, but not in Secure Enclave. Secure Enclave can only store 256-bit keys for signing, not general-purpose encryption keys. We'd need to re-architect to use Secure Enclave for key wrapping.
Mitigation:
Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly is still hardware-protected on modern devices. Future enhancement: Secure Enclave key wrapping.
Due to Core Data type constraints, dates (purchase dates, birth dates) and numeric values (prices, quantities) are stored in plaintext. Encrypting these would require converting to string types, breaking Core Data queries and sorting.
Mitigation:
Sensitive data (serial numbers, names, locations, notes) IS encrypted. Dates/numbers are less sensitive. Alternative: Use full-database encryption (different tradeoffs).
If you lose all devices and don't have a backup QR, your data is gone forever. True zero-knowledge architecture means we can't help you recover. This is by design, but it's also a support burden.
Mitigation:
Strong user education, mandatory backup QR recommendations, testing recovery flow before you need it.
Use Secure Enclave to wrap the master encryption key (iPhone XS+ with A12 chip or later). Prevents Keychain extraction even on jailbroken devices.
Timeline: 6-12 months
Add support for quantum-resistant algorithms (Kyber, Dilithium) alongside current AES-256. Future-proofs against quantum computing attacks.
Timeline: 12-24 months
Allow YubiKey or other FIDO2 hardware keys for authentication. Physical key required for unlock (something you have + something you know).
Timeline: 12-18 months
Opt-in server-side key backup with account recovery (email verification, security questions). Would break zero-knowledge architecture but give recovery option.
Timeline: Not planned (conflicts with zero-knowledge promise)
Require Face ID/Touch ID again for: export data, disable encryption, remove PIN, generate QR codes. Prevents unauthorized actions if device is unlocked and left unattended.
Timeline: 3-6 months
Write-only log of security events (PIN changes, encryption toggles, QR generations) using os_log. Helps forensic analysis if breach occurs.
Timeline: 3-6 months
Note: ArmoryHub is a single-developer project built by an independent developer passionate about firearms and security. While we strive for professional-grade security implementation, we don't have the resources of large security teams. We welcome community input and security research to help us improve.
We believe in security through transparency. If you're a security researcher interested in auditing ArmoryHub:
Component | Specification | Standard |
---|---|---|
Encryption Algorithm | AES-256-GCM | NIST FIPS 197, NSA Suite B |
Key Derivation Function | PBKDF2-HMAC-SHA256 | NIST SP 800-132 |
KDF Iterations | 310,000 | OWASP 2023 Minimum |
Master Key Size | 256 bits | NSA TOP SECRET approved |
Salt Generation | SecRandomCopyBytes (32 bytes) | FIPS 140-2 Level 1 |
PIN Length | 6 digits (1 million combinations) | Industry standard |
Keychain Protection | kSecAttrAccessibleWhenUnlockedThisDeviceOnly | iOS Security Guide |
Keychain Sync | Disabled (kSecAttrSynchronizable = false) | Zero-knowledge architecture |
Biometric Auth | LAContext.deviceOwnerAuthentication | iOS LocalAuthentication |
Nonce Generation | Automatic (CryptoKit AES.GCM) | NIST SP 800-38D |
Authentication Tag | 128 bits (GCM default) | NIST SP 800-38D |