# Tails Offline Air-Gapped Workflow Playbook

## Overview

This playbook provides step-by-step instructions for using seedpgp-web in a secure, air-gapped environment on Tails, eliminating network exposure entirely.

---

## Phase 1: Prerequisites & Preparation

### 1.1 Requirements

- **Machine A (Build Machine)**: macOS with Bun, TypeScript, and Git installed
- **Tails USB**: 8GB+ USB drive with Tails installed (from tails.boum.org)
- **Application USB**: Separate 2GB+ USB drive for seedpgp-web
- **Network**: Initial internet access on Machine A only

### 1.2 Verify Prerequisites on Machine A (macOS with Bun)

```bash
# Verify Bun is installed
bun --version  # Should be v1.0+

# Verify TypeScript tools
which tsc

# Verify git
git --version

# Clone repository
cd ~/workspace
git clone <repository-url> seedpgp-web
cd seedpgp-web
```

### 1.3 Security Checklist Before Starting

- [ ] Machine A (macOS) is trusted and malware-free (or at minimum risk)
- [ ] Bun is installed and up-to-date
- [ ] Tails USB is downloaded from official tails.boum.org
- [ ] You have physical access to verify USB connections
- [ ] You understand this is offline-only after transfer to Application USB

---

## Phase 2: Build Application Locally (Machine A)

### 2.1 Clone and Verify Code

```bash
cd ~/workspace
git clone https://github.com/seedpgp/seedpgp-web.git
cd seedpgp-web
git log --oneline -5  # Document the commit hash for reference
```

### 2.2 Install Dependencies with Bun

```bash
# Use Bun for faster installation
bun install
```

### 2.3 Code Audit (CRITICAL)

Before building, audit the source for security issues:

- [ ] Review `src/lib/seedpgp.ts` - main crypto logic
- [ ] Review `src/lib/seedblend.ts` - seed blending algorithm
- [ ] Check `src/lib/bip39.ts` - BIP39 implementation
- [ ] Verify no external API calls in code
- [ ] Run `grep -r "fetch\|axios\|http\|api" src/` to find network calls
- [ ] Confirm all dependencies in `bunfig.toml` and `package.json` are necessary

```bash
# Perform initial audit with Bun
bun run audit  # If audit script exists
grep -r "fetch\|axios\|XMLHttpRequest" src/
grep -r "localStorage\|sessionStorage" src/  # Check what data persists
```

### 2.4 Build Production Bundle Using Makefile

```bash
# Using Makefile (recommended)
make build-offline

# Or directly with Bun
bun run build
```

This generates:

- `dist/index.html` - Main HTML file
- `dist/assets/` - Bundled JavaScript, CSS (using relative paths)
- All static assets

### 2.5 Verify Build Output & Test Locally

```bash
# List all generated files
find dist -type f

# Verify no external resource links
grep -r "cloudflare\|googleapis\|cdn\|http:" dist/ || echo "✓ No external URLs found"

# Test locally with Bun's simple HTTP server
bun ./dist/index.html

# Or serve on port 8000
bun --serve --port 8000 ./dist  # deprecated: Bun does not provide a built-in static server
# Use the Makefile: `make serve-local` (runs a Python http.server) or run directly:
# cd dist && python3 -m http.server 8000
# Then open http://localhost:8000 in Safari
```

**Why not file://?**: Safari and Firefox restrict loading local assets via `file://` protocol for security. Using a local HTTP server bypasses this while keeping everything offline.

---

## Phase 3: Prepare Application USB (Machine A - macOS with Bun)

### 3.1 Format USB Drive

```bash
# List USB drives
diskutil list

# Replace diskX with your Application USB (e.g., disk2)
diskutil secureErase freespace 0 /dev/diskX

# Create new partition
diskutil partitionDisk /dev/diskX 1 MBR FAT32 SEEDPGP 0b
```

### 3.2 Copy Built Files to USB

```bash
# Mount should happen automatically, verify:
ls /Volumes/SEEDPGP

# Copy entire dist folder built with make build-offline
cp -R dist/* /Volumes/SEEDPGP/

# Verify copy completed
ls /Volumes/SEEDPGP/
ls /Volumes/SEEDPGP/assets/

# (Optional) Generate integrity hash for verification on Tails
sha256sum dist/* > /Volumes/SEEDPGP/INTEGRITY.sha256
```

