diff --git a/bounty-7/DESIGN.md b/bounty-7/DESIGN.md new file mode 100644 index 00000000..6f6f7455 --- /dev/null +++ b/bounty-7/DESIGN.md @@ -0,0 +1,123 @@ +# Design: Note Locking with Biometrics / PIN + +## Architecture Overview + +The note-locking system uses a **centralized MobX store** (`LockStore`) that holds all lock-related state, with a **custom React hook** (`useNoteLock`) bridging the store to components. The auth layer supports **biometric** (Face ID / Touch ID) and **PIN** authentication, with configurable fallback priority. + +``` +┌─────────────────────────────────────────────────┐ +│ Components │ +│ LockToggle LockScreen LockedNoteCard │ +│ BiometricAuth PINSetup │ +└──────────────────────┬──────────────────────────┘ + │ useNoteLock() +┌──────────────────────▼──────────────────────────┐ +│ useNoteLock hook │ +│ - AppState listeners (foreground/background) │ +│ - Biometric availability check │ +│ - Auth orchestration (biometric → PIN fallback) │ +└──────────────────────┬──────────────────────────┘ + │ reads / writes +┌──────────────────────▼──────────────────────────┐ +│ LockStore (MobX) │ +│ - Per-note LockState map │ +│ - Global lock / unlock │ +│ - PIN hashing & verification │ +│ - Auto-lock timeout tracking │ +│ - Biometric status │ +│ - Serialization for persistence │ +└─────────────────────────────────────────────────┘ +``` + +## Component Tree + +``` +App +├── NoteList +│ ├── LockedNoteCard ← shows locked preview +│ │ └── LockToggle ← lock/unlock button +│ └── LockScreen ← full-screen auth gate (modal) +│ ├── BiometricAuth ← Face ID / Touch ID prompt +│ └── PIN entry ← inline 4-digit keypad +└── Settings + ├── BiometricAuth ← toggle / status + └── PINSetup ← create or change PIN +``` + +## Data Flow + +### Locking a Note +1. User taps `LockToggle` → `useNoteLock.toggle()` → `lockStore.lockNote(id)` +2. `LockState` → `'locked'`, `note.isLocked = true` +3. `LockedNoteCard` re-renders (via MobX observer) showing locked UI +4. `LockScreen` modal is presented when user tries to view the locked note + +### Unlocking a Note +1. User taps locked note or unlock button → `useNoteLock.unlock()` +2. Hook checks `authMethod` priority (biometric → PIN fallback) +3. `BiometricAuth` component triggers `LocalAuthentication.authenticateAsync()` +4. If biometrics succeed → `lockStore.unlockNote(id)` → `LockState` → `'unlocked'` +5. If biometrics fail / fallback → PIN entry is shown +6. PIN is verified against stored hash → success unlocks, failure increments counter +7. On 5 consecutive failures → 60-second lockout + +### Auto-Lock (Timeout + Restart) +- `lastUnlockTimestamp` is updated on every unlock +- `AppState` listener in `useNoteLock` detects foreground/background +- On foreground: `checkAutoLock()` computes elapsed time; if > `timeoutMs` (default 5 min), `lockAll()` is called +- On restart: config option `requireAuthOnRestart` ensures locked state is restored from persisted store + +## Auth Method Priority + +| `authMethod` | Behavior | +|----------------------|-------------------------------------------------------| +| `biometric` | Biometric only; no PIN fallback | +| `pin` | PIN only; no biometric fallback | +| `biometric_or_pin` | Biometric first; fallback to PIN on cancel/failure | +| `none` | No authentication (dev/debug) | + +## PIN Security + +- PINs are stored as **non-reversible hashes** (`hashPIN` function — simple FNV-1a-like hash; replace with `expo-crypto` SHA-256 in production) +- Max **5 attempts** before **60-second lockout** +- PIN creation requires confirmation (enter → re-enter) +- PIN change requires old PIN verification + +## State Machine (Per-Note) + +``` + ┌──────────┐ + │ locked │ ◄──── lockAll(), timeout, restart + └────┬─────┘ + │ tap unlock + ▼ + ┌──────────┐ auth success ┌──────────┐ + │ unlocking│ ──────────────────► │ unlocked │ + └──────────┘ └────┬─────┘ + │ tap lock + ▼ + ┌──────────┐ + │ locked │ + └──────────┘ +``` + +## Persistence + +`lockStore.toJSON()` / `lockStore.fromJSON()` serializes: +- `authMethod` +- `_pinHash` +- `lockedNoteIds` +- `isGloballyLocked` +- `config` + +This data should be stored in **expo-secure-store** (keychain) for encrypted persistence across app restarts. + +## Dependencies + +| Package | Purpose | +|------------------------------|----------------------------------| +| `mobx` / `mobx-react-lite` | State management | +| `expo-local-authentication` | Face ID / Touch ID API | +| `@expo/vector-icons` | Lock icons, biometric icons | +| `expo-secure-store` | Encrypted persistence (optional) | +| `expo-crypto` | Secure PIN hashing (optional) | diff --git a/bounty-7/README.md b/bounty-7/README.md new file mode 100644 index 00000000..61f19bf0 --- /dev/null +++ b/bounty-7/README.md @@ -0,0 +1,147 @@ +# Warpspeed Bounty #7 — Note Locking with Biometrics / PIN + +**Bounty: $660** — Lock notes with Face ID, Touch ID, or PIN. + +## Features + +- 🔒 **Lock / unlock individual notes** via lock toggle button +- 👤 **Face ID / Touch ID** authentication using `expo-local-authentication` +- 🔢 **4-digit PIN** with confirmation, change flow, and brute-force lockout +- 🔀 **Smart fallback** — biometrics first, PIN as backup +- 🕐 **Auto-lock after timeout** (configurable, default 5 min) +- 🔄 **Re-auth on app restart** — locks persist across sessions +- 🚫 **Content hidden until auth** — locked cards show blurred preview +- 🎨 **Clear locked-state UI** — lock icons, red accents, locked badge + +## Project Structure + +``` +src/ +├── types.ts # All TypeScript types & constants +├── state/ +│ └── lockStore.ts # MobX store — all lock state & logic +├── hooks/ +│ └── useNoteLock.ts # Custom hook — bridges store → components +└── components/ + ├── LockToggle.tsx # Lock/unlock icon button + ├── LockScreen.tsx # Full-screen auth gate (modal) + ├── LockedNoteCard.tsx # Card showing locked state in list + ├── BiometricAuth.tsx # Face ID / Touch ID handler + └── PINSetup.tsx # PIN creation / change flow +``` + +## Getting Started + +```bash +npm install mobx mobx-react-lite expo-local-authentication @expo/vector-icons +``` + +Wrap your app root with MobX provider (optional — the global `lockStore` singleton can be imported directly): + +```tsx +import { lockStore } from './state/lockStore'; +``` + +## Quick Usage + +### Lock/Unlock a Note + +```tsx +import { LockToggle } from './components/LockToggle'; + + +``` + +### Show Lock Screen for Locked Notes + +```tsx +import { LockScreen } from './components/LockScreen'; + + setIsLocked(false)} + onDismiss={() => navigation.goBack()} +/> +``` + +### Display Locked Note in List + +```tsx +import { LockedNoteCard } from './components/LockedNoteCard'; + + setShowLockScreen(true)} +/> +``` + +### Set Up / Change PIN + +```tsx +import { PINSetup } from './components/PINSetup'; + + {}} + onCancel={() => {}} +/> +``` + +### Biometric Auth Component + +```tsx +import { BiometricAuth } from './components/BiometricAuth'; + + +``` + +### Using the Hook + +```tsx +import { useNoteLock } from './hooks/useNoteLock'; + +function NoteDetail({ id }: { id: string }) { + const { + isLocked, + lockState, + lock, + unlock, + toggle, + hasBiometrics, + hasPIN, + authenticateWithBiometrics, + authenticateWithPIN, + } = useNoteLock({ noteId: id }); + + return ( + + Status: {lockState} +