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 block_modes::block_padding::Pkcs7;
14use block_modes::{BlockMode, BlockModeError, Cbc, InvalidKeyIvLength};
15use dusk_bytes::{DeserializableSlice, Serializable};
16use dusk_core::signatures::bls::{
17    PublicKey as BlsPublicKey, SecretKey as BlsSecretKey,
18};
19use rand::rngs::{OsRng, StdRng};
20use rand::RngCore;
21use rand::SeedableRng;
22use serde::{Deserialize, Serialize};
23use serde_with::base64::Base64;
24use serde_with::serde_as;
25use sha2::{Digest, Sha256};
26
27pub const PUBLIC_BLS_SIZE: usize = BlsPublicKey::SIZE;
28
29/// Extends BlsPublicKey by implementing a few traits
30///
31/// See also PublicKey::bytes(&self)
32#[derive(Default, Eq, PartialEq, Clone)]
33pub struct PublicKey {
34    inner: BlsPublicKey,
35    as_bytes: PublicKeyBytes,
36}
37
38impl TryFrom<[u8; 96]> for PublicKey {
39    type Error = dusk_bytes::Error;
40    fn try_from(bytes: [u8; 96]) -> Result<Self, Self::Error> {
41        let inner = BlsPublicKey::from_slice(&bytes)?;
42        let as_bytes = PublicKeyBytes(bytes);
43        Ok(Self { as_bytes, inner })
44    }
45}
46
47impl PublicKey {
48    pub fn new(inner: BlsPublicKey) -> Self {
49        let b = inner.to_bytes();
50        Self {
51            inner,
52            as_bytes: PublicKeyBytes(b),
53        }
54    }
55
56    /// from_sk_seed_u64 generates a sk from the specified seed and returns the
57    /// associated public key
58    pub fn from_sk_seed_u64(state: u64) -> Self {
59        let rng = &mut StdRng::seed_from_u64(state);
60        let sk = BlsSecretKey::random(rng);
61
62        Self::new(BlsPublicKey::from(&sk))
63    }
64
65    /// `bytes` returns a reference to the pk.to_bytes() initialized on
66    /// PublicKey::new call. NB: Frequent use of `to_bytes()` creates a
67    /// noticeable performance overhead.
68    pub fn bytes(&self) -> &PublicKeyBytes {
69        &self.as_bytes
70    }
71
72    pub fn inner(&self) -> &BlsPublicKey {
73        &self.inner
74    }
75
76    pub fn into_inner(self) -> BlsPublicKey {
77        self.inner
78    }
79
80    /// Truncated base58 representation of inner data
81    pub fn to_bs58(&self) -> String {
82        self.bytes().to_bs58()
83    }
84
85    /// Full base58 representation of inner data
86    pub fn to_base58(&self) -> String {
87        self.bytes().to_base58()
88    }
89}
90
91impl PartialOrd<PublicKey> for PublicKey {
92    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
93        Some(self.cmp(other))
94    }
95}
96
97impl Ord for PublicKey {
98    fn cmp(&self, other: &Self) -> Ordering {
99        self.as_bytes.inner().cmp(other.as_bytes.inner())
100    }
101}
102
103impl std::fmt::Debug for PublicKey {
104    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
105        let bs = self.to_base58();
106        f.debug_struct("PublicKey").field("bs58", &bs).finish()
107    }
108}
109/// A wrapper of 96-sized array
110#[derive(Clone, Copy, Eq, Hash, PartialEq, Serialize)]
111pub struct PublicKeyBytes(
112    #[serde(serialize_with = "crate::serialize_b58")] pub [u8; PUBLIC_BLS_SIZE],
113);
114
115impl Default for PublicKeyBytes {
116    fn default() -> Self {
117        PublicKeyBytes([0; 96])
118    }
119}
120
121impl PublicKeyBytes {
122    pub fn inner(&self) -> &[u8; 96] {
123        &self.0
124    }
125
126    /// Full base58 representation of inner data
127    pub fn to_base58(&self) -> String {
128        bs58::encode(&self.0).into_string()
129    }
130
131    /// Truncated base58 representation of inner data
132    pub fn to_bs58(&self) -> String {
133        let mut bs = self.to_base58();
134        bs.truncate(16);
135        bs
136    }
137}
138
139impl Debug for PublicKeyBytes {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        f.write_str(&self.to_bs58())
142    }
143}
144
145/// Loads consensus keys from an encrypted file.
146pub fn load_keys(
147    path: String,
148    pwd: String,
149) -> anyhow::Result<(BlsSecretKey, PublicKey)> {
150    let path_buf = PathBuf::from(path);
151    let (pk, sk) = read_from_file(path_buf, &pwd)?;
152
153    Ok((sk, PublicKey::new(pk)))
154}
155
156/// Fetches BLS public and secret keys from an encrypted consensus keys file.
157fn read_from_file(
158    path: PathBuf,
159    pwd: &str,
160) -> anyhow::Result<(BlsPublicKey, BlsSecretKey)> {
161    let ciphertext = fs::read(&path).map_err(|e| {
162        anyhow::anyhow!(
163            "{} should be valid consensus keys file {e}",
164            path.display()
165        )
166    })?;
167
168    let mut hasher = Sha256::new();
169    hasher.update(pwd.as_bytes());
170    let hashed_pwd = hasher.finalize().to_vec();
171
172    let bytes = decrypt(&ciphertext[..], &hashed_pwd)
173        .map_err(|e| anyhow::anyhow!("Invalid consensus keys password {e}"))?;
174
175    let keys: BlsKeyPair = serde_json::from_slice(&bytes)
176        .map_err(|e| anyhow::anyhow!("keys files should contain json {e}"))?;
177
178    let sk = BlsSecretKey::from_slice(&keys.secret_key_bls)
179        .map_err(|e| anyhow::anyhow!("sk should be valid {e:?}"))?;
180
181    let pk = BlsPublicKey::from_slice(&keys.public_key_bls)
182        .map_err(|e| anyhow::anyhow!("pk should be valid {e:?}"))?;
183
184    Ok((pk, sk))
185}
186
187pub fn save_consensus_keys(
188    path: &Path,
189    filename: &str,
190    pk: &BlsPublicKey,
191    sk: &BlsSecretKey,
192    pwd: &[u8],
193) -> Result<(PathBuf, PathBuf), ConsensusKeysError> {
194    let path = path.join(filename);
195    let bytes = pk.to_bytes();
196    fs::write(path.with_extension("cpk"), bytes)?;
197
198    let bls = BlsKeyPair {
199        public_key_bls: pk.to_bytes().to_vec(),
200        secret_key_bls: sk.to_bytes().to_vec(),
201    };
202    let json = serde_json::to_string(&bls)?;
203
204    let mut bytes = json.as_bytes().to_vec();
205    bytes = encrypt(&bytes, pwd)?;
206
207    fs::write(path.with_extension("keys"), bytes)?;
208
209    Ok((path.with_extension("keys"), path.with_extension("cpk")))
210}
211
212#[serde_as]
213#[derive(Serialize, Deserialize)]
214struct BlsKeyPair {
215    #[serde_as(as = "Base64")]
216    secret_key_bls: Vec<u8>,
217    #[serde_as(as = "Base64")]
218    public_key_bls: Vec<u8>,
219}
220
221type Aes256Cbc = Cbc<Aes256, Pkcs7>;
222
223fn encrypt(
224    plaintext: &[u8],
225    pwd: &[u8],
226) -> Result<Vec<u8>, ConsensusKeysError> {
227    let mut iv = vec![0; 16];
228    let mut rng = OsRng;
229    rng.fill_bytes(&mut iv);
230
231    let cipher = Aes256Cbc::new_from_slices(pwd, &iv)?;
232    let enc = cipher.encrypt_vec(plaintext);
233
234    let ciphertext = iv.into_iter().chain(enc).collect();
235    Ok(ciphertext)
236}
237
238fn decrypt(data: &[u8], pwd: &[u8]) -> Result<Vec<u8>, BlockModeError> {
239    let iv = &data[..16];
240    let enc = &data[16..];
241
242    let cipher = Aes256Cbc::new_from_slices(pwd, iv).expect("valid data");
243    cipher.decrypt_vec(enc)
244}
245
246#[derive(Debug, thiserror::Error)]
247pub enum ConsensusKeysError {
248    #[error("Consensus keys file corrupted")]
249    InvalidKeyIvLength(#[from] InvalidKeyIvLength),
250
251    #[error(transparent)]
252    Json(#[from] serde_json::Error),
253
254    #[error(transparent)]
255    Io(#[from] std::io::Error),
256}
257
258#[cfg(test)]
259mod tests {
260    use anyhow::anyhow;
261    use tempfile::tempdir;
262
263    use super::*;
264
265    #[test]
266    fn test_save_load_consensus_keys() -> Result<(), Box<dyn std::error::Error>>
267    {
268        let dir = tempdir()?;
269
270        let mut rng = StdRng::seed_from_u64(64);
271        let sk = BlsSecretKey::random(&mut rng);
272        let pk = BlsPublicKey::from(&sk);
273        let pwd = "password";
274
275        save_consensus_keys(
276            dir.path(),
277            "consensus",
278            &pk,
279            &sk,
280            &hashed_password(pwd),
281        )?;
282        let keys_path = dir.path().join("consensus.keys");
283        let (loaded_sk, loaded_pk) = load_keys(
284            keys_path
285                .to_str()
286                .ok_or(anyhow!("Failed to convert path to string"))?
287                .to_string(),
288            pwd.to_string(),
289        )?;
290        let pk_bytes = fs::read(dir.path().join("consensus.cpk"))?;
291        let pk_bytes: [u8; PUBLIC_BLS_SIZE] = pk_bytes
292            .try_into()
293            .map_err(|_| anyhow!("Invalid BlsPublicKey bytes"))?;
294        let loaded_cpk = BlsPublicKey::from_bytes(&pk_bytes)
295            .map_err(|err| anyhow!("{err:?}"))?;
296
297        assert_eq!(loaded_sk, sk);
298        assert_eq!(loaded_pk.inner, pk);
299        assert_eq!(loaded_cpk, pk);
300
301        Ok(())
302    }
303
304    #[test]
305    fn test_can_still_load_keys_saved_by_wallet_impl(
306    ) -> Result<(), Box<dyn std::error::Error>> {
307        // test-data/wallet-generated-consensus-keys contains consensus keys
308        // exported by the former rusk-wallet implementation to save consensus
309        // keys.
310        // This test checks if what is saved by the former implementation
311        // is still loaded correctly.
312        let mut rng = StdRng::seed_from_u64(64);
313        let sk = BlsSecretKey::random(&mut rng);
314        let pk = BlsPublicKey::from(&sk);
315
316        let pwd = "password".to_string();
317        let wallet_gen_keys_path = get_wallet_gen_consensus_keys_path();
318        let (loaded_sk, loaded_pk) =
319            load_keys(wallet_gen_keys_path.to_str().unwrap().to_string(), pwd)?;
320
321        assert_eq!(loaded_sk, sk);
322        assert_eq!(loaded_pk.inner, pk);
323
324        Ok(())
325    }
326
327    fn hashed_password(pwd: &str) -> Vec<u8> {
328        let mut hasher = Sha256::new();
329        hasher.update(pwd.as_bytes());
330        hasher.finalize().to_vec()
331    }
332
333    fn get_wallet_gen_consensus_keys_path() -> PathBuf {
334        let mut path = PathBuf::from(file!());
335        // Remove the filename
336        path.pop();
337        // Remove the current directory
338        let path: PathBuf = path.components().skip(1).collect();
339        path.join("test-data")
340            .join("wallet-generated-consensus-keys")
341            .join("consensus.keys")
342    }
343}