1use std::path::PathBuf;
2
3use age::{Identity, Recipient};
4use clap::Parser;
5use dialoguer::Password;
6use miette::{miette, Context as _, IntoDiagnostic as _, Result};
7use pinentry::PassphraseInput;
8use tokio::fs::read_to_string;
9
10pub use age::secrecy::{ExposeSecret, SecretString};
13
14#[derive(Debug, Clone, Parser)]
38pub struct PassphraseArgs {
39 #[arg(short = 'P', long)]
43 pub passphrase_path: Option<PathBuf>,
44
45 #[arg(long, conflicts_with = "passphrase_path")]
51 pub insecure_passphrase: Option<SecretString>,
52}
53
54impl PassphraseArgs {
55 pub async fn require(&self) -> Result<Passphrase> {
57 self.get(false).await
58 }
59
60 pub async fn require_with_confirmation(&self) -> Result<Passphrase> {
62 self.get(true).await
63 }
64
65 pub async fn require_phrase(&self) -> Result<SecretString> {
67 self.get_phrase(false).await
68 }
69
70 pub async fn require_phrase_with_confirmation(&self) -> Result<SecretString> {
72 self.get_phrase(true).await
73 }
74
75 async fn get(&self, confirm: bool) -> Result<Passphrase> {
76 self.get_phrase(confirm).await.map(Passphrase::new)
77 }
78
79 async fn get_phrase(&self, confirm: bool) -> Result<SecretString> {
80 if let Some(ref phrase) = self.insecure_passphrase {
81 Ok(phrase.clone())
82 } else if let Some(ref path) = self.passphrase_path {
83 Ok(read_to_string(path)
84 .await
85 .into_diagnostic()
86 .wrap_err("reading keyfile")?
87 .trim()
88 .into())
89 } else if let Some(mut input) = PassphraseInput::with_default_binary() {
90 input
91 .with_prompt("Passphrase:")
92 .required("Cannot use an empty passphrase");
93 if confirm {
94 input.with_confirmation("Confirm passphrase:", "Passphrases do not match");
95 }
96 input.interact().map_err(|err| miette!("{err}"))
97 } else {
98 let mut prompt = Password::new().with_prompt("Passphrase");
99 if confirm {
100 prompt = prompt.with_confirmation("Confirm passphrase", "Passphrases do not match");
101 }
102 let phrase = prompt.interact().into_diagnostic()?;
103 Ok(phrase.into())
104 }
105 }
106}
107
108pub struct Passphrase(age::scrypt::Recipient, age::scrypt::Identity);
113
114impl Passphrase {
115 pub fn new(secret: SecretString) -> Self {
117 Self(
118 age::scrypt::Recipient::new(secret.clone()),
119 age::scrypt::Identity::new(secret),
120 )
121 }
122}
123
124impl Recipient for Passphrase {
125 fn wrap_file_key(
126 &self,
127 file_key: &age_core::format::FileKey,
128 ) -> std::result::Result<
129 (
130 Vec<age_core::format::Stanza>,
131 std::collections::HashSet<String>,
132 ),
133 age::EncryptError,
134 > {
135 self.0.wrap_file_key(file_key)
136 }
137}
138
139impl Identity for Passphrase {
140 fn unwrap_stanza(
141 &self,
142 stanza: &age_core::format::Stanza,
143 ) -> Option<std::result::Result<age_core::format::FileKey, age::DecryptError>> {
144 self.1.unwrap_stanza(stanza)
145 }
146
147 fn unwrap_stanzas(
148 &self,
149 stanzas: &[age_core::format::Stanza],
150 ) -> Option<std::result::Result<age_core::format::FileKey, age::DecryptError>> {
151 self.1.unwrap_stanzas(stanzas)
152 }
153}