-
Notifications
You must be signed in to change notification settings - Fork 0
Vault and Secret Sharing
Occulta Vault stores encrypted entries (label + content). Each entry has its own randomly generated per-entry key (PEK) — the vault master key only wraps the PEK, not the content directly. Recovery can be scoped per entry: different entries can have different trustees and different thresholds.
vault key (SE-derived)
└── encryptedEntryKey → per-entry key (PEK, 32 random bytes)
├── encryptedLabel (AES-GCM, AAD = entry.aad())
└── encryptedContent (AES-GCM, AAD = entry.aad())
The PEK is split using SSS over GF(2⁸) (Lagrange interpolation, field polynomial 0x11B). A threshold-of-n split produces n shards; any k ≥ threshold reconstruct the PEK. With fewer than k shards an attacker learns nothing about the PEK — information-theoretic security, not computational.
Splitting is per-entry. Different entries use independent random polynomials; shards from different distributions are incompatible and fail GCM authentication if mixed.
Each shard is wrapped in a SignedAttribute(category: .shard) signed by the owner's SE identity key. The v2 signing payload binds:
"occulta-signed-attribute-v2" ∥ attrID ∥ "shard" ∥ entryID
∥ createdAt UInt64 BE ∥ expiry flag ∥ shardBytes
entryID binding prevents a shard from a previous PEK generation from being accepted against a rotated entry. createdAt/expiresAt in the payload prevent a trustee from extending validity by editing stored JSON — any modification invalidates the ECDSA signature.
On a new device the owner's SE key is gone; signature verification is skipped. GCM authentication on reconstruction substitutes as the integrity check.
Shards are delivered as .occ bundles using longTermFallback + mandatory ML-KEM. A contact without ML-KEM key material cannot be selected as a trustee. This gates every shard bundle behind a session key that requires breaking ML-KEM-1024 in addition to P-256 — the only practical defence against harvest-now-decrypt-later attacks.
A trustee stores a CustodyShard row containing only:
- A random per-row
id(plaintext SwiftData key). - An
encryptedPayloadblob — AES-GCM seal of{ ownerKeyFingerprint, ownerContactIdentifier, signedAttribute }under the shard custody key, AAD =id.
No plaintext column links a shard to a specific contact. Cold-disk forensics learns "N shards stored" — nothing about ownership, timing, or which entries are covered.
A dedicated SE key (tag "shard.custody.occulta", device-unlock level, no biometric) is used exclusively for shard operations. Two symmetric keys are HKDF-derived from it:
| Symmetric key | HKDF info | Seals |
|---|---|---|
| Shard custody key | Occulta-v1-shard-custody-2026 |
CustodyShard |
| Recovery buffer key | Occulta-v1-recovery-buffer-2026 |
ReconstructShard |
Device-unlock (not biometric) allows shard bundles to be stored automatically on receipt without requiring the user to open the app.
- Trustees detect Alice's key change during proximity exchange and auto-return shards via
.handbackoperations in the next outbound bundle. - Alice's app buffers arriving shards in
ReconstructShard(encrypted under the recovery buffer key). - When ≥ k shards are buffered,
tryFinalizeReconstructionruns automatically. - Old device: verify each shard's ECDSA signature. New device: skip, rely on GCM.
-
ShamirSecretSharing.reconstruct(shares:)→ 32-byte PEK. -
AES.GCM.open(encryptedContent, using: PEK)— success confirms shard integrity. - Re-wrap PEK under current vault key; zero PEK bytes immediately.
| Threat | Defence |
|---|---|
| < k shards | Information-theoretically zero leakage |
| Stale shards (old PEK) | GCM decryption fails immediately |
| Tampered shard | ECDSA fails (normal); GCM fails on new device |
| Shard mixing across entries | GCM authentication rejects incompatible shares |
| HNDL (quantum adversary archives bundles) | Mandatory ML-KEM-1024 in session key |
| Trustee device loss | Owner detects fingerprint change → marks shard .lost; UI prompts redistribution |
The Vault tab surfaces three readiness signals inline.
Per-entry key (PEK) health — entries whose shard distribution has fallen below threshold (trustees lost devices, shards revoked) appear in a "Needs Attention" section with a critical or degraded label. Only active (.pending / .confirmed) shards count toward coverage.
Backup encryption key (BEK) status — a permanent row in the vault list tracks whether the BEK has been distributed and how many trustees have confirmed receipt. Vault export is gated on reaching threshold confirmation.
Backup staleness — three conditions invalidate an existing backup and surface as tappable rows: new entries added since last export, BEK rotated (existing backup permanently unrestorable), or trustee set changed. Each routes directly to the export flow.
Every entry in the vault list carries a glyph in its trailing position. When no shard distribution has been configured the glyph is grayscale and dimmed — a passive cue that the entry's encryption key has no recovery path. Once any distribution is marked for delivery the glyph switches to full colour with a soft purple glow.