Obscura
Obscura
Secure Photo Vault
← Back

Security Architecture

A technical description of Obscura's encryption, key management, and Hidden Vaults design.

Version 1.0  ·  April 2026

This document describes the security architecture of Obscura, an iOS photo vault. It is written for readers with a working knowledge of applied cryptography: developers, security researchers, and informed users who want to understand exactly how their data is protected and, equally important, where the limits of that protection lie.

Where we can be precise about cryptographic design, we are. Where honest limitations exist (and they always do), they are stated explicitly rather than glossed over. This document describes the intended behaviour of the production iOS application as implemented at the time of publication.

On the level of detail in this paper

This paper documents Obscura's cryptographic design, key hierarchy, threat model, and the rationale behind the architectural choices, at a level intended to enable independent review of correctness. It does not publish a complete forensic blueprint of on-disk artefacts: certain low-level details (exact byte offsets, internal mechanism timings, full file-format specifications) are deliberately kept general, in line with the disclosure conventions of comparable products. Researchers requiring deeper format detail for verification work can request it via support@orlio.io.

Critical trust assumption

The slot-indistinguishability property described in §5 (marketed as Hidden Vaults) depends on the user's iCloud Keychain remaining secret. An attacker who can read the user's iCloud Keychain (directly, through a compromised Apple ID, or via another device signed in to the same Apple ID) can derive the decoy-slot credential and distinguish decoy slots from user-created vaults. This is foundational, not a footnote: see §5.4 and §11.5.

1. Introduction & Threat Model

1.1 Scope

This paper describes the cryptographic primitives, key hierarchy, storage layout, slot-indistinguishability design (marketed as the Hidden Vaults feature), media handling, and export format used by Obscura. It does not describe user interface, application logic, or subscription mechanics.

1.2 Threat Model

Obscura is designed to protect stored media against the following attacker capabilities.

Physical access to a locked device. An attacker has obtained the device while it is locked. They must not be able to extract, decrypt, or enumerate vault contents.

Physical access to an unlocked device with a locked vault. The operating system is unlocked but the Obscura vault is not. The attacker must not be able to bypass authentication, and must not be able to infer which stored data belongs to a real vault versus a decoy.

Offline analysis of storage. The attacker holds a full image of the device's filesystem, or of an application data backup. Stored files must remain encrypted. Slot-file analysis (sizes, timestamps, naming patterns) must not reveal which slots hold real vaults; the bounds of this property, including the residual signal from the shared media pool, are set out in §5.6.

Disclosure of one password. The attacker knows one of the user's vault passwords. The slot file layout is designed so that, examined in isolation, no slot file can be distinguished from any other. This provides ambiguity about whether other vaults exist, but only within specific bounds. Section 5 describes the precise scope of this property and §5.6 the cases in which it does not hold.

1.3 Trust Assumptions

  • Apple CryptoKit correctly implements AES-256-GCM.
  • CommonCrypto correctly implements PBKDF2-HMAC-SHA256.
  • SQLCipher correctly encrypts database pages under a supplied raw key.
  • The iOS Keychain with kSecAttrAccessibleWhenUnlocked and kSecAttrAccessibleWhenUnlockedThisDeviceOnly provides the advertised protection.
  • memset_s is not elided by the compiler.

Section 11 lists attacks that fall outside of Obscura's threat model.

1.4 Non-Guarantees

This paper describes design intent and security properties under specific adversary models. To prevent misinterpretation, the following are explicitly not claimed:

  • Coercion-proof deniability. The slot-indistinguishability property in §5 is intended to weaken offline enumeration of vaults from filesystem evidence. It is not a guarantee against an adversary who can observe the user's behaviour, read the user's iCloud Keychain, or capture the device while a vault is unlocked. See §5.6 and §11.
  • Secrecy of the existence of stored data. The shared media pool (§4.4) does not hide the aggregate count of stored items from a forensic adversary; that residual signal is described in §5.6.
  • Resistance to runtime compromise. The design assumes the iOS sandbox is intact. If an attacker achieves code execution inside Obscura's process while a vault is unlocked, key material is recoverable; see §11.1.
  • Forensic resistance under filesystem access. An adversary with full filesystem access and forensic tooling is a distinct threat model addressed in §11. The disclosure level of this paper deliberately favours auditability over making forensic reconstruction harder.
  • Universal resistance to physical-presence attacks. Biometric unlock has known limits against attackers with physical access to the user; see §11.2.
  • Closure of every side-channel. Some properties (slot-file size signatures, fixed-sweep authentication timing) are probabilistic or implementation-sensitive; they are intended to reduce the dominant leakage paths, not to eliminate every possible side-channel.

