#![forbid(unsafe_code)] mod support; use std::{collections::BTreeMap, fs, io::Cursor}; use ironstorage::{ crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError}, git::GitIdentity, kdbx::{KdbxImportMode, KdbxImportRequest, KdbxImporter}, repository::{Repository, SecretBytes}, write::VaultWriter, }; use keepass::{ Database, DatabaseKey, db::{AutoType, Value, fields}, }; use support::compatibility::{FixtureSet, TestResult}; struct FixtureSecrets(BTreeMap>); impl FixtureSecrets { fn all(fixture: &FixtureSet) -> Self { Self( fixture .generated .keys .iter() .map(|key| { ( key.primary_fingerprint.clone(), key.passphrase.as_bytes().to_vec(), ) }) .collect(), ) } } impl SecretProvider for FixtureSecrets { fn secret_for(&mut self, key: &KeyInfo) -> Result { self.0 .get(key.fingerprint().as_str()) .cloned() .map(SecretBytes::new) .ok_or(SecretProviderError::Unavailable) } } #[test] fn imports_adds_updates_and_quick_adds_without_deleting() -> TestResult { let fixture = FixtureSet::load()?; let store = fixture.materialize_store("basic")?; let repository = Repository::open(store.path())?; let keys = KeyStore::load(fixture.path("keys"))?; let mut secrets = FixtureSecrets::all(&fixture); let source = store.path().join("source.kdbx"); let unrelated = repository.read_entry(&ironstorage::repository::EntryPath::parse( "email/personal", )?)?; save_database( &source, "database password", "first password", "line one\nwww.bahn.de Login:\n\nend", b"first attachment", None, )?; let request = KdbxImportRequest::new(&source, None, KdbxImportMode::AddAndUpdate); let importer = KdbxImporter::new(&repository, &keys); let first = importer.import( &request, SecretBytes::new(b"database password".to_vec()), &mut secrets, GitIdentity::ironstorage(), )?; assert_eq!((first.added(), first.updated()), (2, 0)); assert_eq!( plaintext(&repository, &keys, &mut secrets, "Work/Example")?, b"first password\nlogin: alice\nurl: https://example.test\notpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example\ncomments: line one\n www.bahn.de Login:\n \n end\nattachments: manual.txt\nautotype_enabled: true\nautotype_sequence: {USERNAME}{TAB}{PASSWORD}{ENTER}\nCustom_Label: alpha\n beta: still one field\n" ); assert_eq!( plaintext(&repository, &keys, &mut secrets, "Work/manual.txt")?, b"first attachment" ); let second = importer.import( &request, SecretBytes::new(b"database password".to_vec()), &mut secrets, GitIdentity::ironstorage(), )?; assert_eq!( (second.unchanged(), second.added(), second.updated()), (2, 0, 0) ); save_database( &source, "database password", "updated password", "updated\ncomments", b"updated attachment", None, )?; let updated = importer.import( &request, SecretBytes::new(b"database password".to_vec()), &mut secrets, GitIdentity::ironstorage(), )?; assert_eq!((updated.updated(), updated.added()), (2, 0)); assert!( plaintext(&repository, &keys, &mut secrets, "Work/Example")? .starts_with(b"updated password\n") ); save_database( &source, "database password", "ignored by quick add", "ignored", b"ignored", None, )?; let quick = importer.import( &KdbxImportRequest::new(&source, None, KdbxImportMode::QuickAdd), SecretBytes::new(b"database password".to_vec()), &mut secrets, GitIdentity::ironstorage(), )?; assert_eq!((quick.skipped(), quick.added(), quick.updated()), (2, 0, 0)); assert!( plaintext(&repository, &keys, &mut secrets, "Work/Example")? .starts_with(b"updated password\n") ); assert_eq!( repository.read_entry(&ironstorage::repository::EntryPath::parse( "email/personal" )?)?, unrelated ); Ok(()) } #[test] fn password_and_key_file_are_both_required_before_any_store_change() -> TestResult { let fixture = FixtureSet::load()?; let store = fixture.materialize_store("basic")?; let repository = Repository::open(store.path())?; let keys = KeyStore::load(fixture.path("keys"))?; let mut secrets = FixtureSecrets::all(&fixture); let source = store.path().join("protected.kdbx"); let key_file = store.path().join("database.key"); fs::write(&key_file, b"key file material")?; save_database( &source, "password", "secret", "notes", b"attachment", Some(b"key file material"), )?; let request = KdbxImportRequest::new(&source, Some(key_file), KdbxImportMode::AddAndUpdate); let importer = KdbxImporter::new(&repository, &keys); assert!( importer .import( &request, SecretBytes::new(b"wrong".to_vec()), &mut secrets, GitIdentity::ironstorage(), ) .is_err() ); assert!(!VaultWriter::new(&repository, &keys).entry_exists("Work/Example")?); let outcome = importer.import( &request, SecretBytes::new(b"password".to_vec()), &mut secrets, GitIdentity::ironstorage(), )?; assert_eq!(outcome.added(), 2); Ok(()) } fn save_database( path: &std::path::Path, database_password: &str, entry_password: &str, notes: &str, attachment: &[u8], key_file: Option<&[u8]>, ) -> TestResult { let mut database = Database::new(); { let mut root = database.root_mut(); let mut group = root.add_group(); group.name = "Work".to_owned(); let mut entry = group.add_entry(); entry.set_unprotected(fields::TITLE, "Example"); entry.set_unprotected(fields::USERNAME, "alice"); entry.set_protected(fields::PASSWORD, entry_password); entry.set_unprotected(fields::URL, "https://example.test"); entry.set_unprotected(fields::NOTES, notes); entry.set_protected( fields::OTP, "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example", ); entry.set_unprotected("Custom Label", "alpha\nbeta: still one field"); entry.autotype = Some(AutoType { enabled: true, default_sequence: Some("{USERNAME}{TAB}{PASSWORD}{ENTER}".to_owned()), ..AutoType::default() }); entry.add_attachment("manual.txt", Value::protected(attachment.to_vec())); } let mut key = DatabaseKey::new().with_password(database_password); if let Some(key_file) = key_file { key = key.with_keyfile(&mut Cursor::new(key_file))?; } database.save(&mut fs::File::create(path)?, key)?; Ok(()) } fn plaintext( repository: &Repository, keys: &KeyStore, secrets: &mut impl SecretProvider, entry: &str, ) -> Result, Box> { Ok(VaultWriter::new(repository, keys) .begin_edit( &ironstorage::command::EditRequest { entry: entry.to_owned(), }, secrets, )? .plaintext() .expose() .to_vec()) }