use crate::crypto::{algorithm_ids, CryptoRegistry, SignatureAlgorithm};
use crate::error::{LicenseError, Result};
use pem::{encode, Pem};
use rand::rngs::OsRng;
use rsa::pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey};
use rsa::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey};
use rsa::{RsaPrivateKey, RsaPublicKey};
use std::path::Path;
use zeroize::Zeroizing;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KeySize {
Bits2048,
#[default]
Bits3072,
Bits4096,
}
impl KeySize {
pub fn bits(&self) -> usize {
match self {
KeySize::Bits2048 => 2048,
KeySize::Bits3072 => 3072,
KeySize::Bits4096 => 4096,
}
}
}
pub struct KeyPair {
private_key: RsaPrivateKey,
pub public_key: RsaPublicKey,
}
impl std::fmt::Debug for KeyPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KeyPair")
.field("private_key", &"[REDACTED]")
.field("public_key", &"<RsaPublicKey>")
.finish()
}
}
impl KeyPair {
pub fn private_key(&self) -> &RsaPrivateKey {
&self.private_key
}
pub fn into_private_key(self) -> RsaPrivateKey {
self.private_key
}
pub fn generate(size: KeySize) -> Result<Self> {
let mut rng = OsRng;
let private_key = RsaPrivateKey::new(&mut rng, size.bits())
.map_err(|e| LicenseError::KeyGenerationFailed(e.to_string()))?;
let public_key = RsaPublicKey::from(&private_key);
Ok(Self {
private_key,
public_key,
})
}
pub fn export_private_pem(&self) -> Result<String> {
let der = self
.private_key
.to_pkcs8_der()
.map_err(|e| LicenseError::InvalidKeyFormat(e.to_string()))?;
let pem = Pem::new("PRIVATE KEY", der.as_bytes());
Ok(encode(&pem))
}
pub fn export_public_pem(&self) -> Result<String> {
let der = self
.public_key
.to_public_key_der()
.map_err(|e| LicenseError::InvalidKeyFormat(e.to_string()))?;
let pem = Pem::new("PUBLIC KEY", der.as_bytes());
Ok(encode(&pem))
}
pub fn save_to_files(&self, private_path: &Path, public_path: &Path) -> Result<()> {
std::fs::write(private_path, self.export_private_pem()?)?;
std::fs::write(public_path, self.export_public_pem()?)?;
Ok(())
}
pub fn load_from_files(private_path: &Path, public_path: &Path) -> Result<Self> {
check_private_key_permissions(private_path)?;
let private_pem = std::fs::read_to_string(private_path)?;
let public_pem = std::fs::read_to_string(public_path)?;
let private_key = parse_private_key(&private_pem)?;
let public_key = parse_public_key(&public_pem)?;
Ok(Self {
private_key,
public_key,
})
}
}
pub fn parse_private_key(pem_str: &str) -> Result<RsaPrivateKey> {
let pem_str = pem_str.replace("\\n", "\n");
if let Ok(key) = RsaPrivateKey::from_pkcs8_pem(&pem_str) {
return Ok(key);
}
if let Ok(key) = RsaPrivateKey::from_pkcs1_pem(&pem_str) {
return Ok(key);
}
Err(LicenseError::InvalidKeyFormat(
"Could not parse private key (tried PKCS#8 and PKCS#1 formats)".into(),
))
}
pub fn parse_public_key(pem_str: &str) -> Result<RsaPublicKey> {
let pem_str = pem_str.replace("\\n", "\n");
if let Ok(key) = RsaPublicKey::from_public_key_pem(&pem_str) {
return Ok(key);
}
if let Ok(key) = RsaPublicKey::from_pkcs1_pem(&pem_str) {
return Ok(key);
}
Err(LicenseError::InvalidKeyFormat(
"Could not parse public key (tried SPKI and PKCS#1 formats)".into(),
))
}
pub fn extract_public_key(private_key: &RsaPrivateKey) -> RsaPublicKey {
RsaPublicKey::from(private_key)
}
pub struct CryptoKeyPair {
private_key_pem: Zeroizing<String>,
pub public_key_pem: String,
pub algorithm_id: String,
}
impl std::fmt::Debug for CryptoKeyPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CryptoKeyPair")
.field("private_key_pem", &"[REDACTED]")
.field(
"public_key_pem",
&format!(
"{}...",
&self.public_key_pem[..40.min(self.public_key_pem.len())]
),
)
.field("algorithm_id", &self.algorithm_id)
.finish()
}
}
impl CryptoKeyPair {
pub fn private_key_pem(&self) -> &str {
&self.private_key_pem
}
pub fn generate(algorithm_id: &str) -> Result<Self> {
let algorithm = CryptoRegistry::get_signature_algorithm(algorithm_id)?;
let (private_key_pem, public_key_pem) = algorithm.generate_keypair()?;
Ok(Self {
private_key_pem: Zeroizing::new(private_key_pem),
public_key_pem,
algorithm_id: algorithm_id.to_string(),
})
}
pub fn from_pem(private_key_pem: String, public_key_pem: String, algorithm_id: &str) -> Self {
Self {
private_key_pem: Zeroizing::new(private_key_pem),
public_key_pem,
algorithm_id: algorithm_id.to_string(),
}
}
pub fn load_from_files(
private_path: &Path,
public_path: &Path,
algorithm_id: &str,
) -> Result<Self> {
check_private_key_permissions(private_path)?;
let private_key_pem = std::fs::read_to_string(private_path)?;
let public_key_pem = std::fs::read_to_string(public_path)?;
Ok(Self::from_pem(
private_key_pem,
public_key_pem,
algorithm_id,
))
}
pub fn save_to_files(&self, private_path: &Path, public_path: &Path) -> Result<()> {
std::fs::write(private_path, self.private_key_pem.as_str())?;
std::fs::write(public_path, &self.public_key_pem)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(private_path, perms)?;
}
Ok(())
}
pub fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
let algorithm = CryptoRegistry::get_signature_algorithm(&self.algorithm_id)?;
algorithm.sign(data, &self.private_key_pem)
}
pub fn verify(&self, data: &[u8], signature: &[u8]) -> Result<()> {
let algorithm = CryptoRegistry::get_signature_algorithm(&self.algorithm_id)?;
algorithm.verify(data, signature, &self.public_key_pem)
}
pub fn get_algorithm(&self) -> Result<&'static dyn SignatureAlgorithm> {
CryptoRegistry::get_signature_algorithm(&self.algorithm_id)
}
pub fn from_rsa_keypair(keypair: &KeyPair) -> Result<Self> {
Ok(Self {
private_key_pem: Zeroizing::new(keypair.export_private_pem()?),
public_key_pem: keypair.export_public_pem()?,
algorithm_id: algorithm_ids::RSA_SHA256.to_string(),
})
}
}
fn check_private_key_permissions(path: &Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if !path.exists() {
return Ok(()); }
let metadata = std::fs::metadata(path)?;
let mode = metadata.permissions().mode();
if mode & 0o077 != 0 {
return Err(LicenseError::InsecureKeyPermissions {
path: path.to_path_buf(),
mode: format!("{:04o}", mode & 0o7777),
suggestion: "Run: chmod 600 <file>".to_string(),
});
}
}
#[cfg(not(unix))]
{
let _ = path; }
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_generation() {
let keypair = KeyPair::generate(KeySize::Bits2048).unwrap();
let private_pem = keypair.export_private_pem().unwrap();
let public_pem = keypair.export_public_pem().unwrap();
assert!(private_pem.contains("PRIVATE KEY"));
assert!(public_pem.contains("PUBLIC KEY"));
}
#[test]
fn test_key_round_trip() {
let keypair = KeyPair::generate(KeySize::Bits2048).unwrap();
let private_pem = keypair.export_private_pem().unwrap();
let public_pem = keypair.export_public_pem().unwrap();
let parsed_private = parse_private_key(&private_pem).unwrap();
let parsed_public = parse_public_key(&public_pem).unwrap();
assert_eq!(*keypair.private_key(), parsed_private);
assert_eq!(keypair.public_key, parsed_public);
}
#[test]
fn test_crypto_keypair_rsa() {
let keypair = CryptoKeyPair::generate(algorithm_ids::RSA_SHA256).unwrap();
assert_eq!(keypair.algorithm_id, algorithm_ids::RSA_SHA256);
assert!(keypair.private_key_pem().contains("PRIVATE KEY"));
assert!(keypair.public_key_pem.contains("PUBLIC KEY"));
let data = b"test message for RSA";
let signature = keypair.sign(data).unwrap();
assert!(keypair.verify(data, &signature).is_ok());
}
#[test]
fn test_crypto_keypair_ed25519() {
let keypair = CryptoKeyPair::generate(algorithm_ids::ED25519).unwrap();
assert_eq!(keypair.algorithm_id, algorithm_ids::ED25519);
assert!(keypair.private_key_pem().contains("PRIVATE KEY"));
assert!(keypair.public_key_pem.contains("PUBLIC KEY"));
let data = b"test message for Ed25519";
let signature = keypair.sign(data).unwrap();
assert!(keypair.verify(data, &signature).is_ok());
assert_eq!(signature.len(), 64);
}
#[test]
fn test_crypto_keypair_from_rsa_keypair() {
let rsa_keypair = KeyPair::generate(KeySize::Bits2048).unwrap();
let crypto_keypair = CryptoKeyPair::from_rsa_keypair(&rsa_keypair).unwrap();
assert_eq!(crypto_keypair.algorithm_id, algorithm_ids::RSA_SHA256);
let data = b"conversion test";
let signature = crypto_keypair.sign(data).unwrap();
assert!(crypto_keypair.verify(data, &signature).is_ok());
}
}