@@ -39,8 +39,8 @@ use ironstorage::{
|
||||
},
|
||||
repository::{DirectoryPath, Repository, SecretBytes},
|
||||
secret_store::{
|
||||
NativeSecretStore, SecretCachePolicy, SecretProtectionPolicy, SecretStore,
|
||||
SecretStoreBackend,
|
||||
NativeSecretStore, OpenPgpPassphrasePrompt, OpenPgpPassphrasePromptError,
|
||||
SecretCachePolicy, SecretProtectionPolicy, SecretStore, SecretStoreBackend,
|
||||
},
|
||||
write::{
|
||||
EntryCommit, EntryCommitError, EntryCommitter, InsertContent, NoGitEntryCommitter,
|
||||
@@ -128,7 +128,7 @@ where
|
||||
)
|
||||
.and_then(|store| {
|
||||
store.unlock()?;
|
||||
Ok(store)
|
||||
Ok(store.with_openpgp_passphrase_prompt(NativeOpenPgpPassphrasePrompt))
|
||||
}) {
|
||||
Ok(secrets) => secrets,
|
||||
Err(error) => {
|
||||
@@ -1010,6 +1010,30 @@ trait CliInteraction: OtpInteraction {
|
||||
|
||||
struct NativeOtpInteraction;
|
||||
|
||||
struct NativeOpenPgpPassphrasePrompt;
|
||||
|
||||
impl OpenPgpPassphrasePrompt for NativeOpenPgpPassphrasePrompt {
|
||||
fn request_passphrase(
|
||||
&mut self,
|
||||
key: &ironstorage::crypto::KeyInfo,
|
||||
) -> Result<SecretBytes, OpenPgpPassphrasePromptError> {
|
||||
if !std::io::stdin().is_terminal() {
|
||||
return Err(OpenPgpPassphrasePromptError::Unavailable);
|
||||
}
|
||||
rpassword::prompt_password(format!(
|
||||
"Enter OpenPGP passphrase for {}: ",
|
||||
key.fingerprint()
|
||||
))
|
||||
.map(|value| SecretBytes::new(value.into_bytes()))
|
||||
.map_err(|error| match error.kind() {
|
||||
std::io::ErrorKind::Interrupted | std::io::ErrorKind::UnexpectedEof => {
|
||||
OpenPgpPassphrasePromptError::Cancelled
|
||||
}
|
||||
_ => OpenPgpPassphrasePromptError::Unavailable,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OtpInteraction for NativeOtpInteraction {
|
||||
fn standard_input_is_terminal(&self) -> bool {
|
||||
std::io::stdin().is_terminal()
|
||||
@@ -1477,13 +1501,15 @@ mod tests {
|
||||
RemoveRequest, ShowRequest,
|
||||
},
|
||||
config::Config,
|
||||
crypto::KeyInfo,
|
||||
git::GitCredentialProvider as _,
|
||||
otp::OtpInput,
|
||||
presentation::{ClipboardTimeout, QrMatrix},
|
||||
repository::{EntryPath, Repository, SecretBytes},
|
||||
secret_store::{
|
||||
SecretCachePolicy, SecretLocator, SecretProtection, SecretProtectionPolicy,
|
||||
SecretReference, SecretStore, SecretStoreBackend, SecretStoreError,
|
||||
OpenPgpPassphrasePrompt, OpenPgpPassphrasePromptError, SecretCachePolicy,
|
||||
SecretLocator, SecretProtection, SecretProtectionPolicy, SecretReference, SecretStore,
|
||||
SecretStoreBackend, SecretStoreError,
|
||||
},
|
||||
write::{InsertContent, OverwriteDecision},
|
||||
};
|
||||
@@ -1499,6 +1525,47 @@ mod tests {
|
||||
#[derive(Clone, Default)]
|
||||
struct MemoryBackend(Arc<Mutex<BTreeMap<SecretLocator, SecretBytes>>>);
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct MemoryOpenPgpPrompt(Arc<Mutex<MemoryOpenPgpPromptState>>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct MemoryOpenPgpPromptState {
|
||||
responses: VecDeque<Result<Vec<u8>, OpenPgpPassphrasePromptError>>,
|
||||
requests: Vec<String>,
|
||||
}
|
||||
|
||||
impl MemoryOpenPgpPrompt {
|
||||
fn with_passphrase(passphrase: &[u8]) -> Self {
|
||||
let prompt = Self::default();
|
||||
prompt
|
||||
.0
|
||||
.lock()
|
||||
.expect("test mutex")
|
||||
.responses
|
||||
.push_back(Ok(passphrase.to_vec()));
|
||||
prompt
|
||||
}
|
||||
|
||||
fn requests(&self) -> Vec<String> {
|
||||
self.0.lock().expect("test mutex").requests.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenPgpPassphrasePrompt for MemoryOpenPgpPrompt {
|
||||
fn request_passphrase(
|
||||
&mut self,
|
||||
key: &KeyInfo,
|
||||
) -> Result<SecretBytes, OpenPgpPassphrasePromptError> {
|
||||
let mut state = self.0.lock().expect("test mutex");
|
||||
state.requests.push(key.fingerprint().as_str().to_owned());
|
||||
state
|
||||
.responses
|
||||
.pop_front()
|
||||
.unwrap_or(Err(OpenPgpPassphrasePromptError::Unavailable))
|
||||
.map(SecretBytes::new)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MemoryPresentation {
|
||||
clipboard: Vec<Vec<u8>>,
|
||||
@@ -1802,6 +1869,62 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_prompts_for_a_missing_openpgp_passphrase_then_stores_and_reuses_it() -> TestResult {
|
||||
const FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
|
||||
const PASSPHRASE: &[u8] = b"fixture-alice-passphrase";
|
||||
let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../crates/storage/tests/fixtures/compatibility");
|
||||
let temporary = tempfile::tempdir()?;
|
||||
let config_path = temporary.path().join("config.toml");
|
||||
fs::write(
|
||||
&config_path,
|
||||
format!(
|
||||
"vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\n",
|
||||
fixtures.join("stores/basic"),
|
||||
FINGERPRINT,
|
||||
fixtures.join("keys"),
|
||||
),
|
||||
)?;
|
||||
let config = Config::load(Some(&config_path))?;
|
||||
let prompt = MemoryOpenPgpPrompt::with_passphrase(PASSPHRASE);
|
||||
let mut secrets = SecretStore::new(
|
||||
MemoryBackend::default(),
|
||||
SecretCachePolicy::Disabled,
|
||||
SecretProtectionPolicy::device_unlocked(),
|
||||
)
|
||||
.with_openpgp_passphrase_prompt(prompt.clone());
|
||||
secrets.unlock()?;
|
||||
let request = CommandRequest::Show(ShowRequest {
|
||||
entry: Some("email/personal".to_owned()),
|
||||
presentation: Presentation::Terminal,
|
||||
});
|
||||
|
||||
for _ in 0..2 {
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
assert_eq!(
|
||||
execute_secure(&config, &request, &mut secrets, &mut stdout, &mut stderr)
|
||||
.expect("memory output cannot fail"),
|
||||
EXIT_SUCCESS
|
||||
);
|
||||
assert_eq!(
|
||||
stdout,
|
||||
fs::read(fixtures.join("expected/basic/email/personal.txt"))?
|
||||
);
|
||||
assert!(stderr.is_empty());
|
||||
}
|
||||
|
||||
assert_eq!(prompt.requests(), [FINGERPRINT.to_owned()]);
|
||||
assert_eq!(
|
||||
secrets
|
||||
.retrieve(&SecretReference::openpgp_passphrase(FINGERPRINT)?)?
|
||||
.expose(),
|
||||
PASSPHRASE
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_clipboard_and_qr_paths_never_emit_plaintext() -> TestResult {
|
||||
const FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
|
||||
|
||||
Reference in New Issue
Block a user