PIN & Duress Mode
Your PIN is not a password. It is the encryption key.
How the PIN System Works
In most applications, a PIN or password is used to authenticate you to the application. The application then unlocks the data using a key stored somewhere (keychain, server, etc.). This means someone with access to the key storage can bypass the PIN entirely.
RVNT's PIN system is different. The PIN is the encryption key. More precisely, the PIN is the sole input to a key derivation function that produces the encryption key for the local database. There is no separate stored key. There is no server that knows the key. There is no "forgot PIN" recovery path. If you do not know the PIN, the data is cryptographically inaccessible.
PIN → Argon2id → database_key → SQLCipher(AES-256-CBC + HMAC-SHA256)
There is no other path to the data.
The database_key exists only in memory while the app is unlocked.
When the app locks, database_key is securely zeroed. Argon2id Key Derivation
RVNT uses Argon2id (the hybrid variant of Argon2, combining resistance to side-channel attacks and GPU/ASIC attacks) to derive the database encryption key from the user's PIN.
Parameters
| Parameter | Value | Purpose |
|---|---|---|
| Memory (m) | 65536 KB (64 MB) | Memory-hard: prevents GPU/ASIC brute force |
| Iterations (t) | 3 | Time cost: ~300ms on modern hardware |
| Parallelism (p) | 4 | Number of parallel lanes |
| Output length | 32 bytes (256 bits) | AES-256 key |
| Salt | 32 bytes (random, per device) | Prevents rainbow tables |
| Version | Argon2id v1.3 | RFC 9106 |
Key Derivation Flow
Input:
pin: User's PIN (6+ digits, UTF-8 encoded)
salt: 32 random bytes (generated at identity creation, stored in Keychain)
Process:
raw_key = Argon2id(
password: pin,
salt: salt,
m_cost: 65536, // 64 MB
t_cost: 3, // 3 iterations
p_cost: 4, // 4 parallel lanes
output: 32 // 32 bytes
)
// Derive separate keys for different purposes
db_key = HKDF-SHA256(ikm=raw_key, info="rvnt-db-key", len=32)
backup_key = HKDF-SHA256(ikm=raw_key, info="rvnt-backup-key", len=32)
export_key = HKDF-SHA256(ikm=raw_key, info="rvnt-export-key", len=32)
// Zero the raw key
secure_zero(raw_key)
Output:
db_key: Used to open the SQLCipher database
backup_key: Used to encrypt local backups
export_key: Used to encrypt data exports
All three keys are zeroed from memory when the app locks. Why These Parameters
64 MB memory cost means an attacker needs 64 MB of RAM per concurrent brute-force attempt. A GPU with 24 GB VRAM can run ~375 parallel attempts. At ~300ms per attempt, the attack rate is approximately:
GPU attack rate: 375 / 0.3s ≈ 1,250 attempts/second
For a 6-digit PIN (10^6 = 1,000,000 possibilities):
Time to exhaust: 1,000,000 / 1,250 ≈ 800 seconds ≈ 13 minutes
For an 8-digit PIN (10^8 = 100,000,000 possibilities):
Time to exhaust: 100,000,000 / 1,250 ≈ 80,000 seconds ≈ 22 hours
For a 10-digit PIN (10^10):
Time to exhaust: ~93 days
For an alphanumeric PIN (62^8 ≈ 2 × 10^14):
Time to exhaust: ~5,000 years Lockout Escalation
To further slow brute-force attacks against the PIN, RVNT implements an escalating lockout system:
| Failed Attempts | Lockout Duration | Cumulative Wait |
|---|---|---|
| 1-3 | None | 0 |
| 4-5 | 30 seconds | 1 minute |
| 6-7 | 5 minutes | 11 minutes |
| 8-9 | 30 minutes | 71 minutes |
| 10 | 1 hour | 131 minutes |
| 15 | 4 hours | ~21 hours |
| 20 | 24 hours | ~5 days |
| 25+ | Optional: auto-wipe | N/A |
The lockout counter is stored in the Secure Enclave (Apple) or StrongBox (Android) and cannot be reset by reinstalling the app or manipulating local storage. On desktop platforms without hardware-backed storage, the counter is stored in a tamper-evident file protected by a hardware-bound key (where available) or a platform-specific credential store.
Duress Mode
Duress mode is designed for the scenario where you are compelled to unlock your device -- at a border crossing, during a police stop, under physical threat, or during a legal proceeding where you must comply.
How It Works
Setup (done in advance):
1. Go to Settings > Security > Duress PIN
2. Set a secondary PIN (must differ from primary PIN)
3. Choose duress behavior:
a. Full wipe (panic mode) + show empty state
b. Full wipe (panic mode) + show decoy conversations
c. Show restricted view (hide sensitive conversations only)
Activation:
1. At the lock screen, enter the duress PIN instead of the real PIN
2. The app appears to unlock normally
3. Behind the scenes:
a. Panic mode executes (key destruction, database wipe)
b. A pre-staged decoy database is loaded
c. OR: an empty state is presented
4. The person compelling you sees a normal-looking app
5. Your actual cryptographic material is already destroyed Decoy Conversations
If you configure decoy conversations, the duress mode will present a pre-staged set of innocuous conversations after destroying the real data. These conversations are generated from templates and appear to contain mundane messages (grocery lists, meeting confirmations, family check-ins). They use realistic timestamps spread over the previous 2 weeks.
Decoy database:
- 3-5 contacts with generic names
- 10-20 messages per conversation
- Timestamps distributed over 14 days
- No media attachments (avoids suspicion of staging)
- Message content is mundane and culturally appropriate
- All decoy data is encrypted with a derived key
(derived from the duress PIN, not the real PIN) Lock Modes
RVNT supports multiple lock modes for different security contexts:
| Mode | Behavior | Use Case |
|---|---|---|
| Standard Lock | PIN required to unlock. Auto-lock after 5 minutes (configurable). | Normal daily use |
| Instant Lock | Lock immediately on app background. No grace period. | High-risk environments |
| Travel Mode | Only whitelisted conversations visible. All others hidden behind a secondary PIN. | Border crossings, device inspections |
| Per-Conversation Lock | Individual conversations can require a separate PIN to access. | Compartmentalized security |
Per-Conversation Lock
Individual conversations can be locked with a separate PIN. This provides compartmentalized security: even if someone unlocks the main app, specific conversations remain hidden and encrypted.
Per-conversation encryption:
conv_key = HKDF-SHA256(
ikm: Argon2id(conversation_pin, conversation_salt),
info: "rvnt-conv-lock-" || conversation_id,
len: 32
)
The conversation's messages are re-encrypted with conv_key.
The conversation does not appear in the conversation list
unless the per-conversation PIN is entered. Biometric Unlock
RVNT supports biometric unlock (Face ID, Touch ID, fingerprint) as a convenience feature. Biometric unlock works by storing the database key in the platform's Secure Enclave / StrongBox, protected by a biometric access policy.
Biometric unlock flow:
1. User sets PIN (Argon2id derives db_key)
2. User enables biometric unlock
3. db_key is stored in Secure Enclave with biometric policy:
- Access requires valid biometric (face/fingerprint)
- Key is hardware-bound (cannot be extracted)
- Key is invalidated if biometrics are modified
4. On subsequent unlock:
a. Biometric prompt appears
b. If valid: Secure Enclave releases db_key to app memory
c. If invalid: Fall back to PIN entry
Security considerations:
- Biometric unlock does NOT bypass the PIN. The PIN-derived key is
what's stored in the Secure Enclave.
- The Secure Enclave provides hardware-backed protection.
- Biometrics can be compelled (fingerprint on sleeping person, etc.).
- For maximum security, disable biometric unlock and use PIN only. Further Reading
- Panic Mode -- The complete data destruction system
- Threat Model -- Physical device seizure threat analysis
- Forward Secrecy -- Why past messages are safe even if current keys are compromised