Files
seedpgp-web/doc/TAILS_OFFLINE_PLAYBOOK.md
LC mac 3bcb343fe3 docs: update version to v1.4.7 and organize documentation
- Update package.json version to v1.4.7
- Update README.md header to v1.4.7
- Update GEMINI.md version references to v1.4.7
- Update RECOVERY_PLAYBOOK.md version to v1.4.7
- Update SECURITY_AUDIT_REPORT.md version to v1.4.7
- Move documentation files to doc/ directory for better organization
- Add new documentation files: LOCAL_TESTING_GUIDE.md, SERVE.md, TAILS_OFFLINE_PLAYBOOK.md
- Add Makefile and serve.ts for improved development workflow
2026-02-13 23:24:26 +08:00

16 KiB

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)

# 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

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

# 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
# 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

# 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

# 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

# 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

# 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

# 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

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

# 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:

# 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

# 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)

# 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:

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:

    cd /media/amnesia/SEEDPGP
    python3 -m http.server 8080 &
    
  4. Open Firefoxhttp://localhost:8080

  5. Generate seed phrase (choose entropy source)

  6. Write on paper (pen & paper only, no screenshots)

  7. Stop server and shutdown:

    killall python3
    sudo poweroff
    

Troubleshooting

Issue: Application USB Not Mounting on Tails

Solution:

# 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:

# 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:

# 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:

# 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:

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:

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:

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:

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


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