Skip to main content

cast/cmd/tempo/zone/
encryption.rs

1//! Client-side deposit ECIES, mirrored from tempoxyz/zones at a1c15e9f.
2//!
3//! Source: <https://github.com/tempoxyz/zones/tree/a1c15e9f002a5efd150f56c5076fb35519288396/crates/precompiles/src/ecies.rs>
4
5use super::abi::DepositPayload;
6use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
7use alloy_primitives::{Address, B256, U256};
8use eyre::{Result, eyre};
9use hkdf::Hkdf;
10use k256::{
11    AffinePoint, ProjectivePoint, PublicKey, SecretKey, elliptic_curve::sec1::ToEncodedPoint,
12};
13use sha2::Sha256;
14
15/// Encrypt `[recipient(20) | memo(32) | padding(12)]` for the sequencer.
16pub(super) fn encrypt_deposit(
17    x: B256,
18    parity: u8,
19    to: Address,
20    memo: B256,
21    sender: Address,
22    portal: Address,
23    key_index: U256,
24) -> Result<DepositPayload> {
25    let mut compressed = [0u8; 33];
26    compressed[0] = match parity {
27        0 | 1 => parity + 2,
28        2 | 3 => parity,
29        _ => return Err(eyre!("invalid sequencer encryption key parity")),
30    };
31    compressed[1..].copy_from_slice(x.as_slice());
32    let public = PublicKey::from_sec1_bytes(&compressed)
33        .map_err(|_| eyre!("invalid sequencer encryption public key"))?;
34    let ephemeral = SecretKey::random(&mut rand_08::thread_rng());
35    let encoded = ephemeral.public_key().to_encoded_point(true);
36    let ephemeral_x = B256::from_slice(encoded.x().unwrap());
37    let shared = AffinePoint::from(
38        ProjectivePoint::from(*public.as_affine()) * *ephemeral.to_nonzero_scalar(),
39    );
40    let shared = shared.to_encoded_point(true);
41    let mut info = [0u8; 104];
42    info[..20].copy_from_slice(portal.as_slice());
43    info[20..52].copy_from_slice(&key_index.to_be_bytes::<32>());
44    info[52..84].copy_from_slice(ephemeral_x.as_slice());
45    info[84..].copy_from_slice(sender.as_slice());
46    let mut key = [0u8; 32];
47    Hkdf::<Sha256>::new(Some(b"ecies-aes-key"), shared.x().unwrap())
48        .expand(&info, &mut key)
49        .map_err(|_| eyre!("deposit key derivation failed"))?;
50    let mut plaintext = [0u8; 64];
51    plaintext[..20].copy_from_slice(to.as_slice());
52    plaintext[20..52].copy_from_slice(memo.as_slice());
53    let nonce: [u8; 12] = rand_08::random();
54    let mut encrypted = Aes256Gcm::new((&key).into())
55        .encrypt(Nonce::from_slice(&nonce), plaintext.as_ref())
56        .map_err(|_| eyre!("deposit encryption failed"))?;
57    let tag = encrypted.split_off(64);
58    Ok(DepositPayload {
59        ephemeralPubkeyX: ephemeral_x,
60        ephemeralPubkeyYParity: encoded.as_bytes()[0],
61        ciphertext: encrypted.into(),
62        nonce: nonce.into(),
63        tag: tag.as_slice().try_into()?,
64    })
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn deposit_roundtrip_and_context_binding() {
73        let sequencer = SecretKey::from_slice(&[7; 32]).unwrap();
74        let public = sequencer.public_key().to_encoded_point(true);
75        let x = B256::from_slice(public.x().unwrap());
76        let to = Address::repeat_byte(0xbb);
77        let memo = B256::repeat_byte(0xcc);
78        let sender = Address::repeat_byte(0xdd);
79        let portal = Address::repeat_byte(0xaa);
80        let index = U256::from(42);
81        let payload =
82            encrypt_deposit(x, public.as_bytes()[0], to, memo, sender, portal, index).unwrap();
83        assert_eq!(payload.ciphertext.len(), 64);
84        let mut ephemeral = [0u8; 33];
85        ephemeral[0] = payload.ephemeralPubkeyYParity;
86        ephemeral[1..].copy_from_slice(payload.ephemeralPubkeyX.as_slice());
87        let ephemeral = PublicKey::from_sec1_bytes(&ephemeral).unwrap();
88        let shared = AffinePoint::from(
89            ProjectivePoint::from(*ephemeral.as_affine()) * *sequencer.to_nonzero_scalar(),
90        );
91        let shared = shared.to_encoded_point(true);
92        // Zones' decryption context is packed, not ABI-padded.
93        let mut info = [
94            portal.as_slice(),
95            &index.to_be_bytes::<32>(),
96            payload.ephemeralPubkeyX.as_slice(),
97            sender.as_slice(),
98        ]
99        .concat();
100        let mut ciphertext = payload.ciphertext.to_vec();
101        ciphertext.extend_from_slice(payload.tag.as_slice());
102        let mut key = [0u8; 32];
103        Hkdf::<Sha256>::new(Some(b"ecies-aes-key"), shared.x().unwrap())
104            .expand(&info, &mut key)
105            .unwrap();
106        let cipher = Aes256Gcm::new((&key).into());
107        let plaintext = cipher
108            .decrypt(Nonce::from_slice(payload.nonce.as_slice()), ciphertext.as_slice())
109            .unwrap();
110        assert_eq!(&plaintext[..20], to.as_slice());
111        assert_eq!(&plaintext[20..52], memo.as_slice());
112        assert_eq!(&plaintext[52..], &[0; 12]);
113        // Changing any bound field must fail authentication.
114        for offset in [0, 20, 52, 84] {
115            info[offset] ^= 1;
116            Hkdf::<Sha256>::new(Some(b"ecies-aes-key"), shared.x().unwrap())
117                .expand(&info, &mut key)
118                .unwrap();
119            assert!(
120                Aes256Gcm::new((&key).into())
121                    .decrypt(Nonce::from_slice(payload.nonce.as_slice()), ciphertext.as_slice())
122                    .is_err()
123            );
124            info[offset] ^= 1;
125        }
126        let second =
127            encrypt_deposit(x, public.as_bytes()[0], to, memo, sender, portal, index).unwrap();
128        assert_ne!(payload.ephemeralPubkeyX, second.ephemeralPubkeyX);
129        assert_ne!(payload.nonce, second.nonce);
130    }
131
132    #[test]
133    fn rejects_invalid_encryption_keys() {
134        for parity in [0, 1, 2, 3, 4, 255] {
135            assert!(
136                encrypt_deposit(
137                    B256::repeat_byte(0xff),
138                    parity,
139                    Address::ZERO,
140                    B256::ZERO,
141                    Address::ZERO,
142                    Address::ZERO,
143                    U256::ZERO
144                )
145                .is_err()
146            );
147        }
148    }
149}