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