# PIN Lock System Documentation
**Version:** v0.3.0
**Implementation Date:** 2026-01-30
---
## Overview
The PIN Lock system provides session security for nself-chat with client-side PIN verification, biometric authentication support, and automatic session locking based on user preferences.
### Key Features
- **4-6 Digit PIN Protection** - Client-side PIN with PBKDF2 hashing
- **Auto-lock Timeouts** - 5/15/30/60 minute inactivity timeouts
- **Lock on Background/Close** - Configurable lock triggers
- **Biometric Unlock** - WebAuthn-based fingerprint/face unlock
- **Failed Attempt Lockout** - 5 attempts = 30 minute lockout
- **Emergency Unlock** - Password-based PIN reset
---
## Architecture
### Security Model
```
Client-Side PIN Verification (LocalStorage)
├── PIN Hash (PBKDF2, 100k iterations)
├── Salt (Random, 128 bits)
└── Lock Settings
Server-Side Audit Trail (PostgreSQL)
├── PIN Attempts Log
├── Session Lock State
└── Biometric Credentials
```
**Why Client-Side?**
- Faster unlock (no network round-trip)
- Privacy (PIN never transmitted)
- Offline support
- Server still tracks attempts for security monitoring
---
## Database Schema
### Tables Created by Migration 012
#### `user_pin_settings`
```sql
- id (UUID, PK)
- user_id (UUID, FK to auth.users)
- pin_hash (TEXT) -- PBKDF2 hash
- pin_salt (TEXT) -- Random salt
- lock_on_close (BOOLEAN)
- lock_on_background (BOOLEAN)
- lock_timeout_minutes (INTEGER: 0|5|15|30|60)
- biometric_enabled (BOOLEAN)
- biometric_credential_id (TEXT)
- created_at (TIMESTAMPTZ)
- updated_at (TIMESTAMPTZ)
- last_changed_at (TIMESTAMPTZ)
```
#### `user_pin_attempts`
```sql
- id (UUID, PK)
- user_id (UUID, FK)
- success (BOOLEAN)
- attempt_time (TIMESTAMPTZ)
- ip_address (INET)
- user_agent (TEXT)
- device/browser/os (TEXT)
- failure_reason (TEXT)
- location_* (TEXT) -- city, country, country_code
```
#### `user_sessions` (extended)
```sql
-- Added columns:
- locked_until (TIMESTAMPTZ)
- lock_reason (TEXT)
- failed_pin_attempts (INTEGER)
```
#### `user_biometric_credentials`
```sql
- id (UUID, PK)
- user_id (UUID, FK)
- credential_id (TEXT, WebAuthn credential)
- public_key (TEXT)
- counter (INTEGER)
- device_name (TEXT)
- credential_type (TEXT: fingerprint|face|other)
- created_at/last_used_at (TIMESTAMPTZ)
```
### Database Functions
- `check_pin_lockout(user_id)` - Returns lockout status
- `clear_expired_pin_lockouts()` - Cleanup function
- `record_pin_attempt(...)` - Logs attempt with device info
- `get_recent_failed_pin_attempts(user_id, minutes)` - Security monitoring
---
## File Structure
```
src/
├── lib/security/
│ ├── pin.ts # PIN hashing, verification, storage
│ ├── session.ts # Session timeout, lock management
│ └── biometric.ts # WebAuthn biometric support
├── components/security/
│ ├── PinSetup.tsx # PIN setup wizard
│ ├── PinLock.tsx # Lock screen overlay
│ ├── PinManage.tsx # Settings management
│ └── PinLockWrapper.tsx # Root wrapper component
├── hooks/
│ ├── use-pin-lock.ts # PIN lock state hook
│ └── use-session-timeout.ts # Timeout monitoring hook
└── app/settings/security/pin-lock/
└── page.tsx # Settings page
.backend/migrations/
└── 012_pin_lock_security.sql # Database migration
```
---
## Usage Guide
### 1. Enable PIN Lock in Root Layout
```tsx
// src/app/layout.tsx
import { PinLockWrapper } from '@/components/security/PinLockWrapper'
export default function RootLayout({ children }) {
return (
{children}
)
}
```
### 2. Add PIN Lock Settings Page
```tsx
// src/app/settings/security/pin-lock/page.tsx
import { PinManage } from '@/components/security/PinManage'
export default function PinLockPage() {
const { user } = useAuth()
return
}
```
### 3. Use PIN Lock Hook
```tsx
import { usePinLock } from '@/hooks/use-pin-lock'
function MyComponent() {
const { isLocked, hasPinSetup, lock, unlock, verifyAndUnlock, lockoutInfo } = usePinLock()
// Manual lock
const handleLock = () => lock('manual')
// Verify PIN
const handleUnlock = async (pin: string) => {
const result = await verifyAndUnlock(pin)
if (result.success) {
console.log('Unlocked!')
} else {
console.error(result.error)
}
}
return (
{isLocked ? 'Locked' : 'Unlocked'}
{lockoutInfo.isLocked &&
Locked for {lockoutInfo.remainingMinutes} minutes
}
)
}
```
### 4. Setup Biometric Authentication
```tsx
import { registerBiometric, verifyBiometric } from '@/lib/security/biometric'
// Register
const result = await registerBiometric(userId, userName)
if (result.success) {
console.log('Biometric registered:', result.credential)
}
// Verify
const verification = await verifyBiometric()
if (verification.success) {
console.log('Biometric verified!')
}
```
---
## Configuration Options
### Lock Timeout Options
```typescript
type LockTimeout = 0 | 5 | 15 | 30 | 60 // minutes
0 = Never auto-lock
5 = Lock after 5 minutes of inactivity
15 = Lock after 15 minutes (default)
30 = Lock after 30 minutes
60 = Lock after 1 hour
```
### Lock Triggers
```typescript
interface LockTriggers {
lockOnClose: boolean // Lock when app closes
lockOnBackground: boolean // Lock when app goes to background
lockTimeout: LockTimeout // Inactivity timeout
}
```
### Failed Attempt Policy
```typescript
const LOCKOUT_POLICY = {
maxAttempts: 5, // Failed attempts before lockout
lookbackMinutes: 15, // Check attempts in last 15 min
lockoutDuration: 30, // Lockout for 30 minutes
}
```
---
## Security Features
### 1. Client-Side PIN Security
- **PBKDF2-SHA256** with 100,000 iterations
- **128-bit random salt** per user
- **256-bit hash output**
- PIN never transmitted to server
- Constant-time comparison
### 2. Failed Attempt Protection
- Track last 15 minutes of attempts
- Lock after 5 failed attempts
- 30 minute lockout period
- Local and server-side tracking
### 3. Biometric Security
- WebAuthn standard (FIDO2)
- Platform authenticator only
- User verification required
- Per-device credentials
- Counter-based replay protection
### 4. Session Security
- Activity tracking
- Visibility monitoring
- Timeout enforcement
- Manual lock support
- Emergency unlock with password
---
## API Reference
### PIN Module (`src/lib/security/pin.ts`)
#### Validation
```typescript
isValidPinFormat(pin: string): boolean
getPinStrength(pin: string): { strength, message }
```
#### Cryptography
```typescript
generateSalt(): string
hashPin(pin: string, salt?: string): Promise<{ hash, salt }>
verifyPin(pin: string, hash: string, salt: string): Promise
```
#### Storage
```typescript
storePinSettings(settings: PinSettings): void
loadPinSettings(): PinSettings | null
clearPinSettings(): void
hasPinConfigured(): boolean
```
#### Setup/Management
```typescript
setupPin(pin, confirmPin, options): Promise
changePin(current, newPin, confirm): Promise
updatePinSettings(updates): boolean
disablePin(currentPin): Promise
```
#### Attempt Tracking
```typescript
recordLocalPinAttempt(success: boolean, reason?: string): void
getRecentFailedAttempts(minutes?: number): StoredAttempt[]
checkLocalLockout(): { isLocked, remainingMinutes, failedAttempts }
clearAttemptHistory(): void
```
### Session Module (`src/lib/security/session.ts`)
#### Activity Tracking
```typescript
updateLastActivity(): void
getLastActivityTime(): number
getTimeSinceLastActivity(): number
getMinutesSinceLastActivity(): number
getFormattedTimeSinceActivity(): string
```
#### Lock State
```typescript
getLockState(): LockState
lockSession(reason?: string): void
unlockSession(): void
isSessionLocked(): boolean
clearLockState(): void
```
#### Timeout Management
```typescript
checkSessionTimeout(): { hasTimedOut, minutesSinceActivity, timeoutMinutes }
checkAndLockIfNeeded(): boolean
setupAutoLockChecker(onLocked: () => void): () => void
```
#### App Lifecycle
```typescript
handleAppVisible(): { shouldLock, reason }
handleAppHidden(): void
handleAppClose(): void
setupVisibilityListener(onVisible, onHidden): () => void
setupBeforeUnloadListener(): () => void
setupActivityListeners(): () => void
```
### Biometric Module (`src/lib/security/biometric.ts`)
#### Availability
```typescript
isWebAuthnSupported(): boolean
isBiometricAvailable(): Promise
getBiometricType(): Promise
```
#### Registration
```typescript
registerBiometric(userId, userName, deviceName?): Promise
```
#### Verification
```typescript
verifyBiometric(): Promise
```
#### Credential Management
```typescript
getStoredCredentials(): BiometricCredential[]
removeCredential(credentialId: string): boolean
clearAllCredentials(): void
hasRegisteredCredentials(): boolean
```
---
## Testing
### Manual Testing Checklist
- [ ] Setup PIN with 4-6 digits
- [ ] Verify PIN strength validation
- [ ] Test PIN unlock (correct PIN)
- [ ] Test PIN unlock (incorrect PIN)
- [ ] Test failed attempt lockout (5 attempts)
- [ ] Test timeout locking (5/15/30/60 min)
- [ ] Test lock on app close
- [ ] Test lock on app background
- [ ] Test biometric registration
- [ ] Test biometric unlock
- [ ] Test change PIN
- [ ] Test disable PIN
- [ ] Test emergency unlock (forgot PIN)
- [ ] Test multiple devices/browsers
### Unit Tests
```bash
# Test PIN hashing
npm test src/lib/security/pin.test.ts
# Test session management
npm test src/lib/security/session.test.ts
# Test biometric
npm test src/lib/security/biometric.test.ts
```
### E2E Tests
```bash
# Test PIN lock flow
npm run test:e2e -- pin-lock.spec.ts
```
---
## Migration Guide
### Running the Migration
```bash
# Development
cd .backend
nself exec postgres -- psql -U postgres -d nself_chat -f migrations/012_pin_lock_security.sql
# Production
# Use your database migration tool (Hasura, Flyway, etc.)
```
### Rollback
```sql
-- Rollback script
DROP TABLE IF EXISTS user_biometric_credentials CASCADE;
DROP TABLE IF EXISTS user_pin_attempts CASCADE;
DROP TABLE IF EXISTS user_pin_settings CASCADE;
ALTER TABLE user_sessions
DROP COLUMN IF EXISTS locked_until,
DROP COLUMN IF EXISTS lock_reason,
DROP COLUMN IF EXISTS failed_pin_attempts;
DROP FUNCTION IF EXISTS check_pin_lockout(UUID);
DROP FUNCTION IF EXISTS clear_expired_pin_lockouts();
DROP FUNCTION IF EXISTS record_pin_attempt(UUID, BOOLEAN, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT);
DROP FUNCTION IF EXISTS get_recent_failed_pin_attempts(UUID, INTEGER);
```
---
## Troubleshooting
### Issue: PIN not persisting
**Solution:** Check localStorage quota and browser settings. PIN settings are stored in `localStorage`.
```javascript
// Check storage
console.log(localStorage.getItem('nself_chat_pin_settings'))
```
### Issue: Biometric not available
**Solution:** Verify WebAuthn support and HTTPS requirement.
```javascript
// Check support
console.log('WebAuthn:', window.PublicKeyCredential)
console.log('Secure context:', window.isSecureContext)
```
### Issue: Lockout not expiring
**Solution:** Clear expired lockouts or reset attempt history.
```javascript
// Clear lockout
import { clearAttemptHistory } from '@/lib/security/pin'
clearAttemptHistory()
```
### Issue: Session not auto-locking
**Solution:** Verify PIN is configured and timeout is set.
```javascript
// Debug session
import { getSessionDebugInfo } from '@/lib/security/session'
console.log(getSessionDebugInfo())
```
---
## Performance Considerations
### Client-Side Hashing
- PBKDF2 with 100k iterations takes ~50-100ms
- Acceptable for unlock UX
- Runs in background (non-blocking)
### Storage Impact
- PIN settings: ~500 bytes per user
- Biometric credentials: ~1KB per credential
- Attempt history: ~200 bytes per attempt (max 20 stored)
### Network Impact
- No network calls for PIN verification
- Optional server-side attempt logging
- Biometric uses local device APIs
---
## Future Enhancements
### Planned Features
- [ ] PIN recovery via email
- [ ] Multiple PINs (device-specific)
- [ ] Pattern lock option
- [ ] Custom lockout duration
- [ ] Whitelist trusted networks
- [ ] Session persistence across devices
- [ ] Admin force-unlock
- [ ] Audit log export
### Browser Support
| Browser | PIN Lock | Biometric |
| ------------- | -------- | --------- |
| Chrome 90+ | ✅ | ✅ |
| Firefox 90+ | ✅ | ✅ |
| Safari 14+ | ✅ | ✅ |
| Edge 90+ | ✅ | ✅ |
| Mobile Safari | ✅ | ✅ |
| Mobile Chrome | ✅ | ✅ |
---
## Security Best Practices
### For Users
1. Use a strong PIN (not 1234 or 0000)
2. Enable biometric for convenience
3. Set appropriate timeout (15-30 min recommended)
4. Enable lock on background for sensitive data
5. Don't share your PIN
6. Change PIN periodically
### For Developers
1. Never log PINs or hashes
2. Use HTTPS in production
3. Monitor failed attempts
4. Implement rate limiting
5. Rotate salts on PIN change
6. Clear sensitive data on sign out
7. Test across devices/browsers
---
## Support
For issues or questions:
- GitHub Issues: https://github.com/nself/nself-chat/issues
- Documentation: https://docs.nself.org
- Email: support@nself.org
---
## Changelog
### v0.3.0 (2026-01-30)
- Initial PIN lock implementation
- Client-side PBKDF2 hashing
- WebAuthn biometric support
- Failed attempt lockout
- Session timeout management
- Emergency unlock flow