PIN Lock
ZestSSH supports PIN-based app locking as an alternative or supplement to biometric authentication. This is a free feature — security is not paywalled.
PIN Storage
Section titled “PIN Storage”PINs are never stored in plaintext. When you set a PIN, ZestSSH:
- Generates a 16-byte random salt using
Random.secure(). - Hashes the PIN with Argon2id using the salt.
- Stores the result as
base64(salt):base64(hash)in Flutter Secure Storage.
Argon2id Parameters (PIN)
Section titled “Argon2id Parameters (PIN)”| Parameter | Value | Notes |
|---|---|---|
| Memory | 4,096 KB (4 MB) | Tuned for mobile — tested on 2019-era mid-range Android |
| Iterations | 4 | Number of passes |
| Parallelism | 2 | Lanes of computation |
| Hash length | 32 bytes (256 bits) | Output size |
These parameters produce a ~120—180 ms hash time on mid-range hardware. This is slow enough to defeat offline brute force on a short PIN while fast enough to feel instant to the user. Higher memory (16 MB) was tested but caused jank on low-RAM devices and OOM on Android Go targets.
Note: These are different from the Cloud Sync Argon2id parameters (64 MB / 3 iter / 4 par), which are tuned for stronger passwords on more capable hardware.
Verification
Section titled “Verification”When the user enters their PIN:
- The stored
salt:hashpair is read from secure storage. - The entered PIN is hashed with Argon2id using the stored salt.
- The derived hash is compared to the stored hash using constant-time comparison to prevent timing attacks.
Constant-Time Comparison
Section titled “Constant-Time Comparison”var diff = 0;for (var i = 0; i < derivedHash.length; i++) { diff |= derivedHash[i] ^ expectedHash[i];}return diff == 0;The XOR/OR accumulation ensures the comparison takes the same amount of time regardless of how many bytes match, preventing an attacker from deducing correct digits based on verification timing.
Legacy Migration
Section titled “Legacy Migration”PINs set before the Argon2id migration were stored as unsalted SHA-256 hex strings. On successful verification of a legacy PIN, ZestSSH automatically re-hashes it with Argon2id and overwrites the stored value. This migration is transparent to the user.
Brute-Force Protection
Section titled “Brute-Force Protection”Escalating Lockout
Section titled “Escalating Lockout”After consecutive failed PIN attempts, ZestSSH enforces escalating lockout periods:
| Failed Attempts | Lockout Duration |
|---|---|
| 5 | 30 seconds |
| 10 | 60 seconds |
| 15+ | 300 seconds (5 minutes) |
The maximum attempts before the first lockout is 5 (configurable internally as _maxAttempts).
Lockout Behavior
Section titled “Lockout Behavior”- During a lockout, all PIN verification attempts are immediately rejected without checking the hash.
- The remaining lockout time is exposed via
remainingLockoutSecondsfor the UI countdown display. - Failed attempt count is not reset when a lockout expires — it continues to escalate. Only a successful PIN entry resets the counter to zero.
Persistence Across Restarts
Section titled “Persistence Across Restarts”Lockout state is persisted to secure storage so that restarting the app does not bypass the lockout:
pin_failed_attempts— Current failed attempt countpin_lockout_until— ISO 8601 timestamp when the lockout expires
On app launch, the lockout state is lazily loaded before the first verification check. Expired lockouts are cleared, but the failed attempt count is preserved to maintain escalation.
Lock Methods
Section titled “Lock Methods”| Method | Behavior |
|---|---|
pin | PIN only |
both | Biometric first, PIN as fallback |
When the lock method is both, the user sees the biometric prompt first. If biometric authentication fails or is cancelled, the PIN entry screen appears.
Auto-Lock Timeout
Section titled “Auto-Lock Timeout”The auto-lock timeout (in minutes) controls how long the app can be backgrounded before requiring re-authentication:
- 0 — Immediate lock on background
- 1 (default) — Lock after 1 minute
- Go to Settings > Security.
- Under App Lock, select PIN or Biometric + PIN.
- Enter and confirm a PIN.
- The PIN is hashed and stored securely.
Clearing the Lock
Section titled “Clearing the Lock”Going to Settings > Security and selecting None as the lock method calls clearLock(), which:
- Deletes the PIN hash from secure storage.
- Deletes the lock method preference.
- Deletes the auto-lock timeout.
- Resets the failed attempt counter and lockout state.