@@ -3,7 +3,7 @@
|
||||
mod support;
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
collections::{BTreeMap, VecDeque},
|
||||
error::Error,
|
||||
num::NonZeroUsize,
|
||||
sync::{Arc, Mutex},
|
||||
@@ -12,12 +12,13 @@ use std::{
|
||||
|
||||
use ironstorage::{
|
||||
config::ConfigLoader,
|
||||
crypto::KeyStore,
|
||||
crypto::{CryptoError, KeyInfo, KeyStore, SecretProvider as _, SecretProviderError},
|
||||
git::{GitCredentialProvider as _, GitError},
|
||||
repository::{EncryptedEntry, SecretBytes},
|
||||
secret_store::{
|
||||
SecretCachePolicy, SecretLocator, SecretProtection, SecretProtectionPolicy,
|
||||
SecretReference, SecretStore, SecretStoreBackend, SecretStoreError,
|
||||
OpenPgpPassphrasePrompt, OpenPgpPassphrasePromptError, SecretCachePolicy, SecretLocator,
|
||||
SecretProtection, SecretProtectionPolicy, SecretReference, SecretStore, SecretStoreBackend,
|
||||
SecretStoreError,
|
||||
},
|
||||
};
|
||||
use support::compatibility::FixtureSet;
|
||||
@@ -28,6 +29,7 @@ type TestResult = Result<(), Box<dyn Error>>;
|
||||
struct MemoryState {
|
||||
values: BTreeMap<SecretLocator, SecretBytes>,
|
||||
fault: Option<SecretStoreError>,
|
||||
create_fault: Option<SecretStoreError>,
|
||||
retrieves: usize,
|
||||
protections: Vec<SecretProtection>,
|
||||
}
|
||||
@@ -40,6 +42,10 @@ impl MemoryBackend {
|
||||
self.0.lock().expect("test mutex").fault = Some(error);
|
||||
}
|
||||
|
||||
fn fail_next_create(&self, error: SecretStoreError) {
|
||||
self.0.lock().expect("test mutex").create_fault = Some(error);
|
||||
}
|
||||
|
||||
fn corrupt_first(&self) {
|
||||
let mut state = self.0.lock().expect("test mutex");
|
||||
let value = state.values.values_mut().next().expect("stored test value");
|
||||
@@ -71,6 +77,9 @@ impl SecretStoreBackend for MemoryBackend {
|
||||
) -> Result<(), SecretStoreError> {
|
||||
let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
|
||||
Self::take_fault(&mut state)?;
|
||||
if let Some(error) = state.create_fault.take() {
|
||||
return Err(error);
|
||||
}
|
||||
state.protections.push(protection);
|
||||
if state.values.contains_key(locator) {
|
||||
return Err(SecretStoreError::AlreadyExists);
|
||||
@@ -140,6 +149,47 @@ impl SecretStoreBackend for MemoryBackend {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct MemoryPrompt(Arc<Mutex<MemoryPromptState>>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct MemoryPromptState {
|
||||
responses: VecDeque<Result<Vec<u8>, OpenPgpPassphrasePromptError>>,
|
||||
requested: Vec<String>,
|
||||
}
|
||||
|
||||
impl MemoryPrompt {
|
||||
fn responding(response: Result<&[u8], OpenPgpPassphrasePromptError>) -> Self {
|
||||
let prompt = Self::default();
|
||||
prompt
|
||||
.0
|
||||
.lock()
|
||||
.expect("test mutex")
|
||||
.responses
|
||||
.push_back(response.map(<[u8]>::to_vec));
|
||||
prompt
|
||||
}
|
||||
|
||||
fn requested(&self) -> Vec<String> {
|
||||
self.0.lock().expect("test mutex").requested.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenPgpPassphrasePrompt for MemoryPrompt {
|
||||
fn request_passphrase(
|
||||
&mut self,
|
||||
key: &KeyInfo,
|
||||
) -> Result<SecretBytes, OpenPgpPassphrasePromptError> {
|
||||
let mut state = self.0.lock().expect("test mutex");
|
||||
state.requested.push(key.fingerprint().as_str().to_owned());
|
||||
state
|
||||
.responses
|
||||
.pop_front()
|
||||
.unwrap_or(Err(OpenPgpPassphrasePromptError::Unavailable))
|
||||
.map(SecretBytes::new)
|
||||
}
|
||||
}
|
||||
|
||||
fn store(backend: MemoryBackend) -> SecretStore<MemoryBackend> {
|
||||
SecretStore::new(
|
||||
backend,
|
||||
@@ -339,6 +389,121 @@ fn one_unlocked_provider_supplies_openpgp_and_https_git_secrets() -> TestResult
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_openpgp_passphrases_are_prompted_verified_and_only_then_persisted() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
let key = fixture.key("alice")?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let entry = fixture
|
||||
.generated
|
||||
.entries
|
||||
.iter()
|
||||
.find(|entry| entry.store == "basic" && entry.path == "email/personal.gpg")
|
||||
.expect("compatibility entry exists");
|
||||
let ciphertext = EncryptedEntry::new(fixture.read(format!("stores/basic/{}", entry.path))?);
|
||||
let expected = fixture.read("expected/basic/email/personal.txt")?;
|
||||
let reference = SecretReference::openpgp_passphrase(&key.primary_fingerprint)?;
|
||||
let key_info = keys
|
||||
.infos()
|
||||
.find(|info| info.fingerprint().as_str() == key.primary_fingerprint)
|
||||
.expect("fixture key info");
|
||||
|
||||
let mut unprompted = store(MemoryBackend::default());
|
||||
unprompted.unlock()?;
|
||||
assert!(matches!(
|
||||
unprompted.secret_for(&key_info),
|
||||
Err(SecretProviderError::Missing)
|
||||
));
|
||||
|
||||
let backend = MemoryBackend::default();
|
||||
let prompt = MemoryPrompt::responding(Ok(key.passphrase.as_bytes()));
|
||||
let mut provisioned = store(backend).with_openpgp_passphrase_prompt(prompt.clone());
|
||||
provisioned.unlock()?;
|
||||
assert_eq!(
|
||||
keys.decrypt(&ciphertext, &mut provisioned)?.expose(),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
provisioned.retrieve(&reference)?.expose(),
|
||||
key.passphrase.as_bytes()
|
||||
);
|
||||
assert_eq!(
|
||||
keys.decrypt(&ciphertext, &mut provisioned)?.expose(),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
prompt.requested(),
|
||||
std::slice::from_ref(&key.primary_fingerprint)
|
||||
);
|
||||
|
||||
let wrong_backend = MemoryBackend::default();
|
||||
let wrong_prompt = MemoryPrompt::responding(Ok(b"incorrect passphrase"));
|
||||
let mut wrong = store(wrong_backend).with_openpgp_passphrase_prompt(wrong_prompt.clone());
|
||||
wrong.unlock()?;
|
||||
let wrong_error = keys
|
||||
.decrypt(&ciphertext, &mut wrong)
|
||||
.expect_err("incorrect prompted passphrase must fail");
|
||||
assert!(matches!(&wrong_error, CryptoError::DecryptionFailed));
|
||||
assert!(!format!("{wrong_error:?} {wrong_error}").contains("incorrect passphrase"));
|
||||
assert!(matches!(
|
||||
wrong.retrieve(&reference),
|
||||
Err(SecretStoreError::Missing)
|
||||
));
|
||||
assert_eq!(
|
||||
wrong_prompt.requested(),
|
||||
std::slice::from_ref(&key.primary_fingerprint)
|
||||
);
|
||||
|
||||
let cancelled_prompt = MemoryPrompt::responding(Err(OpenPgpPassphrasePromptError::Cancelled));
|
||||
let mut cancelled =
|
||||
store(MemoryBackend::default()).with_openpgp_passphrase_prompt(cancelled_prompt);
|
||||
cancelled.unlock()?;
|
||||
assert!(matches!(
|
||||
keys.decrypt(&ciphertext, &mut cancelled),
|
||||
Err(CryptoError::SecretProvider {
|
||||
reason: SecretProviderError::Cancelled,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(matches!(
|
||||
cancelled.retrieve(&reference),
|
||||
Err(SecretStoreError::Missing)
|
||||
));
|
||||
|
||||
let denied_backend = MemoryBackend::default();
|
||||
let denied_prompt = MemoryPrompt::responding(Ok(key.passphrase.as_bytes()));
|
||||
let mut denied =
|
||||
store(denied_backend.clone()).with_openpgp_passphrase_prompt(denied_prompt.clone());
|
||||
denied.unlock()?;
|
||||
denied_backend.fail_next(SecretStoreError::Denied);
|
||||
assert!(matches!(
|
||||
keys.decrypt(&ciphertext, &mut denied),
|
||||
Err(CryptoError::SecretProvider {
|
||||
reason: SecretProviderError::Denied,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(denied_prompt.requested().is_empty());
|
||||
|
||||
let failed_backend = MemoryBackend::default();
|
||||
let failed_prompt = MemoryPrompt::responding(Ok(key.passphrase.as_bytes()));
|
||||
let mut failed = store(failed_backend.clone()).with_openpgp_passphrase_prompt(failed_prompt);
|
||||
failed.unlock()?;
|
||||
failed_backend.fail_next_create(SecretStoreError::AlreadyExists);
|
||||
assert!(matches!(
|
||||
keys.decrypt(&ciphertext, &mut failed),
|
||||
Err(CryptoError::SecretProvider {
|
||||
reason: SecretProviderError::PersistenceFailed,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(matches!(
|
||||
failed.retrieve(&reference),
|
||||
Err(SecretStoreError::Missing)
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fs_config(temporary: &tempfile::TempDir) -> TestResult {
|
||||
std::fs::create_dir_all(temporary.path().join("keys"))?;
|
||||
std::fs::create_dir_all(temporary.path().join("vault"))?;
|
||||
|
||||
Reference in New Issue
Block a user