Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
19 changes: 14 additions & 5 deletions src/openhuman/config/schema/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down
43 changes: 43 additions & 0 deletions src/openhuman/config/schema/load_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading