chore(hardening): tighten security and footprint defaults

This commit is contained in:
2026-04-24 19:03:57 +01:00
parent 55b34d8ffc
commit b333c6229e
36 changed files with 8702 additions and 254 deletions

View File

@@ -1,19 +1,24 @@
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use tauri::{Emitter, Manager};
use tokio::sync::{mpsc as tokio_mpsc, Mutex as AsyncMutex};
use tokio::task::JoinHandle;
use kon_audio::{DeviceInfo, MicrophoneCapture};
use crate::commands::security::ensure_main_window;
use kon_audio::{DeviceInfo, MicrophoneCapture, WavWriter};
use kon_core::constants::WHISPER_SAMPLE_RATE;
use kon_core::types::AudioSamples;
const MAX_NATIVE_CAPTURE_RETURN_SAMPLES: usize = WHISPER_SAMPLE_RATE as usize * 60 * 10;
/// Enumerate every input device available to cpal, with metadata for the
/// Settings device-picker UI. Includes a flag for likely PulseAudio /
/// PipeWire monitor sources so the UI can warn the user.
#[tauri::command]
pub async fn list_audio_devices() -> Result<Vec<DeviceInfo>, String> {
pub async fn list_audio_devices(window: tauri::WebviewWindow) -> Result<Vec<DeviceInfo>, String> {
ensure_main_window(&window)?;
tokio::task::spawn_blocking(MicrophoneCapture::list_devices)
.await
.map_err(|e| format!("join error: {e}"))?
@@ -51,8 +56,12 @@ pub struct NativeCaptureState {
/// holding the lock — a `std::sync::Mutex` would have to be released
/// and reacquired around each await point.
worker: AsyncMutex<Option<CaptureWorker>>,
/// All captured samples (16kHz mono) for save_audio.
/// Compatibility buffer returned by stop_native_capture. Capped; the
/// authoritative capture is streamed to temp WAV on disk.
all_samples: Arc<Mutex<Vec<f32>>>,
wav_writer: Arc<Mutex<Option<WavWriter>>>,
temp_audio_path: Arc<Mutex<Option<PathBuf>>>,
capture_truncated: Arc<AtomicBool>,
}
impl NativeCaptureState {
@@ -60,6 +69,36 @@ impl NativeCaptureState {
Self {
worker: AsyncMutex::new(None),
all_samples: Arc::new(Mutex::new(Vec::new())),
wav_writer: Arc::new(Mutex::new(None)),
temp_audio_path: Arc::new(Mutex::new(None)),
capture_truncated: Arc::new(AtomicBool::new(false)),
}
}
}
fn append_recorded_chunk(
all_samples: &Arc<Mutex<Vec<f32>>>,
wav_writer: &Arc<Mutex<Option<WavWriter>>>,
truncated: &Arc<AtomicBool>,
chunk: &[f32],
) {
if let Ok(mut writer) = wav_writer.lock() {
if let Some(writer) = writer.as_mut() {
if let Err(e) = writer.append(chunk) {
eprintln!("[native-capture] temp WAV append failed: {e}");
}
}
}
if let Ok(mut all) = all_samples.lock() {
let remaining = MAX_NATIVE_CAPTURE_RETURN_SAMPLES.saturating_sub(all.len());
if remaining >= chunk.len() {
all.extend_from_slice(chunk);
} else {
if remaining > 0 {
all.extend_from_slice(&chunk[..remaining]);
}
truncated.store(true, Ordering::Relaxed);
}
}
}
@@ -72,10 +111,12 @@ impl NativeCaptureState {
/// user's pick from Settings → Audio → Microphone takes effect.
#[tauri::command]
pub async fn start_native_capture(
window: tauri::WebviewWindow,
app: tauri::AppHandle,
state: tauri::State<'_, NativeCaptureState>,
device_name: Option<String>,
) -> Result<(), String> {
ensure_main_window(&window)?;
eprintln!(
"[native-capture] start_native_capture called (device='{}')",
device_name.as_deref().unwrap_or("<auto>")
@@ -116,10 +157,19 @@ pub async fn start_native_capture(
let all_samples = state.all_samples.clone();
all_samples.lock().unwrap().clear();
state.capture_truncated.store(false, Ordering::Relaxed);
let temp_path = std::env::temp_dir().join(recording_filename());
let writer = WavWriter::create(&temp_path, WHISPER_SAMPLE_RATE, 1)
.map_err(|e| format!("Failed to create temp capture WAV: {e}"))?;
*state.wav_writer.lock().unwrap() = Some(writer);
*state.temp_audio_path.lock().unwrap() = Some(temp_path);
let (stop_tx, mut stop_rx) = tokio_mpsc::channel::<()>(1);
let all_samples_clone = all_samples.clone();
let wav_writer_clone = state.wav_writer.clone();
let truncated_clone = state.capture_truncated.clone();
// Spawn a task that reads cpal chunks, downsamples to 16kHz mono,
// and emits events to the frontend. The JoinHandle is retained in
@@ -189,10 +239,12 @@ pub async fn start_native_capture(
while pcm_buffer.len() >= chunk_size {
let chunk: Vec<f32> = pcm_buffer.drain(..chunk_size).collect();
// Store for save_audio
if let Ok(mut all) = all_samples_clone.lock() {
all.extend_from_slice(&chunk);
}
append_recorded_chunk(
&all_samples_clone,
&wav_writer_clone,
&truncated_clone,
&chunk,
);
let _ = app.emit(
"native-pcm",
@@ -213,9 +265,12 @@ pub async fn start_native_capture(
// Emit any remaining samples
if !pcm_buffer.is_empty() {
if let Ok(mut all) = all_samples_clone.lock() {
all.extend_from_slice(&pcm_buffer);
}
append_recorded_chunk(
&all_samples_clone,
&wav_writer_clone,
&truncated_clone,
&pcm_buffer,
);
let _ = app.emit(
"native-pcm",
serde_json::json!({
@@ -228,6 +283,13 @@ pub async fn start_native_capture(
if let Ok(mut cap) = capture_clone.lock() {
cap.take();
}
if let Ok(mut writer) = wav_writer_clone.lock() {
if let Some(writer) = writer.take() {
if let Err(e) = writer.finalize() {
eprintln!("[native-capture] temp WAV finalize failed: {e}");
}
}
}
});
*state.worker.lock().await = Some(CaptureWorker { stop_tx, join });
@@ -242,8 +304,10 @@ pub async fn start_native_capture(
/// nothing from a worker that technically outlived the call (RB-06).
#[tauri::command]
pub async fn stop_native_capture(
window: tauri::WebviewWindow,
state: tauri::State<'_, NativeCaptureState>,
) -> Result<Vec<f32>, String> {
ensure_main_window(&window)?;
if let Some(worker) = state.worker.lock().await.take() {
stop_worker(worker).await;
}
@@ -252,6 +316,11 @@ pub async fn stop_native_capture(
let mut all = state.all_samples.lock().unwrap();
std::mem::take(&mut *all)
};
if state.capture_truncated.load(Ordering::Relaxed) {
eprintln!(
"[native-capture] stop_native_capture returned a capped compatibility buffer; temp WAV contains the full capture"
);
}
Ok(samples)
}
@@ -461,9 +530,11 @@ pub async fn persist_audio_samples(
/// Save PCM f32 samples as a WAV file. Returns the file path.
#[tauri::command]
pub async fn save_audio(
window: tauri::WebviewWindow,
app: tauri::AppHandle,
samples: Vec<f32>,
output_folder: Option<String>,
) -> Result<String, String> {
ensure_main_window(&window)?;
persist_audio_samples(&app, samples, output_folder).await
}