247 lines
7.6 KiB
Rust
247 lines
7.6 KiB
Rust
#![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<String, Vec<u8>>);
|
|
|
|
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<SecretBytes, SecretProviderError> {
|
|
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<Vec<u8>, Box<dyn std::error::Error>> {
|
|
Ok(VaultWriter::new(repository, keys)
|
|
.begin_edit(
|
|
&ironstorage::command::EditRequest {
|
|
entry: entry.to_owned(),
|
|
},
|
|
secrets,
|
|
)?
|
|
.plaintext()
|
|
.expose()
|
|
.to_vec())
|
|
}
|