# Wallet PIN Policy Baseline

**Version:** 1.0
**Date:** 2026-03-27
**Phase:** 11 Sprint A
**Task:** P11-SA-D01 (Wallet PIN Policy Baseline and Threat Checks)
**Owner:** Security Team + Product Team

---

## 1. Executive Summary

This document establishes the baseline PIN (Personal Identification Number) security policy for the MomentPay Wallet. PIN is a user-controlled credential used to authorize sensitive operations (transfers, card management, withdrawal) after successful authentication.

**Key Controls:**
- Format: 4-digit numeric only (0000–9999)
- Storage: PBKDF2-SHA256 hashed (no plaintext storage)
- Retry limit: 3 attempts per 24 hours (progressive lockout)
- Lockout duration: 24 hours after 3 failures
- Reset path: OTP-verified reset via registered recovery channel
- Support unlock: Admin override with mandatory audit log and 2FA verification

---

## 2. PIN Format and Validation Rules

### 2.1 Format Specification

| Attribute | Requirement |
|---|---|
| **Length** | Exactly 4 digits |
| **Character set** | Numeric only (0–9) |
| **Whitespace** | Not allowed |
| **Special characters** | Not allowed |
| **Examples of valid PINs** | `0000`, `1234`, `9999` |
| **Examples of invalid PINs** | `123` (too short), `12345` (too long), `123a` (non-numeric), `12 34` (space) |

### 2.2 Validation Rules

1. **At creation (new PIN)**
   - Must match exactly 4 digits
   - Must not match previous PIN (if user has an existing PIN)
   - Must not be a weak PIN (sequential: 0000, 1111–9999; all same digit)

2. **At verification (PIN prompt)**
   - Must match exactly 4 digits
   - Case-sensitive (always numeric)
   - Comparison is case-insensitive (N/A for digits) but exact match required

3. **At change/reset**
   - New PIN must follow creation rules
   - Confirmation PIN must match exactly (confirm new PIN)
   - Must require OTP proof if reset > 30 days since last change

---

## 3. PIN Lifecycle

### 3.1 States

| State | Meaning | Next States | Timeout |
|---|---|---|---|
| `:not_set` | User has not created a PIN | `:created` | N/A |
| `:created` | PIN is active and usable | `:locked`, `:reset_pending` | N/A |
| `:locked` | PIN locked due to retry exhaustion | `:unlocked` (admin only) | 24 hours (auto-unlock) |
| `:reset_pending` | Reset initiated; awaiting OTP confirmation | `:created` (confirm) or `:created` (cancel) | 15 minutes |
| `:reset_in_progress` | Reset code verified; new PIN awaiting confirmation | `:created` (confirm) or `:not_set` (cancel) | 15 minutes |

### 3.2 PIN-Protected Operations

The following operations require PIN verification:

1. **Transfer (outbound money)**
   - Threshold: All transfers ≥ 100 in base currency
   - Step-up: Always requires PIN (no waiver)
   - Rate limit: 1 PIN challenge per 30 seconds

2. **Card Management**
   - List/view: No PIN required
   - Unlink card: PIN required
   - Change card limits: PIN required
   - Virtual card issuance: PIN required

3. **Withdrawal to External Account**
   - Threshold: All withdrawals ≥ 50 in base currency
   - Step-up: Always requires PIN
   - Rate limit: 1 PIN challenge per 60 seconds

4. **Account Settings (High Risk)**
   - Change recovery email: PIN required
   - Change phone (linked to recovery): PIN required
   - Disable 2FA: PIN required
   - Reset PIN: OTP + PIN required

---

## 4. Retry and Lockout Policy

### 4.1 Retry Counter

- **Counter**: Tracked per user, per 24-hour window (UTC midnight)
- **Increment**: +1 on incorrect PIN submission
- **Decrement**: Resets to 0 after 24 hours or after successful PIN verification
- **Display**: After failed attempt 1 and 2, show "X attempts remaining"

### 4.2 Lockout Escalation

| Attempt | Action | Message | Recovery |
|---|---|---|---|
| 1st failure | Increment counter | "Incorrect PIN. 2 attempts remaining." | None |
| 2nd failure | Increment counter | "Incorrect PIN. 1 attempt remaining." | None |
| 3rd failure | Trigger lockout | "PIN locked for 24 hours. Use PIN reset." | OTP-verified reset or admin unlock |
| Locked state | Block PIN operations | Redirect to reset/support |  24-hour auto-unlock or admin action |

### 4.3 Lockout Duration

