| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
use std::path::Path; |
| 8 |
|
| 9 |
|
| 10 |
const SYNCKIT_TOML: &str = include_str!("../../../synckit.toml"); |
| 11 |
|
| 12 |
|
| 13 |
fn parse_synckit_toml_key() -> Option<String> { |
| 14 |
let table: std::collections::HashMap<String, String> = toml::from_str(SYNCKIT_TOML).ok()?; |
| 15 |
table.get("api_key").filter(|s| !s.is_empty()).cloned() |
| 16 |
} |
| 17 |
|
| 18 |
|
| 19 |
|
| 20 |
|
| 21 |
|
| 22 |
fn api_key_keychain_entry() -> Option<keyring::Entry> { |
| 23 |
keyring::Entry::new("audiofiles", "sync_api_key").ok() |
| 24 |
} |
| 25 |
|
| 26 |
|
| 27 |
|
| 28 |
|
| 29 |
|
| 30 |
|
| 31 |
const KEY_SENTINEL: &str = "sync_api_key.stored"; |
| 32 |
|
| 33 |
|
| 34 |
fn read_key_file(data_dir: &Path) -> Option<String> { |
| 35 |
let key = std::fs::read_to_string(data_dir.join("sync_api_key")).ok()?; |
| 36 |
let key = key.trim().to_string(); |
| 37 |
(!key.is_empty()).then_some(key) |
| 38 |
} |
| 39 |
|
| 40 |
|
| 41 |
fn api_key_from_env_or_bundled() -> Option<String> { |
| 42 |
if let (Ok(_url), Ok(key)) = ( |
| 43 |
std::env::var("AF_SYNC_SERVER_URL"), |
| 44 |
std::env::var("AF_SYNC_API_KEY"), |
| 45 |
) { |
| 46 |
return Some(key); |
| 47 |
} |
| 48 |
parse_synckit_toml_key() |
| 49 |
} |
| 50 |
|
| 51 |
|
| 52 |
|
| 53 |
|
| 54 |
pub(crate) fn load_api_key(data_dir: &Path) -> Option<String> { |
| 55 |
|
| 56 |
if let Some(entry) = api_key_keychain_entry() { |
| 57 |
match entry.get_password() { |
| 58 |
Ok(key) if !key.trim().is_empty() => { |
| 59 |
tracing::info!("Loaded SyncKit API key from OS keychain"); |
| 60 |
return Some(key.trim().to_string()); |
| 61 |
} |
| 62 |
Ok(_) | Err(keyring::Error::NoEntry) => { |
| 63 |
|
| 64 |
|
| 65 |
|
| 66 |
|
| 67 |
|
| 68 |
|
| 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 |
} |
| 78 |
Err(e) => tracing::warn!("Keychain read failed, falling back: {e}"), |
| 79 |
} |
| 80 |
} |
| 81 |
|
| 82 |
|
| 83 |
|
| 84 |
if let Some(key) = read_key_file(data_dir) { |
| 85 |
tracing::info!("Migrating SyncKit API key into the keychain"); |
| 86 |
if save_api_key(data_dir, &key) { |
| 87 |
let _ = std::fs::remove_file(data_dir.join("sync_api_key")); |
| 88 |
} |
| 89 |
return Some(key); |
| 90 |
} |
| 91 |
|
| 92 |
api_key_from_env_or_bundled() |
| 93 |
} |
| 94 |
|
| 95 |
|
| 96 |
|
| 97 |
fn save_api_key(data_dir: &Path, api_key: &str) -> bool { |
| 98 |
let Some(entry) = api_key_keychain_entry() else { |
| 99 |
return false; |
| 100 |
}; |
| 101 |
match entry.set_password(api_key) { |
| 102 |
Ok(()) => { |
| 103 |
|
| 104 |
|
| 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 |
} |
| 110 |
Err(e) => { |
| 111 |
tracing::warn!("Failed to store API key in keychain: {e}"); |
| 112 |
false |
| 113 |
} |
| 114 |
} |
| 115 |
} |
| 116 |
|
| 117 |
#[cfg(test)] |
| 118 |
mod tests { |
| 119 |
use super::*; |
| 120 |
|
| 121 |
|
| 122 |
|
| 123 |
|
| 124 |
|
| 125 |
#[test] |
| 126 |
fn read_key_file_returns_key() { |
| 127 |
let dir = tempfile::tempdir().unwrap(); |
| 128 |
std::fs::write(dir.path().join("sync_api_key"), "test-key-123").unwrap(); |
| 129 |
assert_eq!(read_key_file(dir.path()), Some("test-key-123".to_string())); |
| 130 |
} |
| 131 |
|
| 132 |
#[test] |
| 133 |
fn read_key_file_trims_whitespace() { |
| 134 |
let dir = tempfile::tempdir().unwrap(); |
| 135 |
std::fs::write(dir.path().join("sync_api_key"), " key-with-spaces \n").unwrap(); |
| 136 |
assert_eq!( |
| 137 |
read_key_file(dir.path()), |
| 138 |
Some("key-with-spaces".to_string()) |
| 139 |
); |
| 140 |
} |
| 141 |
|
| 142 |
#[test] |
| 143 |
fn read_key_file_empty_is_none() { |
| 144 |
let dir = tempfile::tempdir().unwrap(); |
| 145 |
std::fs::write(dir.path().join("sync_api_key"), "").unwrap(); |
| 146 |
assert_eq!(read_key_file(dir.path()), None); |
| 147 |
} |
| 148 |
|
| 149 |
#[test] |
| 150 |
fn read_key_file_whitespace_only_is_none() { |
| 151 |
let dir = tempfile::tempdir().unwrap(); |
| 152 |
std::fs::write(dir.path().join("sync_api_key"), " \n ").unwrap(); |
| 153 |
assert_eq!(read_key_file(dir.path()), None); |
| 154 |
} |
| 155 |
|
| 156 |
#[test] |
| 157 |
fn read_key_file_missing_is_none() { |
| 158 |
let dir = tempfile::tempdir().unwrap(); |
| 159 |
assert_eq!(read_key_file(dir.path()), None); |
| 160 |
} |
| 161 |
|
| 162 |
#[test] |
| 163 |
fn env_or_bundled_matches_bundled_without_env() { |
| 164 |
if std::env::var("AF_SYNC_API_KEY").is_err() { |
| 165 |
assert_eq!(api_key_from_env_or_bundled(), parse_synckit_toml_key()); |
| 166 |
} |
| 167 |
} |
| 168 |
|
| 169 |
|
| 170 |
|
| 171 |
|
| 172 |
|
| 173 |
|
| 174 |
} |
| 175 |
|