lm_rs/
installation_key.rs1use anyhow::Result;
2use base64::{engine::general_purpose::STANDARD, Engine as _};
3use p256::{
4 ecdsa::{signature::Signer, Signature, SigningKey},
5 elliptic_curve::rand_core::OsRng,
6 pkcs8::EncodePublicKey,
7 SecretKey,
8};
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11use std::time::{SystemTime, UNIX_EPOCH};
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct InstallationKey {
17 #[serde(
19 serialize_with = "serialize_bytes_as_base64",
20 deserialize_with = "deserialize_base64_as_bytes"
21 )]
22 pub secret: Vec<u8>,
23 #[serde(
25 serialize_with = "serialize_signing_key_as_base64",
26 deserialize_with = "deserialize_base64_as_signing_key"
27 )]
28 pub private_key: SigningKey,
29 pub installation_id: String,
31}
32
33impl InstallationKey {
34 pub fn public_key_b64(&self) -> String {
36 let verifying_key = *self.private_key.verifying_key();
37 let public_key_der = verifying_key.to_public_key_der().unwrap();
39 STANDARD.encode(public_key_der.as_bytes())
40 }
41
42 pub fn base_string(&self) -> String {
44 let verifying_key = *self.private_key.verifying_key();
45 let public_key_der = verifying_key.to_public_key_der().unwrap();
47 let mut hasher = Sha256::new();
48 hasher.update(public_key_der.as_bytes());
49 let pub_hash = hasher.finalize();
50 let pub_hash_b64 = STANDARD.encode(pub_hash);
51 format!("{}.{}", self.installation_id, pub_hash_b64)
52 }
53}
54
55pub fn generate_installation_key(installation_id: String) -> Result<InstallationKey> {
57 let secret_key = SecretKey::random(&mut OsRng);
59 let signing_key = SigningKey::from(secret_key);
60 let verifying_key = *signing_key.verifying_key();
61
62 let public_key_der = verifying_key.to_public_key_der().unwrap();
64 let pub_b64 = STANDARD.encode(public_key_der.as_bytes());
65
66 let mut hasher = Sha256::new();
68 hasher.update(installation_id.as_bytes());
69 let inst_hash = hasher.finalize();
70 let inst_hash_b64 = STANDARD.encode(inst_hash);
71
72 let triple = format!("{}.{}.{}", installation_id, pub_b64, inst_hash_b64);
74
75 let mut secret_hasher = Sha256::new();
77 secret_hasher.update(triple.as_bytes());
78 let secret_bytes = secret_hasher.finalize();
79
80 Ok(InstallationKey {
81 secret: secret_bytes.to_vec(),
82 private_key: signing_key,
83 installation_id,
84 })
85}
86
87pub fn generate_installation_id() -> String {
89 Uuid::new_v4().to_string().to_lowercase()
90}
91
92pub fn generate_request_proof(base_string: &str, secret32: &[u8]) -> Result<String> {
94 if secret32.len() != 32 {
95 return Err(anyhow::anyhow!("secret must be 32 bytes"));
96 }
97
98 let mut work = [0u8; 32];
100 work.copy_from_slice(secret32);
101
102 for byte_val in base_string.as_bytes() {
103 let idx = (*byte_val as usize) % 32;
104 let shift_idx = (idx + 1) % 32;
105 let shift_amount = work[shift_idx] & 7; let xor_result = byte_val ^ work[idx];
109 let rotated = if shift_amount == 0 {
110 xor_result
111 } else {
112 (xor_result << shift_amount) | (xor_result >> (8 - shift_amount))
113 };
114 work[idx] = rotated;
115 }
116
117 let mut hasher = Sha256::new();
118 hasher.update(work);
119 let result = hasher.finalize();
120 Ok(STANDARD.encode(result))
121}
122
123pub fn generate_extra_request_headers(
125 installation_key: &InstallationKey,
126) -> Result<Vec<(String, String)>> {
127 let nonce = Uuid::new_v4().to_string().to_lowercase();
129 let timestamp = SystemTime::now()
130 .duration_since(UNIX_EPOCH)?
131 .as_millis()
132 .to_string();
133
134 let proof_input = format!(
136 "{}.{}.{}",
137 installation_key.installation_id, nonce, timestamp
138 );
139 let proof = generate_request_proof(&proof_input, &installation_key.secret)?;
140
141 let signature_data = format!("{}.{}", proof_input, proof);
143
144 let signature: Signature = installation_key.private_key.sign(signature_data.as_bytes());
146 let signature_b64 = STANDARD.encode(signature.to_der());
147
148 Ok(vec![
150 (
151 "X-App-Installation-Id".to_string(),
152 installation_key.installation_id.clone(),
153 ),
154 ("X-Timestamp".to_string(), timestamp),
155 ("X-Nonce".to_string(), nonce),
156 ("X-Request-Signature".to_string(), signature_b64),
157 ])
158}
159
160fn serialize_bytes_as_base64<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
162where
163 S: serde::Serializer,
164{
165 let b64 = STANDARD.encode(bytes);
166 serializer.serialize_str(&b64)
167}
168
169fn deserialize_base64_as_bytes<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
170where
171 D: serde::Deserializer<'de>,
172{
173 let s = String::deserialize(deserializer)?;
174 STANDARD.decode(s).map_err(serde::de::Error::custom)
175}
176
177fn serialize_signing_key_as_base64<S>(
178 signing_key: &SigningKey,
179 serializer: S,
180) -> Result<S::Ok, S::Error>
181where
182 S: serde::Serializer,
183{
184 let bytes = signing_key.to_bytes();
185 let b64 = STANDARD.encode(bytes);
186 serializer.serialize_str(&b64)
187}
188
189fn deserialize_base64_as_signing_key<'de, D>(deserializer: D) -> Result<SigningKey, D::Error>
190where
191 D: serde::Deserializer<'de>,
192{
193 let s = String::deserialize(deserializer)?;
194 let bytes = STANDARD.decode(s).map_err(serde::de::Error::custom)?;
195 let secret_key = SecretKey::from_slice(&bytes).map_err(serde::de::Error::custom)?;
196 Ok(SigningKey::from(secret_key))
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_installation_id_generation() {
205 let id1 = generate_installation_id();
206 let id2 = generate_installation_id();
207
208 assert_ne!(id1, id2);
210
211 assert_eq!(id1.len(), 36);
213 assert_eq!(id2.len(), 36);
214 assert!(id1.contains('-'));
215 assert!(id2.contains('-'));
216 }
217
218 #[test]
219 fn test_installation_key_generation() {
220 let installation_id = "test-installation-id".to_string();
221 let key = generate_installation_key(installation_id.clone()).unwrap();
222
223 assert_eq!(key.installation_id, installation_id);
224 assert_eq!(key.secret.len(), 32);
225
226 let pub_key_b64 = key.public_key_b64();
228 assert!(!pub_key_b64.is_empty());
229
230 let base_string = key.base_string();
232 assert!(base_string.starts_with(&installation_id));
233 assert!(base_string.contains('.'));
234 }
235
236 #[test]
237 fn test_request_proof_generation() {
238 let secret = vec![0u8; 32]; let base_string = "test.base.string";
240
241 let proof = generate_request_proof(base_string, &secret).unwrap();
242 assert!(!proof.is_empty());
243
244 assert_eq!(proof.len(), 44);
246 }
247
248 #[test]
249 fn test_request_proof_error_on_wrong_secret_size() {
250 let secret = vec![0u8; 31]; let base_string = "test";
252
253 let result = generate_request_proof(base_string, &secret);
254 assert!(result.is_err());
255 assert!(result.unwrap_err().to_string().contains("32 bytes"));
256 }
257
258 #[test]
259 fn test_extra_request_headers_generation() {
260 let installation_id = "test-id".to_string();
261 let key = generate_installation_key(installation_id.clone()).unwrap();
262
263 let headers = generate_extra_request_headers(&key).unwrap();
264
265 assert_eq!(headers.len(), 4);
267
268 let header_names: Vec<String> = headers.iter().map(|(k, _)| k.clone()).collect();
270 assert!(header_names.contains(&"X-App-Installation-Id".to_string()));
271 assert!(header_names.contains(&"X-Timestamp".to_string()));
272 assert!(header_names.contains(&"X-Nonce".to_string()));
273 assert!(header_names.contains(&"X-Request-Signature".to_string()));
274
275 let installation_id_header = headers
277 .iter()
278 .find(|(k, _)| k == "X-App-Installation-Id")
279 .unwrap();
280 assert_eq!(installation_id_header.1, installation_id);
281 }
282
283 #[test]
284 fn test_cross_language_compatibility() {
285 use base64::{engine::general_purpose::STANDARD, Engine as _};
286 use p256::{ecdsa::SigningKey, pkcs8::DecodePrivateKey};
287
288 let installation_id = "test-installation-id-12345";
290 let expected_secret = "liWQoHaK8Sg+auapkWedGzGs/8HDt6JCIP3tw2c8WYA=";
291 let private_key_der = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg25BwIri/hrmMdfAvHgbFk9TQ/nmA70OYEdmrFuhbux+hRANCAASSLyaAtUj6jGO/F2VQJMN9XNGQkNMZNktiENHlVgMKaTrTMXuR/dyQU/4boce+LqcHoOBjZVDYz5JsXyKM6qyE";
292 let expected_public_key_der = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEki8mgLVI+oxjvxdlUCTDfVzRkJDTGTZLYhDR5VYDCmk60zF7kf3ckFP+G6HHvi6nB6DgY2VQ2M+SbF8ijOqshA==";
293 let expected_base_string =
294 "test-installation-id-12345.eZhZZDD3ciI13+s7zV9QlgLW9Eo+lDKGJAKUn8SpAtA=";
295
296 let private_key_bytes = STANDARD.decode(private_key_der).unwrap();
298 let signing_key = SigningKey::from_pkcs8_der(&private_key_bytes).unwrap();
299 let secret_bytes = STANDARD.decode(expected_secret).unwrap();
300
301 let key = InstallationKey {
302 secret: secret_bytes,
303 private_key: signing_key,
304 installation_id: installation_id.to_string(),
305 };
306
307 assert_eq!(key.public_key_b64(), expected_public_key_der);
309 assert_eq!(key.base_string(), expected_base_string);
310
311 let test_proof_input = "test-installation-id-12345.test-nonce.1234567890";
313 let expected_proof = "DXFZPKXiSDRih9U43F+YhJGn2DRt05XLLY9W9dtGl6g=";
314 let proof = generate_request_proof(test_proof_input, &key.secret).unwrap();
315 assert_eq!(proof, expected_proof);
316 }
317
318 #[test]
319 fn test_installation_key_serialization() {
320 let installation_id = "test-id".to_string();
321 let key = generate_installation_key(installation_id.clone()).unwrap();
322
323 let json = serde_json::to_string(&key).unwrap();
325 assert!(!json.is_empty());
326
327 let deserialized: InstallationKey = serde_json::from_str(&json).unwrap();
329 assert_eq!(deserialized.installation_id, key.installation_id);
330 assert_eq!(deserialized.secret, key.secret);
331
332 assert_eq!(deserialized.public_key_b64(), key.public_key_b64());
334 assert_eq!(deserialized.base_string(), key.base_string());
335 }
336}