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..ada17e526 100644 --- a/src/openhuman/config/schema/load_tests.rs +++ b/src/openhuman/config/schema/load_tests.rs @@ -1682,6 +1682,49 @@ 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