From 4239cd3584b01565d9a676c3eaffd234f68f1867 Mon Sep 17 00:00:00 2001 From: Shaan Khosla <35707672+shaankhosla@users.noreply.github.com> Date: Sat, 28 Mar 2026 07:05:36 -0400 Subject: [PATCH] fix: don't log cloud provider keys (#1145) * fix: don't log cloud provider keys * test: make sure keys are redacted * change to newtype * change secret struct to public * test: test secretmap directly * Update bindings.ts --------- Co-authored-by: Shaan Co-authored-by: CJ Pais --- src-tauri/src/settings.rs | 64 +++++++++++++++++++++++++++++++++++++-- src/bindings.ts | 3 +- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 6bf657b..d930599 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -3,6 +3,7 @@ use serde::de::{self, Visitor}; use serde::{Deserialize, Deserializer, Serialize}; use specta::Type; use std::collections::HashMap; +use std::fmt; use tauri::AppHandle; use tauri_plugin_store::StoreExt; @@ -304,6 +305,34 @@ impl Default for OrtAcceleratorSetting { } } +#[derive(Clone, Serialize, Deserialize, Type)] +#[serde(transparent)] +pub(crate) struct SecretMap(HashMap); + +impl fmt::Debug for SecretMap { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let redacted: HashMap<&String, &str> = self + .0 + .iter() + .map(|(k, v)| (k, if v.is_empty() { "" } else { "[REDACTED]" })) + .collect(); + redacted.fmt(f) + } +} + +impl std::ops::Deref for SecretMap { + type Target = HashMap; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for SecretMap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + /* still handy for composing the initial JSON in the store ------------- */ #[derive(Serialize, Deserialize, Debug, Clone, Type)] pub struct AppSettings { @@ -365,7 +394,7 @@ pub struct AppSettings { #[serde(default = "default_post_process_providers")] pub post_process_providers: Vec, #[serde(default = "default_post_process_api_keys")] - pub post_process_api_keys: HashMap, + pub post_process_api_keys: SecretMap, #[serde(default = "default_post_process_models")] pub post_process_models: HashMap, #[serde(default = "default_post_process_prompts")] @@ -573,12 +602,12 @@ fn default_post_process_providers() -> Vec { providers } -fn default_post_process_api_keys() -> HashMap { +fn default_post_process_api_keys() -> SecretMap { let mut map = HashMap::new(); for provider in default_post_process_providers() { map.insert(provider.id, String::new()); } - map + SecretMap(map) } fn default_model_for_provider(provider_id: &str) -> String { @@ -918,4 +947,33 @@ mod tests { assert!(!settings.auto_submit); assert_eq!(settings.auto_submit_key, AutoSubmitKey::Enter); } + + #[test] + fn debug_output_redacts_api_keys() { + let mut settings = get_default_settings(); + settings + .post_process_api_keys + .insert("openai".to_string(), "sk-proj-secret-key-12345".to_string()); + settings.post_process_api_keys.insert( + "anthropic".to_string(), + "sk-ant-secret-key-67890".to_string(), + ); + settings + .post_process_api_keys + .insert("empty_provider".to_string(), "".to_string()); + + let debug_output = format!("{:?}", settings); + + assert!(!debug_output.contains("sk-proj-secret-key-12345")); + assert!(!debug_output.contains("sk-ant-secret-key-67890")); + assert!(debug_output.contains("[REDACTED]")); + } + + #[test] + fn secret_map_debug_redacts_values() { + let map = SecretMap(HashMap::from([("key".into(), "secret".into())])); + let out = format!("{:?}", map); + assert!(!out.contains("secret")); + assert!(out.contains("[REDACTED]")); + } } diff --git a/src/bindings.ts b/src/bindings.ts index 14d98b3..6d50b1f 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -827,7 +827,7 @@ historyUpdatePayload: "history-update-payload" /** user-defined types **/ -export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: number; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; auto_submit?: boolean; auto_submit_key?: AutoSubmitKey; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string; experimental_enabled?: boolean; lazy_stream_close?: boolean; keyboard_implementation?: KeyboardImplementation; show_tray_icon?: boolean; paste_delay_ms?: number; typing_tool?: TypingTool; external_script_path: string | null; custom_filler_words?: string[] | null; whisper_accelerator?: WhisperAcceleratorSetting; ort_accelerator?: OrtAcceleratorSetting; whisper_gpu_device?: number; extra_recording_buffer_ms?: number } +export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: number; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; auto_submit?: boolean; auto_submit_key?: AutoSubmitKey; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: SecretMap; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string; experimental_enabled?: boolean; lazy_stream_close?: boolean; keyboard_implementation?: KeyboardImplementation; show_tray_icon?: boolean; paste_delay_ms?: number; typing_tool?: TypingTool; external_script_path: string | null; custom_filler_words?: string[] | null; whisper_accelerator?: WhisperAcceleratorSetting; ort_accelerator?: OrtAcceleratorSetting; whisper_gpu_device?: number; extra_recording_buffer_ms?: number } export type AudioDevice = { index: string; name: string; is_default: boolean } export type AutoSubmitKey = "enter" | "ctrl_enter" | "cmd_enter" export type AvailableAccelerators = { whisper: string[]; ort: string[]; gpu_devices: GpuDeviceOption[] } @@ -859,6 +859,7 @@ export type PasteMethod = "ctrl_v" | "direct" | "none" | "shift_insert" | "ctrl_ export type PermissionAccess = "allowed" | "denied" | "unknown" export type PostProcessProvider = { id: string; label: string; base_url: string; allow_base_url_edit?: boolean; models_endpoint?: string | null; supports_structured_output?: boolean } export type RecordingRetentionPeriod = "never" | "preserve_limit" | "days_3" | "weeks_2" | "months_3" +export type SecretMap = Partial<{ [key in string]: string }> export type ShortcutBinding = { id: string; name: string; description: string; default_binding: string; current_binding: string } export type SoundTheme = "marimba" | "pop" | "custom" export type TypingTool = "auto" | "wtype" | "kwtype" | "dotool" | "ydotool" | "xdotool"