#![warn(
missing_debug_implementations,
missing_docs,
unsafe_code,
bare_trait_objects
)]
#![warn(clippy::pedantic, clippy::nursery)]
#![allow(
// Next `cast_*` lints don't give alternatives.
clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::cast_sign_loss,
// Next lints produce too much noise/false positives.
clippy::module_name_repetitions, clippy::similar_names, clippy::must_use_candidate,
clippy::pub_enum_variant_names,
// '... may panic' lints.
clippy::indexing_slicing,
// Too much work to fix.
clippy::missing_errors_doc, clippy::missing_const_for_fn
)]
use anyhow::format_err;
use exonum_crypto::{KeyPair, PublicKey, SecretKey, Seed, SEED_LENGTH};
use pwbox::{sodium::Sodium, ErasedPwBox, Eraser, SensitiveData, Suite};
use rand::thread_rng;
use secret_tree::{Name, SecretTree};
use serde_derive::{Deserialize, Serialize};
#[cfg(unix)]
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
use std::{
fs::{File, OpenOptions},
io::{Error, ErrorKind, Read, Write},
path::Path,
};
#[cfg(unix)]
#[cfg_attr(feature = "cargo-clippy", allow(clippy::verbose_bit_mask))]
fn validate_file_mode(mode: u32) -> Result<(), Error> {
if (mode & 0o_077) == 0 {
Ok(())
} else {
Err(Error::new(ErrorKind::Other, "Wrong file's mode"))
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Keys {
pub consensus: KeyPair,
pub service: KeyPair,
}
impl Keys {
pub fn random() -> Self {
Self {
consensus: KeyPair::random(),
service: KeyPair::random(),
}
}
pub fn from_keys(consensus_keys: impl Into<KeyPair>, service_keys: impl Into<KeyPair>) -> Self {
Self {
consensus: consensus_keys.into(),
service: service_keys.into(),
}
}
}
impl Keys {
pub fn consensus_pk(&self) -> PublicKey {
self.consensus.public_key()
}
pub fn consensus_sk(&self) -> &SecretKey {
self.consensus.secret_key()
}
pub fn service_pk(&self) -> PublicKey {
self.service.public_key()
}
pub fn service_sk(&self) -> &SecretKey {
self.service.secret_key()
}
}
fn save_master_key<P: AsRef<Path>>(
path: P,
encrypted_key: &EncryptedMasterKey,
) -> Result<(), Error> {
let file_content =
toml::to_string_pretty(encrypted_key).map_err(|e| Error::new(ErrorKind::Other, e))?;
let mut open_options = OpenOptions::new();
open_options.create(true).write(true);
#[cfg(unix)]
open_options.mode(0o_600);
let mut file = open_options.open(path.as_ref())?;
file.write_all(file_content.as_bytes())?;
Ok(())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EncryptedMasterKey {
key: ErasedPwBox,
}
impl EncryptedMasterKey {
fn encrypt(key: &secret_tree::Seed, pass_phrase: impl AsRef<[u8]>) -> Result<Self, Error> {
let mut rng = thread_rng();
let mut eraser = Eraser::new();
eraser.add_suite::<Sodium>();
let pwbox = Sodium::build_box(&mut rng)
.seal(pass_phrase, key)
.map_err(|_| Error::new(ErrorKind::Other, "Couldn't create a pw box"))?;
let encrypted_key = eraser
.erase(&pwbox)
.map_err(|_| Error::new(ErrorKind::Other, "Couldn't convert a pw box"))?;
Ok(Self { key: encrypted_key })
}
fn decrypt(self, pass_phrase: impl AsRef<[u8]>) -> Result<SensitiveData, Error> {
let mut eraser = Eraser::new();
eraser.add_suite::<Sodium>();
let restored = eraser
.restore(&self.key)
.map_err(|_| Error::new(ErrorKind::Other, "Couldn't restore a secret key"))?;
assert_eq!(restored.len(), SEED_LENGTH);
restored
.open(pass_phrase)
.map_err(|_| Error::new(ErrorKind::Other, "Couldn't open an encrypted key"))
}
}
pub fn generate_keys<P: AsRef<Path>>(path: P, passphrase: &[u8]) -> anyhow::Result<Keys> {
let tree = SecretTree::new(&mut thread_rng());
let encrypted_key = EncryptedMasterKey::encrypt(tree.seed(), passphrase)?;
save_master_key(path, &encrypted_key)?;
Ok(generate_keys_from_master_password(&tree))
}
pub fn generate_keys_from_seed(
passphrase: &[u8],
seed: &[u8],
) -> anyhow::Result<(Keys, EncryptedMasterKey)> {
let tree = SecretTree::from_seed(seed)
.ok_or_else(|| format_err!("Error creating SecretTree from seed"))?;
let encrypted_key = EncryptedMasterKey::encrypt(tree.seed(), passphrase)?;
let keys = generate_keys_from_master_password(&tree);
Ok((keys, encrypted_key))
}
fn generate_keys_from_master_password(tree: &SecretTree) -> Keys {
let mut buffer = [0_u8; 32];
tree.child(Name::new("consensus")).fill(&mut buffer);
let seed = Seed::new(buffer);
let consensus_keys = KeyPair::from_seed(&seed);
tree.child(Name::new("service")).fill(&mut buffer);
let seed = Seed::new(buffer);
let service_keys = KeyPair::from_seed(&seed);
Keys::from_keys(consensus_keys, service_keys)
}
pub fn read_keys_from_file<P: AsRef<Path>, W: AsRef<[u8]>>(
path: P,
pass_phrase: W,
) -> anyhow::Result<Keys> {
let mut key_file = File::open(path)?;
#[cfg(unix)]
validate_file_mode(key_file.metadata()?.mode())?;
let mut file_content = vec![];
key_file.read_to_end(&mut file_content)?;
let keys: EncryptedMasterKey =
toml::from_slice(file_content.as_slice()).map_err(|e| Error::new(ErrorKind::Other, e))?;
let seed = keys.decrypt(pass_phrase)?;
let tree = SecretTree::from_seed(&seed).expect("Error creating secret tree from seed.");
Ok(generate_keys_from_master_password(&tree))
}
#[cfg(test)]
mod tests {
use super::*;
use tempdir::TempDir;
#[test]
fn test_create_and_read_keys_file() {
let dir = TempDir::new("test_utils").expect("Couldn't create TempDir");
let file_path = dir.path().join("private_key.toml");
let pass_phrase = b"passphrase";
let pk1 = generate_keys(file_path.as_path(), pass_phrase).unwrap();
let pk2 = read_keys_from_file(file_path.as_path(), pass_phrase).unwrap();
assert_eq!(pk1, pk2);
}
#[test]
fn encrypt_decrypt() {
let pass_phrase = b"passphrase";
let tree = SecretTree::new(&mut thread_rng());
let seed = tree.seed();
let key =
EncryptedMasterKey::encrypt(seed, pass_phrase).expect("Couldn't encrypt master key");
let decrypted_seed = key
.decrypt(pass_phrase)
.expect("Couldn't decrypt master key ");
assert_eq!(&seed[..], &decrypted_seed[..]);
}
#[test]
fn test_decrypt_from_file() {
let pass_phrase = b"passphrase";
let file_content = r#"
[key]
ciphertext = "cf6c63520e789efc978ad07e218e6fd199ccb5e861e9c893cac40641fc66c89c"
mac = "b3e1a815a2cb316bf209da7bc4203091"
kdf = "scrypt-nacl"
cipher = "xsalsa20-poly1305"
[key.kdfparams]
salt = "7e7a1d9dc5269b0ebedb5fcd433d772880786ebefe8647a275ef1626cfb122b3"
memlimit = 16777216
opslimit = 524288
[key.cipherparams]
iv = "832bded4e12948a022065ace39b31cd7b514bf9c2b2a407f"
"#;
let keys: EncryptedMasterKey =
toml::from_str(file_content).expect("Couldn't deserialize content");
let seed = keys.decrypt(pass_phrase).expect("Couldn't decrypt key");
assert_eq!(
hex::encode(&*seed),
"a05a82575d5f9d1f9469df31896f5b3c14ec4d18b3948cd7c8b09a7eed48b4e0"
);
}
#[cfg(unix)]
#[test]
fn test_validate_file_mode() {
assert!(validate_file_mode(0o_100_600).is_ok());
assert!(validate_file_mode(0o_600).is_ok());
assert!(validate_file_mode(0o_111_111).is_err());
assert!(validate_file_mode(0o_100_644).is_err());
assert!(validate_file_mode(0o_100_666).is_err());
assert!(validate_file_mode(0o_100_777).is_err());
assert!(validate_file_mode(0o_100_755).is_err());
assert!(validate_file_mode(0o_644).is_err());
assert!(validate_file_mode(0o_666).is_err());
}
}