1#![warn(
39 missing_debug_implementations,
40 missing_docs,
41 unsafe_code,
42 bare_trait_objects
43)]
44#![warn(clippy::pedantic, clippy::nursery)]
45#![allow(
46 clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::cast_sign_loss,
48 clippy::module_name_repetitions, clippy::similar_names, clippy::must_use_candidate,
50 clippy::pub_enum_variant_names,
51 clippy::indexing_slicing,
53 clippy::missing_errors_doc, clippy::missing_const_for_fn
55)]
56
57use anyhow::format_err;
58use exonum_crypto::{KeyPair, PublicKey, SecretKey, Seed, SEED_LENGTH};
59use pwbox::{sodium::Sodium, ErasedPwBox, Eraser, SensitiveData, Suite};
60use rand::thread_rng;
61use secret_tree::{Name, SecretTree};
62use serde_derive::{Deserialize, Serialize};
63
64#[cfg(unix)]
65use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
66use std::{
67 fs::{File, OpenOptions},
68 io::{Error, ErrorKind, Read, Write},
69 path::Path,
70};
71
72#[cfg(unix)]
73#[cfg_attr(feature = "cargo-clippy", allow(clippy::verbose_bit_mask))]
74fn validate_file_mode(mode: u32) -> Result<(), Error> {
75 if (mode & 0o_077) == 0 {
77 Ok(())
78 } else {
79 Err(Error::new(ErrorKind::Other, "Wrong file's mode"))
80 }
81}
82
83#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
85#[non_exhaustive]
86pub struct Keys {
87 pub consensus: KeyPair,
89 pub service: KeyPair,
91}
92
93impl Keys {
94 pub fn random() -> Self {
97 Self {
98 consensus: KeyPair::random(),
99 service: KeyPair::random(),
100 }
101 }
102
103 pub fn from_keys(consensus_keys: impl Into<KeyPair>, service_keys: impl Into<KeyPair>) -> Self {
114 Self {
115 consensus: consensus_keys.into(),
116 service: service_keys.into(),
117 }
118 }
119}
120
121impl Keys {
122 pub fn consensus_pk(&self) -> PublicKey {
124 self.consensus.public_key()
125 }
126
127 pub fn consensus_sk(&self) -> &SecretKey {
129 self.consensus.secret_key()
130 }
131
132 pub fn service_pk(&self) -> PublicKey {
134 self.service.public_key()
135 }
136
137 pub fn service_sk(&self) -> &SecretKey {
139 self.service.secret_key()
140 }
141}
142
143fn save_master_key<P: AsRef<Path>>(
144 path: P,
145 encrypted_key: &EncryptedMasterKey,
146) -> Result<(), Error> {
147 let file_content =
148 toml::to_string_pretty(encrypted_key).map_err(|e| Error::new(ErrorKind::Other, e))?;
149 let mut open_options = OpenOptions::new();
150 open_options.create(true).write(true);
151 #[cfg(unix)]
153 open_options.mode(0o_600);
154 let mut file = open_options.open(path.as_ref())?;
155 file.write_all(file_content.as_bytes())?;
156
157 Ok(())
158}
159
160#[derive(Debug, Serialize, Deserialize)]
162pub struct EncryptedMasterKey {
163 key: ErasedPwBox,
164}
165
166impl EncryptedMasterKey {
167 fn encrypt(key: &secret_tree::Seed, pass_phrase: impl AsRef<[u8]>) -> Result<Self, Error> {
168 let mut rng = thread_rng();
169 let mut eraser = Eraser::new();
170 eraser.add_suite::<Sodium>();
171 let pwbox = Sodium::build_box(&mut rng)
172 .seal(pass_phrase, key)
173 .map_err(|_| Error::new(ErrorKind::Other, "Couldn't create a pw box"))?;
174 let encrypted_key = eraser
175 .erase(&pwbox)
176 .map_err(|_| Error::new(ErrorKind::Other, "Couldn't convert a pw box"))?;
177
178 Ok(Self { key: encrypted_key })
179 }
180
181 fn decrypt(self, pass_phrase: impl AsRef<[u8]>) -> Result<SensitiveData, Error> {
182 let mut eraser = Eraser::new();
183 eraser.add_suite::<Sodium>();
184 let restored = eraser
185 .restore(&self.key)
186 .map_err(|_| Error::new(ErrorKind::Other, "Couldn't restore a secret key"))?;
187 assert_eq!(restored.len(), SEED_LENGTH);
188
189 restored
190 .open(pass_phrase)
191 .map_err(|_| Error::new(ErrorKind::Other, "Couldn't open an encrypted key"))
192 }
193}
194
195pub fn generate_keys<P: AsRef<Path>>(path: P, passphrase: &[u8]) -> anyhow::Result<Keys> {
197 let tree = SecretTree::new(&mut thread_rng());
198 let encrypted_key = EncryptedMasterKey::encrypt(tree.seed(), passphrase)?;
199 save_master_key(path, &encrypted_key)?;
200
201 Ok(generate_keys_from_master_password(&tree))
202}
203
204pub fn generate_keys_from_seed(
206 passphrase: &[u8],
207 seed: &[u8],
208) -> anyhow::Result<(Keys, EncryptedMasterKey)> {
209 let tree = SecretTree::from_seed(seed)
210 .ok_or_else(|| format_err!("Error creating SecretTree from seed"))?;
211 let encrypted_key = EncryptedMasterKey::encrypt(tree.seed(), passphrase)?;
212 let keys = generate_keys_from_master_password(&tree);
213
214 Ok((keys, encrypted_key))
215}
216
217fn generate_keys_from_master_password(tree: &SecretTree) -> Keys {
218 let mut buffer = [0_u8; 32];
219
220 tree.child(Name::new("consensus")).fill(&mut buffer);
221 let seed = Seed::new(buffer);
222 let consensus_keys = KeyPair::from_seed(&seed);
223
224 tree.child(Name::new("service")).fill(&mut buffer);
225 let seed = Seed::new(buffer);
226 let service_keys = KeyPair::from_seed(&seed);
227
228 Keys::from_keys(consensus_keys, service_keys)
229}
230
231pub fn read_keys_from_file<P: AsRef<Path>, W: AsRef<[u8]>>(
233 path: P,
234 pass_phrase: W,
235) -> anyhow::Result<Keys> {
236 let mut key_file = File::open(path)?;
237
238 #[cfg(unix)]
239 validate_file_mode(key_file.metadata()?.mode())?;
240
241 let mut file_content = vec![];
242 key_file.read_to_end(&mut file_content)?;
243 let keys: EncryptedMasterKey =
244 toml::from_slice(file_content.as_slice()).map_err(|e| Error::new(ErrorKind::Other, e))?;
245 let seed = keys.decrypt(pass_phrase)?;
246 let tree = SecretTree::from_seed(&seed).expect("Error creating secret tree from seed.");
247
248 Ok(generate_keys_from_master_password(&tree))
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use tempdir::TempDir;
255
256 #[test]
257 fn test_create_and_read_keys_file() {
258 let dir = TempDir::new("test_utils").expect("Couldn't create TempDir");
259 let file_path = dir.path().join("private_key.toml");
260 let pass_phrase = b"passphrase";
261 let pk1 = generate_keys(file_path.as_path(), pass_phrase).unwrap();
262 let pk2 = read_keys_from_file(file_path.as_path(), pass_phrase).unwrap();
263 assert_eq!(pk1, pk2);
264 }
265
266 #[test]
267 fn encrypt_decrypt() {
268 let pass_phrase = b"passphrase";
269 let tree = SecretTree::new(&mut thread_rng());
270 let seed = tree.seed();
271 let key =
272 EncryptedMasterKey::encrypt(seed, pass_phrase).expect("Couldn't encrypt master key");
273
274 let decrypted_seed = key
275 .decrypt(pass_phrase)
276 .expect("Couldn't decrypt master key ");
277 assert_eq!(&seed[..], &decrypted_seed[..]);
278 }
279
280 #[test]
281 fn test_decrypt_from_file() {
282 let pass_phrase = b"passphrase";
283 let file_content = r#"
284 [key]
285 ciphertext = "cf6c63520e789efc978ad07e218e6fd199ccb5e861e9c893cac40641fc66c89c"
286 mac = "b3e1a815a2cb316bf209da7bc4203091"
287 kdf = "scrypt-nacl"
288 cipher = "xsalsa20-poly1305"
289
290 [key.kdfparams]
291 salt = "7e7a1d9dc5269b0ebedb5fcd433d772880786ebefe8647a275ef1626cfb122b3"
292 memlimit = 16777216
293 opslimit = 524288
294
295 [key.cipherparams]
296 iv = "832bded4e12948a022065ace39b31cd7b514bf9c2b2a407f"
297 "#;
298
299 let keys: EncryptedMasterKey =
300 toml::from_str(file_content).expect("Couldn't deserialize content");
301 let seed = keys.decrypt(pass_phrase).expect("Couldn't decrypt key");
302
303 assert_eq!(
304 hex::encode(&*seed),
305 "a05a82575d5f9d1f9469df31896f5b3c14ec4d18b3948cd7c8b09a7eed48b4e0"
306 );
307 }
308
309 #[cfg(unix)]
310 #[test]
311 fn test_validate_file_mode() {
312 assert!(validate_file_mode(0o_100_600).is_ok());
313 assert!(validate_file_mode(0o_600).is_ok());
314 assert!(validate_file_mode(0o_111_111).is_err());
315 assert!(validate_file_mode(0o_100_644).is_err());
316 assert!(validate_file_mode(0o_100_666).is_err());
317 assert!(validate_file_mode(0o_100_777).is_err());
318 assert!(validate_file_mode(0o_100_755).is_err());
319 assert!(validate_file_mode(0o_644).is_err());
320 assert!(validate_file_mode(0o_666).is_err());
321 }
322}