Skip to content

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.

PINs are never stored in plaintext. When you set a PIN, ZestSSH:

  1. Generates a 16-byte random salt using Random.secure().
  2. Hashes the PIN with Argon2id using the salt.
  3. Stores the result as base64(salt):base64(hash) in Flutter Secure Storage.
ParameterValueNotes
Memory4,096 KB (4 MB)Tuned for mobile — tested on 2019-era mid-range Android
Iterations4Number of passes
Parallelism2Lanes of computation
Hash length32 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 sync Argon2id parameters (128 MB / 3 iter / 4 par), which are tuned for stronger passwords on more capable hardware.

When the user enters their PIN:

  1. The stored salt:hash pair is read from secure storage.
  2. The entered PIN is hashed with Argon2id using the stored salt.
  3. The derived hash is compared to the stored hash using constant-time comparison to prevent timing attacks.
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.

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.

After consecutive failed PIN attempts, ZestSSH enforces escalating lockout periods:

Failed AttemptsLockout Duration
1030 seconds
1560 seconds
20+300 seconds (5 minutes)

The maximum attempts before the first lockout is 10 (internally _maxAttempts; it was 5 before 1.8). The lock screen shows a live countdown of the remaining lockout time.

  • During a lockout, all PIN verification attempts are immediately rejected without checking the hash.
  • The remaining lockout time is exposed via remainingLockoutSeconds for 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.

Lockout state is persisted to secure storage so that restarting the app does not bypass the lockout:

  • pin_failed_attempts — Current failed attempt count
  • pin_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.

MethodBehavior
pinPIN only
bothBiometric first, PIN as fallback

These are the only two methods the setup flow produces: a PIN is always set. both means the PIN plus the device factor (fingerprint or Face ID on a phone, Windows Hello on Windows, Touch ID on macOS).

When the lock method is both, the phone app shows the biometric prompt first; if it fails or is cancelled, the PIN entry screen appears. The desktop app shows the PIN field first and offers the device factor through a Use Biometrics button.

The auto-lock timeout (in minutes) controls how long the app can be backgrounded before requiring re-authentication:

  • 0 (Immediately): Lock as soon as the app goes to the background
  • 1: The default on Android and iOS
  • 10: The default on Windows, macOS and Linux
  • Also available: 5, 15, 30 minutes, or Never
  1. Go to Settings > Security (mobile: App Lock & Encryption; desktop: the App Lock section) and turn on App Lock.
  2. Enter a PIN of 4 to 32 digits.
  3. If the device offers a factor, a switch on the same step reads Also enable biometrics / Also enable Windows Hello / Also enable Touch ID. Leave it on for both, or turn it off for a PIN-only lock.
  4. Confirm the PIN. It is hashed and stored securely.

Changing the PIN later (Change PIN) requires the current PIN first, and turning the lock off requires the current PIN on mobile.

Adding Windows Hello, Touch ID or a fingerprint on top of the PIN

Section titled “Adding Windows Hello, Touch ID or a fingerprint on top of the PIN”

The PIN is always the lock. The device factor is an optional shortcut past it, enabled with the “Also enable …” switch during PIN setup and stored as the both method. On Windows this is Windows Hello (Hello PIN, face or fingerprint, whatever Windows has set up); on macOS it is Touch ID; on Android and iOS it is the fingerprint or face sensor. It is not offered when Encrypt data at rest is on, because only the PIN can unwrap the encryption key. Full details, requirements and the unlock flow are on the Biometric Lock page under “Windows Hello unlock on the Windows desktop app”.

Going to Settings > Security and selecting None as the lock method calls clearLock(), which:

  1. Deletes the PIN hash from secure storage.
  2. Deletes the lock method preference.
  3. Deletes the auto-lock timeout.
  4. Resets the failed attempt counter and lockout state.