1use std::path::PathBuf;
2
3use age::{secrecy::SecretString, 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
10#[derive(Debug, Clone, Parser)]
34pub struct PassphraseArgs {
35 #[cfg_attr(docsrs, doc("\n\n**Flag**: `-P, --passphrase-path PATH`"))]
39 #[arg(short = 'P', long)]
40 pub passphrase_path: Option<PathBuf>,
41
42 #[cfg_attr(docsrs, doc("\n\n**Flag**: `--insecure-passphrase STRING`"))]
48 #[arg(long, conflicts_with = "passphrase_path")]
49 pub insecure_passphrase: Option<SecretString>,
50}
51
52impl PassphraseArgs {
53 pub async fn require(&self) -> Result<Passphrase> {
55 self.get(false).await
56 }
57
58 pub async fn require_with_confirmation(&self) -> Result<Passphrase> {
60 self.get(true).await
61 }
62
63 pub async fn require_phrase(&self) -> Result<SecretString> {
65 self.get_phrase(false).await
66 }
67
68 pub async fn require_phrase_with_confirmation(&self) -> Result<SecretString> {
70 self.get_phrase(true).await
71 }
72
73 async fn get(&self, confirm: bool) -> Result<Passphrase> {
74 self.get_phrase(confirm).await.map(Passphrase::new)
75 }
76
77 async fn get_phrase(&self, confirm: bool) -> Result<SecretString> {
78 if let Some(ref phrase) = self.insecure_passphrase {
79 Ok(phrase.clone())
80 } else if let Some(ref path) = self.passphrase_path {
81 Ok(read_to_string(path)
82 .await
83 .into_diagnostic()
84 .wrap_err("reading keyfile")?
85 .trim()
86 .into())
87 } else if let Some(mut input) = PassphraseInput::with_default_binary() {
88 input
89 .with_prompt("Passphrase:")
90 .required("Cannot use an empty passphrase");
91 if confirm {
92 input.with_confirmation("Confirm passphrase:", "Passphrases do not match");
93 }
94 input.interact().map_err(|err| miette!("{err}"))
95 } else {
96 let mut prompt = Password::new().with_prompt("Passphrase");
97 if confirm {
98 prompt = prompt.with_confirmation("Confirm passphrase", "Passphrases do not match");
99 }
100 let phrase = prompt.interact().into_diagnostic()?;
101 Ok(phrase.into())
102 }
103 }
104}
105
106pub struct Passphrase(age::scrypt::Recipient, age::scrypt::Identity);
111
112impl Passphrase {
113 pub fn new(secret: SecretString) -> Self {
115 Self(
116 age::scrypt::Recipient::new(secret.clone()),
117 age::scrypt::Identity::new(secret),
118 )
119 }
120}
121
122impl Recipient for Passphrase {
123 fn wrap_file_key(
124 &self,
125 file_key: &age_core::format::FileKey,
126 ) -> std::result::Result<
127 (
128 Vec<age_core::format::Stanza>,
129 std::collections::HashSet<String>,
130 ),
131 age::EncryptError,
132 > {
133 self.0.wrap_file_key(file_key)
134 }
135}
136
137impl Identity for Passphrase {
138 fn unwrap_stanza(
139 &self,
140 stanza: &age_core::format::Stanza,
141 ) -> Option<std::result::Result<age_core::format::FileKey, age::DecryptError>> {
142 self.1.unwrap_stanza(stanza)
143 }
144
145 fn unwrap_stanzas(
146 &self,
147 stanzas: &[age_core::format::Stanza],
148 ) -> Option<std::result::Result<age_core::format::FileKey, age::DecryptError>> {
149 self.1.unwrap_stanzas(stanzas)
150 }
151}