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