dlp_api/encryption/
mod.rs1use 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
28pub 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
42pub 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
58pub 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
66pub 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
81pub 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}