use std::{fs, io::Write, path::Path};
use crate::error::C5CoreError;
use base64::{prelude::BASE64_STANDARD, Engine};
use ecies_25519::{
KeyParsingError as EciesKeyParsingError, PublicKey as ActualEciesPublicKey, StaticSecret as ActualEciesStaticSecret,
};
use ed25519_dalek::{
pkcs8::{self, spki::der::pem::LineEnding},
SigningKey, VerifyingKey,
};
use rand::{rand_core, rngs::StdRng, CryptoRng, RngCore, SeedableRng};
use rand_core::OsRng;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CryptoAlgorithm {
EciesX25519,
}
#[derive(Debug, Clone)]
pub struct PemEncodedKey(pub String);
#[derive(Debug, Clone)]
pub struct KeyPair {
pub public: PemEncodedKey,
pub private: PemEncodedKey,
}
pub fn generate_c5_keypair(
algo: CryptoAlgorithm,
rng: &mut (impl RngCore + CryptoRng),
) -> Result<KeyPair, C5CoreError> {
match algo {
CryptoAlgorithm::EciesX25519 => {
let keypair_der = ecies_25519::generate_keypair(rng);
let public_pem = PemEncodedKey(keypair_der.public_to_pem());
let private_pem = PemEncodedKey(keypair_der.private_to_pem());
Ok(KeyPair {
public: public_pem,
private: private_pem,
})
} }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SshKeyAlgorithm {
Ed25519,
}
#[derive(Debug, Clone)]
pub struct SshKeyPair {
pub private_key_pem: PemEncodedKey, pub public_key_openssh_format: String,
}
pub fn generate_ssh_keypair(algo: SshKeyAlgorithm, comment_opt: Option<&str>) -> Result<SshKeyPair, C5CoreError> {
match algo {
SshKeyAlgorithm::Ed25519 => {
use ed25519_dalek::pkcs8::EncodePrivateKey;
let mut csprng = StdRng::from_os_rng();
let signing_key: SigningKey = SigningKey::generate(&mut csprng);
let verifying_key: VerifyingKey = signing_key.verifying_key();
let secret_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = signing_key.to_bytes();
let ed_secret_key_for_pem = ed25519_dalek::SecretKey::try_from(secret_bytes)
.map_err(|e| C5CoreError::PemParse(format!("Failed to create Ed25519 SecretKey from bytes: {}", e)))?;
let private_pem_string = signing_key
.to_pkcs8_pem(LineEnding::LF) .map_err(|e: pkcs8::Error| {
C5CoreError::PemParse(format!("Ed25519 private key to PKCS#8 PEM failed: {}", e))
})?
.as_str() .to_string();
let public_key_bytes: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = verifying_key.to_bytes();
let openssh_payload_to_encode = build_ed25519_openssh_payload(&public_key_bytes);
let b64_encoded_key = BASE64_STANDARD.encode(&openssh_payload_to_encode);
let comment_str = comment_opt.unwrap_or("");
let openssh_public_key_string = if comment_str.is_empty() {
format!("ssh-ed25519 {}", b64_encoded_key)
} else {
format!("ssh-ed25519 {} {}", b64_encoded_key, comment_str)
};
Ok(SshKeyPair {
private_key_pem: PemEncodedKey(private_pem_string),
public_key_openssh_format: openssh_public_key_string,
})
}
}
}
fn build_ssh_key_part(name: &str, data: &[u8]) -> Vec<u8> {
let mut part = Vec::new();
part.write_all(&(name.len() as u32).to_be_bytes()).unwrap();
part.write_all(name.as_bytes()).unwrap();
part.write_all(&(data.len() as u32).to_be_bytes()).unwrap();
part.write_all(data).unwrap();
part
}
fn build_ed25519_openssh_payload(public_key_bytes: &[u8; 32]) -> Vec<u8> {
let key_type_name = "ssh-ed25519";
let mut payload = Vec::new();
payload.write_all(&(key_type_name.len() as u32).to_be_bytes()).unwrap();
payload.write_all(key_type_name.as_bytes()).unwrap();
payload
.write_all(&(public_key_bytes.len() as u32).to_be_bytes())
.unwrap();
payload.write_all(public_key_bytes).unwrap();
payload
}
pub fn load_ecies_public_key(key_path: &Path) -> Result<ActualEciesPublicKey, C5CoreError> {
let key_bytes = fs::read(key_path).map_err(|e| C5CoreError::IoWithPath {
path: key_path.to_path_buf(), source: e,
})?;
ecies_25519::parse_public_key(&key_bytes).map_err(C5CoreError::from)
}
pub fn load_ecies_private_key(key_path: &Path) -> Result<ActualEciesStaticSecret, C5CoreError> {
let key_bytes = fs::read(key_path).map_err(|e| C5CoreError::IoWithPath {
path: key_path.to_path_buf(), source: e,
})?;
ecies_25519::parse_private_key(&key_bytes).map_err(C5CoreError::from)
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::pkcs8::DecodePrivateKey;
use sshkeys::PublicKey as SshPublicKeyExternal;
use std::io::Write;
use tempfile::NamedTempFile;
fn test_rng() -> StdRng {
StdRng::from_seed([42u8; 32]) }
#[test]
fn test_generate_ecies_keypair_pem_format() {
let mut rng = test_rng();
let keypair_result = generate_c5_keypair(CryptoAlgorithm::EciesX25519, &mut rng);
assert!(keypair_result.is_ok());
let keypair = keypair_result.unwrap();
assert!(keypair.public.0.starts_with("-----BEGIN PUBLIC KEY-----"));
assert!(keypair.public.0.contains("-----END PUBLIC KEY-----"));
assert!(keypair.private.0.starts_with("-----BEGIN PRIVATE KEY-----"));
assert!(keypair.private.0.contains("-----END PRIVATE KEY-----"));
let parsed_pub = ecies_25519::parse_public_key(keypair.public.0.as_bytes());
assert!(
parsed_pub.is_ok(),
"Generated public PEM failed to re-parse: {:?}",
parsed_pub.err()
);
let parsed_priv = ecies_25519::parse_private_key(keypair.private.0.as_bytes());
assert!(
parsed_priv.is_ok(),
"Generated private PEM failed to re-parse: {:?}",
parsed_priv.err()
);
}
#[test]
fn test_generate_ed25519_ssh_keypair_formats() {
let comment = Some("test-key@example.com");
let ssh_keypair_result = generate_ssh_keypair(SshKeyAlgorithm::Ed25519, comment);
assert!(
ssh_keypair_result.is_ok(),
"generate_ssh_keypair failed: {:?}",
ssh_keypair_result.err()
);
let ssh_keypair = ssh_keypair_result.unwrap();
assert!(ssh_keypair.private_key_pem.0.starts_with("-----BEGIN PRIVATE KEY-----"));
assert!(ssh_keypair.private_key_pem.0.contains("-----END PRIVATE KEY-----"));
let signing_key_from_pem = SigningKey::from_pkcs8_pem(&ssh_keypair.private_key_pem.0);
assert!(
signing_key_from_pem.is_ok(),
"Generated SSH private PEM failed to re-parse with ed25519_dalek: {:?}",
signing_key_from_pem.err()
);
assert!(ssh_keypair.public_key_openssh_format.starts_with("ssh-ed25519 AAAA"));
assert!(ssh_keypair.public_key_openssh_format.ends_with(comment.unwrap()));
let parsed_ssh_pubkey = SshPublicKeyExternal::from_string(&ssh_keypair.public_key_openssh_format); assert!(
parsed_ssh_pubkey.is_ok(),
"Generated SSH public key string failed to re-parse with sshkeys: {:?}",
parsed_ssh_pubkey.err()
);
}
fn write_to_temp_file(content: &str) -> Result<NamedTempFile, std::io::Error> {
let mut file = NamedTempFile::new()?;
file.write_all(content.as_bytes())?;
Ok(file)
}
#[test]
fn test_load_valid_ecies_keys() -> Result<(), C5CoreError> {
let mut rng = test_rng();
let keypair = generate_c5_keypair(CryptoAlgorithm::EciesX25519, &mut rng)?;
let pub_pem_file = write_to_temp_file(&keypair.public.0)?;
let priv_pem_file = write_to_temp_file(&keypair.private.0)?;
let loaded_pub = load_ecies_public_key(pub_pem_file.path())?;
let loaded_priv = load_ecies_private_key(priv_pem_file.path())?;
let original_pub_from_der = ecies_25519::parse_public_key(keypair.public.0.as_bytes()).unwrap();
let original_priv_from_der = ecies_25519::parse_private_key(keypair.private.0.as_bytes()).unwrap();
assert_eq!(loaded_pub.as_bytes(), original_pub_from_der.as_bytes());
assert_eq!(loaded_priv.to_bytes(), original_priv_from_der.to_bytes());
Ok(())
}
#[test]
fn test_load_invalid_ecies_public_key_pem() {
let bad_structure_pem = "-----BEGIN FOO KEY-----\nABC\n-----END FOO KEY-----";
let file1 = write_to_temp_file(bad_structure_pem).unwrap();
let result1 = load_ecies_public_key(file1.path());
eprintln!("Debug Case 1 (Public - Bad Structure): {:?}", result1);
assert!(matches!(
result1,
Err(C5CoreError::EciesKeyParse(
ecies_25519::KeyParsingError::InvalidDerPrefix
))
));
let corrupted_b64_pem = "-----BEGIN PUBLIC KEY-----\n!!!NotValidBase64!!!\n-----END PUBLIC KEY-----";
let file2 = write_to_temp_file(corrupted_b64_pem).unwrap();
let result2 = load_ecies_public_key(file2.path());
eprintln!("Debug Case 2 (Public - Corrupted Base64): {:?}", result2);
assert!(matches!(
result2,
Err(C5CoreError::EciesKeyParse(
ecies_25519::KeyParsingError::InvalidDerPrefix
))
));
let mut rng = test_rng();
let keypair = generate_c5_keypair(CryptoAlgorithm::EciesX25519, &mut rng).unwrap();
let private_key_pem_content = &keypair.private.0;
let file3 = write_to_temp_file(private_key_pem_content).unwrap();
let result3 = load_ecies_public_key(file3.path());
eprintln!("Debug Case 3 (Public - Private Key Content): {:?}", result3);
assert!(matches!(
result3,
Err(C5CoreError::EciesKeyParse(
ecies_25519::KeyParsingError::InvalidPemTag { expected: _, actual: _ }
))
));
let empty_file = NamedTempFile::new().unwrap();
let result4 = load_ecies_public_key(empty_file.path());
eprintln!("Debug Case 4 (Public - Empty File): {:?}", result4);
assert!(matches!(
result4,
Err(C5CoreError::EciesKeyParse(
ecies_25519::KeyParsingError::InvalidDerPrefix
))
));
}
#[test]
fn test_load_invalid_ecies_private_key_pem() {
let bad_header_pem = "-----BEGIN FOO KEY-----\nABC\n-----END FOO KEY-----";
let file1 = write_to_temp_file(bad_header_pem).unwrap();
let result1 = load_ecies_private_key(file1.path());
match &result1 {
Ok(_) => eprintln!("Debug Case 1 (Private - Bad Structure): Ok(StaticSecret) - [Sensitive data not printed]"),
Err(e) => eprintln!("Debug Case 1 (Private - Bad Structure): Err({:?})", e),
}
assert!(matches!(
result1,
Err(C5CoreError::EciesKeyParse(
ecies_25519::KeyParsingError::InvalidDerPrefix
))
));
let corrupted_b64_pem = "-----BEGIN PRIVATE KEY-----\n!!!NotValidBase64!!!\n-----END PRIVATE KEY-----";
let file2 = write_to_temp_file(corrupted_b64_pem).unwrap();
let result2 = load_ecies_private_key(file2.path());
match &result2 {
Ok(_) => eprintln!("Debug Case 2 (Private - Corrupted Base64): Ok(StaticSecret) - [Sensitive data not printed]"),
Err(e) => eprintln!("Debug Case 2 (Private - Corrupted Base64): Err({:?})", e),
}
assert!(matches!(
result2,
Err(C5CoreError::EciesKeyParse(
ecies_25519::KeyParsingError::InvalidDerPrefix ))
));
let mut rng = test_rng();
let keypair = generate_c5_keypair(CryptoAlgorithm::EciesX25519, &mut rng).unwrap();
let public_key_pem_content = &keypair.public.0;
let file3 = write_to_temp_file(public_key_pem_content).unwrap();
let result3 = load_ecies_private_key(file3.path());
match &result3 {
Ok(_) => eprintln!("Debug Case 3 (Private - Public Key Content): Ok(StaticSecret) - [Sensitive data not printed]"),
Err(e) => eprintln!("Debug Case 3 (Private - Public Key Content): Err({:?})", e),
}
assert!(matches!(
result3,
Err(C5CoreError::EciesKeyParse(
ecies_25519::KeyParsingError::InvalidPemTag { expected: _, actual: _ }
))
));
let empty_file = NamedTempFile::new().unwrap();
let result4 = load_ecies_private_key(empty_file.path());
match &result4 {
Ok(_) => eprintln!("Debug Case 4 (Private - Empty File): Ok(StaticSecret) - [Sensitive data not printed]"),
Err(e) => eprintln!("Debug Case 4 (Private - Empty File): Err({:?})", e),
}
assert!(matches!(
result4,
Err(C5CoreError::EciesKeyParse(
ecies_25519::KeyParsingError::InvalidDerPrefix
))
));
}
}