mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-06 17:37:51 +08:00
polished items from the re-audit report by Claude, add Ubuntu live ISO method to README
This commit is contained in:
4
Makefile
4
Makefile
@@ -96,8 +96,8 @@ build-tails:
|
|||||||
@echo "🔨 Building for TailsOS (relative paths + embedded CSP)..."
|
@echo "🔨 Building for TailsOS (relative paths + embedded CSP)..."
|
||||||
VITE_BASE_PATH="./" bun run vite build
|
VITE_BASE_PATH="./" bun run vite build
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "🔒 Injecting production CSP into index.html..."
|
@echo "🔒 Injecting production CSP into index.html (replacing baseline CSP)..."
|
||||||
@perl -i.bak -pe 's|(<head>)|$$1\n<meta http-equiv="Content-Security-Policy" content="default-src '"'"'self'"'"'; script-src '"'"'self'"'"' '"'"'unsafe-inline'"'"' '"'"'wasm-unsafe-eval'"'"'; style-src '"'"'self'"'"' '"'"'unsafe-inline'"'"'; img-src '"'"'self'"'"' data: blob:; connect-src '"'"'self'"'"' blob: data:; font-src '"'"'self'"'"'; object-src '"'"'none'"'"'; media-src '"'"'self'"'"' blob:; base-uri '"'"'self'"'"'; form-action '"'"'none'"'"';" data-env="tails">|' dist/index.html
|
@perl -i.bak -0777 -pe 's|<meta\s+http-equiv="Content-Security-Policy"[^>]*/>|<meta http-equiv="Content-Security-Policy" content="default-src '"'"'self'"'"'; script-src '"'"'self'"'"' '"'"'unsafe-inline'"'"' '"'"'wasm-unsafe-eval'"'"'; style-src '"'"'self'"'"' '"'"'unsafe-inline'"'"'; img-src '"'"'self'"'"' data: blob:; connect-src '"'"'self'"'"' blob: data:; font-src '"'"'self'"'"'; object-src '"'"'none'"'"'; media-src '"'"'self'"'"' blob:; base-uri '"'"'self'"'"'; form-action '"'"'none'"'"';" data-env="tails">|' dist/index.html
|
||||||
@rm -f dist/index.html.bak
|
@rm -f dist/index.html.bak
|
||||||
@echo "✅ CSP embedded in dist/index.html"
|
@echo "✅ CSP embedded in dist/index.html"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|||||||
282
README.md
282
README.md
@@ -36,11 +36,13 @@ make serve-local
|
|||||||
|----------------|-------------------|---------------|------|
|
|----------------|-------------------|---------------|------|
|
||||||
| **Testing** (<$100) | Any computer, local mode | `make build-offline` | 5 min |
|
| **Testing** (<$100) | Any computer, local mode | `make build-offline` | 5 min |
|
||||||
| **Real Use** ($100–$10K) | Clean computer, network disabled | `make build-offline` | 15 min |
|
| **Real Use** ($100–$10K) | Clean computer, network disabled | `make build-offline` | 15 min |
|
||||||
| **Serious** ($10K–$100K) | **TailsOS airgapped** | `make full-build-tails` | 30 min |
|
| **Serious** ($10K–$100K) | **TailsOS or Ubuntu Live (airgapped)** | `make full-build-tails` | 30 min |
|
||||||
| **Vault** (>$100K) | TailsOS + hardware wallet + multisig | `make full-build-tails` | 1+ hour |
|
| **Vault** (>$100K) | TailsOS + hardware wallet + multisig | `make full-build-tails` | 1+ hour |
|
||||||
|
|
||||||
**The more funds at stake, the more security precautions you take.**
|
**The more funds at stake, the more security precautions you take.**
|
||||||
|
|
||||||
|
**Note:** TailsOS and Ubuntu Live USB provide equivalent security for offline seed operations. See Path 1 (TailsOS) and Path 3 (Ubuntu Live) below for detailed workflows.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 Makefile Commands Reference
|
## 🔧 Makefile Commands Reference
|
||||||
@@ -172,6 +174,8 @@ make install
|
|||||||
make full-build-tails
|
make full-build-tails
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** All builds include a baseline CSP in index.html, but the `make full-build-tails` pipeline injects a stricter, WASM-compatible CSP tailored for TailsOS.
|
||||||
|
|
||||||
**What `make full-build-tails` does:**
|
**What `make full-build-tails` does:**
|
||||||
|
|
||||||
1. **Cleans** all previous build artifacts
|
1. **Cleans** all previous build artifacts
|
||||||
@@ -270,6 +274,282 @@ make serve-local
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🐧 Path 3: Ubuntu Live USB (Alternative to TailsOS)
|
||||||
|
|
||||||
|
**Ubuntu Live USB provides equivalent security to TailsOS** for offline seed operations. It's RAM-only, amnesic (data erased on shutdown), and may be more familiar if you're already comfortable with Ubuntu.
|
||||||
|
|
||||||
|
### When to Use Ubuntu Live USB
|
||||||
|
|
||||||
|
- ✅ You're already familiar with Ubuntu/Linux workflows
|
||||||
|
- ✅ You only need offline operations (no Tor required)
|
||||||
|
- ✅ You want faster boot time (~1 min vs Tails ~2 min)
|
||||||
|
- ✅ You might need to install additional tools during the session
|
||||||
|
|
||||||
|
### Security Properties
|
||||||
|
|
||||||
|
| Feature | Ubuntu Live USB | TailsOS |
|
||||||
|
|---------|-----------------|---------|
|
||||||
|
| RAM-only execution | ✅ Yes | ✅ Yes |
|
||||||
|
| Amnesic (data erased on poweroff) | ✅ Yes | ✅ Yes |
|
||||||
|
| Network isolation | ⚠️ Manual disable | ✅ Automatic (Tor-only) |
|
||||||
|
| Pre-installed crypto tools | ❌ Need Python | ✅ GPG, KeePassXC built-in |
|
||||||
|
| Boot time | ~1 min | ~2 min |
|
||||||
|
| Best for | Offline seed ops | Offline + Tor workflows |
|
||||||
|
|
||||||
|
**For your use case (offline seed blending): Both are equivalent.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 1: Prepare Ubuntu Live USB
|
||||||
|
|
||||||
|
**On your regular computer:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download Ubuntu Desktop LTS ISO
|
||||||
|
wget https://releases.ubuntu.com/24.04/ubuntu-24.04-desktop-amd64.iso
|
||||||
|
|
||||||
|
# Verify SHA256 checksum
|
||||||
|
sha256sum ubuntu-24.04-desktop-amd64.iso
|
||||||
|
# Compare against official checksum from ubuntu.com/download
|
||||||
|
|
||||||
|
# Create bootable USB (Linux/Mac)
|
||||||
|
sudo dd if=ubuntu-24.04-desktop-amd64.iso of=/dev/sdX bs=4M status=progress
|
||||||
|
# ⚠️ Replace /dev/sdX with your USB device (check with 'lsblk')
|
||||||
|
|
||||||
|
# Windows: Use Rufus or balenaEtcher instead
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prepare SeedPGP on a separate USB drive:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone and build on your trusted computer
|
||||||
|
git clone https://github.com/kccleoc/seedpgp-web.git
|
||||||
|
cd seedpgp-web
|
||||||
|
make install
|
||||||
|
make full-build-tails # Creates dist-tails/ with embedded CSP
|
||||||
|
|
||||||
|
# Generate checksum file
|
||||||
|
cd dist-tails
|
||||||
|
sha256sum index.html > CHECKSUM.txt
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Copy to second USB drive (label it "SEEDPGP-OFFLINE")
|
||||||
|
cp -r dist-tails/ /media/your-usb/seedpgp-offline/
|
||||||
|
```
|
||||||
|
|
||||||
|
**You now have:**
|
||||||
|
1. **USB #1:** Ubuntu Live bootable installer
|
||||||
|
2. **USB #2:** Pre-verified SeedPGP build with checksums
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: Boot Ubuntu Live (Network Disabled)
|
||||||
|
|
||||||
|
**Physical security checklist:**
|
||||||
|
|
||||||
|
```
|
||||||
|
□ Unplug Ethernet cable from computer
|
||||||
|
□ Remove SIM card (if using a laptop with cellular)
|
||||||
|
□ Put phone in airplane mode (away from desk)
|
||||||
|
□ Close curtains (prevent shoulder surfing)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Boot process:**
|
||||||
|
|
||||||
|
1. Insert Ubuntu Live USB (#1)
|
||||||
|
2. Reboot computer and press **F12/F2/ESC** during startup
|
||||||
|
3. Select USB drive from boot menu
|
||||||
|
4. Choose **"Try Ubuntu"** (NOT "Install Ubuntu")
|
||||||
|
5. **IMMEDIATELY after desktop loads:** Click network icon → **Disable Wi-Fi**
|
||||||
|
6. Verify network status in terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ip link show
|
||||||
|
# All interfaces should show 'state DOWN' except 'lo' (loopback)
|
||||||
|
|
||||||
|
# Confirm no external routes
|
||||||
|
ip route
|
||||||
|
# Should ONLY show: 127.0.0.0/8 dev lo
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3: Verify Clean State
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Open Terminal (Ctrl+Alt+T)
|
||||||
|
|
||||||
|
# Check no mounted writable drives
|
||||||
|
mount | grep -v "ro,"
|
||||||
|
# Should only show read-only mounts (iso9660, squashfs)
|
||||||
|
|
||||||
|
# Check no swap space
|
||||||
|
swapon --show
|
||||||
|
# Should return nothing
|
||||||
|
|
||||||
|
# Verify RAM usage
|
||||||
|
free -h
|
||||||
|
# Should show ~2-4GB used (OS running entirely in RAM)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4: Load and Verify SeedPGP
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Insert USB #2 (SEEDPGP-OFFLINE)
|
||||||
|
# It will auto-mount to /media/ubuntu/SEEDPGP-OFFLINE or similar
|
||||||
|
|
||||||
|
# Navigate to the build folder
|
||||||
|
cd /media/ubuntu/*/seedpgp-offline/
|
||||||
|
# Or use: cd /media/ubuntu/SEEDPGP-OFFLINE/seedpgp-offline/
|
||||||
|
|
||||||
|
# Verify integrity before running
|
||||||
|
sha256sum -c CHECKSUM.txt
|
||||||
|
# Should output: index.html: OK
|
||||||
|
|
||||||
|
# If verification fails → STOP! Do not proceed.
|
||||||
|
# Re-build on your trusted computer and copy again.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 5: Serve Locally with Python
|
||||||
|
|
||||||
|
**Important:** You cannot open `file://` URLs directly in modern browsers due to CORS restrictions. You must serve over HTTP on localhost.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start Python HTTP server (Python 3 is pre-installed)
|
||||||
|
python3 -m http.server 8000 &
|
||||||
|
# The '&' runs it in background
|
||||||
|
|
||||||
|
# You'll see:
|
||||||
|
# Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security verification: Localhost is safe**
|
||||||
|
|
||||||
|
Even though the server listens on `0.0.0.0:8000` (all interfaces), there are **no active network interfaces** to reach it from outside:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify localhost-only access
|
||||||
|
sudo ss -tlnp | grep 8000
|
||||||
|
# Shows: LISTEN 0.0.0.0:8000 (looks exposed, but...)
|
||||||
|
|
||||||
|
# Check which interfaces exist
|
||||||
|
ip addr show
|
||||||
|
# Should ONLY show 'lo' (loopback 127.0.0.1) with status UP
|
||||||
|
# No eth0, wlan0, or other interfaces should be UP
|
||||||
|
|
||||||
|
# Try accessing from "outside" (this should fail)
|
||||||
|
curl http://192.168.1.100:8000 # Use any typical LAN IP
|
||||||
|
# Should instantly fail: "Network unreachable"
|
||||||
|
```
|
||||||
|
|
||||||
|
**The key:** Even though Python binds to `0.0.0.0`, there are no physical network paths to reach it. Localhost is a kernel-internal loopback interface.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 6: Open in Firefox
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Launch Firefox with localhost URL
|
||||||
|
firefox http://localhost:8000 &
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify the app loaded correctly:**
|
||||||
|
|
||||||
|
1. SeedPGP interface appears
|
||||||
|
2. Check browser console (F12) for CSP enforcement:
|
||||||
|
- Should see no CSP violation errors
|
||||||
|
- Network tab should show only localhost requests
|
||||||
|
3. Verify "Network BLOCKED" indicator in app header
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 7: Use SeedPGP
|
||||||
|
|
||||||
|
Now proceed with your seed operations (see "Using SeedPGP: The Workflow" section below):
|
||||||
|
|
||||||
|
- Generate entropy (dice rolls recommended)
|
||||||
|
- Blend multiple hardware wallet seeds (if applicable)
|
||||||
|
- Encrypt to PGP key or password
|
||||||
|
- Export QR backup
|
||||||
|
- **Write final seed to paper immediately**
|
||||||
|
|
||||||
|
**⚠️ CRITICAL:** Never save anything to disk. All data stays in RAM.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 8: Shutdown and Verify Data Erasure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop the Python server (not strictly necessary, but good practice)
|
||||||
|
killall python3
|
||||||
|
|
||||||
|
# Power off Ubuntu Live
|
||||||
|
sudo poweroff
|
||||||
|
|
||||||
|
# Physical verification:
|
||||||
|
□ Remove both USB drives
|
||||||
|
□ All RAM contents are erased (power loss = data loss)
|
||||||
|
□ No trace left on computer's hard drive
|
||||||
|
```
|
||||||
|
|
||||||
|
**What just happened:**
|
||||||
|
|
||||||
|
- ✅ All seed operations occurred in RAM only
|
||||||
|
- ✅ Python HTTP server never had external network access
|
||||||
|
- ✅ SeedPGP never wrote to persistent storage
|
||||||
|
- ✅ Shutdown wiped all RAM contents
|
||||||
|
- ✅ Computer's hard drive was never touched (read-only boot)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Optional: Advanced Hardening
|
||||||
|
|
||||||
|
If you want to match TailsOS-level security:
|
||||||
|
|
||||||
|
**1. Disable swap (already disabled by default, but verify):**
|
||||||
|
```bash
|
||||||
|
sudo swapoff -a
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Clear clipboard before shutdown:**
|
||||||
|
```bash
|
||||||
|
# If you copied anything sensitive
|
||||||
|
echo "" | xclip -selection clipboard
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Wipe RAM on shutdown (paranoid mode):**
|
||||||
|
```bash
|
||||||
|
# For protection against cold-boot attacks (freezing RAM with liquid nitrogen)
|
||||||
|
sudo apt install secure-delete
|
||||||
|
sudo sdmem -v # Takes ~2 min, overwrites RAM with random data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** For your threat model (protecting seeds from remote attackers, not physical access to frozen RAM), step 3 is unnecessary.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Ubuntu Live vs TailsOS: Summary
|
||||||
|
|
||||||
|
**Use Ubuntu Live USB if:**
|
||||||
|
- You're already comfortable with Ubuntu
|
||||||
|
- You only need offline seed operations
|
||||||
|
- You want faster boot time
|
||||||
|
- You value familiarity over maximum security
|
||||||
|
|
||||||
|
**Use TailsOS if:**
|
||||||
|
- You want zero-config maximum security
|
||||||
|
- You might need Tor for other operations
|
||||||
|
- You're handling $100K+ and want the most audited option
|
||||||
|
- You want automatic MAC randomization and anti-forensics
|
||||||
|
|
||||||
|
**For your use case (three-hardware-wallet blend on Ubuntu Live): ✅ Perfectly safe.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🔐 Using SeedPGP: The Workflow
|
## 🔐 Using SeedPGP: The Workflow
|
||||||
|
|
||||||
### Step 1: Generate Entropy (New Seed)
|
### Step 1: Generate Entropy (New Seed)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# SeedPGP Web - TailsOS Offline Build
|
# SeedPGP Web - TailsOS Offline Build
|
||||||
|
|
||||||
Built: Wed Feb 18 03:15:54 HKT 2026
|
Built: Thu 19 Feb 2026 22:31:58 HKT
|
||||||
|
|
||||||
Usage Instructions:
|
Usage Instructions:
|
||||||
1. Copy this entire folder to a USB drive
|
1. Copy this entire folder to a USB drive
|
||||||
@@ -18,6 +18,6 @@ Security Features:
|
|||||||
|
|
||||||
SHA-256 Checksums:
|
SHA-256 Checksums:
|
||||||
5cbbcb8adc7acc3b78a3fd31c76d573302705ff5fd714d03f5a2602591197cb5 ./assets/secp256k1-Cao5Swmf.wasm
|
5cbbcb8adc7acc3b78a3fd31c76d573302705ff5fd714d03f5a2602591197cb5 ./assets/secp256k1-Cao5Swmf.wasm
|
||||||
78cb021ce6777d4ca58fa225d60de2401a14624187297d4bc9f5394b0de6c05c ./assets/index-DTLOeMVw.js
|
|
||||||
aab3ea208db02b2cb40902850c203f23159f515288b26ca5a131e1188b4362af ./assets/index-DW74Yc8k.css
|
aab3ea208db02b2cb40902850c203f23159f515288b26ca5a131e1188b4362af ./assets/index-DW74Yc8k.css
|
||||||
f8f37cb2c6c247c87b17cf50458150d81cd7fd15d354ab5b38f2a56e9f00cf32 ./index.html
|
c5d6ba57285386d3c4a4e082b831ca24e6e925d7e25a4c38533a10e06c37b238 ./assets/index-Bwz_2nW3.js
|
||||||
|
c7cd63f8c0a39b0aca861668029aa569597e3b4f9bcd2e40aa274598522e0e8e ./index.html
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2,15 +2,15 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' blob: data:; font-src 'self'; object-src 'none'; media-src 'self' blob:; base-uri 'self'; form-action 'none';" data-env="tails">
|
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>SeedPGP Web</title>
|
<title>SeedPGP Web</title>
|
||||||
|
|
||||||
<!-- CSP is enforced by _headers file in production deployment -->
|
<!-- Baseline CSP for generic builds.
|
||||||
<!-- No CSP in dev mode to allow Vite HMR -->
|
TailsOS builds override this via Makefile (build-tails target). -->
|
||||||
<script type="module" crossorigin src="./assets/index-DTLOeMVw.js"></script>
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' blob: data:; font-src 'self'; object-src 'none'; media-src 'self' blob:; base-uri 'self'; form-action 'none';" data-env="tails">
|
||||||
|
<script type="module" crossorigin src="./assets/index-Bwz_2nW3.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-DW74Yc8k.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-DW74Yc8k.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -18,4 +18,4 @@
|
|||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
20
index.html
20
index.html
@@ -7,8 +7,22 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>SeedPGP Web</title>
|
<title>SeedPGP Web</title>
|
||||||
|
|
||||||
<!-- CSP is enforced by _headers file in production deployment -->
|
<!-- Baseline CSP for generic builds.
|
||||||
<!-- No CSP in dev mode to allow Vite HMR -->
|
TailsOS builds override this via Makefile (build-tails target). -->
|
||||||
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="
|
||||||
|
default-src 'self';
|
||||||
|
script-src 'self' 'unsafe-inline';
|
||||||
|
style-src 'self' 'unsafe-inline';
|
||||||
|
img-src 'self' data: blob:;
|
||||||
|
connect-src 'self';
|
||||||
|
font-src 'self';
|
||||||
|
object-src 'none';
|
||||||
|
base-uri 'self';
|
||||||
|
form-action 'none';
|
||||||
|
"
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -16,4 +30,4 @@
|
|||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -552,9 +552,18 @@ function App() {
|
|||||||
|
|
||||||
// 6. Block Service Workers
|
// 6. Block Service Workers
|
||||||
if (navigator.serviceWorker) {
|
if (navigator.serviceWorker) {
|
||||||
|
// Block new registrations
|
||||||
(navigator.serviceWorker as any).register = async () => {
|
(navigator.serviceWorker as any).register = async () => {
|
||||||
throw new Error('Network blocked: Service Workers disabled');
|
throw new Error('Network blocked: Service Workers disabled');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Unregister any existing service workers (defense-in-depth)
|
||||||
|
navigator.serviceWorker
|
||||||
|
.getRegistrations()
|
||||||
|
.then(regs => regs.forEach(reg => reg.unregister()))
|
||||||
|
.catch(() => {
|
||||||
|
// Ignore errors; SWs are defense-in-depth only.
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -72,24 +72,28 @@ const AudioEntropy: React.FC<AudioEntropyProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestMicrophoneAccess = async () => {
|
const requestMicrophoneAccess = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('🎤 Requesting microphone access...');
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('🎤 Requesting microphone access...');
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up any existing audio context first
|
// Clean up any existing audio context first
|
||||||
await teardownAudio();
|
await teardownAudio();
|
||||||
|
|
||||||
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: {
|
audio: {
|
||||||
echoCancellation: false,
|
echoCancellation: false,
|
||||||
noiseSuppression: false,
|
noiseSuppression: false,
|
||||||
autoGainControl: false,
|
autoGainControl: false,
|
||||||
sampleRate: { ideal: 44100 }, // Safari prefers this
|
sampleRate: { ideal: 44100 }, // Safari prefers this
|
||||||
channelCount: 1,
|
channelCount: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ Microphone access granted');
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('✅ Microphone access granted');
|
||||||
|
}
|
||||||
|
|
||||||
// Set up Web Audio API
|
// Set up Web Audio API
|
||||||
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||||
@@ -138,7 +142,7 @@ const AudioEntropy: React.FC<AudioEntropyProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deterministic logging every 30 frames
|
// Deterministic logging every 30 frames
|
||||||
if (frameCounterRef.current++ % 30 === 0) {
|
if (frameCounterRef.current++ % 30 === 0 && import.meta.env.DEV) {
|
||||||
console.log('🎙️ RAW mic RMS:', rawRms.toFixed(4), 'Sample:', inputBuffer.slice(0, 5));
|
console.log('🎙️ RAW mic RMS:', rawRms.toFixed(4), 'Sample:', inputBuffer.slice(0, 5));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -148,17 +152,23 @@ const AudioEntropy: React.FC<AudioEntropyProps> = ({
|
|||||||
scriptProcessor.connect(silentGain); // pull it via the same sink path
|
scriptProcessor.connect(silentGain); // pull it via the same sink path
|
||||||
scriptProcessorRef.current = scriptProcessor;
|
scriptProcessorRef.current = scriptProcessor;
|
||||||
|
|
||||||
console.log('✅ ScriptProcessor active (Safari fallback)');
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('✅ ScriptProcessor active (Safari fallback)');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('⚠️ ScriptProcessor not supported');
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('⚠️ ScriptProcessor not supported');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🎧 Pipeline primed:', {
|
if (import.meta.env.DEV) {
|
||||||
sampleRate: audioContext.sampleRate,
|
console.log('🎧 Pipeline primed:', {
|
||||||
state: audioContext.state,
|
sampleRate: audioContext.sampleRate,
|
||||||
fftSize: analyser.fftSize,
|
state: audioContext.state,
|
||||||
channels: analyser.channelCount,
|
fftSize: analyser.fftSize,
|
||||||
});
|
channels: analyser.channelCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
audioContextRef.current = audioContext;
|
audioContextRef.current = audioContext;
|
||||||
analyserRef.current = analyser;
|
analyserRef.current = analyser;
|
||||||
@@ -167,20 +177,26 @@ const AudioEntropy: React.FC<AudioEntropyProps> = ({
|
|||||||
// Resume context
|
// Resume context
|
||||||
if (audioContext.state === 'suspended') {
|
if (audioContext.state === 'suspended') {
|
||||||
await audioContext.resume();
|
await audioContext.resume();
|
||||||
console.log('▶️ Audio context resumed:', audioContext.state);
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('▶️ Audio context resumed:', audioContext.state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give pipeline 300ms to fill buffer
|
// Give pipeline 300ms to fill buffer
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (analyserRef.current) {
|
if (analyserRef.current) {
|
||||||
console.log('▶️ Starting analysis after buffer fill');
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('▶️ Starting analysis after buffer fill');
|
||||||
|
}
|
||||||
startAudioAnalysis();
|
startAudioAnalysis();
|
||||||
setStep('capture');
|
setStep('capture');
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('❌ Microphone error:', err);
|
if (import.meta.env.DEV) {
|
||||||
|
console.error('❌ Microphone error:', err);
|
||||||
|
}
|
||||||
setError(`Microphone access denied: ${err.message}`);
|
setError(`Microphone access denied: ${err.message}`);
|
||||||
setTimeout(() => onCancel(), 2000);
|
setTimeout(() => onCancel(), 2000);
|
||||||
}
|
}
|
||||||
@@ -188,11 +204,15 @@ const AudioEntropy: React.FC<AudioEntropyProps> = ({
|
|||||||
|
|
||||||
const startAudioAnalysis = () => {
|
const startAudioAnalysis = () => {
|
||||||
if (!analyserRef.current) {
|
if (!analyserRef.current) {
|
||||||
console.error('❌ No analyser');
|
if (import.meta.env.DEV) {
|
||||||
|
console.error('❌ No analyser');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Analysis loop started');
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('✅ Analysis loop started');
|
||||||
|
}
|
||||||
|
|
||||||
const analyze = () => {
|
const analyze = () => {
|
||||||
if (!analyserRef.current) return;
|
if (!analyserRef.current) return;
|
||||||
@@ -235,14 +255,18 @@ const AudioEntropy: React.FC<AudioEntropyProps> = ({
|
|||||||
// Log first few + random
|
// Log first few + random
|
||||||
if (!audioLevelLoggedRef.current) {
|
if (!audioLevelLoggedRef.current) {
|
||||||
audioLevelLoggedRef.current = true;
|
audioLevelLoggedRef.current = true;
|
||||||
console.log('📊 First frame:', {
|
if (import.meta.env.DEV) {
|
||||||
rms: rms.toFixed(4),
|
console.log('📊 First frame:', {
|
||||||
level: level.toFixed(1),
|
rms: rms.toFixed(4),
|
||||||
timeSample: timeData.slice(0, 5),
|
level: level.toFixed(1),
|
||||||
freqSample: freqData.slice(0, 5)
|
timeSample: timeData.slice(0, 5),
|
||||||
});
|
freqSample: freqData.slice(0, 5)
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (Math.random() < 0.03) {
|
} else if (Math.random() < 0.03) {
|
||||||
console.log('🎵 Level:', clampedLevel.toFixed(1), 'RMS:', rms.toFixed(4));
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('🎵 Level:', clampedLevel.toFixed(1), 'RMS:', rms.toFixed(4));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setAudioLevel(clampedLevel);
|
setAudioLevel(clampedLevel);
|
||||||
@@ -257,7 +281,9 @@ const AudioEntropy: React.FC<AudioEntropyProps> = ({
|
|||||||
// Auto-start analysis when analyser is ready
|
// Auto-start analysis when analyser is ready
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (analyserRef.current && step === 'capture' && !animationRef.current) {
|
if (analyserRef.current && step === 'capture' && !animationRef.current) {
|
||||||
console.log('🎬 useEffect: Starting audio analysis');
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('🎬 useEffect: Starting audio analysis');
|
||||||
|
}
|
||||||
startAudioAnalysis();
|
startAudioAnalysis();
|
||||||
}
|
}
|
||||||
}, [analyserRef.current, step]);
|
}, [analyserRef.current, step]);
|
||||||
@@ -266,13 +292,17 @@ const AudioEntropy: React.FC<AudioEntropyProps> = ({
|
|||||||
// Ensure audio context is running
|
// Ensure audio context is running
|
||||||
if (audioContextRef.current && audioContextRef.current.state === 'suspended') {
|
if (audioContextRef.current && audioContextRef.current.state === 'suspended') {
|
||||||
await audioContextRef.current.resume();
|
await audioContextRef.current.resume();
|
||||||
console.log('▶️ Audio context resumed on capture');
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('▶️ Audio context resumed on capture');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setStep('processing');
|
setStep('processing');
|
||||||
setCaptureProgress(0);
|
setCaptureProgress(0);
|
||||||
|
|
||||||
console.log('🎙️ Capturing audio entropy...');
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('🎙️ Capturing audio entropy...');
|
||||||
|
}
|
||||||
|
|
||||||
// Capture 3 seconds of audio data
|
// Capture 3 seconds of audio data
|
||||||
const captureDuration = 3000; // 3 seconds
|
const captureDuration = 3000; // 3 seconds
|
||||||
@@ -301,11 +331,15 @@ const AudioEntropy: React.FC<AudioEntropyProps> = ({
|
|||||||
|
|
||||||
// Use raw audio data if available (from ScriptProcessor)
|
// Use raw audio data if available (from ScriptProcessor)
|
||||||
if (rawAudioDataRef.current.length > 0) {
|
if (rawAudioDataRef.current.length > 0) {
|
||||||
console.log('✅ Using raw audio data from ScriptProcessor:', rawAudioDataRef.current.length, 'samples');
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('✅ Using raw audio data from ScriptProcessor:', rawAudioDataRef.current.length, 'samples');
|
||||||
|
}
|
||||||
audioDataRef.current = rawAudioDataRef.current.slice(-totalSamples); // Use most recent samples
|
audioDataRef.current = rawAudioDataRef.current.slice(-totalSamples); // Use most recent samples
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Audio captured:', audioDataRef.current.length, 'samples');
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('✅ Audio captured:', audioDataRef.current.length, 'samples');
|
||||||
|
}
|
||||||
|
|
||||||
// Analyze captured audio
|
// Analyze captured audio
|
||||||
const audioStats = await analyzeAudioEntropy();
|
const audioStats = await analyzeAudioEntropy();
|
||||||
|
|||||||
Reference in New Issue
Block a user