Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions bounty-7/DESIGN.md
Original file line number Diff line number Diff line change
@@ -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) |
147 changes: 147 additions & 0 deletions bounty-7/README.md
Original file line number Diff line number Diff line change
@@ -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';

<LockToggle noteId="note-1" />
```

### Show Lock Screen for Locked Notes

```tsx
import { LockScreen } from './components/LockScreen';

<LockScreen
noteId="note-1"
visible={isLocked}
onUnlocked={() => setIsLocked(false)}
onDismiss={() => navigation.goBack()}
/>
```

### Display Locked Note in List

```tsx
import { LockedNoteCard } from './components/LockedNoteCard';

<LockedNoteCard
note={note}
onPress={() => setShowLockScreen(true)}
/>
```

### Set Up / Change PIN

```tsx
import { PINSetup } from './components/PINSetup';

<PINSetup
mode="create" // or "change"
onComplete={() => {}}
onCancel={() => {}}
/>
```

### Biometric Auth Component

```tsx
import { BiometricAuth } from './components/BiometricAuth';

<BiometricAuth
onAuthenticated={handleSuccess}
onFallback={handleFallbackToPIN}
/>
```

### 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 (
<View>
<Text>Status: {lockState}</Text>
<Button title={isLocked ? 'Unlock' : 'Lock'} onPress={toggle} />
</View>
);
}
```

## Configuration

Adjust global defaults via `lockStore.updateConfig()`:

```ts
lockStore.updateConfig({
timeoutMs: 300000, // 5 min auto-lock
maxPinAttempts: 5, // lockout after 5 failures
lockoutDurationMs: 60000, // 60 sec lockout
authMethod: 'biometric_or_pin',
});
```

## Important Notes

- **PINs are hashed** using a non-cryptographic hash by default. Replace `hashPIN()` in `lockStore.ts` with `expo-crypto` SHA-256 for production use.
- **Biometric state** is checked once on mount. Call `checkBiometrics()` from the hook to re-check.
- **Persistence** is not automatic. Serialize `lockStore.toJSON()` to `expo-secure-store` and restore on launch via `lockStore.fromJSON()`.
- All components are wrapped with `observer()` from `mobx-react-lite` for reactive updates.
Loading