Skip to main content

dlp_api/encryption/
mod.rs

1use libsodium_rs::{crypto_box, crypto_sign, ensure_init};
2
3pub const KEY_LEN: usize = 32;
4
5#[derive(Debug, thiserror::Error)]
6pub enum EncryptionError {
7    #[error("libsodium init failed")]
8    SodiumInitFailed,
9    #[error("invalid ed25519 public key for x25519 conversion")]
10    InvalidEd25519PublicKey,
11    #[error("invalid ed25519 secret key for x25519 conversion")]
12    InvalidEd25519SecretKey,
13    #[error("invalid x25519 public key")]
14    InvalidX25519PublicKey,
15    #[error("invalid x25519 secret key")]
16    InvalidX25519SecretKey,
17    #[error("failed to encrypt payload")]
18    SealFailed,
19    #[error("failed to decrypt payload")]
20    OpenFailed,
21}
22
23fn init_sodium() -> Result<(), EncryptionError> {
24    ensure_init().map_err(|_| EncryptionError::SodiumInitFailed)?;
25    Ok(())
26}
27
28/// Convert an Ed25519 public key into an X25519 public key.
29pub fn ed25519_pubkey_to_x25519(
30    ed25519_pubkey: &[u8; KEY_LEN],
31) -> Result<[u8; KEY_LEN], EncryptionError> {
32    init_sodium()?;
33    let ed_pk = crypto_sign::PublicKey::from_bytes(ed25519_pubkey)
34        .map_err(|_| EncryptionError::InvalidEd25519PublicKey)?;
35    let x_pk = crypto_sign::ed25519_pk_to_curve25519(&ed_pk)
36        .map_err(|_| EncryptionError::InvalidEd25519PublicKey)?;
37    let mut out = [0u8; KEY_LEN];
38    out.copy_from_slice(&x_pk);
39    Ok(out)
40}
41
42/// Convert an Ed25519 secret key into an X25519 secret key.
43pub fn ed25519_secret_to_x25519(
44    ed25519_secret_key: &[u8],
45) -> Result<[u8; KEY_LEN], EncryptionError> {
46    assert_eq!(ed25519_secret_key.len(), 64);
47
48    init_sodium()?;
49    let ed_sk = crypto_sign::SecretKey::from_bytes(ed25519_secret_key)
50        .map_err(|_| EncryptionError::InvalidEd25519SecretKey)?;
51    let x_sk = crypto_sign::ed25519_sk_to_curve25519(&ed_sk)
52        .map_err(|_| EncryptionError::InvalidEd25519SecretKey)?;
53    let mut out = [0u8; KEY_LEN];
54    out.copy_from_slice(&x_sk);
55    Ok(out)
56}
57
58/// Convenience helper for SDK usage: derive X25519 secret key bytes from a Solana Keypair.
59pub fn keypair_to_x25519_secret(
60    keypair: &solana_sdk::signature::Keypair,
61) -> Result<[u8; KEY_LEN], EncryptionError> {
62    let keypair_bytes = keypair.to_bytes();
63    ed25519_secret_to_x25519(&keypair_bytes)
64}
65
66/// High-level API: encrypt for validator using sealed boxes.
67pub fn encrypt_ed25519_recipient(
68    plaintext: &[u8],
69    recipient_ed25519_pubkey: &[u8; KEY_LEN],
70) -> Result<Vec<u8>, EncryptionError> {
71    init_sodium()?;
72    let ed_pk = crypto_sign::PublicKey::from_bytes(recipient_ed25519_pubkey)
73        .map_err(|_| EncryptionError::InvalidEd25519PublicKey)?;
74    let x_pk = crypto_sign::ed25519_pk_to_curve25519(&ed_pk)
75        .map_err(|_| EncryptionError::InvalidEd25519PublicKey)?;
76    let x_pk = crypto_box::PublicKey::from_bytes_exact(x_pk);
77    crypto_box::seal_box(plaintext, &x_pk)
78        .map_err(|_| EncryptionError::SealFailed)
79}
80
81/// Decrypt sealed box bytes back to plaintext bytes.
82pub fn decrypt(
83    encrypted_payload: &[u8],
84    recipient_x25519_pubkey: &[u8; KEY_LEN],
85    recipient_x25519_secret: &[u8; KEY_LEN],
86) -> Result<Vec<u8>, EncryptionError> {
87    init_sodium()?;
88    let pk = crypto_box::PublicKey::from_bytes_exact(*recipient_x25519_pubkey);
89    let sk = crypto_box::SecretKey::from_bytes_exact(*recipient_x25519_secret);
90    crypto_box::open_sealed_box(encrypted_payload, &pk, &sk)
91        .map_err(|_| EncryptionError::OpenFailed)
92}
93
94#[cfg(test)]
95mod tests {
96    use solana_sdk::signer::Signer;
97
98    use super::*;
99
100    #[test]
101    fn test_encrypt_decrypt_roundtrip() {
102        let validator = solana_sdk::signature::Keypair::new();
103        let validator_x25519_secret =
104            keypair_to_x25519_secret(&validator).unwrap();
105        let validator_x25519_pubkey =
106            ed25519_pubkey_to_x25519(validator.pubkey().as_array()).unwrap();
107        let plaintext = b"hello compact actions";
108
109        let encrypted =
110            encrypt_ed25519_recipient(plaintext, validator.pubkey().as_array())
111                .unwrap();
112        let decrypted = decrypt(
113            &encrypted,
114            &validator_x25519_pubkey,
115            &validator_x25519_secret,
116        )
117        .unwrap();
118        assert_eq!(decrypted, plaintext);
119    }
120
121    #[test]
122    fn test_random_ephemeral_changes_ciphertext() {
123        let validator = solana_sdk::signature::Keypair::new();
124        let plaintext = b"same bytes";
125
126        let c1 =
127            encrypt_ed25519_recipient(plaintext, validator.pubkey().as_array())
128                .unwrap();
129        let c2 =
130            encrypt_ed25519_recipient(plaintext, validator.pubkey().as_array())
131                .unwrap();
132        assert_ne!(c1, c2);
133    }
134}