radicle_keystore/
crypto.rs

1// This file is part of radicle-link
2// <https://github.com/radicle-dev/radicle-link>
3//
4// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
5//
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License version 3 or
8// later as published by the Free Software Foundation.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18use chacha20poly1305::{
19    aead,
20    aead::{Aead, NewAead},
21};
22use generic_array::GenericArray;
23use secstr::{SecStr, SecUtf8};
24use serde::{Deserialize, Serialize};
25use thiserror::Error;
26
27use crate::pinentry::Pinentry;
28
29/// Parameters for the key derivation function.
30pub type KdfParams = scrypt::Params;
31
32lazy_static! {
33    /// [`KdfParams`] suitable for production use.
34    pub static ref KDF_PARAMS_PROD: KdfParams = scrypt::Params::new(15, 8, 1).unwrap();
35
36    /// [`KdfParams`] suitable for use in tests.
37    ///
38    /// # Warning
39    ///
40    /// These parameters allows a brute-force attack against an encrypted
41    /// [`SecretBox`] to be carried out at significantly lower cost. Care must
42    /// be taken by users of this library to prevent accidental use of test
43    /// parameters in a production setting.
44    pub static ref KDF_PARAMS_TEST: KdfParams = scrypt::Params::new(4, 8, 1).unwrap();
45}
46
47/// Nonce used for secret box.
48type Nonce = GenericArray<u8, <chacha20poly1305::ChaCha20Poly1305 as aead::AeadCore>::NonceSize>;
49
50/// Size of the salt, in bytes.
51const SALT_SIZE: usize = 24;
52
53/// 192-bit salt.
54type Salt = [u8; SALT_SIZE];
55
56/// Class of types which can seal (encrypt) a secret, and unseal (decrypt) it
57/// from it's sealed form.
58///
59/// It is up to the user to perform conversion from and to domain types.
60pub trait Crypto: Sized {
61    type SecretBox;
62    type Error;
63
64    fn seal<K: AsRef<[u8]>>(&self, secret: K) -> Result<Self::SecretBox, Self::Error>;
65    fn unseal(&self, secret_box: Self::SecretBox) -> Result<SecStr, Self::Error>;
66}
67
68#[derive(Clone, Serialize, Deserialize)]
69pub struct SecretBox {
70    nonce: Nonce,
71    salt: Salt,
72    sealed: Vec<u8>,
73}
74
75#[derive(Debug, Error)]
76pub enum SecretBoxError<PinentryError: std::error::Error + 'static> {
77    #[error("Unable to decrypt secret box using the derived key")]
78    InvalidKey,
79
80    #[error("Error returned from underlying crypto")]
81    CryptoError,
82
83    #[error("Error getting passphrase")]
84    Pinentry(#[from] PinentryError),
85}
86
87/// A [`Crypto`] implementation based on `libsodium`'s "secretbox".
88///
89/// While historically based on `libsodium`, the underlying implementation is
90/// now based on the [`chacha20poly1305`] crate. The encryption key is derived
91/// from a passphrase using [`scrypt`].
92///
93/// The resulting [`SecretBox`] stores the ciphertext alongside cleartext salt
94/// and nonce values.
95#[derive(Clone)]
96pub struct Pwhash<P> {
97    pinentry: P,
98    params: KdfParams,
99}
100
101impl<P> Pwhash<P> {
102    /// Create a new [`Pwhash`] value
103    pub fn new(pinentry: P, params: KdfParams) -> Self {
104        Self { pinentry, params }
105    }
106}
107
108impl<P> Crypto for Pwhash<P>
109where
110    P: Pinentry,
111    P::Error: std::error::Error + 'static,
112{
113    type SecretBox = SecretBox;
114    type Error = SecretBoxError<P::Error>;
115
116    fn seal<K: AsRef<[u8]>>(&self, secret: K) -> Result<Self::SecretBox, Self::Error> {
117        use rand::RngCore;
118
119        let passphrase = self
120            .pinentry
121            .get_passphrase()
122            .map_err(SecretBoxError::Pinentry)?;
123
124        let mut rng = rand::thread_rng();
125
126        // Generate nonce.
127        let mut nonce = [0; 12];
128        rng.fill_bytes(&mut nonce);
129
130        // Generate salt.
131        let mut salt: Salt = [0; SALT_SIZE];
132        rng.fill_bytes(&mut salt);
133
134        // Derive key from passphrase.
135        let nonce = *Nonce::from_slice(&nonce[..]);
136        let derived = derive_key(&salt, &passphrase, &self.params);
137        let key = chacha20poly1305::Key::from_slice(&derived[..]);
138        let cipher = chacha20poly1305::ChaCha20Poly1305::new(key);
139
140        let sealed = cipher
141            .encrypt(&nonce, secret.as_ref())
142            .map_err(|_| Self::Error::CryptoError)?;
143
144        Ok(SecretBox {
145            nonce,
146            salt,
147            sealed,
148        })
149    }
150
151    fn unseal(&self, secret_box: Self::SecretBox) -> Result<SecStr, Self::Error> {
152        let passphrase = self
153            .pinentry
154            .get_passphrase()
155            .map_err(SecretBoxError::Pinentry)?;
156
157        let derived = derive_key(&secret_box.salt, &passphrase, &self.params);
158        let key = chacha20poly1305::Key::from_slice(&derived[..]);
159        let cipher = chacha20poly1305::ChaCha20Poly1305::new(key);
160
161        cipher
162            .decrypt(&secret_box.nonce, secret_box.sealed.as_slice())
163            .map_err(|_| SecretBoxError::InvalidKey)
164            .map(SecStr::new)
165    }
166}
167
168fn derive_key(salt: &Salt, passphrase: &SecUtf8, params: &KdfParams) -> [u8; 32] {
169    let mut key = [0u8; 32];
170    scrypt::scrypt(passphrase.unsecure().as_bytes(), salt, params, &mut key)
171        .expect("Output length must not be zero");
172
173    key
174}