Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ multiaddr = "0.18.2"
num_cpus = "1"
openssl = "0.10.68"
parking_lot = "0.12"
pbkdf2 = "0.12.2"
r2d2 = "0.8.10"
r2d2_sqlite = "0.21.0"
rand = "0.9"
Expand Down
7 changes: 7 additions & 0 deletions anchor/client/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,13 @@ pub struct Node {
help_heading = FLAG_HEADER,
)]
pub subscribe_all_subnets: bool,

#[clap(
long,
help = "Optional password to decrypt rsa keystore",
display_order = 0
)]
pub rsa_key_password: Option<String>,
}

pub fn get_color_style() -> Styles {
Expand Down
5 changes: 5 additions & 0 deletions anchor/client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ pub struct Config {
pub execution_nodes_tls_certs: Option<Vec<PathBuf>>,
/// Configuration for the processor
pub processor: processor::Config,
/// Password used to encrypt rsa keyfile
pub password: Option<String>,
}

impl Config {
Expand Down Expand Up @@ -96,6 +98,7 @@ impl Config {
beacon_nodes_tls_certs: None,
execution_nodes_tls_certs: None,
processor: <_>::default(),
password: None,
}
}
}
Expand Down Expand Up @@ -137,6 +140,8 @@ pub fn from_cli(cli_args: &Node) -> Result<Config, String> {
.map_err(|e| format!("Unable to parse execution node URL: {:?}", e))?;
}

config.password = cli_args.rsa_key_password.to_owned();

