1use encryptman::MasterKey;
67use thiserror::Error;
68
69const KEY_USERNAME: &str = "master-key";
71
72#[derive(Debug, Error)]
74pub enum Error {
75 #[error("keychain error: {0}")]
77 Keychain(#[from] keyring::Error),
78
79 #[error("invalid master key in keychain: expected 32 bytes, got {0}")]
81 InvalidKeyLength(usize),
82
83 #[error("failed to read key file: {0}")]
85 FileRead(#[from] std::io::Error),
86
87 #[error("invalid key file: expected 32 bytes, got {0}")]
89 InvalidFileKeyLength(usize),
90
91 #[error("crypto error: {0}")]
93 Crypto(#[from] encryptman::CryptoError),
94}
95
96pub struct Vault {
117 service: String,
118 master_key: MasterKey,
119}
120
121impl Vault {
122 pub fn new(service: &str) -> Result<Self, Error> {
134 Self::new_with_target(service, KEY_USERNAME)
135 }
136
137 pub fn new_with_target(service: &str, target: &str) -> Result<Self, Error> {
146 let entry = keyring::Entry::new(service, target)?;
147 let master_key = match entry.get_secret() {
148 Ok(bytes) => MasterKey::try_from(bytes.as_slice())
149 .map_err(|_| Error::InvalidKeyLength(bytes.len()))?,
150 Err(keyring::Error::NoEntry) => {
151 let key = MasterKey::generate();
152 entry.set_secret(key.as_bytes())?;
153 key
154 }
155 Err(e) => return Err(e.into()),
156 };
157 Ok(Self {
158 service: service.to_string(),
159 master_key,
160 })
161 }
162
163 pub fn migrate_from_file(service: &str, key_path: &std::path::Path) -> Result<Self, Error> {
176 Self::migrate_from_file_with_target(service, KEY_USERNAME, key_path)
177 }
178
179 pub fn migrate_from_file_with_target(
183 service: &str,
184 target: &str,
185 key_path: &std::path::Path,
186 ) -> Result<Self, Error> {
187 let raw = std::fs::read(key_path)?;
188 if raw.len() != 32 {
189 return Err(Error::InvalidFileKeyLength(raw.len()));
190 }
191 let mut bytes = [0u8; 32];
192 bytes.copy_from_slice(&raw);
193
194 let entry = keyring::Entry::new(service, target)?;
195 entry.set_secret(&bytes)?;
196
197 std::fs::remove_file(key_path)?;
199
200 let master_key = MasterKey::from_bytes(bytes);
201 Ok(Self {
202 service: service.to_string(),
203 master_key,
204 })
205 }
206
207 pub fn encrypt(&self, plaintext: &str) -> Result<String, Error> {
216 Ok(encryptman::encrypt(&self.master_key, plaintext)?)
217 }
218
219 pub fn decrypt(&self, ciphertext: &str) -> Result<String, Error> {
228 Ok(encryptman::decrypt(&self.master_key, ciphertext)?)
229 }
230
231 pub fn encrypt_with_context(&self, context: &str, plaintext: &str) -> Result<String, Error> {
240 Ok(encryptman::encrypt_with_context(
241 &self.master_key,
242 context,
243 plaintext,
244 )?)
245 }
246
247 pub fn decrypt_with_context(&self, context: &str, ciphertext: &str) -> Result<String, Error> {
255 Ok(encryptman::decrypt_with_context(
256 &self.master_key,
257 context,
258 ciphertext,
259 )?)
260 }
261
262 pub fn master_key(&self) -> &MasterKey {
267 &self.master_key
268 }
269
270 pub fn encrypt_bytes(&self, plaintext: &[u8]) -> Result<Vec<u8>, Error> {
279 Ok(encryptman::encrypt_bytes_with_context(
280 &self.master_key,
281 &self.service,
282 plaintext,
283 )?)
284 }
285
286 pub fn decrypt_bytes(&self, packed: &[u8]) -> Result<Vec<u8>, Error> {
295 Ok(encryptman::decrypt_bytes_with_context(
296 &self.master_key,
297 &self.service,
298 packed,
299 )?)
300 }
301
302 pub fn encrypt_bytes_with_context(
308 &self,
309 context: &str,
310 plaintext: &[u8],
311 ) -> Result<Vec<u8>, Error> {
312 Ok(encryptman::encrypt_bytes_with_context(
313 &self.master_key,
314 context,
315 plaintext,
316 )?)
317 }
318
319 pub fn decrypt_bytes_with_context(
325 &self,
326 context: &str,
327 packed: &[u8],
328 ) -> Result<Vec<u8>, Error> {
329 Ok(encryptman::decrypt_bytes_with_context(
330 &self.master_key,
331 context,
332 packed,
333 )?)
334 }
335
336 pub fn delete(service: &str) -> Result<(), Error> {
348 Self::delete_with_target(service, KEY_USERNAME)
349 }
350
351 pub fn delete_with_target(service: &str, target: &str) -> Result<(), Error> {
357 let entry = keyring::Entry::new(service, target)?;
358 entry.delete_credential()?;
359 Ok(())
360 }
361}
362
363impl std::fmt::Debug for Vault {
364 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365 f.debug_struct("Vault")
366 .field("service", &self.service)
367 .field("master_key", &"***")
368 .finish()
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use serial_test::serial;
376
377 #[test]
378 #[serial]
379 fn encrypt_decrypt_roundtrip() {
380 let vault = Vault::new("test-encryptman-keyring").unwrap();
381 let original = "my_secret_password_123!";
382 let encrypted = vault.encrypt(original).unwrap();
383 let decrypted = vault.decrypt(&encrypted).unwrap();
384 assert_eq!(original, decrypted);
385 let _ = Vault::delete("test-encryptman-keyring");
386 }
387
388 #[test]
389 #[serial]
390 fn encrypt_produces_different_output_each_time() {
391 let vault = Vault::new("test-encryptman-keyring-ne").unwrap();
392 let a = vault.encrypt("same_password").unwrap();
393 let b = vault.encrypt("same_password").unwrap();
394 assert_ne!(a, b);
395 let _ = Vault::delete("test-encryptman-keyring-ne");
396 }
397
398 #[test]
399 #[serial]
400 fn wrong_key_fails() {
401 let vault1 = Vault::new("test-encryptman-keyring-wk1").unwrap();
402 let vault2 = Vault::new("test-encryptman-keyring-wk2").unwrap();
403 let encrypted = vault1.encrypt("secret").unwrap();
404 assert!(vault2.decrypt(&encrypted).is_err());
405 let _ = Vault::delete("test-encryptman-keyring-wk1");
406 let _ = Vault::delete("test-encryptman-keyring-wk2");
407 }
408
409 #[test]
410 #[serial]
411 fn unicode_roundtrip() {
412 let vault = Vault::new("test-encryptman-keyring-unicode").unwrap();
413 let original = "รหัสผ่านภาษาไทย 🔐";
414 let encrypted = vault.encrypt(original).unwrap();
415 let decrypted = vault.decrypt(&encrypted).unwrap();
416 assert_eq!(original, decrypted);
417 let _ = Vault::delete("test-encryptman-keyring-unicode");
418 }
419
420 #[test]
421 #[serial]
422 fn context_isolation() {
423 let vault = Vault::new("test-encryptman-keyring-ctx").unwrap();
424 let enc_a = vault.encrypt_with_context("ctx-a", "same").unwrap();
425 let enc_b = vault.encrypt_with_context("ctx-b", "same").unwrap();
426 assert_ne!(enc_a, enc_b);
427 assert!(vault.decrypt_with_context("ctx-b", &enc_a).is_err());
428 let _ = Vault::delete("test-encryptman-keyring-ctx");
429 }
430
431 #[test]
432 #[serial]
433 fn debug_does_not_leak_key() {
434 let vault = Vault::new("test-encryptman-keyring-debug").unwrap();
435 let debug = format!("{:?}", vault);
436 assert_eq!(
437 debug,
438 "Vault { service: \"test-encryptman-keyring-debug\", master_key: \"***\" }"
439 );
440 let _ = Vault::delete("test-encryptman-keyring-debug");
441 }
442
443 #[test]
444 #[serial]
445 fn new_with_target() {
446 let vault =
447 Vault::new_with_target("test-encryptman-keyring-target", "custom-user").unwrap();
448 let ct = vault.encrypt("hello").unwrap();
449 let pt = vault.decrypt(&ct).unwrap();
450 assert_eq!(pt, "hello");
451 let _ = Vault::delete_with_target("test-encryptman-keyring-target", "custom-user");
452 }
453
454 #[test]
455 #[serial]
456 fn migrate_from_file() {
457 use std::fs;
458 use tempfile::TempDir;
459
460 let dir = TempDir::new().unwrap();
461 let key_path = dir.path().join(".key");
462 let key_bytes = [42u8; 32];
463 fs::write(&key_path, key_bytes).unwrap();
464
465 let vault = Vault::migrate_from_file("test-encryptman-keyring-migrate", &key_path).unwrap();
466 assert_eq!(vault.master_key().as_bytes(), &key_bytes);
467 assert!(
468 !key_path.exists(),
469 "key file should be deleted after migration"
470 );
471
472 let ct = vault.encrypt("migrated").unwrap();
473 let pt = vault.decrypt(&ct).unwrap();
474 assert_eq!(pt, "migrated");
475 let _ = Vault::delete("test-encryptman-keyring-migrate");
476 }
477
478 #[test]
479 #[serial]
480 fn migrate_from_file_wrong_length() {
481 use std::fs;
482 use tempfile::TempDir;
483
484 let dir = TempDir::new().unwrap();
485 let key_path = dir.path().join(".key");
486 fs::write(&key_path, [1u8; 16]).unwrap();
487
488 let result = Vault::migrate_from_file("test-encryptman-keyring-migrate-err", &key_path);
489 assert!(result.is_err());
490 assert!(key_path.exists());
491 }
492
493 #[test]
494 #[serial]
495 fn encrypt_bytes_roundtrip() {
496 let vault = Vault::new("test-encryptman-keyring-bytes").unwrap();
497 let data = b"binary secret data";
498 let encrypted = vault.encrypt_bytes(data).unwrap();
499 assert_ne!(encrypted, data.to_vec());
500 let decrypted = vault.decrypt_bytes(&encrypted).unwrap();
501 assert_eq!(decrypted, data);
502 let _ = Vault::delete("test-encryptman-keyring-bytes");
503 }
504
505 #[test]
506 #[serial]
507 fn encrypt_bytes_produces_different_output_each_time() {
508 let vault = Vault::new("test-encryptman-keyring-bytes-ne").unwrap();
509 let data = b"same data";
510 let a = vault.encrypt_bytes(data).unwrap();
511 let b = vault.encrypt_bytes(data).unwrap();
512 assert_ne!(a, b);
513 let _ = Vault::delete("test-encryptman-keyring-bytes-ne");
514 }
515
516 #[test]
517 #[serial]
518 fn encrypt_bytes_context_isolation() {
519 let vault = Vault::new("test-encryptman-keyring-bytes-ctx").unwrap();
520 let data = b"same data";
521 let a = vault.encrypt_bytes_with_context("ctx-a", data).unwrap();
522 let b = vault.encrypt_bytes_with_context("ctx-b", data).unwrap();
523 assert_ne!(a, b);
524 assert!(vault.decrypt_bytes_with_context("ctx-b", &a).is_err());
525 let _ = Vault::delete("test-encryptman-keyring-bytes-ctx");
526 }
527
528 #[test]
529 #[serial]
530 fn delete_as_associated_function() {
531 let vault = Vault::new("test-encryptman-keyring-del").unwrap();
532 let ct = vault.encrypt("test").unwrap();
533 Vault::delete("test-encryptman-keyring-del").unwrap();
534 assert!(
535 Vault::new("test-encryptman-keyring-del")
536 .unwrap()
537 .decrypt(&ct)
538 .is_err()
539 );
540 }
541}