use std::fmt;
use std::path::Path;
use ed25519_dalek::{SigningKey, SECRET_KEY_LENGTH};
use zeroize::Zeroizing;
use crate::Error;
pub struct Ed25519PrivateKey {
key: SigningKey,
}
impl Ed25519PrivateKey {
pub fn generate() -> Self {
let mut csprng = rand::rngs::OsRng;
Self {
key: SigningKey::generate(&mut csprng),
}
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() != SECRET_KEY_LENGTH {
return Err(Error::configuration(format!(
"Ed25519 private key must be {} bytes, got {}",
SECRET_KEY_LENGTH,
bytes.len()
)));
}
let mut key_bytes = [0u8; SECRET_KEY_LENGTH];
key_bytes.copy_from_slice(bytes);
let zeroizing_bytes = Zeroizing::new(key_bytes);
Ok(Self {
key: SigningKey::from_bytes(&zeroizing_bytes),
})
}
pub fn from_hex(hex_str: &str) -> Result<Self, Error> {
let bytes = hex::decode(hex_str).map_err(|e| {
Error::configuration(format!("invalid hex string for Ed25519 key: {}", e))
})?;
Self::from_bytes(&bytes)
}
pub fn from_pem(pem: &str) -> Result<Self, Error> {
use ed25519_dalek::pkcs8::DecodePrivateKey;
let key = SigningKey::from_pkcs8_pem(pem)
.map_err(|e| Error::configuration(format!("failed to parse Ed25519 PEM: {}", e)))?;
Ok(Self { key })
}
pub fn from_pem_file(path: impl AsRef<Path>) -> Result<Self, Error> {
let path = path.as_ref();
let pem = std::fs::read_to_string(path).map_err(|e| {
Error::configuration(format!(
"failed to read Ed25519 key file '{}': {}",
path.display(),
e
))
})?;
let pem = Zeroizing::new(pem);
Self::from_pem(&pem)
}
pub fn public_key_bytes(&self) -> [u8; 32] {
self.key.verifying_key().to_bytes()
}
pub fn public_key_hex(&self) -> String {
hex::encode(self.public_key_bytes())
}
pub fn sign(&self, message: &[u8]) -> [u8; 64] {
use ed25519_dalek::Signer;
self.key.sign(message).to_bytes()
}
pub fn sign_hex(&self, message: &[u8]) -> String {
hex::encode(self.sign(message))
}
pub fn sign_base64url(&self, message: &[u8]) -> String {
use base64::prelude::*;
BASE64_URL_SAFE_NO_PAD.encode(self.sign(message))
}
pub(crate) fn signing_key(&self) -> &SigningKey {
&self.key
}
}
impl fmt::Debug for Ed25519PrivateKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Ed25519PrivateKey")
.field("public_key", &self.public_key_hex())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate() {
let key = Ed25519PrivateKey::generate();
let public = key.public_key_bytes();
assert_eq!(public.len(), 32);
}
#[test]
fn test_from_bytes() {
let bytes = [42u8; 32];
let key = Ed25519PrivateKey::from_bytes(&bytes).unwrap();
assert_eq!(key.public_key_bytes().len(), 32);
}
#[test]
fn test_from_bytes_wrong_length() {
let bytes = [0u8; 16];
let result = Ed25519PrivateKey::from_bytes(&bytes);
assert!(result.is_err());
}
#[test]
fn test_from_hex() {
let hex_key = "00".repeat(32);
let key = Ed25519PrivateKey::from_hex(&hex_key).unwrap();
assert_eq!(key.public_key_bytes().len(), 32);
}
#[test]
fn test_from_hex_invalid() {
let result = Ed25519PrivateKey::from_hex("not_hex");
assert!(result.is_err());
}
#[test]
fn test_sign() {
let key = Ed25519PrivateKey::generate();
let signature = key.sign(b"test message");
assert_eq!(signature.len(), 64);
}
#[test]
fn test_sign_deterministic() {
let bytes = [1u8; 32];
let key = Ed25519PrivateKey::from_bytes(&bytes).unwrap();
let sig1 = key.sign(b"message");
let sig2 = key.sign(b"message");
assert_eq!(sig1, sig2);
}
#[test]
fn test_sign_different_messages() {
let key = Ed25519PrivateKey::generate();
let sig1 = key.sign(b"message1");
let sig2 = key.sign(b"message2");
assert_ne!(sig1, sig2);
}
#[test]
fn test_public_key_hex() {
let key = Ed25519PrivateKey::generate();
let hex = key.public_key_hex();
assert_eq!(hex.len(), 64); }
#[test]
fn test_debug_hides_key() {
let key = Ed25519PrivateKey::generate();
let debug = format!("{:?}", key);
assert!(debug.contains("Ed25519PrivateKey"));
assert!(debug.contains("public_key"));
assert!(!debug.contains("[0"));
}
#[test]
fn test_sign_base64url() {
let key = Ed25519PrivateKey::generate();
let sig = key.sign_base64url(b"test");
assert_eq!(sig.len(), 86);
assert!(!sig.contains('='));
assert!(!sig.contains('+'));
assert!(!sig.contains('/'));
}
#[test]
fn test_sign_hex() {
let key = Ed25519PrivateKey::generate();
let sig = key.sign_hex(b"test message");
assert_eq!(sig.len(), 128);
assert!(hex::decode(&sig).is_ok());
}
#[test]
fn test_from_pem_invalid() {
let result = Ed25519PrivateKey::from_pem("not a valid PEM");
assert!(result.is_err());
}
#[test]
fn test_from_pem_file_not_found() {
let result = Ed25519PrivateKey::from_pem_file("/nonexistent/path/key.pem");
assert!(result.is_err());
}
#[test]
fn test_signing_key_accessor() {
let key = Ed25519PrivateKey::generate();
let _signing_key = key.signing_key();
}
}