max / audiofiles
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 |