diff options
author | Justus Winter <justus@sequoia-pgp.org> | 2023-07-17 16:47:14 +0200 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2023-07-25 11:44:27 +0200 |
commit | 64a5636b6926092015c60b109e6fe51500abc163 (patch) | |
tree | 3c2142c31460832f3071cf850a66606b58e3561d | |
parent | 4fef5e28650b287d0c43f944b2f864f7d33aa8a9 (diff) |
ipc: Implement loopback password entry.
-rw-r--r-- | ipc/src/gnupg.rs | 79 | ||||
-rw-r--r-- | ipc/tests/gpg-agent.rs | 34 |
2 files changed, 83 insertions, 30 deletions
diff --git a/ipc/src/gnupg.rs b/ipc/src/gnupg.rs index 3a070d04..ca833040 100644 --- a/ipc/src/gnupg.rs +++ b/ipc/src/gnupg.rs @@ -351,6 +351,10 @@ impl Agent { self.send_simple(option).await?; } + if key.password.is_some() { + self.send_simple("OPTION pinentry-mode=loopback").await?; + } + let grip = Keygrip::of(key.public.mpis())?; self.send_simple(format!("SIGKEY {}", grip)).await?; self.send_simple( @@ -369,8 +373,15 @@ impl Agent { | assuan::Response::Comment { .. } | assuan::Response::Status { .. } => (), // Ignore. - assuan::Response::Inquire { .. } => - acknowledge_inquiry(self).await?, + assuan::Response::Inquire { keyword, .. } => + match (keyword.as_str(), &key.password) { + ("PASSPHRASE", Some(p)) => { + p.map(|p| self.data(p))?; + // Dummy read to send the data. + self.next().await; + }, + _ => acknowledge_inquiry(self).await?, + }, assuan::Response::Error { ref message, .. } => return assuan::operation_failed(self, message).await, assuan::Response::Data { ref partial } => @@ -392,41 +403,38 @@ impl Agent { self.send_simple(option).await?; } + if key.password.is_some() { + self.send_simple("OPTION pinentry-mode=loopback").await?; + } + let grip = Keygrip::of(key.public.mpis())?; self.send_simple(format!("SETKEY {}", grip)).await?; self.send_simple(format!("SETKEYDESC {}", assuan::escape(&key.password_prompt))).await?; self.send("PKDECRYPT")?; - while let Some(r) = self.next().await { - match r? { - assuan::Response::Inquire { ref keyword, .. } => - if keyword == "CIPHERTEXT" { - // What we expect. - } else { - acknowledge_inquiry(self).await?; - }, - assuan::Response::Comment { .. } - | assuan::Response::Status { .. } => - (), // Ignore. - assuan::Response::Error { ref message, .. } => - return assuan::operation_failed(self, message).await, - r => - return assuan::protocol_error(&r), - } - } - - let mut buf = Vec::new(); - Sexp::try_from(ciphertext)?.serialize(&mut buf)?; - self.data(&buf)?; let mut padding = true; let mut data = Vec::new(); while let Some(r) = self.next().await { match r? { - assuan::Response::Ok { .. } - | assuan::Response::Comment { .. } => + assuan::Response::Ok { .. } | + assuan::Response::Comment { .. } => (), // Ignore. - assuan::Response::Inquire { .. } => - acknowledge_inquiry(self).await?, + assuan::Response::Inquire { ref keyword, .. } => + match (keyword.as_str(), &key.password) { + ("PASSPHRASE", Some(p)) => { + p.map(|p| self.data(p))?; + // Dummy read to send the data. + self.next().await; + }, + ("CIPHERTEXT", _) => { + let mut buf = Vec::new(); + Sexp::try_from(ciphertext)?.serialize(&mut buf)?; + self.data(&buf)?; + // Dummy read to send the data. + self.next().await; + }, + _ => acknowledge_inquiry(self).await?, + }, assuan::Response::Status { ref keyword, ref message } => if keyword == "PADDING" { padding = message != "0"; @@ -504,6 +512,7 @@ impl Agent { pub struct KeyPair { public: Key<key::PublicParts, key::UnspecifiedRole>, agent_socket: PathBuf, + password: Option<crypto::Password>, password_prompt: String, } @@ -518,6 +527,7 @@ impl KeyPair { where R: key::KeyRole { Ok(KeyPair { + password: None, password_prompt: format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ @@ -587,6 +597,21 @@ impl KeyPair { self.with_password_prompt(prompt) } + /// Supplies a password to unlock the secret key. + /// + /// This will be used when the secret key operation is performed, + /// e.g. when signing or decrypting a message. + /// + /// Note: This is the equivalent of GnuPG's + /// `--pinentry-mode=loopback` and requires explicit opt-in in the + /// gpg-agent configuration using the `allow-loopback-pinentry` + /// option. If this is not enabled in the agent, the secret key + /// operation will fail. It is likely only useful during testing. + pub fn with_password(mut self, password: crypto::Password) -> Self { + self.password = Some(password); + self + } + /// Changes the password prompt. /// /// Use this function to give more context to the user when she is diff --git a/ipc/tests/gpg-agent.rs b/ipc/tests/gpg-agent.rs index 331cc298..e5c7858a 100644 --- a/ipc/tests/gpg-agent.rs +++ b/ipc/tests/gpg-agent.rs @@ -36,6 +36,10 @@ macro_rules! make_context { return Ok(()); }, }; + + std::fs::write(ctx.homedir().unwrap().join("gpg-agent.conf"), + "allow-loopback-pinentry\n").unwrap(); + match ctx.start("gpg-agent") { Ok(_) => (), Err(e) => { @@ -71,6 +75,7 @@ async fn help() -> openpgp::Result<()> { } const MESSAGE: &str = "дружба"; +const PASSWORD: &str = "streng geheim"; fn gpg_import(ctx: &Context, what: &[u8]) -> openpgp::Result<()> { use std::process::{Command, Stdio}; @@ -80,6 +85,7 @@ fn gpg_import(ctx: &Context, what: &[u8]) -> openpgp::Result<()> { .stdout(Stdio::piped()) .stderr(Stdio::piped()) .arg("--homedir").arg(ctx.homedir().unwrap()) + .arg("--batch") .arg("--import") .spawn() .context("failed to start gpg")?; @@ -126,22 +132,28 @@ fn sign() -> openpgp::Result<()> { let ctx = make_context!(); for cs in &[RSA2k, Cv25519, P521] { + for password in vec![None, Some(PASSWORD.into())] { let (cert, _) = CertBuilder::new() .set_cipher_suite(*cs) .add_userid("someone@example.org") .add_signing_subkey() + .set_password(password.clone()) .generate().unwrap(); let mut buf = Vec::new(); cert.as_tsk().serialize(&mut buf).unwrap(); gpg_import(&ctx, &buf)?; - let keypair = KeyPair::new( + let mut keypair = KeyPair::new( &ctx, cert.keys().with_policy(p, None).alive().revoked(false) .for_signing().take(1).next().unwrap().key()) .unwrap(); + if let Some(p) = password.clone() { + keypair = keypair.with_password(p); + } + let mut message = Vec::new(); { // Start streaming an OpenPGP message. @@ -222,6 +234,7 @@ fn sign() -> openpgp::Result<()> { Err(anyhow::anyhow!("Signature verification failed")) } } + } } Ok(()) } @@ -252,12 +265,14 @@ fn decrypt(also_try_explicit_async: bool) -> openpgp::Result<()> { .generate()?; for cs in &[RSA2k, Cv25519, P521] { + for password in vec![None, Some(PASSWORD.into())] { let ctx = make_context!(); let (cert, _) = CertBuilder::new() .set_cipher_suite(*cs) .add_userid("someone@example.org") .add_transport_encryption_subkey() + .set_password(password.clone()) .generate().unwrap(); let mut buf = Vec::new(); @@ -320,11 +335,15 @@ fn decrypt(also_try_explicit_async: bool) -> openpgp::Result<()> { .is_err()); // Now try "our" key. - let keypair = KeyPair::new( + let mut keypair = KeyPair::new( &ctx, cert.keys().with_policy(p, None) .for_storage_encryption().for_transport_encryption() .take(1).next().unwrap().key())?; + if let Some(p) = password.clone() { + keypair = keypair.with_password(p); + } + assert!(rt.block_on(agent.decrypt(&keypair, pkesk_0.esk())) .is_ok()); @@ -334,7 +353,10 @@ fn decrypt(also_try_explicit_async: bool) -> openpgp::Result<()> { // Make a helper that that feeds the recipient's secret key to the // decryptor. - let helper = Helper { policy: p, ctx: &ctx, cert: &cert, other: &other}; + let helper = Helper { + policy: p, ctx: &ctx, cert: &cert, other: &other, + password: &password, + }; // Now, create a decryptor with a helper using the given Certs. let mut decryptor = DecryptorBuilder::from_bytes(&message).unwrap() @@ -350,6 +372,7 @@ fn decrypt(also_try_explicit_async: bool) -> openpgp::Result<()> { ctx: &'a Context, cert: &'a openpgp::Cert, other: &'a openpgp::Cert, + password: &'a Option<openpgp::crypto::Password>, } impl<'a> VerificationHelper for Helper<'a> { @@ -396,6 +419,10 @@ fn decrypt(also_try_explicit_async: bool) -> openpgp::Result<()> { .take(1).next().unwrap().key()) .unwrap(); + if let Some(p) = self.password.clone() { + keypair = keypair.with_password(p); + } + for pkesk in pkesks { if *pkesk.recipient() != keypair.public().keyid() { continue; @@ -412,6 +439,7 @@ fn decrypt(also_try_explicit_async: bool) -> openpgp::Result<()> { Ok(None) } } + } } Ok(()) } |