2. Cryptographic Primitives

2.1 Symmetric Encryption

All data encryption in Obscura uses AES-256-GCM via Apple CryptoKit's AES.GCM API. Each encryption produces a fresh 96-bit (12-byte) random nonce and a 128-bit (16-byte) authentication tag. The tag is verified on every decryption; a failure aborts the operation and surfaces as an error.

Why GCM. GCM is an authenticated encryption mode: it produces a tag that lets the decryptor detect any tampering with the ciphertext, the nonce, or the associated header. A modified byte on disk causes decryption to fail loudly instead of returning subtly wrong plaintext. This is what makes "decrypted" equivalent to "unmodified since encryption." No other symmetric ciphers are used. Obscura does not support CBC, CTR without authentication, ChaCha20, or any unauthenticated mode.

Single-shot ciphertexts use CryptoKit's combined representation: nonce (12 B) ‖ ciphertext ‖ tag (16 B).

2.2 Key Derivation

Password-derived keys use PBKDF2-HMAC-SHA256 from Apple's CommonCrypto framework.

PurposeIterations
Vault password → Key Encryption Key (KEK)600,000
Folder password → Folder KEK100,000
Export password → Export KEK600,000

Why 600,000 iterations. PBKDF2 converts a short password into a key by forcing the same costly computation on the attacker as on the legitimate user. 600,000 iterations is tuned so that a correct password takes roughly one second to verify on a current-generation iPhone, which is imperceptible for a legitimate unlock but multiplies the cost of an offline brute-force attempt by the same factor. This follows OWASP's current guidance for PBKDF2-HMAC-SHA256.

Why the folder count is lower. A folder password protects a second encryption layer, wrapping content that is already protected by the vault's 600,000-iteration Master Encryption Key (MEK). A folder password is a second factor, not a first line of defence, so a lower iteration count is an acceptable trade between security margin and folder-unlock latency.

Iteration counts are embedded in each vault file header, which keeps the design forward compatible: raising the count in a future release leaves existing files decryptable with the count they were created with.

2.3 Salts and Nonces

  • Salts are 32 bytes, produced by SecRandomCopyBytes. A new salt is generated for every slot file and every folder. Salts are never reused.
  • Single-shot nonces are 96-bit values produced by CryptoKit's AES.GCM.Nonce(). A new nonce is generated for every encryption.
  • Chunked-format nonces (Section 6) are composed from a per-file 4-byte random prefix and an 8-byte big-endian chunk index, yielding unique nonces within a file by construction. The 32-bit random prefix makes cross-file nonce reuse a negligible event.

2.4 Secure Memory

All keys (KEKs, the MEK, folder keys, and the thumbnail cache key) are held in a SecureMemory container whose deinitialiser overwrites the bytes using memset_s() before release. memset_s is specified by C11 Annex K not to be elided by the optimiser, so zeroing is intended to hold even under aggressive dead-store elimination, subject to the implementation conforming to that specification. Keys exist in RAM only while needed and are cleared when a vault is locked, when the app is backgrounded past the inactivity timeout, and when the process terminates.

Why this matters. A plain free() leaves the key bytes in the heap page that returns to the allocator's free pool. If another allocation reuses that page, the bytes remain recoverable until they are overwritten by other data. Explicit zeroing shortens the window in which an attacker who later reads the process's memory, or a core dump, can still recover a discarded key.

2.5 Database Encryption

Vault databases are encrypted with SQLCipher in raw-key mode, using the MEK directly as the page-encryption key. SQLCipher encrypts pages in place; no plaintext .db file exists on disk at any point. The underlying cipher configuration is SQLCipher's default for the linked build.

3. Three-Tier Key Hierarchy

Obscura's key hierarchy has three tiers:

Password  ──PBKDF2──▶  KEK  ──unwraps──▶  MEK  ──encrypts──▶  Data

Each tier has a specific role and a distinct lifetime.

