encryption/asymmetric/
x25519.rs1use rand_core::OsRng;
12use x25519_dalek::{PublicKey, StaticSecret};
13
14use crate::asymmetric::KeyPairHex;
15use crate::contract::{Identified, KeyAgreement};
16use crate::error::{Error, Result};
17use crate::internal::{hex_decode, hex_encode};
18
19const LABEL: &str = "X25519";
20
21#[derive(Debug, Default, Clone, Copy)]
23pub struct X25519Agreement;
24
25impl X25519Agreement {
26 pub const IDENTIFIER: &'static str = "x25519";
27
28 pub fn new() -> Self {
29 Self
30 }
31
32 fn agree_hex(private_key_hex: &str, peer_public_key_hex: &str) -> Result<String> {
34 let private_bytes =
35 parse_32_bytes(private_key_hex).map_err(|_| Error::InvalidKey { label: LABEL })?;
36 let peer_bytes =
37 parse_32_bytes(peer_public_key_hex).map_err(|_| Error::InvalidKey { label: LABEL })?;
38
39 let shared_secret =
40 StaticSecret::from(private_bytes).diffie_hellman(&PublicKey::from(peer_bytes));
41 if !shared_secret.was_contributory() {
42 return Err(Error::InvalidKey { label: LABEL });
43 }
44 Ok(hex_encode(shared_secret.as_bytes()))
45 }
46
47 pub fn generate_key_pair_hex() -> Result<KeyPairHex> {
49 let secret = StaticSecret::random_from_rng(OsRng);
50 Ok(KeyPairHex {
51 private_key_hex: hex_encode(secret.as_bytes()),
52 public_key_hex: hex_encode(PublicKey::from(&secret).as_bytes()),
53 })
54 }
55}
56
57fn parse_32_bytes(hex: &str) -> Result<[u8; 32]> {
59 hex_decode(hex)?.try_into().map_err(|_| Error::InvalidHex)
60}
61
62impl Identified for X25519Agreement {
63 fn identifier(&self) -> &str {
64 Self::IDENTIFIER
65 }
66}
67
68impl KeyAgreement for X25519Agreement {
69 fn agree(&self, private_key_hex: &str, peer_public_key_hex: &str) -> Result<String> {
70 Self::agree_hex(private_key_hex, peer_public_key_hex)
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
81 fn rfc7748_5_2_shared_secret_matches_vector() {
82 const ALICE_PRIVATE: &str =
83 "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a";
84 const ALICE_PUBLIC: &str =
85 "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a";
86 const BOB_PRIVATE: &str =
87 "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb";
88 const BOB_PUBLIC: &str = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f";
89 const SHARED: &str = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742";
90
91 let agreement = X25519Agreement::new();
92 assert_eq!(agreement.agree(ALICE_PRIVATE, BOB_PUBLIC).unwrap(), SHARED);
93 assert_eq!(agreement.agree(BOB_PRIVATE, ALICE_PUBLIC).unwrap(), SHARED);
94 assert_eq!(
96 agreement.agree(ALICE_PRIVATE, BOB_PUBLIC).unwrap(),
97 agreement.agree(BOB_PRIVATE, ALICE_PUBLIC).unwrap(),
98 );
99 }
100
101 #[test]
103 fn rfc7748_6_1_scalar_multiplication_matches_vector() {
104 const SCALAR: &str = "a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4";
105 const U_COORDINATE: &str =
106 "e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c";
107 const OUTPUT: &str = "c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552";
108
109 assert_eq!(
110 X25519Agreement::new().agree(SCALAR, U_COORDINATE).unwrap(),
111 OUTPUT
112 );
113 }
114
115 #[test]
116 fn keypair_roundtrip_and_hex_shapes() {
117 let alice = X25519Agreement::generate_key_pair_hex().unwrap();
118 let bob = X25519Agreement::generate_key_pair_hex().unwrap();
119 assert_eq!(alice.private_key_hex.len(), 64);
120 assert_eq!(alice.public_key_hex.len(), 64);
121
122 let agreement = X25519Agreement::new();
123 let left = agreement
124 .agree(&alice.private_key_hex, &bob.public_key_hex)
125 .unwrap();
126 let right = agreement
127 .agree(&bob.private_key_hex, &alice.public_key_hex)
128 .unwrap();
129 assert_eq!(left, right);
130 assert_eq!(left.len(), 64);
131
132 let mallory = X25519Agreement::generate_key_pair_hex().unwrap();
134 assert_ne!(
135 agreement
136 .agree(&mallory.private_key_hex, &bob.public_key_hex)
137 .unwrap(),
138 left
139 );
140 }
141
142 #[test]
144 fn low_order_peer_public_key_is_rejected() {
145 let alice = X25519Agreement::generate_key_pair_hex().unwrap();
146 assert_eq!(
147 X25519Agreement::new()
148 .agree(&alice.private_key_hex, &"00".repeat(32))
149 .unwrap_err(),
150 Error::InvalidKey { label: LABEL }
151 );
152 }
153
154 #[test]
155 fn malformed_keys_are_rejected() {
156 let alice = X25519Agreement::generate_key_pair_hex().unwrap();
157 let agreement = X25519Agreement::new();
158
159 assert_eq!(
160 agreement.agree("zz", &alice.public_key_hex).unwrap_err(),
161 Error::InvalidKey { label: LABEL }
162 );
163 assert_eq!(
165 agreement
166 .agree(&alice.private_key_hex[..62], &alice.public_key_hex)
167 .unwrap_err(),
168 Error::InvalidKey { label: LABEL }
169 );
170 }
171
172 #[test]
173 fn identifier_is_stable() {
174 assert_eq!(X25519Agreement::new().identifier(), "x25519");
175 }
176}