1mod error;
2
3use std::fs::{self, File, OpenOptions};
4use std::io::{self, stdin, stdout, Write};
5use std::ops::Deref;
6use std::os::unix::fs::OpenOptionsExt;
7use std::path::{Path, PathBuf};
8
9use hex::FromHex;
10use lazy_static::lazy_static;
11use pkgar_core::{
12 dryoc::{
13 classic::{
14 crypto_pwhash::{crypto_pwhash, PasswordHashAlgorithm},
15 crypto_secretbox::{crypto_secretbox_easy, crypto_secretbox_open_easy, Key, Nonce},
16 crypto_sign::{crypto_sign_keypair, crypto_sign_seed_keypair},
17 },
18 constants::{CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE},
19 types::NewByteArray,
20 },
21 PublicKey, SecretKey,
22};
23use seckey::SecBytes;
24use serde::{Deserialize, Serialize};
25use termion::input::TermRead;
26
27type Salt = [u8; 32];
28
29pub use crate::error::Error;
30
31lazy_static! {
32 static ref HOMEDIR: PathBuf = {
33 dirs::home_dir()
34 .unwrap_or("./".into())
35 };
36
37 pub static ref DEFAULT_PUBKEY: PathBuf = {
42 Path::join(&HOMEDIR, ".pkgar/keys/id_ed25519.pub.toml")
43 };
44
45 pub static ref DEFAULT_SECKEY: PathBuf = {
50 Path::join(&HOMEDIR, ".pkgar/keys/id_ed25519.toml")
51 };
52}
53
54mod ser {
55 use hex::FromHex;
56 use serde::de::Error;
57 use serde::{Deserialize, Deserializer};
58
59 use crate::{Nonce, PublicKey, Salt};
60
61 pub(crate) fn to_salt<'d, D: Deserializer<'d>>(deser: D) -> Result<Salt, D::Error> {
63 String::deserialize(deser)
64 .and_then(|s| <[u8; 32]>::from_hex(s).map_err(|err| Error::custom(err.to_string())))
65 }
66
67 pub(crate) fn to_nonce<'d, D: Deserializer<'d>>(deser: D) -> Result<Nonce, D::Error> {
68 String::deserialize(deser)
69 .and_then(|s| <[u8; 24]>::from_hex(s).map_err(|err| Error::custom(err.to_string())))
70 }
71
72 pub(crate) fn to_pubkey<'d, D: Deserializer<'d>>(deser: D) -> Result<PublicKey, D::Error> {
73 String::deserialize(deser)
74 .and_then(|s| <[u8; 32]>::from_hex(s).map_err(|err| Error::custom(err.to_string())))
75 }
76}
77
78#[derive(Clone, Deserialize, Serialize)]
81pub struct PublicKeyFile {
82 #[serde(serialize_with = "hex::serialize", deserialize_with = "ser::to_pubkey")]
83 pub pkey: PublicKey,
84}
85
86impl PublicKeyFile {
87 pub fn new(pubkey: PublicKey) -> Self {
88 Self { pkey: pubkey }
89 }
90
91 pub fn open(file: impl AsRef<Path>) -> Result<PublicKeyFile, Error> {
93 let content = fs::read_to_string(file).map_err(|source| Error::Io {
94 source,
95 path: None,
96 context: "Reading public key",
97 })?;
98 toml::from_str(&content).map_err(Error::Deser)
99 }
100
101 pub fn write(&self, mut w: impl Write) -> Result<(), Error> {
103 w.write_all(toml::to_string(self)?.as_bytes())
104 .map_err(|source| Error::Io {
105 source,
106 path: None,
107 context: "Writing public key",
108 })
109 }
110
111 pub fn save(&self, file: impl AsRef<Path>) -> Result<(), Error> {
113 self.write(File::create(file).map_err(|source| Error::Io {
114 source,
115 path: None,
116 context: "Writing public key",
117 })?)
118 }
119}
120
121impl std::fmt::Debug for PublicKeyFile {
122 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
123 f.debug_struct("PublicKeyFile")
124 .field("pkey", &hex::encode(self.pkey))
125 .finish()
126 }
127}
128
129enum SKey {
130 Cipher([u8; 80]),
131 Plain(SecretKey),
132}
133
134impl SKey {
135 fn encrypt(&mut self, passwd: Passwd, salt: Salt, nonce: Nonce) -> Result<(), Error> {
136 if let SKey::Plain(skey) = self {
137 if let Some(passwd_key) = passwd.gen_key(salt) {
138 let mut buf = [0; 80];
139 crypto_secretbox_easy(&mut buf, skey.as_ref(), &nonce, &passwd_key)
140 .map_err(pkgar_core::Error::Dryoc)?;
141 *self = SKey::Cipher(buf);
142 }
143 }
144 Ok(())
145 }
146
147 fn decrypt(&mut self, passwd: Passwd, salt: Salt, nonce: Nonce) -> Result<(), Error> {
148 if let SKey::Cipher(ciphertext) = self {
149 let mut buf = [0; 64];
150 if let Some(passwd_key) = passwd.gen_key(salt) {
151 crypto_secretbox_open_easy(&mut buf, ciphertext.as_ref(), &nonce, &passwd_key)
152 .map_err(pkgar_core::Error::Dryoc)?;
153 } else {
154 let skey_plain = &ciphertext[..64];
155 if skey_plain.len() != buf.len() {
156 return Err(Error::KeyInvalid {
157 expected: buf.len(),
158 actual: skey_plain.len(),
159 });
160 }
161 buf.copy_from_slice(skey_plain);
162 }
163 *self = SKey::Plain(buf);
164 }
165 Ok(())
166 }
167}
168
169impl AsRef<[u8]> for SKey {
170 fn as_ref(&self) -> &[u8] {
171 match self {
172 SKey::Cipher(buf) => buf.as_ref(),
173 SKey::Plain(skey) => skey.as_ref(),
174 }
175 }
176}
177
178impl FromHex for SKey {
179 type Error = hex::FromHexError;
180
181 fn from_hex<T: AsRef<[u8]>>(buf: T) -> Result<SKey, hex::FromHexError> {
182 let bytes = hex::decode(buf)?;
183
184 if bytes.len() == 64 {
186 let mut buf = [0; 64];
187 buf.copy_from_slice(&bytes);
188 Ok(SKey::Plain(buf))
189 } else {
190 let mut buf = [0; 80];
191 buf.copy_from_slice(&bytes);
192 Ok(SKey::Cipher(buf))
193 }
194 }
195}
196
197#[derive(Deserialize, Serialize)]
201pub struct SecretKeyFile {
202 #[serde(serialize_with = "hex::serialize", deserialize_with = "ser::to_salt")]
203 salt: Salt,
204 #[serde(serialize_with = "hex::serialize", deserialize_with = "ser::to_nonce")]
205 nonce: Nonce,
206 #[serde(with = "hex")]
207 skey: SKey,
208}
209
210impl SecretKeyFile {
211 pub fn new() -> (PublicKeyFile, SecretKeyFile) {
214 let (pkey, skey) = crypto_sign_keypair();
215
216 let pkey_file = PublicKeyFile { pkey };
217 let skey_file = SecretKeyFile {
218 salt: Salt::gen(),
219 nonce: Nonce::gen(),
220 skey: SKey::Plain(skey),
221 };
222
223 (pkey_file, skey_file)
224 }
225
226 pub fn open(file: impl AsRef<Path>) -> Result<SecretKeyFile, Error> {
228 let content = fs::read_to_string(&file).map_err(|source| Error::Io {
229 source,
230 path: Some(file.as_ref().to_path_buf()),
231 context: "Reading secret",
232 })?;
233 toml::from_str(&content).map_err(Error::Deser)
234 }
235
236 pub fn write(&self, mut w: impl Write) -> Result<(), Error> {
238 w.write_all(toml::to_string(&self)?.as_bytes())
239 .map_err(|source| Error::Io {
240 source,
241 path: None,
242 context: "Writing secret",
243 })?;
244 Ok(())
245 }
246
247 pub fn save(&self, file: impl AsRef<Path>) -> Result<(), Error> {
252 self.write(
253 OpenOptions::new()
254 .write(true)
255 .create(true)
256 .truncate(true)
257 .mode(0o600)
258 .open(&file)
259 .map_err(|source| Error::Io {
260 source,
261 path: Some(file.as_ref().to_path_buf()),
262 context: "Opening file",
263 })?,
264 )
265 }
266
267 pub fn encrypt(&mut self, passwd: Passwd) -> Result<(), Error> {
270 self.skey.encrypt(passwd, self.salt, self.nonce)
271 }
272
273 pub fn decrypt(&mut self, passwd: Passwd) -> Result<(), Error> {
276 self.skey.decrypt(passwd, self.salt, self.nonce)
277 }
278
279 pub fn is_encrypted(&self) -> bool {
281 match self.skey {
282 SKey::Cipher(_) => true,
283 SKey::Plain(_) => false,
284 }
285 }
286
287 pub fn secret_key(&self) -> Option<SecretKey> {
289 match &self.skey {
290 SKey::Plain(skey) => Some(*skey),
291 SKey::Cipher(_) => None,
292 }
293 }
294
295 pub fn public_key(&self) -> Option<PublicKey> {
297 let skey = self.secret_key()?;
298 let mut seed = [0; 32];
299 seed.copy_from_slice(&skey[..32]);
300 let (pkey, new_skey) = crypto_sign_seed_keypair(&seed);
301 assert_eq!(skey, new_skey);
302 Some(pkey)
303 }
304
305 pub fn public_key_file(&self) -> Option<PublicKeyFile> {
307 Some(PublicKeyFile {
308 pkey: self.public_key()?,
309 })
310 }
311}
312
313pub struct Passwd {
315 bytes: SecBytes,
316}
317
318impl Passwd {
319 pub fn new(passwd: &mut String) -> Passwd {
321 let pwd = Passwd {
322 bytes: SecBytes::with(passwd.len(), |buf| buf.copy_from_slice(passwd.as_bytes())),
323 };
324 unsafe {
325 seckey::zero(passwd.as_bytes_mut());
326 }
327 pwd
328 }
329
330 pub fn prompt(prompt: impl AsRef<str>) -> Result<Passwd, Error> {
332 let stdout = stdout();
333 let mut stdout = stdout.lock();
334 let stdin = stdin();
335 let mut stdin = stdin.lock();
336
337 stdout
338 .write_all(prompt.as_ref().as_bytes())
339 .map_err(|source| Error::Io {
340 source,
341 path: None,
342 context: "Writing prompt",
343 })?;
344 stdout.flush().map_err(|source| Error::Io {
345 source,
346 path: None,
347 context: "Flushing prompt",
348 })?;
349
350 let Some(mut passwd) = stdin.read_passwd(&mut stdout).map_err(|source| Error::Io {
351 source,
352 path: None,
353 context: "Reading passwd",
354 })?
355 else {
356 return Err(Error::Io {
357 source: std::io::Error::from(io::ErrorKind::UnexpectedEof),
358 path: None,
359 context: "Invalid Password Input",
360 });
361 };
362
363 println!();
364
365 Ok(Passwd::new(&mut passwd))
366 }
367
368 pub fn prompt_new() -> Result<Passwd, Error> {
371 let passwd = Passwd::prompt(
372 "Please enter a new passphrase (leave empty to store the key in plaintext): ",
373 )?;
374 let confirm = Passwd::prompt("Please re-enter the passphrase: ")?;
375
376 if passwd != confirm {
377 return Err(Error::PassphraseMismatch);
378 }
379 Ok(passwd)
380 }
381
382 fn gen_key(&self, salt: Salt) -> Option<Key> {
384 if self.bytes.read().len() > 0 {
385 let mut key = [0; 32];
386 crypto_pwhash(
387 &mut key,
388 &self.bytes.read(),
389 &salt,
390 CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
391 CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
392 PasswordHashAlgorithm::Argon2id13,
393 )
394 .expect("Failed to get key from password");
395 Some(key)
396 } else {
397 None
398 }
399 }
400}
401
402impl PartialEq for Passwd {
403 fn eq(&self, other: &Passwd) -> bool {
404 self.bytes.read().deref() == other.bytes.read().deref()
405 }
406}
407impl Eq for Passwd {}
408
409pub fn gen_keypair(
414 pkey_path: &Path,
415 skey_path: &Path,
416) -> Result<(PublicKeyFile, SecretKeyFile), Error> {
417 let passwd = Passwd::prompt_new()?;
418
419 let (pkey_file, mut skey_file) = SecretKeyFile::new();
420
421 skey_file.encrypt(passwd)?;
422 skey_file.save(skey_path)?;
423
424 pkey_file.save(pkey_path)?;
425
426 println!(
427 "Generated {} and {}",
428 pkey_path.display(),
429 skey_path.display()
430 );
431 Ok((pkey_file, skey_file))
432}
433
434fn prompt_skey(skey_path: &Path, prompt: impl AsRef<str>) -> Result<SecretKeyFile, Error> {
435 let mut key_file = SecretKeyFile::open(skey_path)?;
436
437 if key_file.is_encrypted() {
438 let passwd = Passwd::prompt(format!("{} {}: ", prompt.as_ref(), skey_path.display()))?;
439 key_file.decrypt(passwd)?;
440 }
441 Ok(key_file)
442}
443
444pub fn get_skey(skey_path: &Path) -> Result<SecretKeyFile, Error> {
446 prompt_skey(skey_path, "Passphrase for")
447}
448
449pub fn re_encrypt(skey_path: &Path) -> Result<(), Error> {
452 let mut skey_file = prompt_skey(skey_path, "Old passphrase for")?;
453
454 let passwd = Passwd::prompt_new()?;
455 skey_file.encrypt(passwd)?;
456
457 skey_file.save(skey_path)
458}