exonum_keys/
lib.rs

1// Copyright 2020 The Exonum Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Key management for [Exonum] nodes.
16//!
17//! This crate provides tools for storing and loading encrypted keys for a node.
18//!
19//! [Exonum]: https://exonum.com/
20//!
21//! # Examples
22//!
23//! ```
24//! use exonum_keys::{generate_keys, read_keys_from_file};
25//! use tempdir::TempDir;
26//!
27//! # fn main() -> anyhow::Result<()> {
28//! let dir = TempDir::new("test_keys")?;
29//! let file_path = dir.path().join("private_key.toml");
30//! let pass_phrase = b"super_secret_passphrase";
31//! let keys = generate_keys(file_path.as_path(), pass_phrase)?;
32//! let restored_keys = read_keys_from_file(file_path.as_path(), pass_phrase)?;
33//! assert_eq!(keys, restored_keys);
34//! # Ok(())
35//! # }
36//! ```
37
38#![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    // Next `cast_*` lints don't give alternatives.
47    clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::cast_sign_loss,
48    // Next lints produce too much noise/false positives.
49    clippy::module_name_repetitions, clippy::similar_names, clippy::must_use_candidate,
50    clippy::pub_enum_variant_names,
51    // '... may panic' lints.
52    clippy::indexing_slicing,
53    // Too much work to fix.
54    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    // Check that group and other bits are not set.
76    if (mode & 0o_077) == 0 {
77        Ok(())
78    } else {
79        Err(Error::new(ErrorKind::Other, "Wrong file's mode"))
80    }
81}
82
83/// Container for all key pairs held by an Exonum node.
84#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
85#[non_exhaustive]
86pub struct Keys {
87    /// Consensus keypair.
88    pub consensus: KeyPair,
89    /// Service keypair.
90    pub service: KeyPair,
91}
92
93impl Keys {
94    /// Creates a random set of keys using the random number generator provided
95    /// by the crypto backend.
96    pub fn random() -> Self {
97        Self {
98            consensus: KeyPair::random(),
99            service: KeyPair::random(),
100        }
101    }
102
103    /// Creates validator keys from the provided keypairs.
104    ///
105    /// # Stability
106    ///
107    /// Since more keys may be added to `Keys` in the future, this method is considered
108    /// unstable.
109    ///
110    /// # Panics
111    ///
112    /// If a public key in any keypair doesn't match with corresponding private key.
113    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    /// Consensus public key.
123    pub fn consensus_pk(&self) -> PublicKey {
124        self.consensus.public_key()
125    }
126
127    /// Consensus private key.
128    pub fn consensus_sk(&self) -> &SecretKey {
129        self.consensus.secret_key()
130    }
131
132    /// Service public key.
133    pub fn service_pk(&self) -> PublicKey {
134        self.service.public_key()
135    }
136
137    /// Service secret key.
138    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    // By agreement we use the same permissions as for SSH private keys.
152    #[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/// Encrypted master key.
161#[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
195/// Creates a TOML file that contains encrypted master and returns `Keys` derived from it.
196pub 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
204/// Creates a TOML file from seed that contains encrypted master and returns `Keys` derived from it.
205pub 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
231/// Reads encrypted master key from file and generate validator keys from it.
232pub 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}