Skip to main content

pkgar_keys/
lib.rs

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    /// The default location for pkgar to look for the user's public key.
38    ///
39    /// Defaults to `$HOME/.pkgar/keys/id_ed25519.pub.toml`. If `$HOME` is
40    /// unset, `./.pkgar/keys/id_ed25519.pub.toml`.
41    pub static ref DEFAULT_PUBKEY: PathBuf = {
42        Path::join(&HOMEDIR, ".pkgar/keys/id_ed25519.pub.toml")
43    };
44
45    /// The default location for pkgar to look for the user's secret key.
46    ///
47    /// Defaults to `$HOME/.pkgar/keys/id_ed25519.toml`. If `$HOME` is unset,
48    /// `./.pkgar/keys/id_ed25519.toml`.
49    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    //TODO: Macro?
62    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/// Standard pkgar public key format definition. Use serde to serialize/deserialize
79/// files into this struct (helper methods available).
80#[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    /// Parse a `PublicKeyFile` from `file` (in toml format).
92    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    /// Write `self` serialized as toml to `w`.
102    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    /// Shortcut to write the public key to `file`
112    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        // Public key is only 64 bytes...
185        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/// Standard pkgar private key format definition. Use serde.
198/// Internally, this struct stores the encrypted state of the private key as an enum.
199/// Manipulate the state using the `encrypt()`, `decrypt()` and `is_encrypted()`.
200#[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    /// Generate a keypair with all the nessesary info to save both keys. You
212    /// must call `save()` on each object to persist them to disk.
213    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    /// Parse a `SecretKeyFile` from `file` (in toml format).
227    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    /// Write `self` serialized as toml to `w`.
237    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    /// Shortcut to write the secret key to `file`.
248    ///
249    /// Make sure to call `encrypt()` in order to encrypt
250    /// the private key, otherwise it will be stored as plain text.
251    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    /// Ensure that the internal state of this struct is encrypted.
268    /// Note that if passwd is empty, this function is a no-op.
269    pub fn encrypt(&mut self, passwd: Passwd) -> Result<(), Error> {
270        self.skey.encrypt(passwd, self.salt, self.nonce)
271    }
272
273    /// Ensure that the internal state of this struct is decrypted.
274    /// If the internal state is already decrypted, this function is a no-op.
275    pub fn decrypt(&mut self, passwd: Passwd) -> Result<(), Error> {
276        self.skey.decrypt(passwd, self.salt, self.nonce)
277    }
278
279    /// Status of the internal state.
280    pub fn is_encrypted(&self) -> bool {
281        match self.skey {
282            SKey::Cipher(_) => true,
283            SKey::Plain(_) => false,
284        }
285    }
286
287    /// Returns `None` if the secret key is encrypted.
288    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    /// Returns `None` if the secret key is encrypted.
296    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    /// Returns `None` if the secret key is encrypted.
306    pub fn public_key_file(&self) -> Option<PublicKeyFile> {
307        Some(PublicKeyFile {
308            pkey: self.public_key()?,
309        })
310    }
311}
312
313/// Secure in-memory representation of a password.
314pub struct Passwd {
315    bytes: SecBytes,
316}
317
318impl Passwd {
319    /// Create a new `Passwd` and zero the old string.
320    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    /// Prompt the user for a `Passwd` on stdin.
331    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    /// Prompt for a password on stdin and confirm it. For configurable
369    /// prompts, use [`Passwd::prompt`](struct.Passwd.html#method.prompt).
370    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    /// Get a key for symmetric key encryption from a password.
383    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
409/// Generate a new keypair. The new keys will be saved to `file`. The user
410/// will be prompted on stdin for a password, empty passwords will cause the
411/// secret key to be stored in plain text. Note that parent
412/// directories will not be created.
413pub 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
444/// Get a SecretKeyFile from a path. If the file is encrypted, prompt for a password on stdin.
445pub fn get_skey(skey_path: &Path) -> Result<SecretKeyFile, Error> {
446    prompt_skey(skey_path, "Passphrase for")
447}
448
449/// Open, decrypt, re-encrypt with a different passphrase from stdin, and save the newly encrypted
450/// secret key at `skey_path`.
451pub 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}