cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

official branding

Dami committed 701d67b
commit
701d67b561c35f1af8d9b726b13a5a75d4ac7607
parent
6a5c76ec2fd10168544529d393326f8c3816e2d1
author
Dami <dami.etoile@gmail.com>
11 files changed, +68 -68
MAGENTS.md4++--
MREADME.md26++++++++++++------------
Mdocs/SETUP.md28+++++++++++++-------------
Msrc-tauri/Cargo.toml4++--
Msrc-tauri/src/commands/auth.rs32+++++++++++++++---------------
Msrc-tauri/src/commands/mod.rs4++--
Msrc-tauri/src/commands/network.rs4++--
Msrc-tauri/src/commands/state.rs10+++++-----
Msrc-tauri/src/main.rs4++--
Msrc-tauri/tauri.conf.json6+++---
Msrc/ui/index.html14+++++++-------
M AGENTS.md +2 -2
M README.md +13 -13
@@ -1,10 +1,10 @@
1-# keyhome1+# sigil22 3-A native remote desktop app secured by FIDO2 hardware keys. Plug in a Google Titan or YubiKey, tap it, and connect to your machine over an end-to-end encrypted Iroh tunnel. No passwords, no addresses — the key derives the identity.3+A native remote desktop app secured by FIDO2 hardware keys. Plug in a YubiKey or Google Titan, tap it, and connect to your machine over an end-to-end encrypted Iroh tunnel. No passwords, no addresses — the key derives the identity.44 55 ## What is it?66 7-Keyhome is a Tauri v2 desktop app (Rust backend, vanilla JS frontend) that lets you remote into your machine using a FIDO2 security key as the sole authentication factor. The key produces a `hmac-secret` that becomes the 32-byte seed for an Iroh P2P endpoint. Both host and client derive the same identity from the same key — no address sharing, no passwords, no relay configuration.7+Sigil is a Tauri v2 desktop app (Rust backend, vanilla JS frontend) that lets you remote into your machine using a FIDO2 security key as the sole authentication factor. The key produces a `hmac-secret` that becomes the 32-byte seed for an Iroh P2P endpoint. Both host and client derive the same identity from the same key — no address sharing, no passwords, no relay configuration.88 99 Screen capture and encoding run through an ffmpeg subprocess with hardware acceleration (NVENC, VAAPI, QSV, AMF, VideoToolbox) up to 60fps. The client decodes via WebCodecs with hardware acceleration for H.264, H.265, and AV1. A software fallback (xcap + openh264) is available if ffmpeg isn't installed.1010
@@ -23,7 +23,7 @@ Screen capture and encoding run through an ffmpeg subprocess with hardware accel
2323 2424 - **Rust 1.85+** — [install](https://rustup.rs)2525 - **ffmpeg** — screen capture + hardware encoding26-- **FIDO2 security key** — Google Titan or YubiKey26+- **FIDO2 security key** — YubiKey, Google Titan, or any FIDO2 key with hmac-secret support2727 2828 ### Linux (Ubuntu/Debian)2929
@@ -35,8 +35,8 @@ sudo apt install -y ffmpeg libwebkit2gtk-4.1-dev build-essential curl wget file
3535 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh3636 source "$HOME/.cargo/env"3737 38-git clone https://github.com/FelineStateMachine/keyhome.git39-cd keyhome38+git clone https://github.com/FelineStateMachine/sigil.git39+cd sigil4040 cargo tauri dev4141 ```4242
@@ -47,8 +47,8 @@ On NVIDIA, set `WEBKIT_DISABLE_DMABUF_RENDERER=1` before running.
4747 ```bash4848 brew install ffmpeg4949 cargo install tauri-cli --version "^2"50-git clone https://github.com/FelineStateMachine/keyhome.git51-cd keyhome50+git clone https://github.com/FelineStateMachine/sigil.git51+cd sigil5252 cargo tauri dev5353 ```5454
@@ -59,14 +59,14 @@ Grant **Accessibility** and **Screen Recording** permissions in System Settings
5959 ```powershell6060 choco install ffmpeg6161 cargo install tauri-cli --version "^2"62-git clone https://github.com/FelineStateMachine/keyhome.git63-cd keyhome62+git clone https://github.com/FelineStateMachine/sigil.git63+cd sigil6464 cargo tauri dev6565 ```6666 6767 ### Prebuilt binaries6868 69-Download from the [releases page](https://github.com/FelineStateMachine/keyhome/releases).69+Download from the [releases page](https://github.com/FelineStateMachine/sigil/releases).7070 7171 ### Usage7272
@@ -86,8 +86,8 @@ Full setup details: [docs/SETUP.md](docs/SETUP.md)
8686 | [Architecture](docs/SETUP.md#architecture) | Transport, encoder backends, wire protocol, connection tracking |8787 | [Spikes](docs/SETUP.md#spikes-evidence) | Seven viability spikes with pass/fail evidence |8888 | [Agent Guide](AGENTS.md) | Development context for AI agents working on the codebase |89-| [FIDO2 HID Evidence](docs/evidence/2026-06-23-ctap-hid-fido2-titan-communication.md) | ctap-hid-fido2 communication with Google Titan |90-| [HMAC → Iroh Derivation](docs/evidence/2026-06-23-hmac-iroh-derivation.md) | Titan hmac-secret → Iroh SecretKey proof |89+| [FIDO2 HID Evidence](docs/evidence/2026-06-23-ctap-hid-fido2-titan-communication.md) | ctap-hid-fido2 communication with FIDO2 keys |90+| [HMAC → Iroh Derivation](docs/evidence/2026-06-23-hmac-iroh-derivation.md) | hmac-secret → Iroh SecretKey proof |9191 | [Iroh Native Ping](docs/evidence/2026-06-23-iroh-native-ping.md) | Iroh connectivity between native endpoints |9292 | [YubiKey HMAC Detection](docs/evidence/2026-06-23-yubikey-hmac-detection.md) | YubiKey HMAC-secret detection |9393 | [FIDO2 HID vs hidraw](docs/evidence/2026-06-23-fido2-hid-raw-hidraw.md) | HID vs hidraw approach comparison |
M docs/SETUP.md +14 -14
@@ -1,10 +1,10 @@
1-# Keyhome Setup1+# Sigil Setup22 33 ## Prerequisites44 55 ### Rust (required on all platforms)66 7-Keyhome uses Rust edition 2024 and requires **Rust 1.85 or later**.7+Sigil uses Rust edition 2024 and requires **Rust 1.85 or later**.88 99 ```bash1010 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
@@ -69,8 +69,8 @@ ffmpeg auto-detects the best available encoder in this order: NVENC → QSV →
6969 ### Clone7070 7171 ```bash72-git clone https://github.com/FelineStateMachine/keyhome.git73-cd keyhome72+git clone https://github.com/FelineStateMachine/sigil.git73+cd sigil7474 ```7575 7676 ## Run
@@ -91,26 +91,26 @@ WEBKIT_DISABLE_DMABUF_RENDERER=1 cargo tauri dev
9191 9292 ## Usage9393 94-Keyhome uses a FIDO2 security key (Google Titan) to derive Iroh peer identities. There is no address copy/paste — the key handles identity for you.94+Sigil uses a FIDO2 security key to derive Iroh peer identities. There is no address copy/paste — the key handles identity for you.9595 9696 ### 1. Register (one-time per machine)9797 98-Click **Register** and tap your Titan key. This creates a resident (discoverable) credential stored on the key itself. You only do this once per machine.98+Click **Register** and tap your key. This creates a resident (discoverable) credential stored on the key itself. You only do this once per machine.9999 100100 ### 2. Host101101 102102 Click **Start host**, then either:103103 104-- **Tap your Titan key** — the app derives the Iroh identity from the key and starts listening, or104+- **Tap your key** — the app derives the Iroh identity from the key and starts listening, or105105 - **Use saved keyring** — if you have previously hosted, the app can reuse the stored identity from the system keyring without requiring a tap.106106 107-The host is now reachable over Iroh at an endpoint ID derived from the Titan key.107+The host is now reachable over Iroh at an endpoint ID derived from your key.108108 109109 ### 3. Connect110110 111-Click **Connect** and tap your Titan key. The app derives the same host endpoint ID from the key and dials it over Iroh. The remote screen appears in the viewer canvas. Input (mouse and keyboard) is forwarded to the host.111+Click **Connect** and tap your key. The app derives the same host endpoint ID from the key and dials it over Iroh. The remote screen appears in the viewer canvas. Input (mouse and keyboard) is forwarded to the host.112112 113-> Both machines must use the same Titan key (or a key that produces the same hmac-secret output). The key derivation replaces manual address sharing entirely.113+> Both machines must use the same key (or a key that produces the same hmac-secret output). The key derivation replaces manual address sharing entirely.114114 115115 ## Architecture116116
@@ -122,7 +122,7 @@ Click **Connect** and tap your Titan key. The app derives the same host endpoint
122122 - **Fallback**: xcap capture + openh264 software encoding (if ffmpeg not installed)123123 - **FIDO2**: ctap-hid-fido2 (CTAP 2.0/2.1 over HID)124124 - **Input injection**: enigo (cross-platform mouse/keyboard)125-- **Identity**: Iroh SecretKey derived from Titan hmac-secret extension; no address copy/paste125+- **Identity**: Iroh SecretKey derived from FIDO2 hmac-secret extension; no address copy/paste126126 - **Protocol**: BiStream with a 14-byte frame header:127127 128128 ```
@@ -140,8 +140,8 @@ Codec byte: `0` = H.264, `1` = H.265, `2` = AV1.
140140 |-------|--------|----------------|141141 | 001-iroh-native-ping | ✅ PASS | Iroh endpoints + ALPN routing work in native Rust |142142 | 002-yubikey-hmac-detection | ⚠️ PARTIAL | challenge_response crate works but only for YubiKey (not Titan) |143-| 003-fido2-hid-enumeration | ✅ PASS | ctap-hid-fido2 communicates with Google Titan v2 |144-| 004-hmac-iroh-derivation | ✅ PASS | Titan hmac-secret → Iroh SecretKey → working endpoint (6.5ms RTT) |143+| 003-fido2-hid-enumeration | ✅ PASS | ctap-hid-fido2 communicates with FIDO2 key |144+| 004-hmac-iroh-derivation | ✅ PASS | hmac-secret → Iroh SecretKey → working endpoint (6.5ms RTT) |145145 | 005-frame-stream | ✅ PASS | Screen capture → JPEG → Iroh stream → client receives frames |146146 | 006-input-injection | ✅ PASS | enigo input injection works over Iroh bidirectional stream |147-| 007-titan-no-copy | ✅ PASS | Both sides derive the same Iroh identity from Titan — no address sharing needed |147+| 007-titan-no-copy | ✅ PASS | Both sides derive the same Iroh identity from the same key — no address sharing needed |
M src-tauri/Cargo.toml +2 -2
M src-tauri/src/commands/auth.rs +16 -16
@@ -50,9 +50,9 @@ pub fn clear_identity_from_keyring() -> anyhow::Result<()> {
5050 }5151 }5252 53-// ─── Titan HMAC-Secret Derivation ────────────────────────────────────────────53+// ─── FIDO2 HMAC-Secret Derivation ────────────────────────────────────────────5454 55-pub fn derive_secret_from_titan(pin: &str) -> anyhow::Result<[u8; 32]> {55+pub fn derive_secret_from_key(pin: &str) -> anyhow::Result<[u8; 32]> {5656 let cfg = Cfg::init();5757 let device = FidoKeyHidFactory::create(&cfg)5858 .context("Security key not found. Make sure it is plugged in.")?;
@@ -80,9 +80,9 @@ pub fn derive_secret_from_titan(pin: &str) -> anyhow::Result<[u8; 32]> {
8080 8181 // No resident key — create one8282 let user_entity = PublicKeyCredentialUserEntity::new(83- Some(b"keyhome-user"),84- Some("keyhome"),85- Some("Keyhome"),83+ Some(b"sigil-user"),84+ Some("sigil"),85+ Some("Sigil"),8686 );8787 8888 let challenge = verifier::create_challenge();
@@ -128,8 +128,8 @@ pub fn extract_hmac_secret(assertions: &[Assertion]) -> anyhow::Result<[u8; 32]>
128128 anyhow::bail!("No hmac-secret in assertion response")129129 }130130 131-pub fn derive_iroh_secret_from_titan(pin: &str) -> anyhow::Result<SecretKey> {132- let secret_bytes = derive_secret_from_titan(pin)?;131+pub fn derive_iroh_secret_from_key(pin: &str) -> anyhow::Result<SecretKey> {132+ let secret_bytes = derive_secret_from_key(pin)?;133133 Ok(SecretKey::from_bytes(&secret_bytes))134134 }135135
@@ -242,26 +242,26 @@ pub fn fido_pin_retries() -> PinRetries {
242242 }243243 244244 #[derive(Serialize)]245-pub struct TitanIdentity {245+pub struct KeyIdentity {246246 pub node_id: String,247247 pub error: Option<String>,248248 }249249 250250 #[tauri::command]251-pub async fn titan_derive_identity(pin: String) -> TitanIdentity {251+pub async fn key_derive_identity(pin: String) -> KeyIdentity {252252 let result = tokio::time::timeout(253253 Duration::from_secs(30),254- tokio::task::spawn_blocking(move || derive_iroh_secret_from_titan(&pin)),254+ tokio::task::spawn_blocking(move || derive_iroh_secret_from_key(&pin)),255255 )256256 .await;257257 match result {258- Err(_) => TitanIdentity {258+ Err(_) => KeyIdentity {259259 node_id: String::new(),260260 error: Some("Security key timed out (30s). Check that your key is connected.".into()),261261 },262- Ok(Err(e)) => TitanIdentity { node_id: String::new(), error: Some(format!("Task error: {}", e)) },263- Ok(Ok(Err(e))) => TitanIdentity { node_id: String::new(), error: Some(format!("{:?}", e)) },264- Ok(Ok(Ok(secret))) => TitanIdentity { node_id: secret.public().to_string(), error: None },262+ Ok(Err(e)) => KeyIdentity { node_id: String::new(), error: Some(format!("Task error: {}", e)) },263+ Ok(Ok(Err(e))) => KeyIdentity { node_id: String::new(), error: Some(format!("{:?}", e)) },264+ Ok(Ok(Ok(secret))) => KeyIdentity { node_id: secret.public().to_string(), error: None },265265 }266266 }267267
@@ -295,10 +295,10 @@ pub fn host_registration_status() -> RegistrationStatus {
295295 }296296 297297 #[tauri::command]298-pub async fn titan_register_host(pin: String) -> Result<RegistrationStatus, String> {298+pub async fn key_register_host(pin: String) -> Result<RegistrationStatus, String> {299299 let secret = tokio::time::timeout(300300 Duration::from_secs(30),301- tokio::task::spawn_blocking(move || derive_secret_from_titan(&pin)),301+ tokio::task::spawn_blocking(move || derive_secret_from_key(&pin)),302302 )303303 .await304304 .map_err(|_| "Security key timed out (30s). Make sure your key is connected.".to_string())?
M src-tauri/src/commands/mod.rs +2 -2
M src-tauri/src/commands/network.rs +2 -2
M src-tauri/src/commands/state.rs +5 -5
M src-tauri/src/main.rs +2 -2
M src-tauri/tauri.conf.json +3 -3
M src/ui/index.html +7 -7
@@ -3,7 +3,7 @@
33 <head>44 <meta charset="UTF-8" />55 <meta name="viewport" content="width=device-width, initial-scale=1.0" />6- <title>keyhome</title>6+ <title>sigil</title>77 <link rel="preconnect" href="https://fonts.googleapis.com" />88 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />99 <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Inconsolata:wght@400;500;600&display=swap" rel="stylesheet" />
@@ -447,10 +447,10 @@
447447 </style>448448 </head>449449 <body>450- <!-- Tap titan overlay -->450+ <!-- Tap key overlay -->451451 <div class="overlay-screen hidden" id="tap-overlay" style="z-index: 200;">452452 <div class="overlay-content">453- <h1 id="tap-overlay-title">tap titan</h1>453+ <h1 id="tap-overlay-title">tap key</h1>454454 <p id="tap-overlay-desc">Touch the sensor on your key when it blinks.</p>455455 <div class="overlay-status pending" id="tap-status">waiting...</div>456456 </div>
@@ -459,7 +459,7 @@
459459 <!-- Intro screen -->460460 <div class="overlay-screen hidden" id="intro">461461 <div class="overlay-content">462- <h1>keyhome</h1>462+ <h1>sigil</h1>463463 <p id="intro-desc">type pin</p>464464 465465 <!-- Unregistered: register or connect -->
@@ -543,7 +543,7 @@
543543 544544 <!-- Top bar -->545545 <div class="topbar">546- <span class="brand">keyhome</span>546+ <span class="brand">sigil</span>547547 <span class="sep">·</span>548548 <div class="status-line" id="status-line">549549 <span class="dot" id="status-dot"></span>
@@ -710,7 +710,7 @@
710710 }711711 function hideIntro() { document.getElementById('intro').classList.add('hidden'); }712712 function showTap() {713- document.getElementById('tap-overlay-title').textContent = 'tap titan';713+ document.getElementById('tap-overlay-title').textContent = 'tap key';714714 document.getElementById('tap-overlay-desc').textContent = 'Touch the sensor on your key when it blinks.';715715 document.getElementById('tap-status').textContent = 'waiting...';716716 document.getElementById('tap-overlay').classList.remove('hidden');
@@ -770,7 +770,7 @@
770770 hideIntro();771771 showTap();772772 try {773- const result = await invoke('titan_register_host', { pin });773+ const result = await invoke('key_register_host', { pin });774774 if (result.registered) {775775 hideTap();776776 registered = true;