use base64::{
Engine,
engine::general_purpose::STANDARD
};
use ed25519_dalek::{
Signature,
Signer,
Verifier,
SigningKey,
VerifyingKey,
PUBLIC_KEY_LENGTH,
SECRET_KEY_LENGTH
};
use rand::rngs::OsRng;
use crate::IronShieldChallenge;
use std::env;
macro_rules! debug_log {
($($arg:tt)*) => {
#[cfg(all(target_arch = "wasm32", feature = "wasm-logging"))]
{
let msg = format!($($arg)*);
web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&msg));
}
#[cfg(not(target_arch = "wasm32"))]
eprintln!($($arg)*);
#[cfg(all(target_arch = "wasm32", not(feature = "wasm-logging")))]
{
let _ = format!($($arg)*);
}
};
}
#[derive(Debug, Clone)]
pub enum CryptoError {
MissingEnvironmentVariable(String),
InvalidKeyFormat(String),
SigningFailed(String),
VerificationFailed(String),
Base64DecodingFailed(String),
PgpParsingFailed(String),
}
impl std::fmt::Display for CryptoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CryptoError::MissingEnvironmentVariable(var) => write!(f, "Missing environment variable: {}", var),
CryptoError::InvalidKeyFormat(msg) => write!(f, "Invalid key format: {}", msg),
CryptoError::SigningFailed(msg) => write!(f, "Signing failed: {}", msg),
CryptoError::VerificationFailed(msg) => write!(f, "Verification failed: {}", msg),
CryptoError::Base64DecodingFailed(msg) => write!(f, "Base64 decoding failed: {}", msg),
CryptoError::PgpParsingFailed(msg) => write!(f, "PGP parsing failed: {}", msg),
}
}
}
impl std::error::Error for CryptoError {}
fn parse_key(key_data: &str, is_private: bool) -> Result<[u8; 32], CryptoError> {
use sequoia_openpgp::{Cert, parse::Parse};
debug_log!("Parsing key data: {} chars", key_data.len());
match Cert::from_bytes(key_data.as_bytes()) {
Ok(cert) => {
debug_log!("Successfully parsed as PGP certificate");
return extract_ed25519_key_from_cert(&cert, is_private);
}
Err(_) => {
debug_log!("Not a PGP certificate, trying base64 decode");
}
}
if let Ok(decoded) = STANDARD.decode(key_data.trim()) {
debug_log!("Decoded base64 to {} bytes", decoded.len());
match Cert::from_bytes(&decoded) {
Ok(cert) => {
debug_log!("Successfully parsed binary PGP certificate");
return extract_ed25519_key_from_cert(&cert, is_private);
}
Err(_) => {
debug_log!("Not binary PGP, checking for raw Ed25519 key");
if decoded.len() == 32 {
debug_log!("Detected raw 32-byte Ed25519 key");
let mut key_array = [0u8; 32];
key_array.copy_from_slice(&decoded);
if is_private {
let _signing_key = SigningKey::from_bytes(&key_array);
debug_log!("Raw Ed25519 private key validated");
} else {
VerifyingKey::from_bytes(&key_array)
.map_err(|e| CryptoError::InvalidKeyFormat(
format!("Invalid raw Ed25519 public key: {}", e)
))?;
debug_log!("Raw Ed25519 public key validated");
}
return Ok(key_array);
}
}
}
}
Err(CryptoError::PgpParsingFailed(
"Could not parse as PGP certificate or raw Ed25519 key".to_string()
))
}
fn extract_ed25519_key_from_cert(cert: &sequoia_openpgp::Cert, is_private: bool) -> Result<[u8; 32], CryptoError> {
use sequoia_openpgp::serialize::Marshal;
let primary_key = cert.primary_key().key();
let mut mpi_bytes = Vec::new();
primary_key.mpis().serialize(&mut mpi_bytes)
.map_err(|e| CryptoError::PgpParsingFailed(
format!("Failed to serialize key MPIs: {}", e)
))?;
debug_log!("Key MPI bytes: {} bytes", mpi_bytes.len());
if mpi_bytes.len() >= 34 {
if (mpi_bytes[0] == 0x00 && mpi_bytes[1] == 0x20) ||
(mpi_bytes[0] == 0x01 && mpi_bytes[1] == 0x00) ||
(mpi_bytes[0] == 0x40 && mpi_bytes[1] == 0x20) {
let mut key_array = [0u8; 32];
key_array.copy_from_slice(&mpi_bytes[2..34]);
if is_private {
let _signing_key = SigningKey::from_bytes(&key_array);
debug_log!("Ed25519 private key validated from PGP certificate");
} else {
VerifyingKey::from_bytes(&key_array)
.map_err(|e| CryptoError::InvalidKeyFormat(
format!("Invalid Ed25519 public key from PGP: {}", e)
))?;
debug_log!("Ed25519 public key validated from PGP certificate");
}
debug_log!("Successfully extracted Ed25519 key from PGP certificate");
return Ok(key_array);
}
}
if mpi_bytes.len() == 32 {
let mut key_array = [0u8; 32];
key_array.copy_from_slice(&mpi_bytes);
if is_private {
let _signing_key = SigningKey::from_bytes(&key_array);
debug_log!("Raw 32-byte private key validated from PGP");
} else {
VerifyingKey::from_bytes(&key_array)
.map_err(|e| CryptoError::InvalidKeyFormat(
format!("Invalid Ed25519 public key from PGP: {}", e)
))?;
debug_log!("Raw 32-byte public key validated from PGP");
}
debug_log!("Extracted raw 32-byte Ed25519 key from PGP");
return Ok(key_array);
}
Err(CryptoError::PgpParsingFailed(
format!("Unexpected key format in PGP certificate: {} bytes, expected Ed25519", mpi_bytes.len())
))
}
pub fn load_private_key(key_data: Option<&str>) -> Result<SigningKey, CryptoError> {
if let Some(data) = key_data {
debug_log!("Attempting to load private key from provided data");
match parse_key(data, true) {
Ok(key_array) => {
let signing_key = SigningKey::from_bytes(&key_array);
debug_log!("Successfully loaded private key from provided data");
return Ok(signing_key);
}
Err(CryptoError::PgpParsingFailed(_)) | Err(CryptoError::Base64DecodingFailed(_)) => {
}
Err(e) => {
debug_log!("Error parsing provided key data: {}, trying env var fallback", e);
}
}
match STANDARD.decode(data.trim()) {
Ok(key_bytes) if key_bytes.len() == SECRET_KEY_LENGTH => {
let mut key_array = [0u8; SECRET_KEY_LENGTH];
key_array.copy_from_slice(&key_bytes);
let signing_key = SigningKey::from_bytes(&key_array);
debug_log!("Successfully loaded private key from provided data (raw format)");
return Ok(signing_key);
}
Ok(key_bytes) => {
debug_log!("Invalid key length in provided data: {} bytes, trying env var fallback", key_bytes.len());
}
Err(e) => {
debug_log!("Base64 decode failed for provided data: {}, trying env var fallback", e);
}
}
}
debug_log!("Loading private key from IRONSHIELD_PRIVATE_KEY environment variable");
let key_str = env::var("IRONSHIELD_PRIVATE_KEY")
.map_err(|_| CryptoError::MissingEnvironmentVariable("IRONSHIELD_PRIVATE_KEY".to_string()))?;
match parse_key(&key_str, true) {
Ok(key_array) => {
let signing_key = SigningKey::from_bytes(&key_array);
debug_log!("Successfully loaded private key from environment variable");
return Ok(signing_key);
}
Err(CryptoError::PgpParsingFailed(_)) | Err(CryptoError::Base64DecodingFailed(_)) => {
}
Err(e) => return Err(e), }
let key_bytes = STANDARD.decode(key_str.trim())
.map_err(|e| CryptoError::Base64DecodingFailed(format!("Private key (legacy fallback): {}", e)))?;
if key_bytes.len() != SECRET_KEY_LENGTH {
return Err(CryptoError::InvalidKeyFormat(
format!("Private key must be {} bytes (raw Ed25519) or valid PGP format, got {} bytes",
SECRET_KEY_LENGTH, key_bytes.len())
));
}
let key_array: [u8; SECRET_KEY_LENGTH] = key_bytes.try_into()
.map_err(|_| CryptoError::InvalidKeyFormat("Failed to convert private key bytes".to_string()))?;
let signing_key = SigningKey::from_bytes(&key_array);
Ok(signing_key)
}
pub fn load_public_key(key_data: Option<&str>) -> Result<VerifyingKey, CryptoError> {
if let Some(data) = key_data {
debug_log!("Attempting to load public key from provided data");
match parse_key(data, false) {
Ok(key_array) => {
let verifying_key = VerifyingKey::from_bytes(&key_array)
.map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid public key from PGP: {}", e)))?;
debug_log!("Successfully loaded public key from provided data");
return Ok(verifying_key);
}
Err(CryptoError::PgpParsingFailed(_)) | Err(CryptoError::Base64DecodingFailed(_)) => {
}
Err(e) => {
debug_log!("Error parsing provided key data: {}, trying env var fallback", e);
}
}
match STANDARD.decode(data.trim()) {
Ok(key_bytes) if key_bytes.len() == PUBLIC_KEY_LENGTH => {
let mut key_array = [0u8; PUBLIC_KEY_LENGTH];
key_array.copy_from_slice(&key_bytes);
match VerifyingKey::from_bytes(&key_array) {
Ok(verifying_key) => {
debug_log!("Successfully loaded public key from provided data (raw format)");
return Ok(verifying_key);
}
Err(e) => {
debug_log!("Invalid Ed25519 public key in provided data: {}, trying env var fallback", e);
}
}
}
Ok(key_bytes) => {
debug_log!("Invalid key length in provided data: {} bytes, trying env var fallback", key_bytes.len());
}
Err(e) => {
debug_log!("Base64 decode failed for provided data: {}, trying env var fallback", e);
}
}
}
debug_log!("Loading public key from IRONSHIELD_PUBLIC_KEY environment variable");
let key_str = env::var("IRONSHIELD_PUBLIC_KEY")
.map_err(|_| CryptoError::MissingEnvironmentVariable("IRONSHIELD_PUBLIC_KEY".to_string()))?;
match parse_key(&key_str, false) {
Ok(key_array) => {
let verifying_key = VerifyingKey::from_bytes(&key_array)
.map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid public key: {}", e)))?;
debug_log!("Successfully loaded public key from environment variable");
return Ok(verifying_key);
}
Err(CryptoError::PgpParsingFailed(_)) | Err(CryptoError::Base64DecodingFailed(_)) => {
}
Err(e) => return Err(e), }
let key_bytes = STANDARD.decode(key_str.trim())
.map_err(|e| CryptoError::Base64DecodingFailed(format!("Public key (legacy fallback): {}", e)))?;
if key_bytes.len() != PUBLIC_KEY_LENGTH {
return Err(CryptoError::InvalidKeyFormat(
format!("Public key must be {} bytes (raw Ed25519) or valid PGP format, got {} bytes",
PUBLIC_KEY_LENGTH, key_bytes.len())
));
}
let key_array: [u8; PUBLIC_KEY_LENGTH] = key_bytes.try_into()
.map_err(|_| CryptoError::InvalidKeyFormat("Failed to convert public key bytes".to_string()))?;
let verifying_key = VerifyingKey::from_bytes(&key_array)
.map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid public key: {}", e)))?;
Ok(verifying_key)
}
pub fn create_signing_message(
random_nonce: &str,
created_time: i64,
expiration_time: i64,
website_id: &str,
challenge_param: &[u8; 32],
public_key: &[u8; 32]
) -> String {
format!(
"{}|{}|{}|{}|{}|{}",
random_nonce,
created_time,
expiration_time,
website_id,
hex::encode(challenge_param),
hex::encode(public_key)
)
}
pub fn generate_signature(signing_key: &SigningKey, message: &str) -> Result<[u8; 64], CryptoError> {
let signature: Signature = signing_key.sign(message.as_bytes());
Ok(signature.to_bytes())
}
pub fn sign_challenge(challenge: &IronShieldChallenge) -> Result<[u8; 64], CryptoError> {
let signing_key: SigningKey = load_private_key(None)?;
let message: String = create_signing_message(
&challenge.random_nonce,
challenge.created_time,
challenge.expiration_time,
&challenge.website_id,
&challenge.challenge_param,
&challenge.public_key
);
generate_signature(&signing_key, &message)
}
pub fn verify_challenge_signature(challenge: &IronShieldChallenge) -> Result<(), CryptoError> {
let verifying_key: VerifyingKey = load_public_key(None)?;
let message: String = create_signing_message(
&challenge.random_nonce,
challenge.created_time,
challenge.expiration_time,
&challenge.website_id,
&challenge.challenge_param,
&challenge.public_key
);
let signature: Signature = Signature::from_slice(&challenge.challenge_signature)
.map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid signature format: {}", e)))?;
verifying_key.verify(message.as_bytes(), &signature)
.map_err(|e| CryptoError::VerificationFailed(format!("Signature verification failed: {}", e)))?;
Ok(())
}
pub fn verify_challenge_signature_with_key(
challenge: &IronShieldChallenge,
public_key_bytes: &[u8; 32]
) -> Result<(), CryptoError> {
let verifying_key: VerifyingKey = VerifyingKey::from_bytes(public_key_bytes)
.map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid public key: {}", e)))?;
let message: String = create_signing_message(
&challenge.random_nonce,
challenge.created_time,
challenge.expiration_time,
&challenge.website_id,
&challenge.challenge_param,
&challenge.public_key
);
let signature: Signature = Signature::from_slice(&challenge.challenge_signature)
.map_err(|e| CryptoError::InvalidKeyFormat(format!("Invalid signature format: {}", e)))?;
verifying_key.verify(message.as_bytes(), &signature)
.map_err(|e| CryptoError::VerificationFailed(format!("Signature verification failed: {}", e)))?;
Ok(())
}
pub fn generate_test_keypair() -> (String, String) {
let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
let verifying_key: VerifyingKey = signing_key.verifying_key();
let private_key_b64: String = STANDARD.encode(signing_key.to_bytes());
let public_key_b64: String = STANDARD.encode(verifying_key.to_bytes());
(private_key_b64, public_key_b64)
}
pub fn validate_challenge(challenge: &IronShieldChallenge) -> Result<(), CryptoError> {
verify_challenge_signature(challenge)?;
if challenge.is_expired() {
return Err(CryptoError::VerificationFailed("Challenge has expired".to_string()));
}
if challenge.website_id.is_empty() {
return Err(CryptoError::VerificationFailed("Empty website_id".to_string()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_raw_ed25519_private_key() {
let (private_b64, _) = generate_test_keypair();
let result = parse_key(&private_b64, true);
assert!(result.is_ok(), "Failed to parse raw Ed25519 private key");
let key_bytes = result.unwrap();
assert_eq!(key_bytes.len(), 32, "Key should be 32 bytes");
let signing_key = SigningKey::from_bytes(&key_bytes);
let _ = signing_key.verifying_key();
println!("Successfully parsed raw Ed25519 private key");
}
#[test]
fn test_parse_raw_ed25519_public_key() {
let (_, public_b64) = generate_test_keypair();
let result = parse_key(&public_b64, false);
assert!(result.is_ok(), "Failed to parse raw Ed25519 public key");
let key_bytes = result.unwrap();
assert_eq!(key_bytes.len(), 32, "Key should be 32 bytes");
let verifying_key = VerifyingKey::from_bytes(&key_bytes);
assert!(verifying_key.is_ok(), "Should be a valid Ed25519 public key");
println!("Successfully parsed raw Ed25519 public key");
}
#[test]
fn test_parse_key_with_whitespace() {
let (private_b64, _) = generate_test_keypair();
let with_spaces = format!(" {} ", private_b64);
let with_newlines = format!("{}\n\n", private_b64);
let with_tabs = format!("\t{}\t", private_b64);
let with_mixed = format!("\n {}\t\n ", private_b64);
for key_str in [with_spaces, with_newlines, with_tabs, with_mixed] {
let result = parse_key(&key_str, true);
assert!(
result.is_ok(),
"Should handle whitespace, got error: {:?}",
result.err()
);
}
println!("Successfully handled various whitespace formats");
}
#[test]
fn test_parse_invalid_base64() {
let invalid_base64 = "this is not valid base64!!!@#$%";
let result = parse_key(invalid_base64, true);
assert!(result.is_err(), "Should fail on invalid base64");
match result.err().unwrap() {
CryptoError::Base64DecodingFailed(_) | CryptoError::PgpParsingFailed(_) => {
println!("Correctly rejected invalid input");
}
other => panic!("Expected Base64DecodingFailed or PgpParsingFailed, got: {:?}", other),
}
}
#[test]
fn test_parse_wrong_size_key() {
let wrong_size = STANDARD.encode(&[0u8; 16]);
let result = parse_key(&wrong_size, true);
assert!(result.is_err(), "Should fail on wrong-sized key");
println!("Correctly rejected wrong-sized key");
}
#[test]
fn test_private_vs_public_key_validation() {
let (private_b64, public_b64) = generate_test_keypair();
let result = parse_key(&private_b64, true);
assert!(result.is_ok(), "Private key should parse as private");
let result = parse_key(&public_b64, false);
assert!(result.is_ok(), "Public key should parse as public");
println!("Correctly validated private vs public keys");
}
#[test]
fn test_parse_key_end_to_end() {
let (private_b64, public_b64) = generate_test_keypair();
let private_bytes = parse_key(&private_b64, true)
.expect("Failed to parse private key");
let public_bytes = parse_key(&public_b64, false)
.expect("Failed to parse public key");
let signing_key = SigningKey::from_bytes(&private_bytes);
let verifying_key = VerifyingKey::from_bytes(&public_bytes)
.expect("Invalid public key");
let message = b"Test message for IronShield";
let signature = signing_key.sign(message);
verifying_key
.verify(message, &signature)
.expect("Signature verification failed");
let derived_public = signing_key.verifying_key();
assert_eq!(
derived_public.to_bytes(),
public_bytes,
"Derived public key should match parsed public key"
);
println!("Successfully completed end-to-end test");
}
#[test]
fn test_parse_empty_string() {
let result = parse_key("", true);
assert!(result.is_err(), "Should fail on empty string");
println!("Correctly rejected empty string");
}
}