1use std::collections::HashMap;
2use std::sync::Arc;
3
4use aes_gcm::{
5 aead::{
6 Aead,
7 KeyInit,
8 },
9 Aes256Gcm,
10 Nonce,
11};
12use async_trait::async_trait;
13use sqlx::{
14 Pool,
15 Sqlite,
16};
17use tokio::sync::RwLock;
18
19use crate::domain::{
20 DomainError,
21 DomainResult,
22};
23use crate::infrastructure::TokenStore;
24
25const SQLITE_KDF_SALT: &[u8] = b"pipedash-sqlite-vault-v1";
26const BACKUP_KDF_SALT: &[u8] = b"pipedash-backup-salt-v1";
27const VAULT_VERIFIER: &str = "pipedash-vault-verifier-v1";
28
29pub struct SqliteTokenStore {
30 pool: Pool<Sqlite>,
31 encryption_key: [u8; 32],
32 cache: Arc<RwLock<HashMap<i64, String>>>,
33}
34
35impl SqliteTokenStore {
36 pub async fn new(pool: Pool<Sqlite>, vault_password: Option<String>) -> DomainResult<Self> {
37 let password = vault_password
38 .or_else(|| std::env::var("PIPEDASH_VAULT_PASSWORD").ok())
39 .ok_or_else(|| {
40 DomainError::InvalidConfig(
41 "PIPEDASH_VAULT_PASSWORD environment variable required for SQLite token encryption".into(),
42 )
43 })?;
44
45 let encryption_key = Self::derive_encryption_key(&password);
46
47 let store = Self {
48 pool,
49 encryption_key,
50 cache: Arc::new(RwLock::new(HashMap::new())),
51 };
52
53 let has_verifier = store.check_verifier().await?;
54 store.load_to_cache().await?;
55 if !has_verifier {
56 store.store_verifier().await?;
57 }
58
59 tracing::info!("SQLite token store initialized");
60 Ok(store)
61 }
62
63 pub async fn with_pool(
64 pool: Pool<Sqlite>, vault_password: Option<String>,
65 ) -> DomainResult<Self> {
66 Self::new(pool, vault_password).await
67 }
68
69 fn derive_encryption_key(password: &str) -> [u8; 32] {
70 use argon2::{
71 Argon2,
72 ParamsBuilder,
73 };
74
75 let mut output = [0u8; 32];
76
77 let params = ParamsBuilder::new()
78 .m_cost(65536)
79 .t_cost(3)
80 .p_cost(1)
81 .output_len(32)
82 .build()
83 .expect("Invalid Argon2 parameters");
84
85 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
86
87 argon2
88 .hash_password_into(password.as_bytes(), SQLITE_KDF_SALT, &mut output)
89 .expect("Failed to derive encryption key");
90
91 output
92 }
93
94 fn derive_backup_key(password: &str) -> [u8; 32] {
95 use argon2::{
96 Argon2,
97 ParamsBuilder,
98 };
99
100 let mut output = [0u8; 32];
101
102 let params = ParamsBuilder::new()
103 .m_cost(65536)
104 .t_cost(3)
105 .p_cost(1)
106 .output_len(32)
107 .build()
108 .expect("Invalid Argon2 parameters");
109
110 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
111
112 argon2
113 .hash_password_into(password.as_bytes(), BACKUP_KDF_SALT, &mut output)
114 .expect("Failed to derive backup key");
115
116 output
117 }
118
119 async fn encrypt_token(&self, plaintext: &str) -> DomainResult<(Vec<u8>, Vec<u8>)> {
120 let cipher = Aes256Gcm::new_from_slice(&self.encryption_key)
121 .map_err(|e| DomainError::InternalError(format!("Failed to create cipher: {}", e)))?;
122
123 let nonce_bytes: [u8; 12] = rand::random();
124 let nonce = Nonce::from(nonce_bytes);
125
126 let ciphertext = cipher
127 .encrypt(&nonce, plaintext.as_bytes())
128 .map_err(|e| DomainError::InternalError(format!("Encryption failed: {}", e)))?;
129
130 Ok((nonce_bytes.to_vec(), ciphertext))
131 }
132
133 async fn decrypt_token(&self, nonce: &[u8], ciphertext: &[u8]) -> DomainResult<String> {
134 if nonce.len() != 12 {
135 return Err(DomainError::InvalidConfig("Invalid nonce length".into()));
136 }
137
138 let cipher = Aes256Gcm::new_from_slice(&self.encryption_key)
139 .map_err(|e| DomainError::InternalError(format!("Failed to create cipher: {}", e)))?;
140
141 let nonce_array: [u8; 12] = nonce
142 .try_into()
143 .map_err(|_| DomainError::InvalidConfig("Invalid nonce".into()))?;
144 let nonce = Nonce::from(nonce_array);
145
146 let plaintext = cipher.decrypt(&nonce, ciphertext).map_err(|_| {
147 DomainError::AuthenticationFailed("Token decryption failed - wrong password?".into())
148 })?;
149
150 String::from_utf8(plaintext)
151 .map_err(|e| DomainError::DatabaseError(format!("Invalid UTF-8 in token: {}", e)))
152 }
153
154 async fn check_verifier(&self) -> DomainResult<bool> {
155 let row = sqlx::query_as::<_, (Vec<u8>, Vec<u8>)>(
156 "SELECT nonce, ciphertext FROM vault_verifier WHERE id = 1",
157 )
158 .fetch_optional(&self.pool)
159 .await
160 .map_err(|e| DomainError::DatabaseError(format!("Failed to read vault verifier: {}", e)))?;
161
162 let Some((nonce, ciphertext)) = row else {
163 return Ok(false);
164 };
165
166 self.decrypt_token(&nonce, &ciphertext)
167 .await
168 .map_err(|_| DomainError::AuthenticationFailed("Wrong vault password".into()))?;
169 Ok(true)
170 }
171
172 async fn store_verifier(&self) -> DomainResult<()> {
173 let (nonce, ciphertext) = self.encrypt_token(VAULT_VERIFIER).await?;
174
175 sqlx::query(
176 "INSERT INTO vault_verifier (id, nonce, ciphertext) VALUES (1, ?1, ?2)
177 ON CONFLICT (id) DO NOTHING",
178 )
179 .bind(&nonce)
180 .bind(&ciphertext)
181 .execute(&self.pool)
182 .await
183 .map_err(|e| {
184 DomainError::DatabaseError(format!("Failed to store vault verifier: {}", e))
185 })?;
186
187 self.check_verifier().await?;
188 Ok(())
189 }
190
191 async fn load_to_cache(&self) -> DomainResult<()> {
192 let rows = sqlx::query_as::<_, (i64, Vec<u8>, Vec<u8>)>(
193 "SELECT id, encrypted_token, token_nonce FROM providers
194 WHERE encrypted_token IS NOT NULL AND token_nonce IS NOT NULL",
195 )
196 .fetch_all(&self.pool)
197 .await
198 .map_err(|e| DomainError::DatabaseError(format!("Failed to load tokens: {}", e)))?;
199
200 let mut cache = self.cache.write().await;
201 cache.clear();
202
203 let mut decryption_errors = Vec::new();
204
205 for (provider_id, ciphertext, nonce) in rows {
206 match self.decrypt_token(&nonce, &ciphertext).await {
207 Ok(token) => {
208 cache.insert(provider_id, token);
209 }
210 Err(e) => {
211 tracing::warn!(
212 provider_id = provider_id,
213 error = %e,
214 "Failed to decrypt token for provider"
215 );
216 decryption_errors.push((provider_id, e));
217 }
218 }
219 }
220
221 if !decryption_errors.is_empty() {
222 let failed_count = decryption_errors.len();
223 return Err(DomainError::AuthenticationFailed(format!(
224 "Failed to decrypt {} token(s) - wrong vault password",
225 failed_count
226 )));
227 }
228
229 tracing::debug!(count = cache.len(), "Loaded tokens to cache");
230 Ok(())
231 }
232}
233
234#[async_trait]
235impl TokenStore for SqliteTokenStore {
236 async fn store_token(&self, provider_id: i64, token: &str) -> DomainResult<()> {
237 tracing::debug!(
238 "[SqliteTokenStore] Storing token for provider {} (token length: {})",
239 provider_id,
240 token.len()
241 );
242
243 let (nonce, ciphertext) = self.encrypt_token(token).await?;
244 tracing::debug!(
245 "[SqliteTokenStore] Token encrypted: nonce={} bytes, ciphertext={} bytes",
246 nonce.len(),
247 ciphertext.len()
248 );
249
250 let result = sqlx::query(
251 "UPDATE providers SET encrypted_token = ?1, token_nonce = ?2, updated_at = datetime('now')
252 WHERE id = ?3",
253 )
254 .bind(&ciphertext)
255 .bind(&nonce)
256 .bind(provider_id)
257 .execute(&self.pool)
258 .await
259 .map_err(|e| DomainError::DatabaseError(format!("Failed to store token: {}", e)))?;
260
261 tracing::info!(
262 "[SqliteTokenStore] Token stored for provider {} (rows affected: {})",
263 provider_id,
264 result.rows_affected()
265 );
266
267 if result.rows_affected() == 0 {
268 tracing::warn!(
269 "[SqliteTokenStore] No rows affected when storing token for provider {} - provider may not exist!",
270 provider_id
271 );
272 }
273
274 let mut cache = self.cache.write().await;
275 cache.insert(provider_id, token.to_string());
276
277 tracing::debug!(provider_id = provider_id, "Token stored successfully");
278 Ok(())
279 }
280
281 async fn get_token(&self, provider_id: i64) -> DomainResult<String> {
282 {
283 let cache = self.cache.read().await;
284 if let Some(token) = cache.get(&provider_id) {
285 return Ok(token.clone());
286 }
287 }
288
289 let row = sqlx::query_as::<_, (Vec<u8>, Vec<u8>)>(
290 "SELECT encrypted_token, token_nonce FROM providers
291 WHERE id = ?1 AND encrypted_token IS NOT NULL",
292 )
293 .bind(provider_id)
294 .fetch_optional(&self.pool)
295 .await
296 .map_err(|e| DomainError::DatabaseError(format!("Failed to get token: {}", e)))?
297 .ok_or_else(|| {
298 DomainError::DatabaseError(format!("Token not found for provider {}", provider_id))
299 })?;
300
301 let token = self.decrypt_token(&row.1, &row.0).await?;
302
303 {
304 let mut cache = self.cache.write().await;
305 cache.insert(provider_id, token.clone());
306 }
307
308 Ok(token)
309 }
310
311 async fn delete_token(&self, provider_id: i64) -> DomainResult<()> {
312 sqlx::query(
313 "UPDATE providers SET encrypted_token = NULL, token_nonce = NULL, updated_at = datetime('now')
314 WHERE id = ?1",
315 )
316 .bind(provider_id)
317 .execute(&self.pool)
318 .await
319 .map_err(|e| DomainError::DatabaseError(format!("Failed to delete token: {}", e)))?;
320
321 let mut cache = self.cache.write().await;
322 cache.remove(&provider_id);
323
324 tracing::debug!(provider_id = provider_id, "Token deleted successfully");
325 Ok(())
326 }
327
328 async fn get_all_tokens(&self) -> DomainResult<HashMap<i64, String>> {
329 let cache = self.cache.read().await;
330 Ok(cache.clone())
331 }
332
333 async fn export_encrypted(&self, password: &str) -> DomainResult<Vec<u8>> {
334 let tokens = self.get_all_tokens().await?;
335 let json = serde_json::to_vec(&tokens).map_err(|e| {
336 DomainError::DatabaseError(format!("Failed to serialize tokens: {}", e))
337 })?;
338
339 let backup_key = Self::derive_backup_key(password);
340 let cipher = Aes256Gcm::new_from_slice(&backup_key)
341 .map_err(|e| DomainError::InternalError(format!("Failed to create cipher: {}", e)))?;
342
343 let nonce_bytes: [u8; 12] = rand::random();
344 let nonce = Nonce::from(nonce_bytes);
345
346 let ciphertext = cipher
347 .encrypt(&nonce, json.as_ref())
348 .map_err(|e| DomainError::InternalError(format!("Encryption failed: {}", e)))?;
349
350 let mut result = nonce_bytes.to_vec();
351 result.extend(ciphertext);
352 Ok(result)
353 }
354
355 async fn import_encrypted(&self, data: &[u8], password: &str) -> DomainResult<()> {
356 tracing::info!(
357 "[SqliteTokenStore] Starting import_encrypted (data size: {} bytes, password length: {})",
358 data.len(),
359 password.len()
360 );
361
362 if data.len() < 12 {
363 return Err(DomainError::InvalidConfig("Invalid backup data".into()));
364 }
365
366 let nonce_bytes: [u8; 12] = data[..12]
367 .try_into()
368 .map_err(|_| DomainError::InvalidConfig("Invalid nonce".into()))?;
369 let ciphertext = &data[12..];
370
371 tracing::debug!(
372 "[SqliteTokenStore] Extracted nonce (12 bytes) and ciphertext ({} bytes)",
373 ciphertext.len()
374 );
375
376 let backup_key = Self::derive_backup_key(password);
377 let cipher = Aes256Gcm::new_from_slice(&backup_key)
378 .map_err(|e| DomainError::InternalError(format!("Failed to create cipher: {}", e)))?;
379
380 let nonce = Nonce::from(nonce_bytes);
381
382 tracing::debug!("[SqliteTokenStore] Attempting to decrypt token blob...");
383 let plaintext = cipher.decrypt(&nonce, ciphertext).map_err(|_| {
384 DomainError::AuthenticationFailed("Decryption failed (wrong password?)".into())
385 })?;
386 tracing::debug!(
387 "[SqliteTokenStore] Decryption successful ({} bytes)",
388 plaintext.len()
389 );
390
391 tracing::debug!("[SqliteTokenStore] Parsing JSON tokens...");
392 let tokens: HashMap<i64, String> = serde_json::from_slice(&plaintext)
393 .map_err(|e| DomainError::DatabaseError(format!("Failed to parse tokens: {}", e)))?;
394
395 tracing::info!(
396 "[SqliteTokenStore] Parsed {} tokens from encrypted blob",
397 tokens.len()
398 );
399
400 let mut stored_count = 0;
401 for (provider_id, token) in tokens {
402 tracing::debug!(
403 "[SqliteTokenStore] Importing token for provider {} (length: {})",
404 provider_id,
405 token.len()
406 );
407 self.store_token(provider_id, &token).await?;
408 stored_count += 1;
409 }
410
411 tracing::info!(
412 "[SqliteTokenStore] Import completed: {} tokens stored",
413 stored_count
414 );
415
416 Ok(())
417 }
418
419 async fn warmup(&self) -> DomainResult<()> {
420 tracing::info!("SQLite token store warmup complete (instant)");
421 Ok(())
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 async fn create_test_pool() -> Pool<Sqlite> {
430 use sqlx::sqlite::SqlitePoolOptions;
431
432 let pool = SqlitePoolOptions::new()
433 .max_connections(1)
434 .connect(":memory:")
435 .await
436 .expect("Failed to create in-memory pool");
437
438 sqlx::query(
439 "CREATE TABLE providers (
440 id INTEGER PRIMARY KEY AUTOINCREMENT,
441 name TEXT NOT NULL UNIQUE,
442 provider_type TEXT NOT NULL,
443 token_encrypted TEXT NOT NULL DEFAULT '',
444 config_json TEXT NOT NULL DEFAULT '{}',
445 encrypted_token BLOB,
446 token_nonce BLOB,
447 created_at TEXT NOT NULL DEFAULT (datetime('now')),
448 updated_at TEXT NOT NULL DEFAULT (datetime('now'))
449 )",
450 )
451 .execute(&pool)
452 .await
453 .expect("Failed to create table");
454
455 sqlx::query(include_str!(
456 "../../../migrations/sqlite/20261020000002_vault_verifier.sql"
457 ))
458 .execute(&pool)
459 .await
460 .expect("Failed to create vault verifier table");
461
462 sqlx::query(
463 "INSERT INTO providers (name, provider_type) VALUES ('test-provider', 'github')",
464 )
465 .execute(&pool)
466 .await
467 .expect("Failed to insert provider");
468
469 pool
470 }
471
472 #[tokio::test]
473 async fn test_sqlite_basic() {
474 let pool = create_test_pool().await;
475 let store = SqliteTokenStore::new(pool, Some("test-password".to_string()))
476 .await
477 .unwrap();
478
479 store.store_token(1, "test-token").await.unwrap();
480 let token = store.get_token(1).await.unwrap();
481 assert_eq!(token, "test-token");
482 }
483
484 #[tokio::test]
485 async fn test_sqlite_cache() {
486 let pool = create_test_pool().await;
487 let store = SqliteTokenStore::new(pool, Some("test-password".to_string()))
488 .await
489 .unwrap();
490
491 store.store_token(1, "cached-token").await.unwrap();
492
493 let token = store.get_token(1).await.unwrap();
494 assert_eq!(token, "cached-token");
495 }
496
497 #[tokio::test]
498 async fn test_sqlite_delete() {
499 let pool = create_test_pool().await;
500 let store = SqliteTokenStore::new(pool, Some("test-password".to_string()))
501 .await
502 .unwrap();
503
504 store.store_token(1, "to-delete").await.unwrap();
505 store.delete_token(1).await.unwrap();
506
507 assert!(store.get_token(1).await.is_err());
508 }
509
510 #[tokio::test]
511 async fn test_sqlite_export_import() {
512 let pool = create_test_pool().await;
513 let store = SqliteTokenStore::new(pool.clone(), Some("test-password".to_string()))
514 .await
515 .unwrap();
516
517 store.store_token(1, "export-token").await.unwrap();
518
519 let encrypted = store.export_encrypted("backup-password").await.unwrap();
520
521 store.delete_token(1).await.unwrap();
522
523 store
524 .import_encrypted(&encrypted, "backup-password")
525 .await
526 .unwrap();
527
528 let token = store.get_token(1).await.unwrap();
529 assert_eq!(token, "export-token");
530 }
531
532 #[tokio::test]
533 async fn test_sqlite_wrong_backup_password() {
534 let pool = create_test_pool().await;
535 let store = SqliteTokenStore::new(pool, Some("test-password".to_string()))
536 .await
537 .unwrap();
538
539 store.store_token(1, "secret").await.unwrap();
540 let encrypted = store.export_encrypted("correct-password").await.unwrap();
541
542 let result = store.import_encrypted(&encrypted, "wrong-password").await;
543 assert!(result.is_err());
544 }
545
546 #[tokio::test]
547 async fn test_sqlite_empty_vault_rejects_another_password() {
548 let pool = create_test_pool().await;
549 SqliteTokenStore::new(pool.clone(), Some("operator-password".to_string()))
550 .await
551 .unwrap();
552
553 let result =
554 SqliteTokenStore::new(pool.clone(), Some("attacker-password".to_string())).await;
555
556 assert!(matches!(result, Err(DomainError::AuthenticationFailed(_))));
557 SqliteTokenStore::new(pool, Some("operator-password".to_string()))
558 .await
559 .unwrap();
560 }
561
562 #[tokio::test]
563 async fn test_sqlite_wrong_password_does_not_claim_vault_without_verifier() {
564 let pool = create_test_pool().await;
565 let store = SqliteTokenStore::new(pool.clone(), Some("operator-password".to_string()))
566 .await
567 .unwrap();
568 store.store_token(1, "secret").await.unwrap();
569 sqlx::query("DELETE FROM vault_verifier")
570 .execute(&pool)
571 .await
572 .unwrap();
573
574 let result =
575 SqliteTokenStore::new(pool.clone(), Some("attacker-password".to_string())).await;
576
577 assert!(result.is_err());
578 let store = SqliteTokenStore::new(pool, Some("operator-password".to_string()))
579 .await
580 .unwrap();
581 assert_eq!(store.get_token(1).await.unwrap(), "secret");
582 }
583}