Skip to main content

dusk_node_data/
bls.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7use std::cmp::Ordering;
8use std::fmt::Debug;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12use aes::Aes256;
13use aes_gcm::aead::Aead;
14use aes_gcm::{AeadCore, Aes256Gcm, Key, KeyInit};
15use block_modes::block_padding::Pkcs7;
16use block_modes::{BlockMode, BlockModeError, Cbc};
17use dusk_bytes::{DeserializableSlice, Serializable};
18use dusk_core::signatures::bls::{
19    PublicKey as BlsPublicKey, SecretKey as BlsSecretKey,
20};
21use rand::RngCore;
22use rand::SeedableRng;
23use rand::rngs::{OsRng, StdRng};
24use serde::{Deserialize, Serialize};
25use serde_with::As;
26use serde_with::base64::Base64;
27use sha2::{Digest, Sha256};
28use tracing::{error, info};
29use zeroize::Zeroize;
30
31pub const PUBLIC_BLS_SIZE: usize = BlsPublicKey::SIZE;
32
33/// Extends BlsPublicKey by implementing a few traits
34///
35/// See also PublicKey::bytes(&self)
36#[derive(Default, Eq, PartialEq, Clone)]
37pub struct PublicKey {
38    inner: BlsPublicKey,
39    as_bytes: PublicKeyBytes,
40}
41
42impl TryFrom<[u8; 96]> for PublicKey {
43    type Error = dusk_bytes::Error;
44    fn try_from(bytes: [u8; 96]) -> Result<Self, Self::Error> {
45        let inner = BlsPublicKey::from_slice(&bytes)?;
46        let as_bytes = PublicKeyBytes(bytes);
47        Ok(Self { as_bytes, inner })
48    }
49}
50
51impl PublicKey {
52    pub fn new(inner: BlsPublicKey) -> Self {
53        let b = inner.to_bytes();
54        Self {
55            inner,
56            as_bytes: PublicKeyBytes(b),
57        }
58    }
59
60    /// from_sk_seed_u64 generates a sk from the specified seed and returns the
61    /// associated public key
62    pub fn from_sk_seed_u64(state: u64) -> Self {
63        let rng = &mut StdRng::seed_from_u64(state);
64        let sk = BlsSecretKey::random(rng);
65
66        Self::new(BlsPublicKey::from(&sk))
67    }
68
69    /// `bytes` returns a reference to the pk.to_bytes() initialized on
70    /// PublicKey::new call. NB: Frequent use of `to_bytes()` creates a
71    /// noticeable performance overhead.
72    pub fn bytes(&self) -> &PublicKeyBytes {
73        &self.as_bytes
74    }
75
76    pub fn inner(&self) -> &BlsPublicKey {
77        &self.inner
78    }
79
80    pub fn into_inner(self) -> BlsPublicKey {
81        self.inner
82    }
83
84    /// Truncated base58 representation of inner data
85    pub fn to_bs58(&self) -> String {
86        self.bytes().to_bs58()
87    }
88
89    /// Full base58 representation of inner data
90    pub fn to_base58(&self) -> String {
91        self.bytes().to_base58()
92    }
93}
94
95impl PartialOrd<PublicKey> for PublicKey {
96    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
97        Some(self.cmp(other))
98    }
99}
100
101impl Ord for PublicKey {
102    fn cmp(&self, other: &Self) -> Ordering {
103        self.as_bytes.inner().cmp(other.as_bytes.inner())
104    }
105}
106
107impl std::fmt::Debug for PublicKey {
108    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
109        let bs = self.to_base58();
110        f.debug_struct("PublicKey").field("bs58", &bs).finish()
111    }
112}
113/// A wrapper of 96-sized array
114#[derive(Clone, Copy, Eq, Hash, PartialEq, Serialize)]
115pub struct PublicKeyBytes(
116    #[serde(serialize_with = "crate::serialize_b58")] pub [u8; PUBLIC_BLS_SIZE],
117);
118
119impl Default for PublicKeyBytes {
120    fn default() -> Self {
121        PublicKeyBytes([0; 96])
122    }
123}
124
125impl PublicKeyBytes {
126    pub fn inner(&self) -> &[u8; 96] {
127        &self.0
128    }
129
130    /// Full base58 representation of inner data
131    pub fn to_base58(&self) -> String {
132        bs58::encode(&self.0).into_string()
133    }
134
135    /// Truncated base58 representation of inner data
136    pub fn to_bs58(&self) -> String {
137        let mut bs = self.to_base58();
138        bs.truncate(16);
139        bs
140    }
141}
142
143impl Debug for PublicKeyBytes {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        f.write_str(&self.to_bs58())
146    }
147}
148
149/// Loads consensus keys from an encrypted file.
150pub fn load_keys(
151    path: String,
152    pwd: String,
153) -> anyhow::Result<(BlsSecretKey, PublicKey)> {
154    let path_buf = PathBuf::from(path);
155    let (pk, sk) = read_from_file(path_buf, &pwd)?;
156
157    Ok((sk, PublicKey::new(pk)))
158}
159
160/// Fetches BLS public and secret keys from an encrypted consensus keys file.
161fn read_from_file(
162    path: PathBuf,
163    pwd: &str,
164) -> anyhow::Result<(BlsPublicKey, BlsSecretKey)> {
165    let contents = fs::read(&path).map_err(|e| {
166        anyhow::anyhow!(
167            "{} should be valid consensus keys file {e}",
168            path.display()
169        )
170    })?;
171
172    let (bytes, file_format_is_old) = match serde_json::from_slice::<
173        ProvisionerFileContents,
174    >(&contents)
175    {
176        Ok(contents) => {
177            let aes_key = derive_aes_key(pwd, &contents.salt);
178            let bytes = decrypt(&contents.key_pair, &aes_key, &contents.iv).map_err(
179                        |_| anyhow::anyhow!("Failed to decrypt: invalid consensus keys password or the file is corrupted"),
180                    )?;
181            (bytes, false)
182        }
183        Err(_) => {
184            let aes_key = hash_sha256(pwd);
185            let bytes = decrypt_aes_cbc(&contents, &aes_key).map_err(|e| {
186                anyhow::anyhow!("Invalid consensus keys password {e}")
187            })?;
188            (bytes, true)
189        }
190    };
191
192    let keys: BlsKeyPair = serde_json::from_slice(&bytes)
193        .map_err(|e| anyhow::anyhow!("keys files should contain json {e}"))?;
194
195    let sk = BlsSecretKey::from_slice(&keys.secret_key_bls)
196        .map_err(|e| anyhow::anyhow!("sk should be valid {e:?}"))?;
197
198    let pk = BlsPublicKey::from_slice(&keys.public_key_bls)
199        .map_err(|e| anyhow::anyhow!("pk should be valid {e:?}"))?;
200
201    if file_format_is_old {
202        info!(
203            "Your consensus keys are in the old format. Migrating to the new format and saving the old file as {}.old",
204            path.display()
205        );
206        let _ =
207            migrate_file_to_new_format(&path, &pk, &sk, pwd).inspect_err(|e| {
208                error!(
209                    "failed to migrate consensus keys to the new format: {e}"
210                );
211            });
212    }
213
214    Ok((pk, sk))
215}
216
217fn migrate_file_to_new_format(
218    path: &Path,
219    pk: &BlsPublicKey,
220    sk: &BlsSecretKey,
221    pwd: &str,
222) -> Result<(), ConsensusKeysError> {
223    save_old_file(path)?;
224    let keys_filename = path
225        .file_name()
226        .expect("keys file should have a name")
227        .to_str()
228        .expect("keys file should be a valid string");
229    let keys_file_dir = path
230        .parent()
231        .expect("keys file should have a parent directory");
232    let temp_keys_name = format!("{}_new", keys_filename);
233    save_consensus_keys(keys_file_dir, &temp_keys_name, pk, sk, pwd)?;
234    fs::rename(
235        keys_file_dir.join(&temp_keys_name).with_extension("keys"),
236        path,
237    )?;
238    fs::remove_file(keys_file_dir.join(temp_keys_name).with_extension("cpk"))
239        .expect("The new cpk file should be deleted");
240    Ok(())
241}
242
243fn save_old_file(path: &Path) -> Result<(), ConsensusKeysError> {
244    let old_path = path.with_extension("keys.old");
245    fs::copy(path, old_path)?;
246    Ok(())
247}
248
249pub fn save_consensus_keys(
250    path: &Path,
251    filename: &str,
252    pk: &BlsPublicKey,
253    sk: &BlsSecretKey,
254    pwd: &str,
255) -> Result<(PathBuf, PathBuf), ConsensusKeysError> {
256    let path = path.join(filename);
257    let bytes = pk.to_bytes();
258    fs::write(path.with_extension("cpk"), bytes)?;
259
260    let iv = gen_iv();
261    let salt = gen_salt();
262    let mut bls = BlsKeyPair {
263        public_key_bls: pk.to_bytes().to_vec(),
264        secret_key_bls: sk.to_bytes().to_vec(),
265    };
266    let key_pair_plain = serde_json::to_vec(&bls);
267    bls.secret_key_bls.zeroize();
268    let mut key_pair_plain = key_pair_plain?;
269
270    let mut aes_key = derive_aes_key(pwd, &salt);
271    let key_pair_enc = encrypt(&key_pair_plain, &aes_key, &iv);
272    aes_key.zeroize();
273    key_pair_plain.zeroize();
274    let contents = serde_json::to_vec(&ProvisionerFileContents {
275        salt,
276        iv,
277        key_pair: key_pair_enc?,
278    })?;
279
280    fs::write(path.with_extension("keys"), contents)?;
281
282    Ok((path.with_extension("keys"), path.with_extension("cpk")))
283}
284
285#[derive(Serialize, Deserialize)]
286struct ProvisionerFileContents {
287    #[serde(with = "As::<Base64>")]
288    salt: [u8; SALT_SIZE],
289    #[serde(with = "As::<Base64>")]
290    iv: [u8; IV_SIZE],
291    key_pair: Vec<u8>,
292}
293
294#[derive(Serialize, Deserialize)]
295struct BlsKeyPair {
296    #[serde(with = "As::<Base64>")]
297    secret_key_bls: Vec<u8>,
298    #[serde(with = "As::<Base64>")]
299    public_key_bls: Vec<u8>,
300}
301
302type Aes256Cbc = Cbc<Aes256, Pkcs7>;
303
304fn encrypt(
305    plaintext: &[u8],
306    key: &[u8],
307    iv: &[u8],
308) -> Result<Vec<u8>, aes_gcm::Error> {
309    let key = Key::<Aes256Gcm>::from_slice(key);
310    let cipher = Aes256Gcm::new(key);
311    let iv = aes_gcm::Nonce::from_slice(iv);
312    let ciphertext = cipher.encrypt(iv, plaintext)?;
313    Ok(ciphertext)
314}
315
316fn decrypt_aes_cbc(data: &[u8], pwd: &[u8]) -> Result<Vec<u8>, BlockModeError> {
317    let iv = &data[..16];
318    let enc = &data[16..];
319
320    let cipher = Aes256Cbc::new_from_slices(pwd, iv).expect("valid data");
321    cipher.decrypt_vec(enc)
322}
323
324pub(crate) fn decrypt(
325    ciphertext: &[u8],
326    key: &[u8],
327    iv: &[u8],
328) -> Result<Vec<u8>, aes_gcm::Error> {
329    let key = Key::<Aes256Gcm>::from_slice(key);
330    let cipher = Aes256Gcm::new(key);
331    let iv = aes_gcm::Nonce::from_slice(iv);
332    let plaintext = cipher.decrypt(iv, ciphertext)?;
333
334    Ok(plaintext)
335}
336
337const SALT_SIZE: usize = 32;
338const IV_SIZE: usize = 12;
339const PBKDF2_ROUNDS: u32 = 10_000;
340
341fn derive_aes_key(pwd: &str, salt: &[u8]) -> Vec<u8> {
342    pbkdf2::pbkdf2_hmac_array::<Sha256, SALT_SIZE>(
343        pwd.as_bytes(),
344        salt,
345        PBKDF2_ROUNDS,
346    )
347    .to_vec()
348}
349
350fn gen_iv() -> [u8; IV_SIZE] {
351    let iv = Aes256Gcm::generate_nonce(OsRng);
352    iv.into()
353}
354
355fn gen_salt() -> [u8; SALT_SIZE] {
356    let mut salt = [0; SALT_SIZE];
357    let mut rng = OsRng;
358    rng.fill_bytes(&mut salt);
359    salt
360}
361
362fn hash_sha256(pwd: &str) -> Vec<u8> {
363    let mut hasher = Sha256::new();
364    hasher.update(pwd.as_bytes());
365    hasher.finalize().to_vec()
366}
367
368#[derive(Debug, thiserror::Error)]
369pub enum ConsensusKeysError {
370    #[error(transparent)]
371    Json(#[from] serde_json::Error),
372
373    #[error(transparent)]
374    Io(#[from] std::io::Error),
375
376    #[error("Encryption error")]
377    Encryption(#[from] aes_gcm::Error),
378}
379
380#[cfg(test)]
381mod tests {
382    use anyhow::anyhow;
383    use tempfile::tempdir;
384
385    use super::*;
386
387    #[test]
388    fn test_save_load_consensus_keys() -> Result<(), Box<dyn std::error::Error>>
389    {
390        let dir = tempdir()?;
391
392        let mut rng = StdRng::seed_from_u64(64);
393        let sk = BlsSecretKey::random(&mut rng);
394        let pk = BlsPublicKey::from(&sk);
395        let pwd = "password";
396
397        save_consensus_keys(dir.path(), "consensus", &pk, &sk, pwd)?;
398        let keys_path = dir.path().join("consensus.keys");
399        let (loaded_sk, loaded_pk) = load_keys(
400            keys_path
401                .to_str()
402                .ok_or(anyhow!("Failed to convert path to string"))?
403                .to_string(),
404            pwd.to_string(),
405        )?;
406        let pk_bytes = fs::read(dir.path().join("consensus.cpk"))?;
407        let pk_bytes: [u8; PUBLIC_BLS_SIZE] = pk_bytes
408            .try_into()
409            .map_err(|_| anyhow!("Invalid BlsPublicKey bytes"))?;
410        let loaded_cpk = BlsPublicKey::from_bytes(&pk_bytes)
411            .map_err(|err| anyhow!("{err:?}"))?;
412
413        assert_eq!(loaded_sk, sk);
414        assert_eq!(loaded_pk.inner, pk);
415        assert_eq!(loaded_cpk, pk);
416
417        Ok(())
418    }
419
420    #[test]
421    fn test_can_still_load_keys_saved_by_wallet_impl()
422    -> Result<(), Box<dyn std::error::Error>> {
423        // test-data/wallet-generated-consensus-keys contains consensus keys
424        // exported by the former rusk-wallet implementation to save consensus
425        // keys.
426        // This test checks if what is saved by the former implementation
427        // is still loaded correctly.
428        let mut rng = StdRng::seed_from_u64(64);
429        let sk = BlsSecretKey::random(&mut rng);
430        let pk = BlsPublicKey::from(&sk);
431
432        let pwd = "password".to_string();
433        let wallet_gen_keys_path = get_wallet_gen_consensus_keys_path();
434        let temp_dir = tempdir()?;
435        let keys_path = temp_dir.path().join("consensus.keys");
436        fs::copy(&wallet_gen_keys_path, &keys_path)?;
437
438        let (loaded_sk, loaded_pk) =
439            load_keys(keys_path.to_str().unwrap().to_string(), pwd)?;
440
441        assert_eq!(loaded_sk, sk);
442        assert_eq!(loaded_pk.inner, pk);
443
444        let old_keys_path = temp_dir.path().join("consensus.keys.old");
445        assert!(old_keys_path.exists(), "Old keys path should exist");
446
447        Ok(())
448    }
449
450    fn get_wallet_gen_consensus_keys_path() -> PathBuf {
451        let mut path = PathBuf::from(file!());
452        // Remove the filename
453        path.pop();
454        // Remove the current directory
455        let path: PathBuf = path.components().skip(1).collect();
456        path.join("test-data")
457            .join("wallet-generated-consensus-keys")
458            .join("consensus.keys")
459    }
460}