1use 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#[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 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 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 pub fn to_bs58(&self) -> String {
82 self.bytes().to_bs58()
83 }
84
85 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#[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 pub fn to_base58(&self) -> String {
128 bs58::encode(&self.0).into_string()
129 }
130
131 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
145pub 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
156fn 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 aes_key = hash_sha256(pwd);
169
170 let bytes = decrypt(&ciphertext[..], &aes_key)
171 .map_err(|e| anyhow::anyhow!("Invalid consensus keys password {e}"))?;
172
173 let keys: BlsKeyPair = serde_json::from_slice(&bytes)
174 .map_err(|e| anyhow::anyhow!("keys files should contain json {e}"))?;
175
176 let sk = BlsSecretKey::from_slice(&keys.secret_key_bls)
177 .map_err(|e| anyhow::anyhow!("sk should be valid {e:?}"))?;
178
179 let pk = BlsPublicKey::from_slice(&keys.public_key_bls)
180 .map_err(|e| anyhow::anyhow!("pk should be valid {e:?}"))?;
181
182 Ok((pk, sk))
183}
184
185pub fn save_consensus_keys(
186 path: &Path,
187 filename: &str,
188 pk: &BlsPublicKey,
189 sk: &BlsSecretKey,
190 pwd: &str,
191) -> Result<(PathBuf, PathBuf), ConsensusKeysError> {
192 let path = path.join(filename);
193 let bytes = pk.to_bytes();
194 fs::write(path.with_extension("cpk"), bytes)?;
195
196 let bls = BlsKeyPair {
197 public_key_bls: pk.to_bytes().to_vec(),
198 secret_key_bls: sk.to_bytes().to_vec(),
199 };
200 let json = serde_json::to_string(&bls)?;
201
202 let mut bytes = json.as_bytes().to_vec();
203 let aes_key = hash_sha256(pwd);
204 bytes = encrypt(&bytes, &aes_key)?;
205
206 fs::write(path.with_extension("keys"), bytes)?;
207
208 Ok((path.with_extension("keys"), path.with_extension("cpk")))
209}
210
211fn hash_sha256(pwd: &str) -> Vec<u8> {
212 let mut hasher = Sha256::new();
213 hasher.update(pwd.as_bytes());
214 hasher.finalize().to_vec()
215}
216
217#[serde_as]
218#[derive(Serialize, Deserialize)]
219struct BlsKeyPair {
220 #[serde_as(as = "Base64")]
221 secret_key_bls: Vec<u8>,
222 #[serde_as(as = "Base64")]
223 public_key_bls: Vec<u8>,
224}
225
226type Aes256Cbc = Cbc<Aes256, Pkcs7>;
227
228fn encrypt(
229 plaintext: &[u8],
230 pwd: &[u8],
231) -> Result<Vec<u8>, ConsensusKeysError> {
232 let mut iv = vec![0; 16];
233 let mut rng = OsRng;
234 rng.fill_bytes(&mut iv);
235
236 let cipher = Aes256Cbc::new_from_slices(pwd, &iv)?;
237 let enc = cipher.encrypt_vec(plaintext);
238
239 let ciphertext = iv.into_iter().chain(enc).collect();
240 Ok(ciphertext)
241}
242
243fn decrypt(data: &[u8], pwd: &[u8]) -> Result<Vec<u8>, BlockModeError> {
244 let iv = &data[..16];
245 let enc = &data[16..];
246
247 let cipher = Aes256Cbc::new_from_slices(pwd, iv).expect("valid data");
248 cipher.decrypt_vec(enc)
249}
250
251#[derive(Debug, thiserror::Error)]
252pub enum ConsensusKeysError {
253 #[error("Consensus keys file corrupted")]
254 InvalidKeyIvLength(#[from] InvalidKeyIvLength),
255
256 #[error(transparent)]
257 Json(#[from] serde_json::Error),
258
259 #[error(transparent)]
260 Io(#[from] std::io::Error),
261}
262
263#[cfg(test)]
264mod tests {
265 use anyhow::anyhow;
266 use tempfile::tempdir;
267
268 use super::*;
269
270 #[test]
271 fn test_save_load_consensus_keys() -> Result<(), Box<dyn std::error::Error>>
272 {
273 let dir = tempdir()?;
274
275 let mut rng = StdRng::seed_from_u64(64);
276 let sk = BlsSecretKey::random(&mut rng);
277 let pk = BlsPublicKey::from(&sk);
278 let pwd = "password";
279
280 save_consensus_keys(dir.path(), "consensus", &pk, &sk, pwd)?;
281 let keys_path = dir.path().join("consensus.keys");
282 let (loaded_sk, loaded_pk) = load_keys(
283 keys_path
284 .to_str()
285 .ok_or(anyhow!("Failed to convert path to string"))?
286 .to_string(),
287 pwd.to_string(),
288 )?;
289 let pk_bytes = fs::read(dir.path().join("consensus.cpk"))?;
290 let pk_bytes: [u8; PUBLIC_BLS_SIZE] = pk_bytes
291 .try_into()
292 .map_err(|_| anyhow!("Invalid BlsPublicKey bytes"))?;
293 let loaded_cpk = BlsPublicKey::from_bytes(&pk_bytes)
294 .map_err(|err| anyhow!("{err:?}"))?;
295
296 assert_eq!(loaded_sk, sk);
297 assert_eq!(loaded_pk.inner, pk);
298 assert_eq!(loaded_cpk, pk);
299
300 Ok(())
301 }
302
303 #[test]
304 fn test_can_still_load_keys_saved_by_wallet_impl(
305 ) -> Result<(), Box<dyn std::error::Error>> {
306 let mut rng = StdRng::seed_from_u64(64);
312 let sk = BlsSecretKey::random(&mut rng);
313 let pk = BlsPublicKey::from(&sk);
314
315 let pwd = "password".to_string();
316 let wallet_gen_keys_path = get_wallet_gen_consensus_keys_path();
317 let (loaded_sk, loaded_pk) =
318 load_keys(wallet_gen_keys_path.to_str().unwrap().to_string(), pwd)?;
319
320 assert_eq!(loaded_sk, sk);
321 assert_eq!(loaded_pk.inner, pk);
322
323 Ok(())
324 }
325
326 fn get_wallet_gen_consensus_keys_path() -> PathBuf {
327 let mut path = PathBuf::from(file!());
328 path.pop();
330 let path: PathBuf = path.components().skip(1).collect();
332 path.join("test-data")
333 .join("wallet-generated-consensus-keys")
334 .join("consensus.keys")
335 }
336}