- commit
- 701d67b561c35f1af8d9b726b13a5a75d4ac7607
- parent
- 6a5c76ec2fd10168544529d393326f8c3816e2d1
- author
- Dami <dami.etoile@gmail.com>
| M | AGENTS.md | 4 | ++-- |
| M | README.md | 26 | ++++++++++++------------ |
| M | docs/ | 28 | +++++++++++++------------- |
| M | src-tauri/ | 4 | ++-- |
| M | src-tauri/ | 32 | +++++++++++++++--------------- |
| M | src-tauri/ | 4 | ++-- |
| M | src-tauri/ | 4 | ++-- |
| M | src-tauri/ | 10 | +++++----- |
| M | src-tauri/ | 4 | ++-- |
| M | src-tauri/ | 6 | +++--- |
| M | src/ | 14 | +++++++------- |
@@ -1,6 +1,6 @@
1-# Keyhome agent guide1+# Sigil agent guide22 3-Keyhome is a Tauri v2 remote desktop app with FIDO2 key authentication over Iroh P2P. The app is built and functional — spikes are done, the core flow works end to end.3+Sigil is a Tauri v2 remote desktop app with FIDO2 key authentication over Iroh P2P. The app is built and functional — spikes are done, the core flow works end to end.44 55 ## Current focus66 @@ -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 |@@ -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 |@@ -1,10 +1,10 @@
11 [package]2-name = "keyhome"2+name = "sigil"33 version = "0.1.0"44 description = "FIDO2-secured remote desktop via Iroh"55 authors = ["Dami"]66 license = "MIT"7-repository = "https://github.com/FelineStateMachine/keyhome"7+repository = "https://github.com/FelineStateMachine/sigil"88 edition = "2024"99 rust-version = "1.85"1010 @@ -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 245-pub struct TitanIdentity {245+pub struct KeyIdentity {246246 pub node_id: String,247247 pub error: Option<String>,248248 }249249 250250 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 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())?@@ -8,8 +8,8 @@ pub mod streaming;
88 99 pub use auth::{1010 fido_device_info, fido_pin_retries, host_registration_status, host_unregister,11- titan_derive_identity, titan_register_host, FidoDeviceInfo, PinRetries, RegistrationStatus,12- TitanIdentity,11+ key_derive_identity, key_register_host, FidoDeviceInfo, KeyIdentity, PinRetries,12+ RegistrationStatus,1313 };1414 pub use input::InputEvent;1515 pub use network::{@@ -8,7 +8,7 @@ use std::sync::Arc;
88 use std::sync::atomic::Ordering;99 use std::time::{Duration, Instant};1010 use tauri::{AppHandle, Emitter, State};11-use super::auth::{derive_iroh_secret_from_titan, load_identity_from_keyring};11+use super::auth::{derive_iroh_secret_from_key, load_identity_from_keyring};1212 use super::input::{InputEvent, InputStreamHandler};1313 use super::state::{AppState, HostState, INPUT_ALPN, FRAME_ALPN};1414 use super::streaming::{byte_to_codec, FrameStreamHandler};@@ -124,7 +124,7 @@ pub async fn iroh_client_connect(
124124 // FIDO2 derivation — 30s timeout so a missing/stuck key surfaces quickly125125 let host_secret = tokio::time::timeout(126126 Duration::from_secs(30),127- tokio::task::spawn_blocking(move || derive_iroh_secret_from_titan(&pin)),127+ tokio::task::spawn_blocking(move || derive_iroh_secret_from_key(&pin)),128128 )129129 .await130130 .map_err(|_| "Security key timed out (30s). Make sure your key is connected.".to_string())?@@ -3,11 +3,11 @@ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
33 use std::sync::{Arc, Mutex};44 use tokio::sync::Mutex as TokioMutex;55 6-pub const FRAME_ALPN: &[u8] = b"keyhome/frame-stream/0";7-pub const INPUT_ALPN: &[u8] = b"keyhome/input-stream/0";8-pub const RPID: &str = "keyhome";9-pub const SALT_MESSAGE: &str = "keyhome-iroh-identity-v1";10-pub const KEYRING_SERVICE: &str = "keyhome";6+pub const FRAME_ALPN: &[u8] = b"sigil/frame-stream/0";7+pub const INPUT_ALPN: &[u8] = b"sigil/input-stream/0";8+pub const RPID: &str = "sigil";9+pub const SALT_MESSAGE: &str = "sigil-iroh-identity-v1";10+pub const KEYRING_SERVICE: &str = "sigil";1111 pub const KEYRING_ENTRY: &str = "host-identity";1212 1313 @@ -37,7 +37,7 @@ fn main() {
3737 .invoke_handler(tauri::generate_handler![3838 commands::auth::fido_device_info,3939 commands::auth::fido_pin_retries,40- commands::auth::titan_derive_identity,40+ commands::auth::key_derive_identity,4141 commands::state::is_daemon_mode,4242 commands::state::set_webcodecs_available,4343 commands::state::is_webcodecs_available,@@ -45,7 +45,7 @@ fn main() {
4545 commands::streaming::set_encoder_config,4646 commands::streaming::detect_available_encoders,4747 commands::auth::host_registration_status,48- commands::auth::titan_register_host,48+ commands::auth::key_register_host,4949 commands::auth::host_unregister,5050 commands::network::iroh_host_start,5151 commands::network::iroh_host_stop,@@ -1,15 +1,15 @@
11 {22 "$schema": "https://schema.tauri.app/config/2",3- "productName": "Keyhome",3+ "productName": "Sigil",44 "version": "0.1.0",5- "identifier": "com.keyhome.app",5+ "identifier": "com.sigil.app",66 "build": {77 "frontendDist": "../src/ui"88 },99 "app": {1010 "windows": [1111 {12- "title": "Keyhome",12+ "title": "Sigil",1313 "width": 1024,1414 "height": 768,1515 "resizable": true,@@ -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;