Purpose: Complete description of the encryption scheme, key lifecycle, authentication flows, security invariants, and AI coding boundaries for CypherAir. Audience: Human developers, security auditors, and AI coding tools.
All cryptographic operations use Sequoia PGP 2.2.0. Two profiles with different algorithm suites:
| Purpose | Algorithm | Notes |
|---|---|---|
| Primary key (sign/certify) | Ed25519 (legacy EdDSA) | v4 key format |
| Encryption subkey | X25519 (legacy ECDH) | v4 key format |
| Symmetric encryption | AES-256 | 256-bit key |
| Message format | SEIPDv1 (MDC) | Non-AEAD; GnuPG compatible |
| Hash | SHA-512 | Accepts SHA-256 for legacy verification |
| S2K (key export) | Iterated+Salted (mode 3) | GnuPG compatible |
| Compression | DEFLATE (read-only) | Enabled for reading compatibility; outgoing messages must not use compression |
| Random | SecRandomCopyBytes | Via getrandom crate on Apple platforms |
| Purpose | Algorithm | Notes |
|---|---|---|
| Primary key (sign/certify) | Ed448 | v6 key format; ~224-bit security |
| Encryption subkey | X448 | v6 key format; inherent AES-256 key wrap |
| Symmetric encryption | AES-256 | 256-bit key |
| AEAD | OCB (primary), GCM (secondary) | SEIPDv2; OCB mandatory per RFC 9580 |
| Hash | SHA-512 | |
| S2K (key export) | Argon2id (512 MB / p=4 / ~3s) | Memory-hard |
| Compression | DEFLATE (read-only) | Enabled for reading compatibility; outgoing messages must not use compression |
| Random | SecRandomCopyBytes | Via getrandom crate on Apple platforms |
Interoperability: Profile A output compatible with GnuPG 2.1+ and all PGP tools. Profile B output compatible with Sequoia 2.0+, OpenPGP.js 6.0+, GopenPGP 3.0+, Bouncy Castle 1.82+. The App reads v4 keys, v6 keys, SEIPDv1, SEIPDv2 (OCB/GCM), Iterated+Salted S2K, and Argon2id S2K. Compression (deflate) read-only for compatibility; outgoing messages never compressed. Bzip2 excluded (extra C dependency).
Generate (Profile A: Ed25519+X25519 v4 / Profile B: Ed448+X448 v6)
│
├──→ SE Wrap (P-256 self-ECDH + HKDF + AES-GCM)
│ │
│ └──→ Store private-key material in Keychain (3 protected items per identity)
│
├──→ Store PGPKeyIdentity metadata in ProtectedData `key-metadata`
│ └──→ Opened after app-session authentication; no private-key material
│
├──→ Auto-generate revocation certificate
│ └──→ Prompt user to export separately
│
└──→ Prompt user to back up private key + share public key
Use (decrypt / sign):
Keychain retrieve → SE reconstruct (biometric auth) → HKDF → AES-GCM unseal
→ Perform PGP operation → Zeroize private key from memory
Export (backup):
Authenticate → User enters strong passphrase
→ Profile A: Iterated+Salted S2K protect → .asc file via Share Sheet
→ Profile B: Argon2id protect → .asc file via Share Sheet
Import (restore):
.asc file → User enters passphrase → S2K derive (detect mode automatically) → Recover key
→ Generate key-level revocation signature for imported key
→ Generate new SE wrapping key → SE wrap → Store in Keychain
Revocation:
Export ASCII-armored revocation signature → Distribute to contacts → They import → Key marked revoked
Deletion:
Double-confirm → Delete SE key from Keychain → Delete salt + sealed box
→ Delete protected key-metadata entry
→ Key permanently inaccessible
Metadata storage note: PGPKeyIdentity metadata is non-sensitive indexing data, but it now lives in the ProtectedData key-metadata domain so key-list loading happens only after app-session authentication opens protected app data. Legacy metadata rows may still exist in the dedicated metadata Keychain account (KeychainConstants.metadataAccount) or older default-account locations; those rows are migration/cleanup sources only and are read after app-session authentication, using the authenticated LAContext handoff when the default account requires it. Private-key blobs, salts, and sealed boxes remain in the protected private-key namespace.
Revocation storage/export note: CypherAir stores revocation signatures internally as binary OpenPGP signature packets. Export converts those bytes to ASCII armor on demand. Imported keys now receive key-level revocation export capability as part of import. Older imported keys that predate this support lazily backfill the binary revocation at export time, then immediately zeroize the temporarily unwrapped secret certificate bytes after use.
Selective revocation note: Subkey and User ID selective revocations are generated and exported on demand. They do not write back into PGPKeyIdentity.revocationCert, and they do not introduce an implicit persisted selective-revocation history alongside the key-level revocation slot.
Certificate-signature workflow note: Generated User ID certification signatures are exported artifacts. The workflow does not automatically insert them into a stored contact certificate, change Contact.isVerified, or introduce trust / web-of-trust policy semantics.
Profile-specific behavior:
- Generation: Profile A →
CipherSuite::Cv25519+Profile::RFC4880. Profile B →CipherSuite::Cv448+Profile::RFC9580. - Export: Profile A → Iterated+Salted S2K. Profile B → Argon2id S2K.
- Encryption format: Determined by recipient key version, not sender profile. See TDD Section 1.4.
The Secure Enclave supports only P-256. Private keys (Ed25519, X25519, Ed448, or X448) are protected via an indirect wrapping scheme. The wrapping scheme is identical for all key algorithms — the SE wraps raw private key bytes regardless of algorithm.
ProtectedData uses a separate app-data root-secret model and must not be
conflated with private-key bundle wrapping. The current ProtectedData v2 model
keeps the Keychain / SecAccessControl / authenticated LAContext gate, but
stores the root-secret Keychain payload as a Secure Enclave device-bound
envelope instead of raw root-secret bytes.
The device-binding key is a ProtectedData-only P-256 Secure Enclave key with
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly and .privateKeyUsage. It
must not include .userPresence, .biometryAny, or .devicePasscode, because
the user authentication prompt remains the existing app-session Keychain gate.
The SE layer is a silent second factor that makes copied Keychain payloads and
ProtectedData files unusable away from the original device. If the SE
device-binding key is missing or unusable, ProtectedData must fail closed and
require framework recovery/reset; there is no production fallback that opens v2
ProtectedData without the SE factor.
The v2 root-secret envelope is a binary-plist CAPDSEV2 payload with
algorithmID = p256-ecdh-hkdf-sha256-aes-gcm-v1. It uses a normal
software-ephemeral P-256 ECDH exchange with the persistent ProtectedData SE
public key; it must not reuse the existing private-key self-ECDH wrapping
scheme as its security design. Its HKDF sharedInfo and AES-GCM AAD bind the
AAD version plus hashes of both persistent SE and ephemeral public keys. After
v2 migration succeeds, registry state plus
a ThisDeviceOnly Keychain format-floor marker must make later v1 raw
root-secret payloads fail closed as downgrade/corruption.
ProtectedData domain payloads must open only after the app privacy gate has
produced an authenticated LAContext or an already-authorized ProtectedData
session. The post-unlock domain coordinator may reuse that context for
registered committed domains, but it must skip pending-mutation, missing
context, and no-domain states without fetching the root secret or starting a
second interactive prompt.
- Generate
SecureEnclave.P256.KeyAgreement.PrivateKey()with access control flags matching the current auth mode. - Self-ECDH: compute shared secret between SE private key and its own public key. This computation happens inside the SE hardware.
- Derive AES-256 key:
HKDF<SHA256>.deriveKey(inputKeyMaterial: sharedSecret, salt: randomSalt, info: infoString, outputByteCount: 32)whereinfoString = "CypherAir-SE-Wrap-v1:" + hexFingerprint. - Seal:
AES.GCM.seal(privateKeyBytes, using: symmetricKey). - Store three Keychain items: SE key
dataRepresentation, random salt, AES-GCM sealed box. Confirm all three writes succeed. - Only after successful storage: zeroize the raw private key bytes and symmetric key from memory.
HKDF info string: The info parameter includes a version prefix (v1) and the key's hex fingerprint to provide domain separation across different keys and future wrapping scheme versions. This exact string must be constructed identically in SecureEnclaveManager.swift. Any mismatch will produce a different derived key and make existing wrapped keys permanently inaccessible.
Ordering rationale (steps 5–6): Storage is performed before zeroization. If storage fails or the process crashes before step 5 completes, the raw key bytes are still in memory and the operation can be retried. If zeroization happened first and storage then failed, the key would be permanently lost.
- Retrieve SE key blob, salt, and sealed box from Keychain.
- Reconstruct SE key from
dataRepresentation— this triggers device authentication (Face ID / Touch ID, with or without passcode fallback depending on auth mode). - Re-derive symmetric key: self-ECDH (inside SE) + HKDF with stored salt and same info string (
"CypherAir-SE-Wrap-v1:" + hexFingerprint). - Open sealed box → raw private key bytes in application memory.
- Perform the PGP operation.
- Zeroize the private key bytes and symmetric key immediately.
- Keychain data extraction without the SE hardware yields an encrypted blob that cannot be decrypted.
- The SE key's
dataRepresentationis bound to the SoC UID (fused at manufacturing, never exposed to software). - The raw private key exists in application memory briefly during use. This is an inherent tradeoff of the P-256-only SE constraint.
- SE ECDH latency: ~2–5ms. Imperceptible to users.
let accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage, .biometryAny, .or, .devicePasscode],
&error
)Face ID / Touch ID with device passcode fallback. Equivalent to LAPolicy.deviceOwnerAuthentication. Suitable for most users.
let accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage, .biometryAny],
&error
)Face ID / Touch ID only. No passcode fallback. If biometrics are unavailable (sensor damaged, face obscured, biometry locked out after 5 failures), all private-key operations (decrypt, sign, export) are blocked until biometric auth is restored.
Inspired by Apple's Stolen Device Protection: prevents a thief who has both the device and the passcode from accessing encrypted data.
When the user changes mode in Settings:
- Display warning. If switching to High Security and no backup exists, show a stronger warning requiring explicit acknowledgment.
- Record the rewrap target and phase in the post-unlock
private-key-control.recoveryJournal. - Authenticate under the current mode (proves the user has authority to change).
- For each private key:
a. Unwrap using the current SE key.
b. Generate a new SE key with the new access control flags.
c. Re-wrap the private key with the new SE key.
d. Store the new Keychain items under temporary key names (e.g.,
com.cypherair.v1.pending-se-key.<fingerprint>). e. Zeroize the raw key bytes from memory. - Verify all new items are successfully stored.
- Delete the old Keychain items (original
com.cypherair.v1.se-key.<fingerprint>etc.). - Rename the temporary items to their permanent key names.
- Persist the new mode to
private-key-control.settings.authMode. - Clear the
private-key-control.recoveryJournalrewrap entry.
Atomicity: Old Keychain items are kept intact until ALL new items are confirmed stored (step 5). If any step fails before step 6, the original keys are unaffected — delete the temporary items and report the error.
Crash recovery: After app-session authentication opens private-key-control, check the rewrap recovery journal. If an entry is present:
- If the permanent bundle is complete and temporary items exist, the permanent bundle is treated as authoritative. Delete the temporary items and keep the original mode.
- If the permanent bundle is partial but the temporary bundle is complete, the temporary bundle is treated as authoritative. Delete the residual permanent items, then promote the temporary bundle to permanent names.
- If the permanent bundle is missing and the temporary bundle is complete, promote the temporary bundle to permanent names.
- If neither namespace contains a complete three-item bundle, recovery is unrecoverable. Clear the journal entry, surface a generic post-unlock warning, and require the user to restore from backup if private-key operations fail.
- If deletion or promotion fails for a retryable reason (for example, transient Keychain write/delete failure), preserve the recovery journal so the app retries recovery after the next successful unlock.
- Recovery diagnostics are surfaced through the app's existing post-unlock warning path and must remain generic — never include fingerprints or other key identifiers.
- Persist the new auth mode only after a full successful promotion of complete pending bundles. Cleaning stale pending items alone must not change auth mode.
- This ensures the app prefers a complete bundle over a partial one and avoids silently finalizing an inconsistent state.
Legacy UserDefaults keys such as com.cypherair.internal.rewrapInProgress and com.cypherair.preference.authMode are migration sources only. Verified Phase 5 migration moves them into private-key-control and removes the legacy keys.
| Mode | LAPolicy | Fallback Button |
|---|---|---|
| Standard | .deviceOwnerAuthentication |
Passcode shown |
| High Security | .deviceOwnerAuthenticationWithBiometrics |
context.localizedFallbackTitle = "" (hidden) |
Protected app data is a separate security domain for CypherAir-owned local state outside private-key material. It must not be conflated with the Secure Enclave wrapping path that protects OpenPGP secret key bytes.
Current protected app-data scope:
private-key-controlstores the private-key control source of truth:settings.authModeplus the rewrap / modify-expiryrecoveryJournal.key-metadatastoresPGPKeyIdentitypayloads after app unlock. Legacy metadata Keychain rows are migration and cleanup sources only.protected-settingsstores protected settings. Schema v2 preservesclipboardNoticeand adds grace period, onboarding completion, color theme, encrypt-to-self, and guided tutorial completion.contactsstores the PR4 compatibility Contacts snapshot after app unlock. LegacyDocuments/contactsandDocuments/contacts.quarantineare migration and cleanup sources only after protected cutover.protected-framework-sentinelis a framework-owned sentinel domain with a schema/purpose marker only. It contains no user data, telemetry, contacts, or UI state.
Current non-goals and pending surfaces:
- Permanent and pending SE-wrapped private-key bundle rows remain in the existing private-key material domain.
appSessionAuthenticationPolicyremains an early-readable boot-authentication setting.- Self-test reports are short-lived export-only data held in memory until explicit user export, reset, or app exit; legacy
Documents/self-test/content is cleanup-only on startup and local-data reset. Decrypted, streaming, export handoff, and tutorial artifacts are Phase 7ephemeral-with-cleanupstate: CypherAir-owned temporary files/directories use verified complete file protection where created by the app, per-operation or owner cleanup, startup cleanup, and Reset All Local Data cleanup. Phase 7 is complete. Phase 7 PR 2 moved the targeted ordinary settings intoprotected-settings; legacy ordinaryUserDefaultskeys are cleanup-only after verified schema v2 readback. Contacts PR4 moved Contacts into the protectedcontactsdomain; legacy Contacts directories are migration/quarantine cleanup sources and are never the source of truth after protected cutover.
The exhaustive row-level local-data classification, current status, and migration-readiness table lives in PERSISTED_STATE_INVENTORY. This security model records the rules and invariants; the inventory records each surface.
UserDefaults is allowed only for documented boot, test, tutorial, and legacy cleanup exceptions. Personal or sensitive app data must not be newly introduced there; post-auth settings use protected-settings unless they are explicit boot-authentication exceptions.
Protected app-data authorization uses AppSessionAuthenticationPolicy, not private-key AuthenticationMode. AppSessionOrchestrator owns launch/resume privacy authentication and the grace window. When app authentication succeeds, it can hand the authenticated LAContext to ProtectedDataSessionCoordinator, which reads the shared app-data root secret through Keychain with kSecUseAuthenticationContext. That same authenticated handoff is reused by post-unlock domain openers so committed registered domains can open without a second Face ID / Touch ID prompt.
ProtectedOrdinarySettingsCoordinator owns ordinary-settings availability after Phase 7 PR 2. It loads grace period, onboarding completion, color theme, encrypt-to-self, and guided tutorial completion from protected-settings schema v2 only after app privacy authentication and an unlocked protected-settings handoff. Existing schema v1 payloads are upgraded through an explicit compatibility path using legacy ordinary settings as a migration source; schema v2 payloads are strict, so missing or corrupt ordinary settings enter protected-settings recovery instead of resetting to defaults. If the setting snapshot is unavailable, the resume grace window fails closed to immediate authentication, startup/onboarding routing waits for a loaded snapshot, and encryption does not silently use the app-default encrypt-to-self value for real work.
The shared root secret is not stored as raw bytes in the current format. Keychain stores a v2 CAPDSEV2 envelope that must also unwrap through the ProtectedData-only Secure Enclave device-binding key described in Section 3. The raw root secret is used only to derive the wrapping root key and is immediately zeroized. Each protected domain has its own random domain master key, persisted only as a wrapped-DMK record under the derived wrapping root key. Unwrapped domain keys and decrypted payloads are session-local and must be cleared on relock.
ProtectedDataRegistry is the only authority for committed protected-domain membership and pending create/delete work. Cold start may read the registry and per-domain bootstrap metadata before app authentication, but it must not retrieve the root secret, unwrap any DMK, open domain payloads, or infer committed membership by directory enumeration. Invalid registry state enters framework recovery. Domain corruption enters the domain's recovery state; no protected domain may silently reset unreadable state to empty data.
Relock is fail-closed. ProtectedDataSessionCoordinator blocks new protected-domain access, fans out to all registered ProtectedDataRelockParticipants, zeroizes the wrapping root key, clears unwrapped DMKs, and returns to sessionLocked only if teardown succeeds. The ordinary-settings coordinator also clears its loaded snapshot on relock/content clear. Any relock participant failure latches runtime-only restartRequired, blocking further protected-domain access until process restart.
ProtectedData files live under Application Support/ProtectedData/. Registry files, bootstrap metadata, scratch writes, wrapped-DMK files, and committed domain files use explicit file-protection verification where the platform supports it. Storage outside the app-owned container is not an allowed fallback for protected-domain files.
The guided tutorial is allowed to run real app services and real OpenPGP operations only inside an isolated tutorial dependency graph. It must not read or mutate the user's real keys, contacts, settings, files, exports, or private-key security assets.
Tutorial isolation boundaries:
TutorialSandboxContaineruses the fixedcom.cypherair.tutorial.sandboxUserDefaultssuite and a temporary contacts directory with verified complete file protection instead of the app's real preferences andDocuments/contactsstorage. The product flow owns a single active tutorial sandbox at a time; container creation and current tutorial cleanup clear the fixed suite and directory. Startup and Reset All Local Data also remove legacy orphanedcom.cypherair.tutorial.<UUID>suites.- Tutorial key management, encryption, decryption, signing, certificate, QR, and self-test services are constructed against tutorial-local storage and the same Rust engine API shape used by the real app.
- Tutorial private-key protection uses mock Secure Enclave and mock Keychain primitives behind a real
AuthenticationManagerinstance, so auth-mode behavior is exercised without touching real Secure Enclave-wrapped private keys or real Keychain rows. OutputInterceptionPolicyand page-level configuration must block or intercept real file import/export, clipboard writes, share-sheet export, URL handoff, app icon changes, onboarding management actions, and other real-workspace side effects.- Tutorial completion state is the only tutorial fact that persists across app restarts. Tutorial keys, contacts, messages, settings, and unfinished module progress are ephemeral and are cleaned up when the tutorial is reset or finished.
Changes to tutorial isolation, output interception, or tutorial security simulation must be reviewed with the same care as other auth and local-data boundaries. A tutorial regression must never weaken the app's zero-network, minimal-permission, no-secret-logging, or real-workspace isolation guarantees.
Used only for private key export (backup) and for importing/unlocking passphrase-protected private key files. Not used for routine message decryption or signing — those operations use the SE-unwrapped private key directly.
Not used by Profile A. Profile A uses Iterated+Salted S2K (mode 3).
| Parameter | Value | RFC 9580 Encoding |
|---|---|---|
| Memory | 512 MB (524,288 KiB) | encoded_m = 19 (2^19 KiB) |
| Parallelism | 4 lanes | p = 4 |
| Time | Fixed at 3 passes (~3s target on contemporary hardware) | t = 3 |
Before Argon2id derivation when importing or unlocking a passphrase-protected private key file (this guard does NOT apply to routine message decryption):
- Parse the S2K specifier from the key file.
- Calculate required memory:
2^encoded_mKiB. - Query
os_proc_available_memory(). - If required > 75% of available memory: refuse with error message: "This key uses memory-intensive protection that exceeds this device's capacity."
- Return a user-facing refusal error before Argon2id derivation begins.
This prevents iOS Jetsam from killing the app. The 75% threshold provides a safety margin.
MIE is built right into Apple hardware and software in all models of iPhone 17 and iPhone Air (A19/A19 Pro). It provides hardware-level defense against buffer overflows and use-after-free in all C/C++ code, including vendored OpenSSL. The system allocator assigns 4-bit tags to heap allocations. Every memory access is checked by hardware in real time. Tag mismatch = immediate process termination.
Enhanced Security is enabled via Signing & Capabilities → Add Capability → Enhanced Security → enable Hardware Memory Tagging. When this capability is added, Xcode writes the required entitlement keys into CypherAir.entitlements:
com.apple.security.hardened-process→truecom.apple.security.hardened-process.enhanced-security-version→1com.apple.security.hardened-process.hardened-heap→truecom.apple.security.hardened-process.platform-restrictions→2com.apple.security.hardened-process.dyld-ro→truecom.apple.security.hardened-process.checked-allocations→true(Hardware Memory Tagging)com.apple.security.hardened-process.checked-allocations.enable-pure-data→truecom.apple.security.hardened-process.checked-allocations.no-tagged-receive→true
These entitlement keys must be committed to source control. Xcode reads the .entitlements file to determine which protections are enabled. Removing the keys disables the corresponding protections.
Additionally, verify ENABLE_ENHANCED_SECURITY = YES in both Debug and Release build settings in project.pbxproj.
- Xcode diagnostics: Enable Hardware Memory Tagging in Scheme → Run → Diagnostics. Run full test suite on A19 device. Any tag mismatch surfaces as a crash with exact location.
- Production: Tag mismatches terminate the process immediately. This is the desired behavior — it converts silent corruption into a detectable, non-exploitable crash.
The openssl-src crate compiles OpenSSL from C source. Any undiscovered buffer overflow or use-after-free in OpenSSL will cause an immediate crash under MIE. This is the desired behavior — it converts silent corruption into a detectable, non-exploitable crash. Test all Sequoia + OpenSSL code paths (AES-256, SHA-512, Ed25519, X25519, Ed448, X448, Argon2id) under Hardware Memory Tagging diagnostics.
| Device | MIE Behavior |
|---|---|
| All models of iPhone 17 and iPhone Air (A19/A19 Pro) | Full hardware memory tagging active |
| Older devices (A15–A18) | Software-only typed allocator. No hardware tagging. |
The Enhanced Security capability is additive. It never breaks compatibility with older devices.
Scope: Affects passphrase-based flows that originate in Swift String, specifically private key import/export and password-message encrypt/decrypt operations. It does not affect routine recipient-key decryption or signing, which use SE-unwrapped key bytes (Data) that are properly zeroized.
Issue: Swift String is a value type with copy-on-write semantics. There is no supported API to overwrite a String's internal buffer in place. When the user enters a passphrase for key export (S2K protection), key import (S2K unlock), or password-message encrypt/decrypt (SKESK), the passphrase exists as a String in memory until ARC deallocates it. The exact lifetime depends on the Swift runtime and is not deterministic.
Why this is not fixed:
- SwiftUI constraint:
SecureField— the only system-provided secure text input — binds toString. There is noData-backed alternative. - FFI boundary: UniFFI transfers
Stringby copying throughRustBuffer. Even if the Swift side could zeroize its copy, the Rust side receives an independent copy (which Sequoia consumes and the Rustzeroizecrate handles on its side). - Platform-wide pattern: No shipping iOS app (including Apple's own Keychain prompts) can zeroize
Stringpassphrases. This is an accepted platform limitation.
Mitigations:
- Short lifetime: The passphrase
Stringis only alive for the duration of the active import/export or password-message call. It is not stored in any persistent state, UserDefaults, or Keychain. - Rust-side zeroize: The
zeroizecrate ensures the Rust copy of the passphrase is overwritten after use. - iOS memory protections: ASLR, sandboxing, and MIE (on A19+ devices) make memory scanning attacks significantly harder.
- Immediate Rust conversion: Password-message APIs convert the Swift
Stringinto SequoiaPasswordat the FFI boundary so the Rust-side representation is encrypted in memory and only decrypted on demand.
Rejected alternatives:
UnsafeMutableBufferPointer<UInt8>with manual zeroing: Would require forkingSecureFieldor building a custom UIKit text field, bypassing system-provided secure input. The security loss from a custom input field (no system-level screen recording protection, no secure text entry mode) would outweigh the benefit of zeroizable memory.Data-based passphrase: UniFFI does not supportData↔Stringconversion at the FFI boundary without an intermediateStringallocation, negating the benefit.
The following files and functions are security-critical. Claude Code must stop and describe proposed changes before editing them. Do not make autonomous modifications.
| File | Reason |
|---|---|
Sources/Security/SecureEnclaveManager.swift |
SE wrapping/unwrapping logic. Error = keys lost or insecure. |
Sources/Security/KeychainManager.swift |
Access control flags. Wrong flags = wrong auth behavior. |
Sources/Security/AuthenticationManager.swift |
Mode switching re-wrap. Error = keys permanently lost. |
Sources/Security/ProtectedData/ |
App-data root-secret authorization, SE device-binding envelope, domain master-key wrapping, reset semantics. Error = protected app data lost or opened under the wrong gate. |
Sources/Security/MemoryZeroingUtility.swift |
Removing a zeroize call = key material leaks. |
Sources/Extensions/Data+Zeroing.swift |
Contains @_optimize(none) zeroing barrier. Weakening = compiler may eliminate all memory zeroing app-wide. |
Sources/Services/DecryptionService.swift |
Phase 1/Phase 2 auth boundary. Skipping Phase 2 auth check = biometric bypass. |
Sources/Services/QRService.swift |
Parses untrusted external input (cypherair:// URLs). Bugs here may trigger Sequoia parser on malicious data. |
pgp-mobile/src/decrypt.rs |
AEAD hard-fail enforcement. Weakening = plaintext leaks. |
pgp-mobile/src/streaming.rs |
Streaming file encrypt/decrypt with buffer zeroing. Error in temp file handling = plaintext leaks to disk. |
pgp-mobile/src/error.rs |
PgpError enum. Must stay 1:1 with Swift. |
Sources/Services/DiskSpaceChecker.swift |
Disk space validation threshold. Wrong threshold = Jetsam termination during file operations. |
CypherAir.entitlements |
MIE, Enhanced Security entitlements. |
CypherAir-Info.plist |
Only NSFaceIDUsageDescription permitted. No other usage descriptions. |
- Any function that calls
SecAccessControlCreateWithFlags - Any function that calls
SecureEnclave.P256.KeyAgreement.PrivateKey() - Any function that calls
AES.GCM.seal()orAES.GCM.open()on key material - Any function that calls
HKDF<SHA256>.deriveKey() - Any function that writes to or deletes from Keychain
- The
os_proc_available_memory()guard in Argon2id handling - Any Rust function marked
pubinpgp-mobile/src/lib.rs - URL parsing logic in
QRServicethat handlescypherair://scheme input - Profile/CipherSuite selection in key generation
Every change to a file listed above must include:
- Positive test: The operation succeeds with correct inputs and proper authentication.
- Negative test: The operation fails gracefully with wrong inputs (wrong key, wrong passphrase, tampered data, unavailable biometrics).
- Round-trip test: For crypto operations — encrypt then decrypt, sign then verify, wrap then unwrap.
- No-leak test: For memory-sensitive changes — verify that sensitive data is zeroized after use (inspect with Xcode Memory Graph Debugger or Instruments).