### 3.3 Verify USB Contents

```bash
# Ensure index.html exists and is readable
cat /Volumes/SEEDPGP/index.html | head -20

# Check file count matches
echo "Source files:" && find dist -type f | wc -l
echo "USB files:" && find /Volumes/SEEDPGP -type f | wc -l

# Check assets are properly included
find /Volumes/SEEDPGP -type d -name "assets" && echo "✅ Assets folder present"

# Verify no external URLs in assets
grep -r "http:" /Volumes/SEEDPGP/ && echo "⚠️  Warning: HTTP URLs found" || echo "✅ No external URLs"
```

### 3.4 Eject USB Safely

```bash
diskutil eject /Volumes/SEEDPGP
```

---

## Phase 4: Boot Tails & Prepare Environment

### 4.1 Boot Tails from Tails USB

- Power off machine
- Insert Tails USB
- Power on and boot from USB (Cmd+Option during boot on Mac)
- Select "Start Tails"
- **DO NOT connect to network** (decline "Connect to Tor" if prompted)

### 4.2 Insert Application USB

- Once Tails is running, insert Application USB
- Tails should auto-mount it to `/media/amnesia/<random-name>/`

### 4.3 Verify Files Accessible & Start HTTP Server

```bash
# Open terminal in Tails
ls /media/amnesia/
# Should see your Application USB mount

# Navigate to application
cd /media/amnesia/SEEDPGP/

# Verify files are present
ls -la
cat index.html | head -5

# Start local HTTP server (runs completely offline)
python3 -m http.server 8080 &
# Output: Serving HTTP on 0.0.0.0 port 8080

# Verify server is running
curl http://localhost:8080/index.html | head -5
```

**Note:** Python3 is pre-installed on Tails. The http.server runs completely offline—no internet access required, just localhost.

---

## Phase 5: Run Application on Tails

### 5.1 Open Application in Browser via Local HTTP Server

**Why HTTP instead of file://?**

- HTTP is more reliable than `file://` protocol
- Eliminates browser security restrictions
- Identical to local testing on macOS
- Still completely offline (no network exposure)

**Steps:**

1. **In Terminal (where you started the server from Phase 4.3):**
   - Verify server is still running: `ps aux | grep http.server`
   - Should show: `python3 -m http.server 8080`
   - If stopped, restart: `cd /media/amnesia/SEEDPGP && python3 -m http.server 8080 &`

2. **Open Firefox:**
   - Click Firefox icon on desktop
   - In address bar, type: `http://localhost:8080`
   - Press Enter

3. **Verify application loaded:**
   - Page should load completely
   - All UI elements visible
   - No errors in browser console (F12 → Console tab)

### 5.2 Verify Offline Functionality

- [ ] Page loads completely
- [ ] All UI elements are visible
- [ ] No error messages in browser console (F12)
- [ ] Images/assets display correctly
- [ ] No network requests are visible in Network tab (F12)

### 5.3 Test Application Features

**Basic Functionality:**

```
- [ ] Can generate new seed phrase
- [ ] Can input existing seed phrase
- [ ] Can encrypt seed phrase
- [ ] Can generate PGP key
- [ ] QR codes generate correctly
```

**Entropy Sources (all should work offline):**

- [ ] Dice entropy input works
- [ ] User mouse/keyboard entropy captures
- [ ] Random.org is NOT accessible (verify UI indicates offline mode)
- [ ] Audio entropy can be recorded

### 5.4 Generate Your Seed Phrase

1. Navigate to main application
2. Choose entropy source (Dice, Audio, or Interaction)
3. Follow prompts to generate entropy
4. Review generated 12/24-word seed phrase
5. **Write down on paper** (do NOT screenshot, use only pen & paper)
6. Verify BIP39 validation passes

---

## Phase 6: Secure Storage & Export

### 6.1 Export Encrypted Backup (Optional)

If you want to save encrypted backup to USB:

1. Use application's export feature
2. Encrypt with strong passphrase
3. Save to Application USB
4. **Do NOT save to host machine**

### 6.2 Generate PGP Key (Optional)

1. Use seedpgp-web to generate PGP key
2. Export private key (encrypted)
3. Save encrypted to USB if desired
4. **Passphrase should be memorable but not written**

### 6.3 Verify No Leaks

In Firefox Developer Tools (F12):

