# 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 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//` ### 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 # Replace 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 **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 (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 , assets load correctly. On Tails via , 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**: - **seedpgp-web Security Audit**: See SECURITY_AUDIT_REPORT.md - **BIP39 Standard**: - **Air-gap Best Practices**: - **Bun Documentation**: - **Python HTTP Server**: --- ## 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