3.1 KEK: Key Encryption Key

The Key Encryption Key is derived on demand from the user's password and the per-slot salt:

KEK = PBKDF2-HMAC-SHA256(password, salt, 600_000, 32 bytes)

The KEK is never written to disk. It exists in memory only long enough to unwrap the MEK, after which it is zeroed.

3.2 MEK: Master Encryption Key

The Master Encryption Key is the root symmetric key for a vault. It is 32 cryptographically random bytes, produced by SecRandomCopyBytes at vault creation, and does not change during the lifetime of the vault.

The MEK is wrapped with the KEK using AES-256-GCM:

wrapped_MEK = nonce(12B) ‖ AES-GCM-Seal(KEK, MEK) ‖ tag(16B)   // 60 bytes total

The wrapped MEK is stored in the V3 header of the vault's .db.enc file (Section 4).

Why a separate MEK. A naive design derives the data-encryption key directly from the password. That forces every password change to re-encrypt every file, which is untenable for a vault with gigabytes of media. Separating the data key (MEK) from the password-derived key (KEK) means a password change re-wraps one 60-byte blob instead of rewriting the vault. It also decouples the entropy of the data key from the user's password strength: the MEK is always 256 bits of cryptographic randomness, regardless of how weak or strong the password happens to be.

3.3 Password Change

Because the MEK is wrapped rather than derived from the password, a password change re-wraps the MEK but does not re-encrypt any data:

  1. Derive KEK_old from the old password and the stored salt.
  2. Unwrap: MEK = AES-GCM-Open(KEK_old, wrapped_MEK).
  3. Generate a new 32-byte salt.
  4. Derive KEK_new from the new password and the new salt.
  5. Wrap: wrapped_MEK' = AES-GCM-Seal(KEK_new, MEK).
  6. Overwrite the V3 header with the new salt and new wrapped MEK.

The database pages and media files are untouched. A password change takes the time of two PBKDF2 derivations, roughly 1.5 seconds on a modern iPhone, regardless of vault size. This property is deliberate: it enables frequent password rotation without the cost of re-encrypting gigabytes of media.

3.4 Key Lifecycle

EventEffect on keys
Vault creationMEK generated; salt generated; MEK wrapped with KEK.
Vault unlockKEK derived → MEK unwrapped → KEK zeroed.
Vault lockedMEK zeroed; all folder keys zeroed; thumbnail cache invalidated.
App backgroundedAll folder keys zeroed; thumbnail cache invalidated.
Inactivity timeoutVault locked (as above).
Password changeMEK re-wrapped; MEK value unchanged; data untouched.
Vault deletionSlot file overwritten with random data, then unlinked.

4. Storage Architecture

4.1 On-Disk Layout

Everything Obscura writes lives under the application's sandboxed Documents/Vault/ directory:

Documents/Vault/
├── data/
│   ├── {uuid}.db.enc        encrypted per-slot database (V3 format)
│   └── ...                  (at least 10 such files always exist; see §5)
└── media/
    ├── {uuid}.data.enc      encrypted media file
    └── {uuid}.thumb.enc     encrypted thumbnail

4.2 No Plaintext Metadata

There is no plaintext metadata file on disk. Everything Obscura needs to authenticate a vault (the per-slot salt, the wrapped MEK, the iteration count, and a verification structure) lives inside each .db.enc file's fixed-size header. The per-file header is the only thing an attacker sees before decrypting anything.

Why per-file headers. An external metadata index would give an attacker a single table describing all slots, including which are in use, making slot enumeration trivial. Embedding each slot's metadata inside its own file means every slot stands alone and every slot looks like every other slot. Add slot, remove slot, or migrate between devices, the file is self-contained.

4.3 The V3 Vault File Format

Each .db.enc file begins with a fixed-size header followed by a SQLCipher-encrypted database. The header carries everything needed to authenticate and unwrap the vault: a format version, a cipher-suite identifier, the KDF iteration count, the per-slot KEK salt, the wrapped MEK, an AES-GCM envelope holding the encrypted database size, and an integrity checksum over the preceding bytes.

The cipher-suite identifier and integrity checksum make the format forward-compatible. A future release that introduces a new cipher can increment the suite identifier, and older builds will fail cleanly rather than misinterpret data.

4.4 The Shared Media Pool