/*
* Network related
*/
Expand Down
20 changes: 17 additions & 3 deletions anchor/client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use config::Config;
use database::NetworkDatabase;
use eth2::reqwest::{Certificate, ClientBuilder};
use eth2::{BeaconNodeHttpClient, Timeouts};
use keygen::{run_keygen, Keygen};
use keygen::{encryption::decrypt, run_keygen, Keygen};
use message_receiver::MessageReceiver;
use message_sender::NetworkMessageSender;
use message_validator::Validator;
Expand Down Expand Up @@ -108,7 +108,7 @@ impl Client {

let spec = Arc::new(config.ssv_network.eth2_network.chain_spec::<E>()?);

let key = read_or_generate_private_key(&config.data_dir.join("key.pem"))?;
let key = read_or_generate_private_key(&config.data_dir.join("key.pem"), config.password)?;
let err = |e| format!("Unable to derive public key: {e:?}");
let pubkey = Rsa::from_public_components(
key.n().to_owned().map_err(err)?,
Expand Down Expand Up @@ -754,7 +754,10 @@ pub fn load_pem_certificate<P: AsRef<Path>>(pem_path: P) -> Result<Certificate,
Certificate::from_pem(&buf).map_err(|e| format!("Unable to parse certificate: {}", e))
}

fn read_or_generate_private_key(path: &Path) -> Result<Rsa<Private>, String> {
fn read_or_generate_private_key(
path: &Path,
password: Option<String>,
) -> Result<Rsa<Private>, String> {
match File::open(path) {
Ok(mut file) => {
// there seems to be an existing file, try to read key
Expand All @@ -766,6 +769,16 @@ fn read_or_generate_private_key(path: &Path) -> Result<Rsa<Private>, String> {
));
file.read_to_string(&mut key_string)
.map_err(|e| format!("Unable to read private key at {path:?}: {e:?}"))?;

// If key file is encrypted, decrypt it
let key_string = if let Some(password) = password {
let decrypted = decrypt(&password, file)
.map_err(|e| format!("Unable to decrypt rsa keyfile: {e:?}"))?;
Zeroizing::new(decrypted)
} else {
key_string
};

// TODO support passphrase
Rsa::private_key_from_pem(key_string.as_ref())
.map_err(|e| format!("Unable to read private key: {e:?}"))
Expand All @@ -787,6 +800,7 @@ fn read_or_generate_private_key(path: &Path) -> Result<Rsa<Private>, String> {
let key = run_keygen(Keygen {
output_path: Some(parent_dir.to_string_lossy().to_string()),
force: false,
password: None,
})
.map_err(|e| format!("Unable to write private key: {e:?}"))?;

Expand Down
4 changes: 4 additions & 0 deletions anchor/keygen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ edition = { workspace = true }
authors = ["Sigma Prime <contact@sigmaprime.io>"]

[dependencies]
aes-gcm = "0.10.3"
base64 = { workspace = true }
clap = { workspace = true }
openssl = { workspace = true }
pbkdf2 = { workspace = true }
rand = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
zeroize = { workspace = true }
120 changes: 120 additions & 0 deletions anchor/keygen/src/encryption.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use pbkdf2::hmac;
use rand::{rngs::OsRng, TryRngCore};
use std::fs::File;
use std::io::{self, Read};
use std::string::FromUtf8Error;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum EncryptionError {
#[error("Failed to generate random bytes")]
Random,

#[error("Failed to encrypt data")]
Encrypt,

#[error("Failed to initialize cipher")]
Cipher,

#[error("Failed to derive key with PBKDF2")]
PBKDF2,

#[error("Failed to read file: {0}")]
IO(#[from] io::Error),

#[error("Input data too small")]
InvalidDataSize,

#[error("Failed to decrypt data")]
Decrypt,

#[error("Failed to convert data: {0}")]
Conversion(#[from] FromUtf8Error),
}

// Encrypt the input with a password
pub fn encrypt(input: &Vec<u8>, password: &str) -> Result<Vec<u8>, EncryptionError> {
// Generate a random salt
let mut salt = [0u8; 16];
OsRng
.try_fill_bytes(&mut salt)
.map_err(|_| EncryptionError::Random)?;

// Derive a key from the password using PBKDF2
let mut derived_key = [0u8; 32];
pbkdf2::pbkdf2::<hmac::Hmac<sha2::Sha256>>(
password.as_bytes(),
&salt,
10000, // Number of iterations
&mut derived_key,
)
.map_err(|_| EncryptionError::PBKDF2)?;

// Generate a random nonce
let mut nonce_bytes = [0u8; 12];
OsRng
.try_fill_bytes(&mut nonce_bytes)
.map_err(|_| EncryptionError::Random)?;
let nonce = Nonce::from_slice(&nonce_bytes);

// Initialize the cipher
let cipher = Aes256Gcm::new_from_slice(&derived_key).map_err(|_| EncryptionError::Cipher)?;

// Encrypt the data
let ciphertext = cipher
.encrypt(nonce, input.as_slice())
.map_err(|_| EncryptionError::Encrypt)?;

// Combine salt, nonce, and ciphertext into a single output
let mut output = Vec::with_capacity(salt.len() + nonce_bytes.len() + ciphertext.len());
output.extend_from_slice(&salt);
output.extend_from_slice(&nonce_bytes);
output.extend_from_slice(&ciphertext);

Ok(output)
}

// Decrypt the contents of the file with the password
pub fn decrypt(password: &str, mut file: File) -> Result<String, EncryptionError> {
// Read the file
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
decrypt_bytes(password, &contents)
}

pub fn decrypt_bytes(password: &str, contents: &[u8]) -> Result<String, EncryptionError> {
if contents.len() < 28 {
return Err(EncryptionError::InvalidDataSize);
}

// Extract the salt, nonce, and ciphertext
let salt = &contents[0..16];
let nonce = Nonce::from_slice(&contents[16..28]);
let ciphertext = &contents[28..];

// Derive the key from the password
let mut derived_key = [0u8; 32]; // 256 bits
pbkdf2::pbkdf2::<hmac::Hmac<sha2::Sha256>>(
password.as_bytes(),
salt,
10000, // Number of iterations
&mut derived_key,
)
.map_err(|_| EncryptionError::PBKDF2)?;

// Initialize the cipher
let cipher = Aes256Gcm::new_from_slice(&derived_key).map_err(|_| EncryptionError::Cipher)?;

// Decrypt the data
let plaintext = cipher
.decrypt(nonce, ciphertext)
.map_err(|_| EncryptionError::Decrypt)?;

// Convert to a string
let decrypted = String::from_utf8(plaintext)?;
Ok(decrypted)
}
82 changes: 62 additions & 20 deletions anchor/keygen/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::encryption::{encrypt, EncryptionError};
use base64::prelude::*;
use clap::Parser;
use openssl::{error::ErrorStack, pkey::Private, rsa::Rsa};
Expand All @@ -7,6 +8,8 @@ use thiserror::Error;
use tracing::info;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};

pub mod encryption;

#[derive(Error, Debug)]
pub enum KeygenError {
#[error("Failed to generate new private key: {0}")]
Expand All @@ -24,6 +27,9 @@ pub enum KeygenError {
#[error("Failed to convert output data to JSON: {0}")]
Json(#[from] serde_json::Error),

#[error("Encryption error: {0}")]
Encryption(#[from] EncryptionError),

#[error("{0}")]
Custom(String),
}
Expand All @@ -41,7 +47,14 @@ pub struct Keygen {
default_value = "false"
)]
pub force: bool,
// TODO: add prompt for password

#[clap(
long,
help = "Password for file encryption",
value_name = "PASSWORD",
default_value = ""
)]
pub password: Option<String>,
}

#[derive(Debug, Serialize, Zeroize, ZeroizeOnDrop)]
Expand All @@ -50,7 +63,6 @@ struct PrettyOutput {
public: String,
private: String,
}
// TODO: add encryption and get password functions

// Run RSA keygeneration
pub fn run_keygen(keygen: Keygen) -> Result<Rsa<Private>, KeygenError> {
Expand Down Expand Up @@ -86,25 +98,31 @@ pub fn run_keygen(keygen: Keygen) -> Result<Rsa<Private>, KeygenError> {
let pem_file = output_dir.join("key.pem");
let json_file = output_dir.join("keys.json");

// Create JSON data structure
let data = PrettyOutput {
public: public_pem_encoded,
private: private_pem_encoded.to_string(),
};

// Convert to pretty JSON
let pretty_json = Zeroizing::new(serde_json::to_string_pretty(&data)?);
// TODO: Encrypt and password protect the private key
if keygen.force || (!pem_file.exists() && !json_file.exists()) {
// Write the PEM file
fs::write(&pem_file, &private_pem)?;

info!("Private key written to: {}", pem_file.display());

// Write the JSON file
fs::write(&json_file, pretty_json)?;

info!("JSON keys written to: {}", json_file.display());
// If a password was provided, encrypt the private key
if let Some(password) = keygen.password {
// Encrypt the private key
let encrypted_private = encrypt(&private_pem, &password)?;

fs::write(&pem_file, &encrypted_private)?;
info!("Encrypted private key written to: {}", pem_file.display());

// Log the public key
info!("Generated public key: {}", public_pem_encoded);
} else {
// Otherwise, write out plainkey keys to respective files
let data = PrettyOutput {
public: public_pem_encoded,
private: private_pem_encoded.to_string(),
};
let pretty_json = Zeroizing::new(serde_json::to_string_pretty(&data)?);

fs::write(&pem_file, &private_pem)?;
info!("Private key written to: {}", pem_file.display());

fs::write(&json_file, pretty_json)?;
info!("JSON keys written to: {}", json_file.display());
}
} else {
return Err(KeygenError::Custom(format!(
"PEM file or JSON file already exist in {}",
Expand All @@ -114,3 +132,27 @@ pub fn run_keygen(keygen: Keygen) -> Result<Rsa<Private>, KeygenError> {

Ok(private_key)
}

#[cfg(test)]
mod keygen_test {
use super::*;
use crate::encryption::decrypt_bytes;

#[test]
// Make sure decrypted output equals encrypted input and output is valid key
fn test_encrypt_decrypt() {
// Generate a random key
let private_key = Rsa::generate(2048).unwrap();
let private_pem = private_key.private_key_to_pem().unwrap();
let private_utf8 = String::from_utf8(private_pem.clone()).unwrap();

let encrypted = encrypt(&private_pem, "password").unwrap();
let decrypted = decrypt_bytes("password", &encrypted).unwrap();

// Make sure it is the same as the original
assert_eq!(private_utf8, decrypted);

// Make sure we can construct a key from the output
assert!(Rsa::private_key_from_pem(decrypted.as_ref()).is_ok());
}
}
2 changes: 1 addition & 1 deletion anchor/keysplit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ eth = { workspace = true }
futures = { workspace = true }
hex = { workspace = true }
openssl = { workspace = true }
pbkdf2 = "0.12.2"
pbkdf2 = { workspace = true }
r2d2 = { workspace = true }
r2d2_sqlite = { workspace = true }
rand = { workspace = true }
Expand Down
Loading