1use pgp::{
7 composed::{
8 ArmorOptions, Deserializable, EncryptionCaps, KeyType, SecretKeyParamsBuilder, SignedPublicKey,
9 SignedPublicSubKey, SignedSecretKey, SubkeyParamsBuilder,
10 },
11 crypto::{aead::AeadAlgorithm, ecc_curve::ECCCurve, sym::SymmetricKeyAlgorithm},
12 ser::Serialize,
13 types::{KeyDetails, KeyVersion, Password, PublicParams, S2kParams, StringToKey},
14};
15use rand::{Rng, rngs::OsRng};
16
17use crate::{Error, Result};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum Kind {
23 Pqc,
26 Compatible,
28}
29
30impl Kind {
31 pub fn of(key: &SignedPublicKey) -> Option<Kind> {
32 match key.version() {
33 KeyVersion::V6 => Some(Kind::Pqc),
34 KeyVersion::V4 => Some(Kind::Compatible),
35 _ => None,
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct SecretKey(pub(crate) SignedSecretKey);
43
44#[derive(Debug, Clone)]
46pub struct PublicKey(pub(crate) SignedPublicKey);
47
48impl SecretKey {
49 pub fn generate(kind: Kind, addresses: &[&str]) -> Result<SecretKey> {
51 let (first, others) = addresses.split_first().ok_or_else(|| Error::Key("no address".to_owned()))?;
52 let (version, primary, encryption) = match kind {
53 Kind::Pqc => (KeyVersion::V6, KeyType::MlDsa65Ed25519, KeyType::MlKem768X25519),
54 Kind::Compatible => (KeyVersion::V4, KeyType::Ed25519Legacy, KeyType::ECDH(ECCCurve::Curve25519Legacy)),
55 };
56 let subkey = SubkeyParamsBuilder::default()
57 .version(version)
58 .key_type(encryption)
59 .can_sign(false)
60 .can_encrypt(EncryptionCaps::All)
61 .can_authenticate(false)
62 .build()
63 .map_err(|e| Error::Key(e.to_string()))?;
64 let mut params = SecretKeyParamsBuilder::default();
65 params
66 .version(version)
67 .key_type(primary)
68 .can_certify(true)
69 .can_sign(true)
70 .can_encrypt(EncryptionCaps::None)
71 .primary_user_id(format!("<{first}>"))
72 .user_ids(others.iter().map(|a| format!("<{a}>")).collect())
73 .feature_seipd_v2(kind == Kind::Pqc)
74 .preferred_symmetric_algorithms(
75 [SymmetricKeyAlgorithm::AES256, SymmetricKeyAlgorithm::AES128].into_iter().collect(),
76 )
77 .subkeys(vec![subkey]);
78 if kind == Kind::Pqc {
79 params
80 .preferred_aead_algorithms([(SymmetricKeyAlgorithm::AES256, AeadAlgorithm::Ocb)].into_iter().collect());
81 }
82 let params = params.build().map_err(|e| Error::Key(e.to_string()))?;
83 Ok(SecretKey(params.generate(OsRng)?))
84 }
85
86 pub fn public(&self) -> PublicKey {
87 PublicKey(SignedPublicKey::from(self.0.clone()))
88 }
89
90 pub fn fingerprint(&self) -> String {
91 hex(self.0.fingerprint().as_bytes())
92 }
93
94 pub fn protect(&self, passphrase: &str) -> Result<Vec<u8>> {
97 let mut key = self.0.clone();
98 let password = Password::from(passphrase);
99 key.primary_key.set_password_with_s2k(&password, s2k())?;
100 for subkey in &mut key.secret_subkeys {
101 subkey.key.set_password_with_s2k(&password, s2k())?;
102 }
103 Ok(key.to_bytes()?)
104 }
105
106 pub fn unlock(protected: &[u8], passphrase: &str) -> Result<SecretKey> {
108 let mut key = SignedSecretKey::from_bytes(protected).map_err(|e| Error::Key(e.to_string()))?;
109 key.verify_bindings().map_err(|e| Error::Key(e.to_string()))?;
110 let password = Password::from(passphrase);
111 key.primary_key.remove_password(&password).map_err(|_| Error::Unlock)?;
112 for subkey in &mut key.secret_subkeys {
113 subkey.key.remove_password(&password).map_err(|_| Error::Unlock)?;
114 }
115 Ok(SecretKey(key))
116 }
117
118 pub fn export(&self) -> Result<String> {
120 Ok(self.0.to_armored_string(ArmorOptions::default())?)
121 }
122
123 pub fn import(armored_or_binary: &[u8]) -> Result<SecretKey> {
124 let (key, _) = SignedSecretKey::from_reader_single(armored_or_binary).map_err(|e| Error::Key(e.to_string()))?;
125 key.verify_bindings().map_err(|e| Error::Key(e.to_string()))?;
126 if key.primary_key.secret_params().is_encrypted() {
127 return Err(Error::Key("the key is protected; remove its passphrase first".to_owned()));
128 }
129 let algorithms = std::iter::once(key.primary_key.public_params())
130 .chain(key.secret_subkeys.iter().map(|s| s.key.public_params()));
131 if !algorithms.into_iter().all(supported) {
132 return Err(Error::Key("only Curve25519 and post-quantum keys can be used here".to_owned()));
133 }
134 Ok(SecretKey(key))
135 }
136}
137
138impl PublicKey {
139 pub fn parse(armored_or_binary: &[u8]) -> Result<PublicKey> {
141 let (key, _) = SignedPublicKey::from_reader_single(armored_or_binary).map_err(|e| Error::Key(e.to_string()))?;
142 key.verify_bindings().map_err(|e| Error::Key(e.to_string()))?;
143 Ok(PublicKey(key))
144 }
145
146 pub fn parse_many(binary: &[u8]) -> Vec<PublicKey> {
148 match SignedPublicKey::from_bytes_many(binary) {
149 Ok(keys) => {
150 keys.filter_map(|key| key.ok()).filter(|key| key.verify_bindings().is_ok()).map(PublicKey).collect()
151 }
152 Err(_) => Vec::new(),
153 }
154 }
155
156 pub fn fingerprint(&self) -> String {
157 hex(self.0.fingerprint().as_bytes())
158 }
159
160 pub fn kind(&self) -> Option<Kind> {
161 Kind::of(&self.0)
162 }
163
164 pub fn to_bytes(&self) -> Result<Vec<u8>> {
165 Ok(self.0.to_bytes()?)
166 }
167
168 pub fn armored(&self) -> Result<String> {
169 Ok(self.0.to_armored_string(ArmorOptions::default())?)
170 }
171
172 pub fn addresses(&self) -> Vec<String> {
174 self.0
175 .details
176 .users
177 .iter()
178 .filter_map(|user| user.id.as_str())
179 .map(|id| match (id.rfind('<'), id.rfind('>')) {
180 (Some(start), Some(end)) if start < end => id[start + 1..end].to_owned(),
181 _ => id.to_owned(),
182 })
183 .collect()
184 }
185
186 pub fn can_encrypt(&self) -> bool {
188 !self.encryption_subkeys().is_empty()
189 }
190
191 pub(crate) fn encryption_subkeys(&self) -> Vec<&SignedPublicSubKey> {
193 self.0
194 .public_subkeys
195 .iter()
196 .filter(|subkey| subkey.key.algorithm().can_encrypt())
197 .filter(|subkey| {
198 subkey.signatures.iter().any(|s| s.key_flags().encrypt_comms() || s.key_flags().encrypt_storage())
199 })
200 .collect()
201 }
202}
203
204pub fn recovery_code() -> String {
206 const ALPHABET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
207 let mut rng = OsRng;
208 (0..32)
209 .map(|i| {
210 let c = ALPHABET[rng.gen_range(0..ALPHABET.len())] as char;
211 if i > 0 && i % 4 == 0 { format!("-{c}") } else { c.to_string() }
212 })
213 .collect()
214}
215
216pub fn normalize_recovery_code(typed: &str) -> String {
218 let code: String = typed
219 .chars()
220 .filter(|c| c.is_ascii_alphanumeric())
221 .map(|c| match c.to_ascii_uppercase() {
222 'O' => '0',
223 'I' | 'L' => '1',
224 c => c,
225 })
226 .collect();
227 code.as_bytes().chunks(4).map(|c| String::from_utf8_lossy(c).into_owned()).collect::<Vec<_>>().join("-")
228}
229
230fn supported(params: &PublicParams) -> bool {
233 match params {
234 PublicParams::EdDSALegacy(_) | PublicParams::Ed25519(_) | PublicParams::X25519(_) => true,
235 PublicParams::MlDsa65Ed25519(_) | PublicParams::MlKem768X25519(_) => true,
236 PublicParams::ECDH(ecdh) => ecdh.curve() == ECCCurve::Curve25519Legacy,
237 _ => false,
238 }
239}
240
241fn s2k() -> S2kParams {
242 let mut rng = OsRng;
243 let aead_mode = AeadAlgorithm::Ocb;
244 let mut nonce = vec![0u8; aead_mode.nonce_size()];
245 rng.fill(&mut nonce[..]);
246 let mut salt = [0u8; 16];
247 rng.fill(&mut salt[..]);
248 S2kParams::Aead {
249 sym_alg: SymmetricKeyAlgorithm::AES256,
250 aead_mode,
251 s2k: StringToKey::Argon2 { salt, t: 3, p: 4, m_enc: 16 },
252 nonce: nonce.into(),
253 }
254}
255
256pub(crate) fn hex(bytes: &[u8]) -> String {
257 bytes.iter().map(|b| format!("{b:02x}")).collect()
258}