1#![forbid(unsafe_code)]
2
3use encryptman::MasterKey;
69use thiserror::Error;
70use zeroize::Zeroize;
71
72const KEY_USERNAME: &str = "master-key";
74
75#[derive(Debug, Error)]
77pub enum Error {
78 #[error("keychain error: {0}")]
80 Keychain(#[from] keyring::Error),
81
82 #[error("invalid master key in keychain: expected 32 bytes, got {0}")]
84 InvalidKeyLength(usize),
85
86 #[error("failed to read key file: {0}")]
88 FileRead(#[from] std::io::Error),
89
90 #[error("invalid key file: expected 32 bytes, got {0}")]
92 InvalidFileKeyLength(usize),
93
94 #[error("crypto error: {0}")]
96 Crypto(#[from] encryptman::CryptoError),
97}
98
99pub struct Vault {
120 service: String,
121 master_key: MasterKey,
122}
123
124impl Vault {
125 pub fn new(service: &str) -> Result<Self, Error> {
137 Self::new_with_target(service, KEY_USERNAME)
138 }
139
140 pub fn new_with_target(service: &str, target: &str) -> Result<Self, Error> {
149 let entry = keyring::Entry::new(service, target)?;
150 let master_key = match entry.get_secret() {
151 Ok(bytes) => MasterKey::try_from(bytes.as_slice())
152 .map_err(|_| Error::InvalidKeyLength(bytes.len()))?,
153 Err(keyring::Error::NoEntry) => {
154 let key = MasterKey::generate()?;
155 entry.set_secret(key.as_bytes())?;
156 key
157 }
158 Err(e) => return Err(e.into()),
159 };
160 Ok(Self {
161 service: service.to_string(),
162 master_key,
163 })
164 }
165
166 pub fn migrate_from_file(service: &str, key_path: &std::path::Path) -> Result<Self, Error> {
179 Self::migrate_from_file_with_target(service, KEY_USERNAME, key_path)
180 }
181
182 pub fn migrate_from_file_with_target(
186 service: &str,
187 target: &str,
188 key_path: &std::path::Path,
189 ) -> Result<Self, Error> {
190 let mut raw = std::fs::read(key_path)?;
191 if raw.len() != 32 {
192 return Err(Error::InvalidFileKeyLength(raw.len()));
193 }
194 let mut bytes = [0u8; 32];
195 bytes.copy_from_slice(&raw);
196 raw.zeroize();
197
198 let entry = keyring::Entry::new(service, target)?;
199 let set_result = entry.set_secret(&bytes);
200 let master_key = MasterKey::from_bytes(bytes);
201 bytes.zeroize();
202 set_result?;
203
204 std::fs::remove_file(key_path)?;
206
207 Ok(Self {
208 service: service.to_string(),
209 master_key,
210 })
211 }
212
213 pub fn encrypt(&self, plaintext: &str) -> Result<String, Error> {
222 Ok(encryptman::encrypt(&self.master_key, plaintext)?)
223 }
224
225 pub fn decrypt(&self, ciphertext: &str) -> Result<String, Error> {
234 Ok(encryptman::decrypt(&self.master_key, ciphertext)?)
235 }
236
237 pub fn encrypt_with_context(&self, context: &str, plaintext: &str) -> Result<String, Error> {
246 Ok(encryptman::encrypt_with_context(
247 &self.master_key,
248 context,
249 plaintext,
250 )?)
251 }
252
253 pub fn decrypt_with_context(&self, context: &str, ciphertext: &str) -> Result<String, Error> {
261 Ok(encryptman::decrypt_with_context(
262 &self.master_key,
263 context,
264 ciphertext,
265 )?)
266 }
267
268 pub fn master_key(&self) -> &MasterKey {
273 &self.master_key
274 }
275
276 pub fn encrypt_bytes(&self, plaintext: &[u8]) -> Result<Vec<u8>, Error> {
285 Ok(encryptman::encrypt_bytes_with_context(
286 &self.master_key,
287 &self.service,
288 plaintext,
289 )?)
290 }
291
292 pub fn decrypt_bytes(&self, packed: &[u8]) -> Result<Vec<u8>, Error> {
301 Ok(encryptman::decrypt_bytes_with_context(
302 &self.master_key,
303 &self.service,
304 packed,
305 )?)
306 }
307
308 pub fn encrypt_bytes_with_context(
314 &self,
315 context: &str,
316 plaintext: &[u8],
317 ) -> Result<Vec<u8>, Error> {
318 Ok(encryptman::encrypt_bytes_with_context(
319 &self.master_key,
320 context,
321 plaintext,
322 )?)
323 }
324
325 pub fn decrypt_bytes_with_context(
331 &self,
332 context: &str,
333 packed: &[u8],
334 ) -> Result<Vec<u8>, Error> {
335 Ok(encryptman::decrypt_bytes_with_context(
336 &self.master_key,
337 context,
338 packed,
339 )?)
340 }
341
342 pub fn delete(service: &str) -> Result<(), Error> {
354 Self::delete_with_target(service, KEY_USERNAME)
355 }
356
357 pub fn delete_with_target(service: &str, target: &str) -> Result<(), Error> {
363 let entry = keyring::Entry::new(service, target)?;
364 entry.delete_credential()?;
365 Ok(())
366 }
367}
368
369impl std::fmt::Debug for Vault {
370 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371 f.debug_struct("Vault")
372 .field("service", &self.service)
373 .field("master_key", &"***")
374 .finish()
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use serial_test::serial;
382
383 #[test]
384 #[serial]
385 fn encrypt_decrypt_roundtrip() {
386 let vault = Vault::new("test-encryptman-keyring").unwrap();
387 let original = "my_secret_password_123!";
388 let encrypted = vault.encrypt(original).unwrap();
389 let decrypted = vault.decrypt(&encrypted).unwrap();
390 assert_eq!(original, decrypted);
391 let _ = Vault::delete("test-encryptman-keyring");
392 }
393
394 #[test]
395 #[serial]
396 fn encrypt_produces_different_output_each_time() {
397 let vault = Vault::new("test-encryptman-keyring-ne").unwrap();
398 let a = vault.encrypt("same_password").unwrap();
399 let b = vault.encrypt("same_password").unwrap();
400 assert_ne!(a, b);
401 let _ = Vault::delete("test-encryptman-keyring-ne");
402 }
403
404 #[test]
405 #[serial]
406 fn wrong_key_fails() {
407 let vault1 = Vault::new("test-encryptman-keyring-wk1").unwrap();
408 let vault2 = Vault::new("test-encryptman-keyring-wk2").unwrap();
409 let encrypted = vault1.encrypt("secret").unwrap();
410 assert!(vault2.decrypt(&encrypted).is_err());
411 let _ = Vault::delete("test-encryptman-keyring-wk1");
412 let _ = Vault::delete("test-encryptman-keyring-wk2");
413 }
414
415 #[test]
416 #[serial]
417 fn unicode_roundtrip() {
418 let vault = Vault::new("test-encryptman-keyring-unicode").unwrap();
419 let original = "รหัสผ่านภาษาไทย 🔐";
420 let encrypted = vault.encrypt(original).unwrap();
421 let decrypted = vault.decrypt(&encrypted).unwrap();
422 assert_eq!(original, decrypted);
423 let _ = Vault::delete("test-encryptman-keyring-unicode");
424 }
425
426 #[test]
427 #[serial]
428 fn context_isolation() {
429 let vault = Vault::new("test-encryptman-keyring-ctx").unwrap();
430 let enc_a = vault.encrypt_with_context("ctx-a", "same").unwrap();
431 let enc_b = vault.encrypt_with_context("ctx-b", "same").unwrap();
432 assert_ne!(enc_a, enc_b);
433 assert!(vault.decrypt_with_context("ctx-b", &enc_a).is_err());
434 let _ = Vault::delete("test-encryptman-keyring-ctx");
435 }
436
437 #[test]
438 #[serial]
439 fn debug_does_not_leak_key() {
440 let vault = Vault::new("test-encryptman-keyring-debug").unwrap();
441 let debug = format!("{:?}", vault);
442 assert_eq!(
443 debug,
444 "Vault { service: \"test-encryptman-keyring-debug\", master_key: \"***\" }"
445 );
446 let _ = Vault::delete("test-encryptman-keyring-debug");
447 }
448
449 #[test]
450 #[serial]
451 fn new_with_target() {
452 let vault =
453 Vault::new_with_target("test-encryptman-keyring-target", "custom-user").unwrap();
454 let ct = vault.encrypt("hello").unwrap();
455 let pt = vault.decrypt(&ct).unwrap();
456 assert_eq!(pt, "hello");
457 let _ = Vault::delete_with_target("test-encryptman-keyring-target", "custom-user");
458 }
459
460 #[test]
461 #[serial]
462 fn migrate_from_file() {
463 use std::fs;
464 use tempfile::TempDir;
465
466 let dir = TempDir::new().unwrap();
467 let key_path = dir.path().join(".key");
468 let key_bytes = [42u8; 32];
469 fs::write(&key_path, key_bytes).unwrap();
470
471 let vault = Vault::migrate_from_file("test-encryptman-keyring-migrate", &key_path).unwrap();
472 assert_eq!(vault.master_key().as_bytes(), &key_bytes);
473 assert!(
474 !key_path.exists(),
475 "key file should be deleted after migration"
476 );
477
478 let ct = vault.encrypt("migrated").unwrap();
479 let pt = vault.decrypt(&ct).unwrap();
480 assert_eq!(pt, "migrated");
481 let _ = Vault::delete("test-encryptman-keyring-migrate");
482 }
483
484 #[test]
485 #[serial]
486 fn migrate_from_file_wrong_length() {
487 use std::fs;
488 use tempfile::TempDir;
489
490 let dir = TempDir::new().unwrap();
491 let key_path = dir.path().join(".key");
492 fs::write(&key_path, [1u8; 16]).unwrap();
493
494 let result = Vault::migrate_from_file("test-encryptman-keyring-migrate-err", &key_path);
495 assert!(result.is_err());
496 assert!(key_path.exists());
497 }
498
499 #[test]
500 #[serial]
501 fn encrypt_bytes_roundtrip() {
502 let vault = Vault::new("test-encryptman-keyring-bytes").unwrap();
503 let data = b"binary secret data";
504 let encrypted = vault.encrypt_bytes(data).unwrap();
505 assert_ne!(encrypted, data.to_vec());
506 let decrypted = vault.decrypt_bytes(&encrypted).unwrap();
507 assert_eq!(decrypted, data);
508 let _ = Vault::delete("test-encryptman-keyring-bytes");
509 }
510
511 #[test]
512 #[serial]
513 fn encrypt_bytes_produces_different_output_each_time() {
514 let vault = Vault::new("test-encryptman-keyring-bytes-ne").unwrap();
515 let data = b"same data";
516 let a = vault.encrypt_bytes(data).unwrap();
517 let b = vault.encrypt_bytes(data).unwrap();
518 assert_ne!(a, b);
519 let _ = Vault::delete("test-encryptman-keyring-bytes-ne");
520 }
521
522 #[test]
523 #[serial]
524 fn encrypt_bytes_context_isolation() {
525 let vault = Vault::new("test-encryptman-keyring-bytes-ctx").unwrap();
526 let data = b"same data";
527 let a = vault.encrypt_bytes_with_context("ctx-a", data).unwrap();
528 let b = vault.encrypt_bytes_with_context("ctx-b", data).unwrap();
529 assert_ne!(a, b);
530 assert!(vault.decrypt_bytes_with_context("ctx-b", &a).is_err());
531 let _ = Vault::delete("test-encryptman-keyring-bytes-ctx");
532 }
533
534 #[test]
535 #[serial]
536 fn delete_as_associated_function() {
537 let vault = Vault::new("test-encryptman-keyring-del").unwrap();
538 let ct = vault.encrypt("test").unwrap();
539 Vault::delete("test-encryptman-keyring-del").unwrap();
540 assert!(
541 Vault::new("test-encryptman-keyring-del")
542 .unwrap()
543 .decrypt(&ct)
544 .is_err()
545 );
546 }
547}