From a93a3ba5fa825fc102962588afd784627d0416ac Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Tue, 26 May 2026 14:10:16 +0530 Subject: [PATCH 1/2] fix: graceful decrypt failure + CSP for Rive WASM mascot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs fixed: 1. config decrypt crash (load.rs): when a channel token (e.g. discord.bot_token) is stored with enc2: encryption but the decryption key is no longer accessible (keyring reset, machine migration, key rotation), decrypt_optional_secret was propagating a hard error via ? on every config load. This made the app unusable after login — every RPC that loaded config failed with "Failed to decrypt discord.bot_token". Fix: match on decrypt result, log a warn and clear the field on failure so config loads successfully with the affected integration disabled. Adds regression test config_load_succeeds_when_decryption_key_inaccessible. 2. Rive mascot invisible (tauri.conf.json): PR #2659 replaced the SVG Ghosty mascot with a Rive WebGL2 renderer. Rive requires WebAssembly compilation (WebAssembly.instantiate), which CEF blocks because the auto-generated script-src CSP only contains 'self' + sha256 hashes — no eval-related source. Fix: add an explicit script-src directive with 'wasm-unsafe-eval' (CSP Level 3 — permits WASM compilation without enabling arbitrary eval). Tauri appends its sha256 hashes to this directive at compile time. --- app/src-tauri/tauri.conf.json | 2 +- src/openhuman/config/schema/load.rs | 19 +++++++--- src/openhuman/config/schema/load_tests.rs | 42 +++++++++++++++++++++++ 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index 98bcdd9dd..66adfd9dc 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -23,7 +23,7 @@ } ], "security": { - "csp": "default-src 'self' 'unsafe-inline' data: blob: https: wss: ipc: http://ipc.localhost http://127.0.0.1:* http://localhost:*; img-src 'self' data: blob: https:; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:* http://localhost:* http: ws://127.0.0.1:* ws://localhost:* ws: https: wss: data: blob:; frame-src 'self' https: data: blob:" + "csp": "default-src 'self' 'unsafe-inline' data: blob: https: wss: ipc: http://ipc.localhost http://127.0.0.1:* http://localhost:*; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; img-src 'self' data: blob: https:; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:* http://localhost:* http: ws://127.0.0.1:* ws://localhost:* ws: https: wss: data: blob:; frame-src 'self' https: data: blob:" }, "macOSPrivateApi": true }, diff --git a/src/openhuman/config/schema/load.rs b/src/openhuman/config/schema/load.rs index 25489534a..70be228b5 100644 --- a/src/openhuman/config/schema/load.rs +++ b/src/openhuman/config/schema/load.rs @@ -398,11 +398,20 @@ fn decrypt_optional_secret( ) -> Result<()> { if let Some(raw) = value.clone() { if crate::openhuman::keyring::SecretStore::is_encrypted(&raw) { - *value = Some( - store - .decrypt(&raw) - .with_context(|| format!("Failed to decrypt {field_name}"))?, - ); + match store.decrypt(&raw) { + Ok(plaintext) => *value = Some(plaintext), + Err(e) => { + // Decryption key is inaccessible (e.g. rotated, keyring reset, or + // migrated across machines). Clear the field so config loads + // successfully — the affected integration will be disabled until + // the user re-enters the credential. A hard error here would block + // every config load and make the app unusable. + log::warn!( + "[config] Failed to decrypt {field_name} — field cleared (key inaccessible): {e}" + ); + *value = None; + } + } } } Ok(()) diff --git a/src/openhuman/config/schema/load_tests.rs b/src/openhuman/config/schema/load_tests.rs index 47c713f31..67f0523c8 100644 --- a/src/openhuman/config/schema/load_tests.rs +++ b/src/openhuman/config/schema/load_tests.rs @@ -1682,6 +1682,48 @@ allowed_users = ["@admin"] ); } +/// Regression for keyring-loss scenario: if a channel token was encrypted with +/// a key that is no longer accessible (e.g. keyring reset, machine migration), +/// config load must NOT fail hard. The field should be cleared and a warning +/// logged, so the rest of the app continues to work. +#[tokio::test] +async fn config_load_succeeds_when_decryption_key_inaccessible() { + let tmp = tempfile::tempdir().unwrap(); + let config_path = tmp.path().join("config.toml"); + let workspace_dir = tmp.path().join("workspace"); + std::fs::create_dir_all(&workspace_dir).unwrap(); + + // Write a config whose discord.bot_token is encrypted with a key from a + // *different* workspace so the current SecretStore (keyed to `tmp`) cannot + // decrypt it. The `enc2:` prefix makes `is_encrypted()` return true. + // The hex blob is garbage — intentionally undecryptable. + let stale_ciphertext = "enc2:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + let toml_content = format!( + r#"[secrets] +encrypt = true + +[channels_config.discord] +bot_token = "{stale_ciphertext}" +"# + ); + std::fs::write(&config_path, toml_content.as_bytes()).unwrap(); + + // Config load must succeed even though the token cannot be decrypted. + let reloaded = load_or_init_for_workspace(tmp.path()).await; + + // Discord config should be cleared (None bot_token → channel won't start) + // rather than crashing the entire config load. + let discord_token = reloaded + .channels_config + .discord + .as_ref() + .map(|d| d.bot_token.as_str()); + assert!( + discord_token.map_or(true, |t| t.is_empty()), + "Expected discord.bot_token to be cleared after decryption failure, got: {discord_token:?}" + ); +} + /// Backwards-compatibility regression for #1900: a pre-upgrade `config.toml` /// that contains plaintext secrets (written by a build from before encryption /// was wired in) must continue to load with `secrets.encrypt = true`. The From ecfbdf37c9593584d0bf518556447eaabf416261 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Tue, 26 May 2026 14:14:18 +0530 Subject: [PATCH 2/2] chore: apply auto-fixes (rustfmt long line in load_tests) --- src/openhuman/config/schema/load_tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/openhuman/config/schema/load_tests.rs b/src/openhuman/config/schema/load_tests.rs index 67f0523c8..ada17e526 100644 --- a/src/openhuman/config/schema/load_tests.rs +++ b/src/openhuman/config/schema/load_tests.rs @@ -1697,7 +1697,8 @@ async fn config_load_succeeds_when_decryption_key_inaccessible() { // *different* workspace so the current SecretStore (keyed to `tmp`) cannot // decrypt it. The `enc2:` prefix makes `is_encrypted()` return true. // The hex blob is garbage — intentionally undecryptable. - let stale_ciphertext = "enc2:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + let stale_ciphertext = + "enc2:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; let toml_content = format!( r#"[secrets] encrypt = true