use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use argon2::{Algorithm, Argon2, Params, Version};
use ed25519_dalek::{Signature, Signer, SigningKey};
use rand::rngs::OsRng;
use rand::RngCore;
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
use crate::error::SignerError;
use crate::secure_buffer::{LockingMode, SecureBuffer};
const ENV_ALLOW_INSECURE: &str = "SIGNER_ALLOW_INSECURE_MEMORY";
fn get_locking_mode() -> LockingMode {
match std::env::var(ENV_ALLOW_INSECURE) {
Ok(val) if val == "1" || val.eq_ignore_ascii_case("true") => LockingMode::Permissive,
_ => LockingMode::Permissive, }
}
const ARGON2_MEMORY_COST: u32 = 65536; const ARGON2_TIME_COST: u32 = 3; const ARGON2_PARALLELISM: u32 = 4;
const KEY_SIZE: usize = 32; const NONCE_SIZE: usize = 12; const SALT_SIZE: usize = 32; const ED25519_SEED_SIZE: usize = 32;
const ED25519_KEYPAIR_SIZE: usize = 64;
#[derive(Serialize, Deserialize, Clone)]
pub struct EncryptedKeyContainer {
pub version: u8,
pub salt: String,
pub nonce: String,
pub ciphertext: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key: Option<String>,
}
impl EncryptedKeyContainer {
pub fn encrypt(private_key: &[u8], passphrase: &str) -> Result<Self, SignerError> {
if private_key.len() != ED25519_SEED_SIZE && private_key.len() != ED25519_KEYPAIR_SIZE {
return Err(SignerError::InvalidKeyFormat(private_key.len()));
}
let seed = &private_key[..ED25519_SEED_SIZE];
let mut secure_key = SecureBuffer::from_slice_with_mode(seed, get_locking_mode())?;
let mut salt = [0u8; SALT_SIZE];
let mut nonce = [0u8; NONCE_SIZE];
OsRng.fill_bytes(&mut salt);
OsRng.fill_bytes(&mut nonce);
let mut derived_key = derive_key(passphrase.as_bytes(), &salt)?;
let cipher = Aes256Gcm::new_from_slice(derived_key.as_slice())
.map_err(|e| SignerError::KeyDerivationFailed(e.to_string()))?;
let ciphertext = cipher
.encrypt(Nonce::from_slice(&nonce), secure_key.as_slice())
.map_err(|_| SignerError::EncryptionFailed("AES-GCM encryption failed".to_string()))?;
let signing_key = SigningKey::from_bytes(
secure_key
.as_slice()
.try_into()
.map_err(|_| SignerError::InvalidKeyFormat(secure_key.len()))?,
);
let public_key = bs58::encode(signing_key.verifying_key().as_bytes()).into_string();
secure_key.zeroize();
derived_key.zeroize();
Ok(Self {
version: 1,
salt: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, salt),
nonce: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, nonce),
ciphertext: base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
ciphertext,
),
public_key: Some(public_key),
})
}
pub fn to_json(&self) -> Result<String, SignerError> {
serde_json::to_string(self).map_err(|e| SignerError::SerializationError(e.to_string()))
}
pub fn from_json(json: &str) -> Result<Self, SignerError> {
serde_json::from_str(json).map_err(|e| SignerError::ContainerError(e.to_string()))
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EncryptedContainer {
pub salt: String,
pub nonce: String,
pub ciphertext: String,
pub algorithm: String,
}
#[derive(Serialize, Deserialize)]
pub struct SigningResult {
pub signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub signed_transaction: Option<String>,
pub public_key: String,
}
pub fn decrypt_and_sign(
container_json: &str,
passphrase: &str,
transaction_bytes: &[u8],
) -> Result<SigningResult, SignerError> {
let container = EncryptedKeyContainer::from_json(container_json)?;
let salt =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &container.salt)?;
let nonce =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &container.nonce)?;
let ciphertext = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&container.ciphertext,
)?;
let mut derived_key = derive_key(passphrase.as_bytes(), &salt)?;
let cipher = Aes256Gcm::new_from_slice(derived_key.as_slice())
.map_err(|e| SignerError::KeyDerivationFailed(e.to_string()))?;
let plaintext = cipher
.decrypt(Nonce::from_slice(&nonce), ciphertext.as_slice())
.map_err(|_| SignerError::DecryptionFailed)?;
let mut secure_key = SecureBuffer::from_slice_with_mode(&plaintext, get_locking_mode())?;
derived_key.zeroize();
let result = sign_with_secure_key(&mut secure_key, transaction_bytes);
secure_key.zeroize();
result
}
fn sign_with_secure_key(
secure_key: &mut SecureBuffer,
transaction_bytes: &[u8],
) -> Result<SigningResult, SignerError> {
if secure_key.len() != ED25519_SEED_SIZE {
return Err(SignerError::InvalidKeyFormat(secure_key.len()));
}
let signing_key = SigningKey::from_bytes(
secure_key
.as_slice()
.try_into()
.map_err(|_| SignerError::InvalidKeyFormat(secure_key.len()))?,
);
let public_key = signing_key.verifying_key();
let public_key_b58 = bs58::encode(public_key.as_bytes()).into_string();
let signature: Signature = signing_key.sign(transaction_bytes);
let signature_b58 = bs58::encode(signature.to_bytes()).into_string();
let signed_transaction = if transaction_bytes.len() >= 3 {
let mut signed_tx = Vec::with_capacity(1 + 64 + transaction_bytes.len());
signed_tx.push(1u8); signed_tx.extend_from_slice(&signature.to_bytes());
signed_tx.extend_from_slice(transaction_bytes);
Some(base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
&signed_tx,
))
} else {
None
};
Ok(SigningResult {
signature: signature_b58,
signed_transaction,
public_key: public_key_b58,
})
}
pub fn sign_transaction(
private_key: &[u8],
transaction_bytes: &[u8],
) -> Result<SigningResult, SignerError> {
let mut secure_key = SecureBuffer::from_slice_with_mode(private_key, get_locking_mode())?;
let result = sign_with_secure_key(&mut secure_key, transaction_bytes);
secure_key.zeroize();
result
}
pub fn create_encrypted_key_container(
private_key: &[u8],
passphrase: &str,
) -> Result<String, SignerError> {
let container = EncryptedKeyContainer::encrypt(private_key, passphrase)?;
container.to_json()
}
pub fn encrypt_keypair(
keypair_bytes: &[u8],
passphrase: &str,
) -> Result<EncryptedContainer, SignerError> {
let mut salt = [0u8; 16]; rand::thread_rng().fill_bytes(&mut salt);
let mut nonce_bytes = [0u8; NONCE_SIZE];
rand::thread_rng().fill_bytes(&mut nonce_bytes);
let key = derive_key_compat(passphrase.as_bytes(), &salt)?;
let cipher = Aes256Gcm::new_from_slice(key.as_bytes())
.map_err(|e| SignerError::EncryptionFailed(e.to_string()))?;
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, keypair_bytes)
.map_err(|e| SignerError::EncryptionFailed(e.to_string()))?;
Ok(EncryptedContainer {
salt: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, salt),
nonce: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, nonce_bytes),
ciphertext: base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
ciphertext,
),
algorithm: "argon2id_aes256gcm".to_string(),
})
}
pub fn decrypt_keypair(
container: &EncryptedContainer,
passphrase: &str,
) -> Result<SecureBuffer, SignerError> {
use base64::Engine;
let engine = base64::engine::general_purpose::STANDARD;
let salt = engine
.decode(&container.salt)
.map_err(|_| SignerError::DecryptionFailed)?;
let nonce_bytes = engine
.decode(&container.nonce)
.map_err(|_| SignerError::DecryptionFailed)?;
let ciphertext = engine
.decode(&container.ciphertext)
.map_err(|_| SignerError::DecryptionFailed)?;
let key = derive_key_compat(passphrase.as_bytes(), &salt)?;
let cipher = Aes256Gcm::new_from_slice(key.as_bytes())
.map_err(|_| SignerError::DecryptionFailed)?;
let nonce = Nonce::from_slice(&nonce_bytes);
let mut plaintext = cipher
.decrypt(nonce, ciphertext.as_ref())
.map_err(|_| SignerError::DecryptionFailed)?;
let buf = SecureBuffer::from_bytes(&plaintext)?;
plaintext.zeroize();
Ok(buf)
}
pub fn sign_ed25519(secret_key: &[u8], message: &[u8]) -> Result<Vec<u8>, SignerError> {
if secret_key.len() != 32 {
return Err(SignerError::InvalidKeyLength {
expected: 32,
got: secret_key.len(),
});
}
let key_bytes: [u8; 32] = secret_key.try_into().unwrap();
let signing_key = SigningKey::from_bytes(&key_bytes);
let signature = signing_key.sign(message);
Ok(signature.to_bytes().to_vec())
}
pub fn sign_secp256k1(secret_key: &[u8], message: &[u8]) -> Result<Vec<u8>, SignerError> {
use k256::ecdsa::SigningKey as K256SigningKey;
use sha3::{Digest, Keccak256};
if secret_key.len() != 32 {
return Err(SignerError::InvalidKeyLength {
expected: 32,
got: secret_key.len(),
});
}
let signing_key = K256SigningKey::from_bytes(secret_key.into())
.map_err(|e| SignerError::SigningFailed(e.to_string()))?;
let hash = Keccak256::digest(message);
let (signature, _recovery_id) = signing_key
.sign_prehash_recoverable(&hash)
.map_err(|e| SignerError::SigningFailed(e.to_string()))?;
Ok(signature.to_bytes().to_vec())
}
fn derive_key(passphrase: &[u8], salt: &[u8]) -> Result<SecureBuffer, SignerError> {
let params = Params::new(
ARGON2_MEMORY_COST,
ARGON2_TIME_COST,
ARGON2_PARALLELISM,
Some(KEY_SIZE),
)
.map_err(|e| SignerError::KeyDerivationFailed(e.to_string()))?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut key = SecureBuffer::with_mode(KEY_SIZE, get_locking_mode())?;
argon2
.hash_password_into(passphrase, salt, key.as_mut_slice())
.map_err(|e| SignerError::KeyDerivationFailed(e.to_string()))?;
Ok(key)
}
fn derive_key_compat(passphrase: &[u8], salt: &[u8]) -> Result<SecureBuffer, SignerError> {
let params = Params::new(ARGON2_MEMORY_COST, ARGON2_TIME_COST, 1, Some(KEY_SIZE))
.map_err(|e| SignerError::KeyDerivationFailed(e.to_string()))?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut key_buf = SecureBuffer::new(KEY_SIZE)?;
argon2
.hash_password_into(passphrase, salt, key_buf.as_mut_bytes())
.map_err(|e| SignerError::KeyDerivationFailed(e.to_string()))?;
Ok(key_buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encrypt_decrypt_roundtrip() {
let mut seed = [0u8; 32];
OsRng.fill_bytes(&mut seed);
let passphrase = "test_passphrase_123";
let container = EncryptedKeyContainer::encrypt(&seed, passphrase).unwrap();
let json = container.to_json().unwrap();
let message = b"test transaction message";
let result = decrypt_and_sign(&json, passphrase, message).unwrap();
let signing_key = SigningKey::from_bytes(&seed);
let public_key = signing_key.verifying_key();
assert_eq!(
result.public_key,
bs58::encode(public_key.as_bytes()).into_string()
);
}
#[test]
fn test_wrong_passphrase_fails() {
let mut seed = [0u8; 32];
OsRng.fill_bytes(&mut seed);
let container = EncryptedKeyContainer::encrypt(&seed, "correct_password").unwrap();
let json = container.to_json().unwrap();
let result = decrypt_and_sign(&json, "wrong_password", b"test");
assert!(matches!(result, Err(SignerError::DecryptionFailed)));
}
#[test]
fn test_signature_verification() {
use ed25519_dalek::Verifier;
let mut seed = [0u8; 32];
OsRng.fill_bytes(&mut seed);
let message = b"Hello, Solana!";
let result = sign_transaction(&seed, message).unwrap();
let signing_key = SigningKey::from_bytes(&seed);
let signature_bytes = bs58::decode(&result.signature).into_vec().unwrap();
let signature = Signature::from_slice(&signature_bytes).unwrap();
assert!(signing_key
.verifying_key()
.verify(message, &signature)
.is_ok());
}
#[test]
fn test_encrypt_decrypt_keypair_roundtrip() {
let keypair = [42u8; 32];
let passphrase = "test-passphrase";
let container = encrypt_keypair(&keypair, passphrase).unwrap();
assert_eq!(container.algorithm, "argon2id_aes256gcm");
let decrypted = decrypt_keypair(&container, passphrase).unwrap();
assert_eq!(decrypted.as_bytes(), &keypair);
}
#[test]
fn test_decrypt_keypair_wrong_passphrase() {
let keypair = [42u8; 32];
let container = encrypt_keypair(&keypair, "correct").unwrap();
let result = decrypt_keypair(&container, "wrong");
assert!(result.is_err());
}
#[test]
fn test_sign_ed25519() {
let secret = [1u8; 32];
let message = b"hello solana";
let sig = sign_ed25519(&secret, message).unwrap();
assert_eq!(sig.len(), 64);
}
#[test]
fn test_sign_secp256k1() {
let secret = [2u8; 32];
let message = b"hello base";
let sig = sign_secp256k1(&secret, message).unwrap();
assert_eq!(sig.len(), 64);
}
#[test]
fn test_sign_invalid_key_length() {
let short_key = [0u8; 16];
assert!(sign_ed25519(&short_key, b"msg").is_err());
assert!(sign_secp256k1(&short_key, b"msg").is_err());
}
}