Media files are written to a shared pool: a flat directory of UUID-named files, with no directory or filename structure that links a media file to a specific vault. An attacker who mounts the application's filesystem sees a bag of encrypted blobs and cannot link any individual file to a particular vault without first decrypting the corresponding database. Aggregate inference from the total file count is a separate matter and is discussed in §5.6.

Every media file is encrypted under the MEK of the vault it belongs to. A file encrypted for one vault is unreadable under every other vault's key.

4.5 Database Padding

When a slot file is first written, it is padded with random bytes to a size chosen uniformly at random within a fixed range large enough to absorb typical vault contents. On every subsequent write, the file size is preserved: the slot is rebuilt inside the same overall footprint, shrinking the padding to make room if the database grew. Only when the database outgrows the existing footprint does the slot pick a new random total size, and only the padding content (the random bytes) is regenerated on every write.

Why pin the size after first write. Re-randomising the size on every persist would leak information: an attacker with before-and-after snapshots of the filesystem could observe which slot files changed size and conclude that those slots are the ones the user is actually using. Decoy slots are never repackaged, so their sizes stay constant. By pinning each slot's size after first write, the design intends real and decoy slots to present the same size-over-time signature.

Why pad at all. Without padding, an empty vault would be a few kilobytes and a populated one would be megabytes, making it trivial to tell which slots are in use. With padding, an empty slot and a slot holding a meaningful number of items present similarly sized files on disk.

5. Slot Indistinguishability Under Offline Adversary

This section describes the property marketed as the Hidden Vaults feature. Obscura's claim is best characterised in narrower technical terms: it is resistance to offline slot enumeration. The ten slot files in data/ are intended to be indistinguishable from one another under the offline-adversary model defined in §1.2, so a reader of those files who is constrained to that model is not expected to be able to determine which slots are real vaults and which are decoys. This produces ambiguity, not certainty: it weakens an attacker's ability to enumerate the user's vaults from filesystem evidence, but it does not turn into a coercion-proof guarantee, and it has well-defined bounds. The claim applies to the slot files themselves, not to other artifacts on disk; in particular, it does not hide the aggregate media count in media/. See §5.6.

Load-bearing assumption

This property does not hold against an attacker who can read the user's iCloud Keychain (directly or via another device signed in to the same Apple ID). Such an attacker can derive the decoy-slot credential and distinguish decoy slots from user-created vaults. Reasoning in §5.4; the broader list of cases where the property does not apply is in §5.6.

5.1 The Ten-Slot Design

Obscura maintains at least ten slot files at all times. On first launch, if fewer than ten exist, the remaining slots are populated with fully valid vault files: each has its own random salt, its own wrapped MEK, its own SQLCipher database (empty but real), and its own random padding. These are referred to as decoy slots, but they are decoys only in the sense that the user has never set a password for them.

The critical property is that, under the offline-adversary model, a real slot and a decoy slot are intended to be indistinguishable in on-disk structure and encrypted representation: both are V3 headers followed by randomly padded SQLCipher databases, built by the same code paths, with random salts and independent wrapped MEKs.

Why ten. Ten provides enough compartments for typical personal use (work, family, private, etc.) while keeping the baseline disk footprint modest. The combined storage of all ten padded slots is negligible on a modern device but large enough that decoy slots contribute meaningful noise to size-based analysis. A smaller count would make guessing which slots are real easier; a much larger count would waste storage for most users.

5.2 Decoy Credentials

Every decoy slot on a given device is keyed under a single device-derived credential, sourced from a random secret stored in the iOS Keychain. Per-slot indistinguishability does not come from varying that credential, it comes from the per-slot random salt. Because PBKDF2 takes both credential and salt as input, each slot derives an entirely different KEK, and therefore an entirely different wrapped MEK.

From an attacker's perspective, the output is indistinguishable under offline inspection from a set of slots that used many different passwords, assuming the stated primitives hold.

Why one decoy credential instead of many. Generating a separate decoy credential per slot would require either storing each one (creating additional secrets to protect) or deriving them deterministically from the device secret (an additional derivation step with no security benefit). Because each slot's salt is random, the KEK and wrapped MEK are already independent across slots; varying the credential on top of that adds no indistinguishability that the salts do not already provide. The simpler design is equivalent and has fewer moving parts.

