1use base64::{engine::general_purpose::STANDARD, Engine as _};
2use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
3use rand::rngs::OsRng;
4use sha2::{Digest, Sha256};
5
6use crate::error::CoreError;
7
8pub fn generate_keypair() -> Result<(String, String), CoreError> {
12 let signing_key = SigningKey::generate(&mut OsRng);
13 let verifying_key = signing_key.verifying_key();
14
15 let private_key_b64 = STANDARD.encode(signing_key.to_bytes());
16 let public_key_b64 = STANDARD.encode(verifying_key.to_bytes());
17
18 Ok((private_key_b64, public_key_b64))
19}
20
21pub fn construct_signing_payload(method: &str, path: &str, timestamp: i64, body: &[u8]) -> String {
25 let hash = Sha256::digest(body);
26 let body_hash: String = hash.iter().map(|b| format!("{b:02x}")).collect();
27 format!("{method}\n{path}\n{timestamp}\n{body_hash}")
28}
29
30pub fn sign_request(private_key_b64: &str, payload: &str) -> Result<String, CoreError> {
34 let key_bytes = STANDARD
35 .decode(private_key_b64)
36 .map_err(|e| CoreError::AuthError(format!("Invalid private key encoding: {e}")))?;
37
38 let key_array: [u8; 32] = key_bytes
39 .try_into()
40 .map_err(|_| CoreError::AuthError("Private key must be 32 bytes".to_string()))?;
41
42 let signing_key = SigningKey::from_bytes(&key_array);
43 let signature = signing_key.sign(payload.as_bytes());
44
45 Ok(STANDARD.encode(signature.to_bytes()))
46}
47
48pub fn verify_signature(
52 public_key_b64: &str,
53 payload: &str,
54 signature_b64: &str,
55) -> Result<(), CoreError> {
56 let key_bytes = STANDARD
57 .decode(public_key_b64)
58 .map_err(|e| CoreError::AuthError(format!("Invalid public key encoding: {e}")))?;
59
60 let key_array: [u8; 32] = key_bytes
61 .try_into()
62 .map_err(|_| CoreError::AuthError("Public key must be 32 bytes".to_string()))?;
63
64 let verifying_key = VerifyingKey::from_bytes(&key_array)
65 .map_err(|e| CoreError::AuthError(format!("Invalid public key: {e}")))?;
66
67 let sig_bytes = STANDARD
68 .decode(signature_b64)
69 .map_err(|e| CoreError::AuthError(format!("Invalid signature encoding: {e}")))?;
70
71 let sig_array: [u8; 64] = sig_bytes
72 .try_into()
73 .map_err(|_| CoreError::AuthError("Signature must be 64 bytes".to_string()))?;
74
75 let signature = Signature::from_bytes(&sig_array);
76
77 verifying_key
78 .verify(payload.as_bytes(), &signature)
79 .map_err(|e| CoreError::AuthError(format!("Signature verification failed: {e}")))
80}
81
82pub fn validate_timestamp(request_timestamp: i64) -> Result<(), CoreError> {
86 let now = chrono::Utc::now().timestamp();
87 let diff = (now as i128 - request_timestamp as i128).unsigned_abs();
91
92 if diff > 300 {
93 return Err(CoreError::AuthError(
94 "Replay detected: timestamp outside acceptable window".to_string(),
95 ));
96 }
97
98 Ok(())
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn test_generate_keypair() {
107 let (private_key, public_key) = generate_keypair().unwrap();
108 let priv_bytes = STANDARD.decode(&private_key).unwrap();
109 let pub_bytes = STANDARD.decode(&public_key).unwrap();
110 assert_eq!(priv_bytes.len(), 32);
111 assert_eq!(pub_bytes.len(), 32);
112 }
113
114 #[test]
115 fn test_sign_and_verify() {
116 let (private_key, public_key) = generate_keypair().unwrap();
117 let payload = construct_signing_payload("POST", "/upload", 1_234_567_890, b"hello world");
118 let signature = sign_request(&private_key, &payload).unwrap();
119 assert!(verify_signature(&public_key, &payload, &signature).is_ok());
120 }
121
122 #[test]
123 fn test_verify_bad_signature() {
124 let (private_key, _) = generate_keypair().unwrap();
125 let (_, other_public_key) = generate_keypair().unwrap();
126 let payload = construct_signing_payload("POST", "/upload", 1_234_567_890, b"hello world");
127 let signature = sign_request(&private_key, &payload).unwrap();
128 assert!(verify_signature(&other_public_key, &payload, &signature).is_err());
129 }
130
131 #[test]
132 fn test_validate_timestamp() {
133 let now = chrono::Utc::now().timestamp();
134 assert!(validate_timestamp(now).is_ok());
135 assert!(validate_timestamp(now - 600).is_err());
136 assert!(validate_timestamp(now + 600).is_err());
137 }
138
139 #[test]
140 fn test_signing_payload_format() {
141 let payload = construct_signing_payload("GET", "/files", 1_234_567_890, b"");
142 let parts: Vec<&str> = payload.splitn(4, '\n').collect();
143 assert_eq!(parts.len(), 4);
144 assert_eq!(parts[0], "GET");
145 assert_eq!(parts[1], "/files");
146 assert_eq!(parts[2], "1234567890");
147 assert_eq!(
149 parts[3],
150 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
151 );
152 }
153}