| 23 |
23 |
|
#![cfg_attr(not(test), deny(clippy::unwrap_used))]
|
| 24 |
24 |
|
|
| 25 |
25 |
|
mod activation;
|
|
26 |
+ |
mod api_key;
|
| 26 |
27 |
|
mod audio;
|
| 27 |
28 |
|
mod license;
|
| 28 |
29 |
|
mod midi;
|
| 163 |
164 |
|
|
| 164 |
165 |
|
// ── API key persistence ──
|
| 165 |
166 |
|
|
| 166 |
|
- |
/// Bundled synckit.toml, embedded at compile time from the project root.
|
| 167 |
|
- |
const SYNCKIT_TOML: &str = include_str!("../../../synckit.toml");
|
| 168 |
|
- |
|
| 169 |
|
- |
/// Extract the api_key value from the bundled synckit.toml.
|
| 170 |
|
- |
fn parse_synckit_toml_key() -> Option<String> {
|
| 171 |
|
- |
let table: std::collections::HashMap<String, String> = toml::from_str(SYNCKIT_TOML).ok()?;
|
| 172 |
|
- |
table.get("api_key").filter(|s| !s.is_empty()).cloned()
|
| 173 |
|
- |
}
|
| 174 |
|
- |
|
| 175 |
|
- |
/// OS keychain entry for the SyncKit API key. The key is a credential, not a
|
| 176 |
|
- |
/// regenerable cache, so it belongs in the keychain rather than a plaintext
|
| 177 |
|
- |
/// file. Returns None when no keychain provider is available (headless Linux
|
| 178 |
|
- |
/// without secret-service, CI) — callers fall back to file/env/bundled.
|
| 179 |
|
- |
fn api_key_keychain_entry() -> Option<keyring::Entry> {
|
| 180 |
|
- |
keyring::Entry::new("audiofiles", "sync_api_key").ok()
|
| 181 |
|
- |
}
|
| 182 |
|
- |
|
| 183 |
|
- |
/// Read a trimmed API key from the legacy plaintext file. None if absent/empty.
|
| 184 |
|
- |
fn read_key_file(data_dir: &Path) -> Option<String> {
|
| 185 |
|
- |
let key = std::fs::read_to_string(data_dir.join("sync_api_key")).ok()?;
|
| 186 |
|
- |
let key = key.trim().to_string();
|
| 187 |
|
- |
(!key.is_empty()).then_some(key)
|
| 188 |
|
- |
}
|
| 189 |
|
- |
|
| 190 |
|
- |
/// API key from env vars (dev/CI) or the bundled synckit.toml (alpha shared key).
|
| 191 |
|
- |
fn api_key_from_env_or_bundled() -> Option<String> {
|
| 192 |
|
- |
if let (Ok(_url), Ok(key)) = (
|
| 193 |
|
- |
std::env::var("AF_SYNC_SERVER_URL"),
|
| 194 |
|
- |
std::env::var("AF_SYNC_API_KEY"),
|
| 195 |
|
- |
) {
|
| 196 |
|
- |
return Some(key);
|
| 197 |
|
- |
}
|
| 198 |
|
- |
parse_synckit_toml_key()
|
| 199 |
|
- |
}
|
| 200 |
|
- |
|
| 201 |
|
- |
/// Load a saved API key. Order: OS keychain, then a legacy plaintext file
|
| 202 |
|
- |
/// (migrated into the keychain and deleted on read), then env vars, then
|
| 203 |
|
- |
/// bundled toml.
|
| 204 |
|
- |
fn load_api_key(data_dir: &Path) -> Option<String> {
|
| 205 |
|
- |
// Keychain takes priority.
|
| 206 |
|
- |
if let Some(entry) = api_key_keychain_entry() {
|
| 207 |
|
- |
match entry.get_password() {
|
| 208 |
|
- |
Ok(key) if !key.trim().is_empty() => {
|
| 209 |
|
- |
tracing::info!("Loaded SyncKit API key from OS keychain");
|
| 210 |
|
- |
return Some(key.trim().to_string());
|
| 211 |
|
- |
}
|
| 212 |
|
- |
Ok(_) | Err(keyring::Error::NoEntry) => {}
|
| 213 |
|
- |
Err(e) => tracing::warn!("Keychain read failed, falling back: {e}"),
|
| 214 |
|
- |
}
|
| 215 |
|
- |
}
|
| 216 |
|
- |
|
| 217 |
|
- |
// Legacy plaintext file: migrate into the keychain, then remove it so the
|
| 218 |
|
- |
// credential stops living on disk in cleartext.
|
| 219 |
|
- |
if let Some(key) = read_key_file(data_dir) {
|
| 220 |
|
- |
tracing::info!("Migrating SyncKit API key into the keychain");
|
| 221 |
|
- |
if save_api_key(&key) {
|
| 222 |
|
- |
let _ = std::fs::remove_file(data_dir.join("sync_api_key"));
|
| 223 |
|
- |
}
|
| 224 |
|
- |
return Some(key);
|
| 225 |
|
- |
}
|
| 226 |
|
- |
|
| 227 |
|
- |
api_key_from_env_or_bundled()
|
| 228 |
|
- |
}
|
| 229 |
|
- |
|
| 230 |
|
- |
/// Persist an API key to the OS keychain. Returns true on success; best-effort,
|
| 231 |
|
- |
/// so a missing keychain provider degrades gracefully rather than failing.
|
| 232 |
|
- |
fn save_api_key(api_key: &str) -> bool {
|
| 233 |
|
- |
let Some(entry) = api_key_keychain_entry() else {
|
| 234 |
|
- |
return false;
|
| 235 |
|
- |
};
|
| 236 |
|
- |
match entry.set_password(api_key) {
|
| 237 |
|
- |
Ok(()) => true,
|
| 238 |
|
- |
Err(e) => {
|
| 239 |
|
- |
tracing::warn!("Failed to store API key in keychain: {e}");
|
| 240 |
|
- |
false
|
| 241 |
|
- |
}
|
| 242 |
|
- |
}
|
| 243 |
|
- |
}
|
| 244 |
|
- |
|
| 245 |
167 |
|
/// Create a SyncManager from a saved or env-provided API key.
|
| 246 |
168 |
|
fn create_sync_manager(
|
| 247 |
169 |
|
data_dir: &Path,
|
| 248 |
170 |
|
runtime: &tokio::runtime::Handle,
|
| 249 |
171 |
|
) -> Option<SyncManager> {
|
| 250 |
|
- |
let api_key = load_api_key(data_dir)?;
|
|
172 |
+ |
let api_key = api_key::load_api_key(data_dir)?;
|
| 251 |
173 |
|
let server_url = std::env::var("AF_SYNC_SERVER_URL")
|
| 252 |
174 |
|
.unwrap_or_else(|_| SYNC_SERVER_URL.to_string());
|
| 253 |
175 |
|
let config = SyncKitConfig {
|
| 1090 |
1012 |
|
mod tests {
|
| 1091 |
1013 |
|
use super::*;
|
| 1092 |
1014 |
|
|
| 1093 |
|
- |
// The plaintext-file path is tested via `read_key_file` so the tests stay
|
| 1094 |
|
- |
// deterministic — `load_api_key` reads the real OS keychain first, whose
|
| 1095 |
|
- |
// state is global and not test-controlled.
|
| 1096 |
|
- |
|
| 1097 |
|
- |
#[test]
|
| 1098 |
|
- |
fn read_key_file_returns_key() {
|
| 1099 |
|
- |
let dir = tempfile::tempdir().unwrap();
|
| 1100 |
|
- |
std::fs::write(dir.path().join("sync_api_key"), "test-key-123").unwrap();
|
| 1101 |
|
- |
assert_eq!(read_key_file(dir.path()), Some("test-key-123".to_string()));
|
| 1102 |
|
- |
}
|
| 1103 |
|
- |
|
| 1104 |
|
- |
#[test]
|
| 1105 |
|
- |
fn read_key_file_trims_whitespace() {
|
| 1106 |
|
- |
let dir = tempfile::tempdir().unwrap();
|
| 1107 |
|
- |
std::fs::write(dir.path().join("sync_api_key"), " key-with-spaces \n").unwrap();
|
| 1108 |
|
- |
assert_eq!(read_key_file(dir.path()), Some("key-with-spaces".to_string()));
|
| 1109 |
|
- |
}
|
| 1110 |
|
- |
|
| 1111 |
|
- |
#[test]
|
| 1112 |
|
- |
fn read_key_file_empty_is_none() {
|
| 1113 |
|
- |
let dir = tempfile::tempdir().unwrap();
|
| 1114 |
|
- |
std::fs::write(dir.path().join("sync_api_key"), "").unwrap();
|
| 1115 |
|
- |
assert_eq!(read_key_file(dir.path()), None);
|
| 1116 |
|
- |
}
|
| 1117 |
|
- |
|
| 1118 |
|
- |
#[test]
|
| 1119 |
|
- |
fn read_key_file_whitespace_only_is_none() {
|
| 1120 |
|
- |
let dir = tempfile::tempdir().unwrap();
|
| 1121 |
|
- |
std::fs::write(dir.path().join("sync_api_key"), " \n ").unwrap();
|
| 1122 |
|
- |
assert_eq!(read_key_file(dir.path()), None);
|
| 1123 |
|
- |
}
|
| 1124 |
|
- |
|
| 1125 |
|
- |
#[test]
|
| 1126 |
|
- |
fn read_key_file_missing_is_none() {
|
| 1127 |
|
- |
let dir = tempfile::tempdir().unwrap();
|
| 1128 |
|
- |
assert_eq!(read_key_file(dir.path()), None);
|
| 1129 |
|
- |
}
|
| 1130 |
|
- |
|
| 1131 |
|
- |
#[test]
|
| 1132 |
|
- |
fn env_or_bundled_matches_bundled_without_env() {
|
| 1133 |
|
- |
if std::env::var("AF_SYNC_API_KEY").is_err() {
|
| 1134 |
|
- |
assert_eq!(api_key_from_env_or_bundled(), parse_synckit_toml_key());
|
| 1135 |
|
- |
}
|
| 1136 |
|
- |
}
|
| 1137 |
|
- |
|
| 1138 |
|
- |
// No save/load round-trip test: `save_api_key` writes to the real OS
|
| 1139 |
|
- |
// keychain under a fixed namespace, so exercising it in unit tests would
|
| 1140 |
|
- |
// pollute (or depend on) the developer's actual keychain. The migration and
|
| 1141 |
|
- |
// fallback ordering are covered by the `read_key_file` / env-or-bundled
|
| 1142 |
|
- |
// tests above.
|
| 1143 |
|
- |
|
| 1144 |
1015 |
|
// ── Initial screen resolution ──
|
| 1145 |
1016 |
|
|
| 1146 |
1017 |
|
fn make_license_cache() -> license::LicenseCache {
|