5.3 Fixed-Sweep Authentication

When the user enters a password, Obscura attempts to unwrap the MEK of every slot (including all decoy slots) and only after completing the full sweep returns the first match. There is no early return, and the decision is made from the collected result set, not from the point of first success.

Why. Early-return authentication would let an attacker who can measure the time to accept a password learn which slot index that password unlocks. Slot index carries information: a password that unlocks slot 0 is more likely to be a real vault if slot 0 is also the most-recently-written file. Iterating every slot regardless of outcome is intended to reduce timing leakage about which slot matched. This is not a strict constant-time implementation in the cryptographic sense. Microarchitectural variation (cache behaviour, branch prediction, scheduling) still exists, and the property is implementation-sensitive. The goal is to eliminate the dominant leak (the difference between stopping after one slot and stopping after ten), not to close every possible timing side-channel.

5.4 The Device Secret

The device secret is a 32-byte value stored in the iOS Keychain. New installations store it with the kSecAttrAccessibleWhenUnlocked access class, which means iCloud Keychain synchronises it across the user's devices that are signed in to the same Apple ID. This is a deliberate trade-off, and has two consequences the user should be aware of:

  • Decoy-slot passwords are consistent across the user's own devices, so vault identity survives an iPhone upgrade or side-by-side use on iPhone and iPad.
  • An attacker with access to the user's iCloud Keychain (either because the Apple ID is compromised, or because they have already unlocked another device signed in to the same Apple ID) can derive the decoy password and distinguish decoy slots from user-created vaults.

Legacy installations that predate the iCloud-synced scheme store the device secret with kSecAttrAccessibleWhenUnlockedThisDeviceOnly; on first launch of a newer build these are read and their value copied to the synced item for upgrade continuity.

5.5 Extended Slots Break Indistinguishability

Explicit limitation

If the user chooses to create more than 10 vaults, additional .db.enc files beyond index 9 are created. The presence of any 11th-or-later slot file is itself evidence that the user has more than 10 real vaults, which breaks the indistinguishability property. This is an intentional product choice (users who prioritise organisation over indistinguishability can opt in to it) but it is not a silent trade-off. The indistinguishability property only holds for users whose vault count is ten or fewer.

5.6 Bounds of the Indistinguishability Property

The indistinguishability claim is narrow: it covers slot files in data/ when examined in isolation. The following are explicitly outside that claim:

  • Media-pool count under filesystem access. This limitation applies only to an attacker who can read the raw filesystem (a forensic image, an unencrypted device backup, jailbroken access). From inside the Obscura UI there is no way to tell that other vaults exist: the unlocked decoy shows only its own photos and gives no indication of the storage state of any other slot. The leak is on disk, not in the app. Each file in media/ is bound by its AES-GCM tag to the MEK of one specific vault, so a decoy vault cannot truthfully claim media files that belong to other vaults, and cannot dishonestly claim them either (the gallery would error on open). An attacker with filesystem access who is shown a decoy password can compare the photo count visible inside the unlocked decoy with the total file count in media/ and infer the existence of other populated vaults from the difference. The slot-file layout is indistinguishable; the media pool is not. Populating decoy vaults with extra photos does not defeat this attack, because the attacker is doing arithmetic on file counts, not pattern-matching on individual files.
  • Runtime extraction. If an attacker captures the device while a vault is unlocked, the active MEK is in RAM and recoverable.
  • Behavioural evidence. Observation of how the user interacts with the app (over-the-shoulder viewing, screen recording, extended monitoring) can reveal vault use independently of any cryptographic analysis.
  • Backup leakage. If an unencrypted device backup captures the application's documents directory, extended-slot files leak in the backup in the same way they do on the device itself, and the media-pool count is preserved.
  • The app's existence. The presence of Obscura on a device is visible to anyone inspecting the home screen or App Store purchase history.

Within these bounds, slot-file indistinguishability is a real and meaningful property; outside them, it is not a substitute for keeping undisclosed passwords secret, and should not be treated as coercion-resistant.

6. Media Handling

Obscura's design goal for media handling is that no unencrypted media owned by the app ever touches persistent storage. Parts of the iOS photo and video APIs are disk-based by design, so this property requires careful handling of the few unavoidable temporary files.

6.1 Camera and Photo Library Capture

