1use aes_gcm::{
11 aead::{Aead, AeadCore, KeyInit, OsRng},
12 Aes256Gcm, Nonce,
13};
14use base64::engine::general_purpose::URL_SAFE_NO_PAD;
15use base64::Engine;
16use hkdf::Hkdf;
17use sha2::Sha256;
18use x25519_dalek::{PublicKey, StaticSecret};
19
20pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<String, String> {
23 let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| format!("aes init: {}", e))?;
24 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
25 let ciphertext = cipher
26 .encrypt(&nonce, plaintext)
27 .map_err(|e| format!("encrypt: {}", e))?;
28 let mut out = nonce.to_vec();
29 out.extend_from_slice(&ciphertext);
30 Ok(URL_SAFE_NO_PAD.encode(&out))
31}
32
33pub fn decrypt(key: &[u8; 32], encoded: &str) -> Result<Vec<u8>, String> {
35 let data = URL_SAFE_NO_PAD
36 .decode(encoded)
37 .map_err(|e| format!("base64 decode: {}", e))?;
38 if data.len() < 12 + 16 {
39 return Err(format!("ciphertext too short: {} bytes", data.len()));
40 }
41 let (nonce_bytes, ciphertext) = data.split_at(12);
42 let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| format!("aes init: {}", e))?;
43 let nonce = Nonce::from_slice(nonce_bytes);
44 cipher
45 .decrypt(nonce, ciphertext)
46 .map_err(|e| format!("decrypt: {}", e))
47}
48
49pub fn derive_shared_key(local_priv_b64: &str, remote_pub_b64: &str) -> Result<[u8; 32], String> {
52 let priv_bytes = URL_SAFE_NO_PAD
53 .decode(local_priv_b64)
54 .map_err(|e| format!("decode private key: {}", e))?;
55 let pub_bytes = URL_SAFE_NO_PAD
56 .decode(remote_pub_b64)
57 .map_err(|e| format!("decode public key: {}", e))?;
58
59 if priv_bytes.len() != 32 {
60 return Err(format!(
61 "private key must be 32 bytes, got {}",
62 priv_bytes.len()
63 ));
64 }
65 if pub_bytes.len() != 32 {
66 return Err(format!(
67 "public key must be 32 bytes, got {}",
68 pub_bytes.len()
69 ));
70 }
71
72 let secret = StaticSecret::from(<[u8; 32]>::try_from(priv_bytes.as_slice()).unwrap());
73 let public = PublicKey::from(<[u8; 32]>::try_from(pub_bytes.as_slice()).unwrap());
74 let shared = secret.diffie_hellman(&public);
75
76 let hk = Hkdf::<Sha256>::new(None, shared.as_bytes());
77 let mut okm = [0u8; 32];
78 hk.expand(b"cinch-key-xfer", &mut okm)
79 .map_err(|e| format!("hkdf expand: {}", e))?;
80 Ok(okm)
81}
82
83pub fn generate_ephemeral_keypair() -> (String, String) {
86 let secret = StaticSecret::random_from_rng(OsRng);
87 let public = PublicKey::from(&secret);
88 let priv_b64 = URL_SAFE_NO_PAD.encode(secret.as_bytes());
89 let pub_b64 = URL_SAFE_NO_PAD.encode(public.as_bytes());
90 (priv_b64, pub_b64)
91}
92
93pub fn generate_aes_key() -> [u8; 32] {
97 use aes_gcm::aead::rand_core::RngCore;
98 let mut key = [0u8; 32];
99 OsRng.fill_bytes(&mut key);
100 key
101}
102
103pub fn generate_device_keypair() -> (String, String) {
106 generate_ephemeral_keypair()
107}
108
109pub fn pub_from_priv(priv_b64: &str) -> Result<String, String> {
114 let raw = URL_SAFE_NO_PAD
115 .decode(priv_b64)
116 .map_err(|e| format!("decode private key: {}", e))?;
117 if raw.len() != 32 {
118 return Err(format!("invalid private key length: {}", raw.len()));
119 }
120 let mut buf = [0u8; 32];
121 buf.copy_from_slice(&raw);
122 let secret = StaticSecret::from(buf);
123 let public = PublicKey::from(&secret);
124 Ok(URL_SAFE_NO_PAD.encode(public.as_bytes()))
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_encrypt_decrypt_roundtrip() {
133 let key = [0x42u8; 32];
134 let plaintext = b"hello world";
135 let encoded = encrypt(&key, plaintext).unwrap();
136 let decrypted = decrypt(&key, &encoded).unwrap();
137 assert_eq!(decrypted, plaintext);
138 }
139
140 #[test]
141 fn test_nonce_uniqueness() {
142 let key = [0x42u8; 32];
143 let a = encrypt(&key, b"same").unwrap();
144 let b = encrypt(&key, b"same").unwrap();
145 assert_ne!(a, b);
146 }
147
148 #[test]
149 fn test_tamper_detection() {
150 let key = [0x42u8; 32];
151 let encoded = encrypt(&key, b"test").unwrap();
152 let mut data = URL_SAFE_NO_PAD.decode(&encoded).unwrap();
153 data[15] ^= 0xFF;
154 let tampered = URL_SAFE_NO_PAD.encode(&data);
155 assert!(decrypt(&key, &tampered).is_err());
156 }
157
158 #[test]
159 fn test_ecdh_symmetric() {
160 let (a_priv, a_pub) = generate_ephemeral_keypair();
161 let (b_priv, b_pub) = generate_ephemeral_keypair();
162 let key_ab = derive_shared_key(&a_priv, &b_pub).unwrap();
163 let key_ba = derive_shared_key(&b_priv, &a_pub).unwrap();
164 assert_eq!(key_ab, key_ba);
165 }
166
167 #[test]
168 fn test_wire_format_layout() {
169 let key = [0x42u8; 32];
170 let encoded = encrypt(&key, b"test").unwrap();
171 let data = URL_SAFE_NO_PAD.decode(&encoded).unwrap();
172 assert_eq!(data.len(), 32);
174 }
175
176 #[test]
177 fn pub_from_priv_matches_generate_device_keypair() {
178 let (priv_b64, expected_pub) = generate_device_keypair();
179 let derived = pub_from_priv(&priv_b64).unwrap();
180 assert_eq!(derived, expected_pub);
181 }
182
183 #[test]
184 fn pub_from_priv_rejects_bad_length() {
185 let bad = URL_SAFE_NO_PAD.encode([0u8; 16]);
186 assert!(pub_from_priv(&bad).is_err());
187 }
188
189 #[test]
193 fn deterministic_vector_decrypts_to_hello() {
194 let key = [0u8; 32];
195 let encoded = "AAAAAAAAAAAAAAAApsIsUSKLkI9_Yv_Opqkvq-85v02T";
196 let got = decrypt(&key, encoded).expect("decrypt");
197 assert_eq!(got, b"hello");
198 }
199
200 #[test]
201 fn test_decrypt_with_different_key_returns_err() {
202 let key_a = generate_aes_key();
203 let key_b = generate_aes_key();
204 assert_ne!(key_a, key_b, "fresh keys must differ");
205
206 let plaintext = b"remote cinch push payload";
207 let blob = encrypt(&key_a, plaintext).expect("encrypt under key A");
208
209 let result = decrypt(&key_b, &blob);
210 assert!(
211 result.is_err(),
212 "decrypting key-A ciphertext under key B must fail"
213 );
214 }
215}