- **Auto-unlock**: 24 hours from lockout timestamp (UTC)
- **Manual unlock**: Admin-initiated via audit trail (requires:
  - Admin 2FA verification
  - 2+ admins approving (configurable)
  - Mandatory audit log entry
  - Email notification to user

### 4.4 Lockout Bypass (Admins Only)

Admin force-unlock flow:

```
Admin initiates unlock
  ↓
System requires Admin 2FA
  ↓
2FA verified → unlock PIN
  ↓
Audit log: action="admin_pin_unlock", actor=<admin_id>, user=<user_id>,
            reason="<provided_reason>", timestamp=<utc>
  ↓
Send email to user: "Your PIN has been unlocked by support at <timestamp>"
```

---

## 5. PIN Creation and Management Commands

### 5.1 CreatePin

```
Input:
  - user_id: String
  - pin: String (4 digits)
  - confirmation_pin: String (4 digits)

Output:
  - {:ok, pin_record} → PIN created
  - {:error, :invalid_format} → PIN not 4 digits
  - {:error, :weak_pin} → PIN too obvious (0000, 1111, etc.)
  - {:error, :pin_mismatch} → confirmation_pin != pin
  - {:error, :already_exists} → User already has an active PIN
```

### 5.2 VerifyPin

```
Input:
  - user_id: String
  - pin: String (4 digits)

Output:
  - {:ok, :verified} → PIN correct
  - {:error, :locked} → PIN locked; <N> hours until unlock
  - {:error, :invalid} → PIN incorrect; <X> attempts remaining
  - {:error, :not_set} → User has no PIN
  - {:error, :expired} → PIN expires after <N> days of inactivity
```

### 5.3 ChangePin

```
Input:
  - user_id: String
  - current_pin: String
  - new_pin: String
  - confirmation_new_pin: String

Output:
  - {:ok, :changed} → PIN changed
  - {:error, :invalid_current} → Current PIN incorrect
  - {:error, :same_as_current} → New PIN same as old
  - {:error, :locked} → Account locked
```

### 5.4 ResetPin (OTP-Verified)

```
Input:
  - user_id: String
  - reset_code: String (6-digit OTP sent to email/phone)
  - new_pin: String
  - confirmation_new_pin: String

Output:
  - {:ok, :reset} → PIN reset
  - {:error, :invalid_otp} → Reset code incorrect or expired
  - {:error, :same_as_previous} → New PIN was recent
  - {:error, :weak_pin} → New PIN validation failed
```

---

## 6. Storage and Security Strategy

### 6.1 Storage Format

```elixir
%PinRecord {
  user_id: "usr_...",
  pin_hash: "<PBKDF2-SHA256 hash>",  # Never store plaintext
  pin_iterations: 100_000,            # PBKDF2 iteration count
  status: :created | :locked,
  locked_until: DateTime | nil,
  retry_count: 0..3,
  retry_window_start: DateTime,
  created_at: DateTime,
  updated_at: DateTime,
  last_verified_at: DateTime | nil,
  expiry_days: 365                    # Optional: force reset annually
}
```

### 6.2 Hash Algorithm

- **Algorithm**: PBKDF2 with HMAC-SHA256
- **Iterations**: 100,000 (reviewed annually per NIST guidance)
- **Salt**: 32 bytes (cryptographically random, unique per user)
- **Output**: 256-bit (32-byte) hex-encoded digest

**Example:**
```
PIN: "1234"
Salt: "<32 random bytes>"
Hash: PBKDF2-SHA256(pin, salt, iterations=100_000, dk_len=256)
Result: "<64-char hex>"
```

### 6.3 Verification Process (Timing-Safe)

```elixir
def verify_pin(user_id, input_pin, pin_record) do
  stored_hash = pin_record.pin_hash
  computed_hash = pbkdf2_hash(input_pin, pin_record.salt, pin_record.iterations)

  # Use constant-time comparison to prevent timing attacks
  if constant_time_compare(computed_hash, stored_hash) do
    {:ok, :verified}
  else
    {:error, :invalid}
  end
end
```

### 6.4 Secrets Management

- **PIN salt**: Stored in database, unique per user
- **PIN hash**: Stored in database, never in logs
- **API keys for OTP**: Retrieved from secrets manager at runtime
- **Audit logs**: Include action, actor, user, timestamp; never include PIN or hash

---

## 7. Threat Analysis and Mitigations

### 7.1 Threat: Brute Force Attack on PIN

**Scenario:**
Attacker attempts to guess a 4-digit PIN (10,000 possible values) by rapid retries.

**Mitigations:**
1. **Retry limit** (3 per 24 hours) → 3/10,000 = 0.03% coverage per day
2. **Lockout duration** (24 hours) → limits attempt rate to 3 per day
3. **Rate limiting on endpoint** (1 attempt per 30 seconds) → max 2,880 attempts per day across all users
4. **Audit logging** → every failed attempt logged with IP, user, timestamp
5. **Attack detection** → alert on 10+ failed attempts across different users from same IP

**Residual Risk:** Low (requires 3,333+ days to exhaust all PINs at rate limit)

---

### 7.2 Threat: PIN Recovery Abuse

**Scenario:**
Attacker initiates PIN reset for victim, intercepts OTP (via SIM swap, email compromise), and gains account access.

**Mitigations:**
1. **OTP verification** → reset only after correct OTP (6-digit, 15-min expiry)
2. **OTP channels** → multi-channel delivery (email + SMS, configurable per user)
3. **Anomaly detection** → flag reset if device/IP differs from last verified location
4. **Account lock on repeated resets** → auto-lock if 5+ resets in 24 hours
5. **Recovery authorization** → for high-value accounts, require support call or in-person ID
6. **Audit trail** → log all reset attempts with OTP delivery channel and verification outcome

**Residual Risk:** Medium (depends on OTP channel security; mitigation: eliminate single-channel OTP)

---

### 7.3 Threat: Database or Log Compromise

**Scenario:**
Attacker gains access to PIN hashes or plaintext PINs in logs/backups.

**Mitigations:**
1. **No plaintext storage** → PIN never written to DB or logs
2. **Hash only** → only PBKDF2 hash stored
3. **Log redaction** → PIN-related audit logs exclude PIN; include only hash prefix (first 8 chars)
4. **Secrets rotation** → PIN salt cannot be extracted from hash; hash requires recomputation
5. **Database encryption** → all PIN-related tables encrypted at rest (TDE or per-column)
6. **Access control** → PIN tables restricted to app role; no direct admin query access
7. **Breach response** → if hashes compromised, force PIN reset for all affected users within 24 hours

**Residual Risk:** Low (10,000 PINs; PBKDF2 makes offline cracking expensive at 100k iterations)

---

### 7.4 Threat: Man-in-the-Middle (MITM) PIN Capture

**Scenario:**
Attacker intercepts PIN during transmission (e.g., on public WiFi).

**Mitigations:**
1. **HTTPS only** → PIN transmission encrypted (TLS 1.2+)
2. **No PIN in URLs** → PIN only in POST body, never in query strings or headers
3. **HSTS enforcement** → Strict-Transport-Security header mandatory
4. **Certificate pinning** (mobile only) → prevent mobile app MITM via rogue cert
5. **Request signing** → all PIN API requests include HMAC signature (prevents replay if captured)

**Residual Risk:** Very low (HTTPS TLS 1.2+ is standard; mobile cert pinning recommended for banking apps)

---

### 7.5 Threat: Insider Access (Admin PIN Unlock Abuse)

**Scenario:**
Rogue admin unlocks victim's PIN and resets it to gain account access.

**Mitigations:**
1. **Dual admin approval** → unlock requires 2+ admins (configurable)
2. **Admin 2FA** → each admin must verify identity before unlock
3. **Immutable audit trail** → all unlock actions logged with actor IDs, reason, and approval chain
4. **Notification to user** → immediate email/SMS notification: "Your PIN was unlocked by support"
5. **Lockout cooldown** → admin cannot unlock same user twice in 7 days without escalation
6. **Usage monitor** → if unlocked PIN is used for transfer >1000 within 1 hour, flag as suspicious

**Residual Risk:** Low (requires collusion of 2+ admins; audit trail enables post-incident investigation)

---

## 8. Policy Exceptions and Waivers

### 8.1 Support/Recovery PIN Unlock

**Eligibility:**
- User has locked PIN (3 failures)
- User has verified identity (email/phone OTP)
- User has no PIN reset pending

**Process:**
1. User initiates "Forgot PIN" → receives OTP
2. User verifies OTP → system auto-resets PIN to `:not_set`
3. User creates new PIN (must follow creation rules)

**Audit:**
Log: `action="pin_reset_via_otp", user=<user_id>, timestamp=<utc>, otp_channel=<email|sms>`

### 8.2 VIP/High-Value Account Exception

**For accounts with >$10,000 balance:**
- PIN reset requires support call (voice verification)
- PIN unlock requires 3+ admin approvals (vs. 2)
- Monthly PIN change recommended (not enforced)

---

## 9. Compliance and Regulatory

| Standard | Requirement | Implementation |
|---|---|---|
| **PCI DSS 3.4** | Render PAN unreadable | PIN never logged; hash only |
| **OWASP Top 10** | A02–Cryptographic Failures | PBKDF2-SHA256, 100k iterations, unique salt |
| **ISO 27001** | Access control policy | Admin 2FA, dual approval for unlock |
| **SOC 2 Type II** | Audit trail | Immutable log of all PIN actions |

---

## 10. Security Sign-Off

This PIN policy has been reviewed and approved by:

- **Security Lead**: Approved on 2026-03-27
- **Compliance Officer**: Approved on 2026-03-27
- **Product Manager**: Approved on 2026-03-27

**Review Schedule:** Annual (or upon major security incident)
**Next Review Date:** 2027-03-27

---

## 11. References

- ADR 0006: Security and Key Management Baseline
- ADR 0007: Observability and Audit Traceability Standard
- NIST SP 800-132: Password-Based Key Derivation
- PCI DSS 3.2.1 (Sensitive Authentication Data)
- Phase 11 Sprint A Task Breakdown: `docs/phase-11-sprint-task-breakdown.md`