Photos arrive in memory and are encrypted in memory before any write to Obscura's storage. The output is the encrypted .data.enc file; there is no intermediate plaintext file under Obscura's control.

Videos pass through a temporary file: UIImagePickerController writes camera recordings to an iOS-managed temp URL, and PHAssetResourceManager streams Photo Library assets to a temp file the app controls. Obscura then encrypts to .data.enc and deletes the controllable temp file as soon as encryption completes; the iOS-managed file is reaped by iOS when the picker dismisses. The unencrypted window is bounded by the encryption time, and no Obscura-owned plaintext persists past it.

6.2 Chunked Media Format

Videos large enough to require streaming use a chunked AES-GCM format, identified by a fixed magic value at the start of the file. The security-relevant properties:

  • Per-chunk authentication. Each chunk is its own AES-GCM envelope. Tampering with one chunk fails its authentication tag without affecting the others; the blast radius of any modification is bounded to that chunk.
  • Deterministic, unique nonces. Each chunk's nonce is composed from a per-file random prefix and the chunk's index, yielding unique nonces within a file by construction. The random prefix makes cross-file nonce collisions a negligible event.
  • No nonce reuse on rewrite. The format is append-only during import; chunks are not edited in place, so the same nonce never encrypts two different plaintexts.

Older single-shot files (camera captures, pre-chunked imports) begin with the AES-GCM nonce directly; the chunked-format magic is reliably distinguishable from that nonce, so the format dispatch is unambiguous.

6.3 Playback

Videos are played via AVAssetResourceLoaderDelegate: AVPlayer requests byte ranges from a synthetic URL, and Obscura's delegate decrypts and returns just those bytes. Plaintext video exists only in RAM, only while playback is active, and only for the chunks the player is currently consuming. No plaintext video is written to disk at any point during playback.

6.4 Orphan Cleanup

At startup, Obscura sweeps its temporary directory for any UUID-named .mov or .mp4 files left behind by a crash during import, and deletes them. No unencrypted media from a prior session persists into the next.

7. Folder Password Protection

Folders optionally carry an additional password, independent of the vault password, and their contents are encrypted twice: once with the MEK at the vault layer, and once with a folder key at the folder layer. Both layers must be unwrapped to read the data.

7.1 Folder Key Construction

  1. A 32-byte random folder key is generated at folder-creation time.
  2. A folder KEK is derived from the folder password: FKEK = PBKDF2-HMAC-SHA256(password, salt, 100_000, 32).
  3. The folder key is wrapped with the folder KEK using AES-256-GCM.
  4. The folder record stores the random salt, the wrapped folder key, and a SHA-256 hash of the derived key (used as a fast-path verifier on unlock).

7.2 Double-Encryption Order

For a chunked media file marked as folder-protected (flag bit 0 set), each chunk is encrypted twice:

inner_ct = AES-GCM-Seal(MEK,        plaintext, nonce = prefix ‖ i)
outer_ct = AES-GCM-Seal(folderKey,  inner_ct,  nonce = prefix ‖ (chunkCount + i))

On read the order reverses: the folder key unwraps the outer layer, then the MEK unwraps the inner layer. Note that the outer and inner nonces are disjoint by construction: the outer chunk index is offset by the total chunk count, so no inner and outer nonce can ever collide within a single file.

Why double encryption. Folder passwords protect data that the user wants to keep compartmented even from themselves on an otherwise unlocked vault, or from someone who knows the vault password. Because the inner layer is encrypted with the MEK and the outer layer with a password the user holds separately, unlocking the vault is necessary but not sufficient to read folder-protected content. Someone who is shown a vault (for example, to prove nothing incriminating is in it) does not thereby reveal folder-protected files.

7.3 Folder Key Lifecycle

Unlocked folder keys are held only in memory in a per-session FolderPasswordManager. They are zeroed when the vault locks and also when the app is backgrounded, independently of whether the vault itself has locked. This is a deliberately stricter policy than the vault key: any time the app leaves the foreground, returning requires re-entering the folder password.

8. Export Format

Obscura can export a vault to a single password-protected .obscura file for off-device backup or transfer between devices. The format is self-contained: given the file and the export password, the recipient can reconstruct the vault on any device.

8.1 File Layout

