1use 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;
23use rand::SeedableRng;
24use serde::{Deserialize, Serialize};
25use serde_with::base64::Base64;
26use serde_with::serde_as;
27use sha2::{Digest, Sha256};
28use tracing::{error, info};
29use zeroize::Zeroize;
30
31pub const PUBLIC_BLS_SIZE: usize = BlsPublicKey::SIZE;
32
33#[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 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 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 pub fn to_bs58(&self) -> String {
86 self.bytes().to_bs58()
87 }
88
89 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#[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 pub fn to_base58(&self) -> String {
132 bs58::encode(&self.0).into_string()
133 }
134
135 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
149pub 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
160fn 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!("Your consensus keys are in the old format. Migrating to the new format and saving the old file as {}.old", path.display());
203 let _ =
204 migrate_file_to_new_format(&path, &pk, &sk, pwd).inspect_err(|e| {
205 error!(
206 "failed to migrate consensus keys to the new format: {e}"
207 );
208 });
209 }
210
211 Ok((pk, sk))
212}
213
214fn migrate_file_to_new_format(
215 path: &Path,
216 pk: &BlsPublicKey,
217 sk: &BlsSecretKey,
218 pwd: &str,
219) -> Result<(), ConsensusKeysError> {
220 save_old_file(path)?;
221 let keys_filename = path
222 .file_name()
223 .expect("keys file should have a name")
224 .to_str()
225 .expect("keys file should be a valid string");
226 let keys_file_dir = path
227 .parent()
228 .expect("keys file should have a parent directory");
229 let temp_keys_name = format!("{}_new", keys_filename);
230 save_consensus_keys(keys_file_dir, &temp_keys_name, pk, sk, pwd)?;
231 fs::rename(
232 keys_file_dir.join(&temp_keys_name).with_extension("keys"),
233 path,
234 )?;
235 fs::remove_file(keys_file_dir.join(temp_keys_name).with_extension("cpk"))
236 .expect("The new cpk file should be deleted");
237 Ok(())
238}
239
240fn save_old_file(path: &Path) -> Result<(), ConsensusKeysError> {
241 let old_path = path.with_extension("keys.old");
242 fs::copy(path, old_path)?;
243 Ok(())
244}
245
246pub fn save_consensus_keys(
247 path: &Path,
248 filename: &str,
249 pk: &BlsPublicKey,
250 sk: &BlsSecretKey,
251 pwd: &str,
252) -> Result<(PathBuf, PathBuf), ConsensusKeysError> {
253 let path = path.join(filename);
254 let bytes = pk.to_bytes();
255 fs::write(path.with_extension("cpk"), bytes)?;
256
257 let iv = gen_iv();
258 let salt = gen_salt();
259 let mut bls = BlsKeyPair {
260 public_key_bls: pk.to_bytes().to_vec(),
261 secret_key_bls: sk.to_bytes().to_vec(),
262 };
263 let key_pair_plain = serde_json::to_vec(&bls);
264 bls.secret_key_bls.zeroize();
265 let mut key_pair_plain = key_pair_plain?;
266
267 let mut aes_key = derive_aes_key(pwd, &salt);
268 let key_pair_enc = encrypt(&key_pair_plain, &aes_key, &iv);
269 aes_key.zeroize();
270 key_pair_plain.zeroize();
271 let contents = serde_json::to_vec(&ProvisionerFileContents {
272 salt,
273 iv,
274 key_pair: key_pair_enc?,
275 })?;
276
277 fs::write(path.with_extension("keys"), contents)?;
278
279 Ok((path.with_extension("keys"), path.with_extension("cpk")))
280}
281
282#[serde_as]
283#[derive(Serialize, Deserialize)]
284struct ProvisionerFileContents {
285 #[serde_as(as = "Base64")]
286 salt: [u8; SALT_SIZE],
287 #[serde_as(as = "Base64")]
288 iv: [u8; IV_SIZE],
289 key_pair: Vec<u8>,
290}
291
292#[serde_as]
293#[derive(Serialize, Deserialize)]
294struct BlsKeyPair {
295 #[serde_as(as = "Base64")]
296 secret_key_bls: Vec<u8>,
297 #[serde_as(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 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 path.pop();
453 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}