- **Network tab**: Should show only `localhost:8080` requests (all local)
- **Application/Storage**: Check nothing persistent was written
- **Console**: No fetch/XHR errors to external sites

**To verify server is local-only:**

```bash
# In terminal, check network connections
sudo netstat -tulpn | grep 8080
# Should show: tcp 0 0 127.0.0.1:8080 (LISTEN only on localhost)
# NOT on 0.0.0.0 or external interface
```

---

## Phase 7: Shutdown & Cleanup

### 7.1 Stop HTTP Server & Secure Shutdown

```bash
# Stop the http.server gracefully
killall python3
# Or find the PID and kill it
ps aux | grep http.server
kill -9 <PID>  # Replace <PID> with actual process ID

# Verify it stopped
ps aux | grep http.server  # Should show nothing

# Then power off Tails completely
sudo poweroff

# You can also:
# - Select "Power Off" from Tails menu
# - Or simply close/restart the laptop
```

**Important:** Killing the server ensures no background processes remain before shutdown.

### 7.2 Physical USB Handling

- [ ] Eject Application USB physically
- [ ] Eject Tails USB physically
- [ ] Store both in secure location
- **Tails memory is volatile** - all session data gone after power-off

### 7.3 Host Machine Cleanup (Machine A)

```bash
# Remove build artifacts if desired (optional)
rm -rf dist/

# Clear sensitive files from shell history
history -c

# Optionally wipe Machine A's work directory
rm -rf ~/workspace/seedpgp-web/
```

---

## Phase 8: Verification & Best Practices

### 8.1 Before Production Use - Full Test Run

**Test on macOS with Bun first:**

```bash
cd seedpgp-web
bun install
make build-offline  # Build with relative paths
make serve-local    # Serve on http://localhost:8000
# Open Safari: http://localhost:8000
# Verify: all assets load, no console errors, no network requests
```

1. Complete Phases 1-7 with test run on Tails
2. Verify seed phrase generation works reliably
3. Test entropy sources work offline
4. Confirm PGP key generation (if using)
5. Verify export/backup functionality

### 8.2 Security Best Practices

- [ ] **Air-gap is primary defense**: No network = no exfiltration
- [ ] **Tails is ephemeral**: Always boot fresh, always clean shutdown
- [ ] **Paper backups**: Write seed phrase with pen/paper only
- [ ] **Multiple USBs**: Keep Tails and Application USB separate
- [ ] **Verify hash**: Optional - generate hash of `dist/` folder to verify integrity on future builds

### 8.3 Future Seed Generation

Repeat these steps for each new seed phrase:

1. **Boot Tails** from Tails USB (network disconnected)
2. **Insert Application USB** when Tails is running
3. **Start HTTP server:**

   ```bash
   cd /media/amnesia/SEEDPGP
   python3 -m http.server 8080 &
   ```

4. **Open Firefox** → `http://localhost:8080`
5. **Generate seed phrase** (choose entropy source)
6. **Write on paper** (pen & paper only, no screenshots)
7. **Stop server and shutdown:**

   ```bash
   killall python3
   sudo poweroff
   ```

---

## Troubleshooting

### Issue: Application USB Not Mounting on Tails

**Solution**:

```bash
# Check if recognized
sudo lsblk

# Manual mount
sudo mkdir -p /media/usb
sudo mount /dev/sdX1 /media/usb
ls /media/usb
```

### Issue: Black Screen / Firefox Won't Start

**Solution**:

- Let Tails fully boot (may take 2-3 minutes)
- Try manually starting Firefox from Applications menu
- Check memory requirements (Tails recommends 2GB+ RAM)

### Issue: Assets Not Loading (Broken Images/Styling)

**Solution**:

```bash
# Verify file structure on USB
ls -la /media/amnesia/SEEDPGP/assets/

# Check permissions
chmod -R 755 /media/amnesia/SEEDPGP/
```

### Issue: Browser Console Shows Errors

**Solution**:

- Check if `index.html` references external URLs
- Verify `vite.config.ts` doesn't have external dependencies
- Review network tab - should show only `localhost:8080` requests

### Issue: Can't Access <http://localhost:8080>

**Solution**:

```bash
# Verify http.server is running
ps aux | grep http.server

# If not running, restart it
cd /media/amnesia/SEEDPGP
python3 -m http.server 8080 &

# If port 8080 is in use, try another port
python3 -m http.server 8081 &
# Then access http://localhost:8081
```