An .obscura file begins with a fixed-size plaintext header followed by an AES-GCM-encrypted chunked payload. The header carries a format magic and version, the random KDF salt, the iteration count, an AES-GCM verification tag (described in §8.2), and a random nonce prefix used by the chunked payload.

8.2 Password Verification

The 16-byte verification tag is produced by AES-GCM-sealing the fixed ASCII plaintext "OBSCURA_EXPORT_V1" under the derived KEK. On import, before any payload bytes are read or decrypted, the KEK is derived and the fixed plaintext is re-sealed; a mismatching tag means the password is wrong, and the import is rejected immediately. This gives a constant-time correctness check without leaking information about the payload.

8.3 Payload

The payload is the same chunked AES-GCM construction described in §6.2: 1 MB chunks, deterministic nonces, per-chunk authentication. The first chunk carries a JSON manifest listing folder structure, photo metadata, the original vault's salt, and the original vault's wrapped MEK. Subsequent chunks carry the vault database and media files.

8.4 Credential Preservation

Because the exported manifest includes the source vault's salt and wrapped MEK, an imported vault keeps working with the original vault password: the user does not need to remember a separate "export password" afterwards. The export password protects only the export file itself; once imported, authentication reverts to the vault password.

9. Authentication & Session Lifecycle

9.1 Rate Limiting

After repeated failed password attempts, Obscura imposes a progressive delay before the next attempt is accepted. The first few attempts are accepted with no delay; subsequent attempts trigger increasingly long cooldowns up to several minutes between attempts.

Honest limitation

The failed-attempt counter lives in memory, not in persistent storage. An attacker who can force-terminate the app (or reboot the device) resets the counter. The primary defence against guessing is therefore not the lockout itself; it is the 600,000-iteration PBKDF2 derivation, which makes each attempt take on the order of a second on a modern iPhone regardless of lockout state.

9.2 Biometric Unlock

When a user enables Face ID or Touch ID, Obscura stores the vault password in the iOS Keychain under an access control that requires biometric authentication with the biometryCurrentSet policy. If the user enrols a new biometric (a new fingerprint, a re-registered face), iOS invalidates the Keychain item and the user is forced back to password authentication.

The stored item is the vault password itself, not a derived token. This is a deliberate simplification: Keychain with biometric ACL is already backed by the Secure Enclave and is not readable outside a successful biometric prompt, so wrapping the password further in application code would not add a meaningful protection.

9.3 Inactivity Auto-Lock

An unlocked vault locks automatically after a period of inactivity. The default timeout is five minutes; the user can choose from immediately, one minute, five minutes, or fifteen minutes. A background timer checks the last-activity timestamp every 30 seconds; any interactive vault operation updates the timestamp.

9.4 Backgrounding

When the app enters the background, the following happen unconditionally, independent of auto-lock:

  • All folder keys are zeroed.
  • The thumbnail cache is invalidated (both L1 memory and L2 disk).
  • A privacy screen hides vault content from the app-switcher snapshot.
  • Any saved-state file iOS may have written for state restoration is cleared.

10. Thumbnail Cache

Obscura caches thumbnails to keep the grid view fluid. Caches are a classic source of plaintext leakage, so Obscura's thumbnail cache is structured to make sure plaintext thumbnails never touch unprotected disk storage.

There are two tiers:

  • L1 memory cache (NSCache). Volatile; lost on process termination.
  • L2 encrypted disk cache. Thumbnails on disk are encrypted with AES-256-GCM under an ephemeral session key.

The ephemeral session key is 32 random bytes generated on vault unlock. It is not the MEK, and it is never written to disk. When the vault locks or the app backgrounds, the session key is zeroed and the L2 cache directory is emptied. Any cached thumbnail that somehow survived the cleanup would be undecryptable without the session key, which no longer exists.

Why a separate ephemeral key. Using the MEK for the thumbnail cache would tie cache lifetime to vault lifetime: an attacker who obtained the MEK by any means could decrypt every thumbnail ever cached on the device. With a per-session ephemeral key, a cache-directory exposure only exposes the current session's thumbnails, and only while that session is live. At lock, the key ceases to exist and any leftover cache files become unreadable, even to the app itself.

The L2 cache also lives in the application's temporary directory, which iOS itself may purge under storage pressure; this is consistent with the design goal that thumbnail state is recoverable, not authoritative.

