Skip to main content

max / audiofiles

Disable sync rather than fall back when the saved API key is lost A missing keychain entry was treated the same as having no key at all, so loading fell through to the env vars and then the bundled synckit.toml key. A user whose stored key disappeared was silently reverted to the shared alpha key and synced under a different identity. The disappearance is real: the previous keyutils-backed store kept entries in memory only and dropped them on reboot, and the legacy plaintext file is deleted once its key migrates into the keychain. Record a marker file when a user key is stored so a later absence can be told apart from a first run. On loss, log the reason and return None, which leaves sync disabled until the key is entered again. The marker holds no secret. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-21 17:45 UTC
Commit: 9f67ff43448b097f35e1070fb2f91bfd79c81c71
Parent: 9137e42
1 file changed, +33 insertions, -4 deletions
@@ -23,6 +23,13 @@ fn api_key_keychain_entry() -> Option<keyring::Entry> {
23 23 keyring::Entry::new("audiofiles", "sync_api_key").ok()
24 24 }
25 25
26 + /// Marker recording that we successfully stored a user key in the OS keychain.
27 + /// Holds no secret — its presence alone distinguishes "first run, no key yet"
28 + /// from "the keychain lost the key it was holding", which we must not treat
29 + /// alike: the latter would otherwise fall through to the bundled shared alpha
30 + /// key and sync under the wrong identity.
31 + const KEY_SENTINEL: &str = "sync_api_key.stored";
32 +
26 33 /// Read a trimmed API key from the legacy plaintext file. None if absent/empty.
27 34 fn read_key_file(data_dir: &Path) -> Option<String> {
28 35 let key = std::fs::read_to_string(data_dir.join("sync_api_key")).ok()?;
@@ -52,7 +59,22 @@ pub(crate) fn load_api_key(data_dir: &Path) -> Option<String> {
52 59 tracing::info!("Loaded SyncKit API key from OS keychain");
53 60 return Some(key.trim().to_string());
54 61 }
55 - Ok(_) | Err(keyring::Error::NoEntry) => {}
62 + Ok(_) | Err(keyring::Error::NoEntry) => {
63 + // Nothing readable in the keychain. If we previously put a key
64 + // there, it is gone (the pre-4.x `linux-native` store kept
65 + // entries in memory only and dropped them on reboot). Falling
66 + // through would quietly hand back the bundled shared alpha key
67 + // and sync as the wrong identity, so stop here and let the user
68 + // re-enter their key.
69 + if data_dir.join(KEY_SENTINEL).exists() {
70 + tracing::error!(
71 + "The saved SyncKit API key is missing from the OS keychain. \
72 + Sync is disabled until the key is entered again — not falling \
73 + back to the bundled key, which would sync under a different identity."
74 + );
75 + return None;
76 + }
77 + }
56 78 Err(e) => tracing::warn!("Keychain read failed, falling back: {e}"),
57 79 }
58 80 }
@@ -61,7 +83,7 @@ pub(crate) fn load_api_key(data_dir: &Path) -> Option<String> {
61 83 // credential stops living on disk in cleartext.
62 84 if let Some(key) = read_key_file(data_dir) {
63 85 tracing::info!("Migrating SyncKit API key into the keychain");
64 - if save_api_key(&key) {
86 + if save_api_key(data_dir, &key) {
65 87 let _ = std::fs::remove_file(data_dir.join("sync_api_key"));
66 88 }
67 89 return Some(key);
@@ -72,12 +94,19 @@ pub(crate) fn load_api_key(data_dir: &Path) -> Option<String> {
72 94
73 95 /// Persist an API key to the OS keychain. Returns true on success; best-effort,
74 96 /// so a missing keychain provider degrades gracefully rather than failing.
75 - fn save_api_key(api_key: &str) -> bool {
97 + fn save_api_key(data_dir: &Path, api_key: &str) -> bool {
76 98 let Some(entry) = api_key_keychain_entry() else {
77 99 return false;
78 100 };
79 101 match entry.set_password(api_key) {
80 - Ok(()) => true,
102 + Ok(()) => {
103 + // Record that a user key now lives in the keychain, so a later
104 + // disappearance is recognised as loss rather than a first run.
105 + if let Err(e) = std::fs::write(data_dir.join(KEY_SENTINEL), b"") {
106 + tracing::warn!("Failed to write API key sentinel: {e}");
107 + }
108 + true
109 + }
81 110 Err(e) => {
82 111 tracing::warn!("Failed to store API key in keychain: {e}");
83 112 false