### Issue: "Connection refused" in Firefox

**Solution**:

```bash
# Check if port is listening
sudo netstat -tulpn | grep 8080

# If not, the server stopped. Restart it:
cd /media/amnesia/SEEDPGP
python3 -m http.server 8080 &

# Wait a few seconds and refresh Firefox (Cmd+R or Ctrl+R)
```

---

## Security Checklist Summary

Before each use:

- [ ] Using Tails booted from USB (never host OS)
- [ ] Application USB inserted (separate from Tails USB)
- [ ] Network disconnected or Tor disabled
- [ ] HTTP server started: `python3 -m http.server 8080` from USB
- [ ] Accessing <http://localhost:8080> (not file://)
- [ ] Firefox console shows no external requests
- [ ] All entropy sources working offline
- [ ] Seed phrase written on paper only
- [ ] HTTP server stopped before shutdown
- [ ] USB ejected after use
- [ ] Tails powered off completely

---

## Appendix A: Makefile Commands Quick Reference

All build commands are available via Makefile on Machine A:

```bash
make help              # Show all available commands
make install           # Install Bun dependencies
make build-offline     # Build with relative paths (for Tails/offline)
make serve-local       # Test locally on http://localhost:8000
make audit             # Security audit for network calls
make verify-offline    # Verify offline compatibility
make full-build-offline  # Complete pipeline: clean → build → verify → audit
make clean             # Remove dist/ folder
```

**Example workflow:**

```bash
cd seedpgp-web
make install
make audit              # Security check
make full-build-offline # Build and verify
# Copy to USB when ready
```

---

## Appendix B: Local Testing on macOS Before Tails

**Why test locally first?**

- Catch build issues early
- Verify all assets load correctly
- Confirm no network requests
- Validation before USB transfer

**Steps:**

```bash
cd seedpgp-web
make build-offline  # Build with relative paths
make serve-local    # Start local server
# Open Safari: http://localhost:8000
# Test functionality, then Ctrl+C to stop
```

When served locally on <http://localhost:8000>, assets load correctly. On Tails via <http://localhost:8080>, the same relative paths work seamlessly.

---

## Appendix C: Why HTTP Server Instead of file:// Protocol?

### The Problem with file:// Protocol

Opening `file:///path/to/index.html` directly has limitations:

- **Browser security restrictions** - Some features may be blocked or behave unexpectedly
- **Asset loading issues** - Sporadic failures with relative/absolute paths
- **localStorage limitations** - Storage APIs may not work reliably
- **CORS restrictions** - Even local files face CORS-like restrictions on some browsers
- **Debugging difficulty** - Hard to distinguish app issues from browser security issues

### Why http.server Solves This

Python's `http.server` module:

1. **Mimics production environment** - Behaves like a real web server
2. **Completely offline** - Server runs only on localhost (127.0.0.1:8080)
3. **No internet required** - No connection to external servers
4. **Browser compatible** - Works reliably across Firefox, Safari, Chrome
5. **Identical to macOS testing** - Same mechanism for both platforms
6. **Simple & portable** - Python3 comes pre-installed on Tails

**Verify the server is local-only:**

```bash
sudo netstat -tulpn | grep 8080
# Output should show: 127.0.0.1:8080 LISTEN (localhost only)
# NOT 0.0.0.0:8080 (would indicate public access)
```

This ensures your seedpgp-web app runs in a standard HTTP environment without any network exposure. ✅

---

## Additional Resources

- **Tails Documentation**: <https://tails.boum.org/doc/>
- **seedpgp-web Security Audit**: See SECURITY_AUDIT_REPORT.md
- **BIP39 Standard**: <https://github.com/trezor/python-mnemonic>
- **Air-gap Best Practices**: <https://en.wikipedia.org/wiki/Air_gap_(networking)>
- **Bun Documentation**: <https://bun.sh/docs>
- **Python HTTP Server**: <https://docs.python.org/3/library/http.server.html>

---

## Version History

- **v2.1** - February 13, 2026 - Updated to use Python http.server instead of file:// for reliability
- **v2.0** - February 13, 2026 - Updated for Bun, Makefile, and offline compatibility
- **v1.0** - February 13, 2026 - Initial playbook creation