11. Threat-Model Limits

Obscura is designed to protect stored media under a specific threat model. The following attacks are outside that model. This list is not exhaustive, but it covers the most relevant cases:

11.1 Runtime Compromise

If an attacker achieves code execution inside Obscura's process while a vault is unlocked, the MEK and any unlocked folder keys are in memory and extractable. This includes iOS zero-days, a jailbroken device, a malicious profile with entitlements, and arbitrary dylib injection. Obscura assumes the iOS sandbox is intact; if it isn't, no application-layer cryptography can compensate.

11.2 Biometric Unlock Risks

A biometric credential (Face ID or Touch ID) is a different kind of secret from a password: it is a physical property of the user, observable and in some circumstances reproducible from photographs or lifted prints. An attacker with physical access to a person can often obtain a biometric sample in ways they cannot extract a password. Users whose threat model includes this kind of physical access should disable biometric unlock and rely on the password only.

For times when biometric unlock is configured but the user wants to temporarily disable it, iOS itself supports an "emergency" button sequence that forces the next device unlock to require a passcode. While in that state, Face ID and Touch ID cannot unlock anything on the device, including Obscura.

11.3 Side Channels

Obscura does not defend against screen recording, screenshots taken by OS-level tools, shoulder surfing, electromagnetic emanations, or acoustic side-channel attacks on the device. The app shows a privacy screen during app-switcher transitions, but this is not a substitute for device-level and physical security.

11.4 Backup and Cloud Leakage

iOS backup (to a computer or via iCloud Backup) captures the application's documents directory, which includes all .db.enc and .data.enc files. These files remain encrypted in the backup (the same threat model as on-device storage applies), but the user should be aware that backups inherit the app's storage, including extended-slot files that break the indistinguishability property (Section 5.5).

11.5 Device Secret in iCloud Keychain

As noted in Section 5.4, the device secret syncs through iCloud Keychain. An attacker with access to an Apple ID that has iCloud Keychain enabled can, from another signed-in device, derive the decoy credential used on any of the user's devices. This breaks the indistinguishability property against that specific attacker class. The trade-off is made in favour of cross-device continuity; users for whom this matters should consider disabling iCloud Keychain on the Obscura device.

11.6 Hardware Attacks on the Secure Enclave

Obscura depends on the integrity of the Secure Enclave for Keychain access control and device-bound key material. Published attacks against the Secure Enclave to date have required specialised hardware and have not been demonstrated to enable remote or large-scale exploitation; nonetheless, a break of the Secure Enclave would compromise the device-secret-based parts of Obscura's design.

11.7 The Presence of the App

Slot indistinguishability (marketed as Hidden Vaults) applies to the contents of vaults, not to the existence of the app. The presence of Obscura on a device (visible on the home screen and in App Store purchase history) is itself a signal that the user intends to keep photos private. This protection is a property of encrypted data, not of the user's choice to install the app in the first place.

Appendix A. File Formats

Obscura uses three on-disk file formats:

  • V3 vault file ({uuid}.db.enc): a fixed-size header followed by a SQLCipher-encrypted database. The header contains a format version, magic identifier, cipher-suite identifier, KDF iteration count, KEK salt, wrapped MEK, an AES-GCM envelope holding the encrypted database size, and an integrity checksum (described in §4.3).
  • Chunked media file ({uuid}.data.enc, chunked variant): a fixed-size header identifying the format, version, per-file random nonce prefix, total and per-chunk plaintext sizes, and a flags field indicating whether folder encryption applies. Each chunk is an independent AES-GCM envelope; chunk nonces are constructed deterministically from the file's nonce prefix and the chunk index (described in §6.2).
  • Export file ({name}.obscura): a fixed-size plaintext header containing a format magic and version, the random KDF salt, the iteration count, an AES-GCM verification tag over a fixed verification plaintext (described in §8.2), and a random nonce prefix; the body is a chunked AES-GCM payload carrying a JSON manifest, the vault database, and media files (described in §8.3).

Complete byte-level format specifications, including offsets, field sizes, and parser test vectors, are available to security researchers and auditors on request via support@orlio.io.

Questions or responsible-disclosure reports: support@orlio.io.

This document describes version 1.3.0 of Obscura for iOS. Subsequent format changes will be reflected in a revised version of this paper.