async_signatory/lib.rs
1use base64::engine::general_purpose; // Using the general-purpose base64 encoding engine
2use base64::Engine;
3use chrono::Utc;
4use md5;
5use serde_json::Value;
6use std::collections::HashMap;
7use std::error::Error;
8
9/// Struct responsible for signing operations.
10pub struct Signatory {
11 key: String, // Secret key used for generating signatures
12}
13
14impl Signatory {
15 /// Creates a new instance of the Signatory struct with the provided secret key.
16 ///
17 /// # Arguments
18 ///
19 /// * `key` - A `String` representing the secret key to be used in signing.
20 pub fn new(key: String) -> Signatory {
21 Signatory { key }
22 }
23
24 /// Generates a signature from a given `HashMap<String, Value>`.
25 ///
26 /// Steps:
27 /// 1. Removes the `sign` field from `params` if it exists.
28 /// 2. Sorts the remaining keys in ascending order.
29 /// 3. Builds a query string from key-value pairs.
30 /// 4. Appends the secret key to the query string.
31 /// 5. Computes the MD5 hash of the string.
32 /// 6. Converts the hash to an uppercase hexadecimal string and returns it.
33 ///
34 /// # Arguments
35 ///
36 /// * `params` - A mutable `HashMap<String, Value>` containing the parameters to sign.
37 ///
38 /// # Returns
39 ///
40 /// Returns the generated signature as a `Result<String, Box<dyn Error>>`.
41 pub fn generate_sign(
42 &self,
43 mut params: HashMap<String, Value>,
44 ) -> Result<String, Box<dyn Error>> {
45 // Ensure `params` is not empty
46 if params.is_empty() {
47 return Err("Params is empty".into());
48 }
49
50 // Remove the "sign" field if it exists
51 params.remove("sign");
52
53 // Collect and sort the keys of the HashMap
54 let mut keys: Vec<String> = params.keys().cloned().collect();
55 keys.sort();
56
57 // Build the query string by iterating over sorted keys and values
58 let payload: String = keys
59 .iter()
60 .filter_map(|key| {
61 // Convert each value to a string, skipping keys with non-string values
62 match params.get(key) {
63 Some(value) => value.as_str().map(|v| format!("{}={}", key, v)),
64 None => None,
65 }
66 })
67 .collect::<Vec<String>>()
68 .join("&");
69
70 // Append the secret key to the query string
71 let payload_with_key = format!("{}&key={}", payload, self.key);
72
73 // Compute the MD5 hash of the final payload
74 let digest = md5::compute(payload_with_key);
75
76 // Convert the hash to uppercase hexadecimal and return the result
77 Ok(format!("{:x}", digest).to_ascii_uppercase())
78 }
79
80 /// Converts a `HashMap<String, Value>` into a Base64-encoded string.
81 ///
82 /// This function:
83 /// 1. Adds the current timestamp if the `timestamp` key does not exist.
84 /// 2. Adds the generated signature to `params` if the `sign` key does not exist.
85 /// 3. Serializes the `params` to a JSON string.
86 /// 4. Base64 encodes the JSON string and returns it.
87 ///
88 /// # Arguments
89 ///
90 /// * `params` - A mutable `HashMap<String, Value>` containing the parameters to encode.
91 ///
92 /// # Returns
93 ///
94 /// Returns the Base64 encoded string as `Result<String, Box<dyn Error>>`.
95 pub fn to_string(&self, mut params: HashMap<String, Value>) -> Result<String, Box<dyn Error>> {
96 // Check if `params` is empty
97 if params.is_empty() {
98 return Err("Params is empty".into());
99 }
100
101 // Insert current timestamp if it doesn't exist
102 if !params.contains_key("timestamp") {
103 let now = Utc::now().timestamp().to_string(); // Get current Unix timestamp as string
104 params.insert("timestamp".to_string(), Value::String(now));
105 }
106
107 // Insert signature if it doesn't exist
108 if !params.contains_key("sign") {
109 let sign = self.generate_sign(params.clone()).unwrap(); // Generate signature
110 params.insert("sign".to_string(), Value::String(sign));
111 }
112
113 // Serialize `HashMap` to a JSON string
114 let body = serde_json::to_string(¶ms)
115 .map_err(|e| format!("Failed to serialize params to JSON: {}", e))?;
116
117 // Encode the JSON string to Base64
118 let encoded = general_purpose::STANDARD.encode(body);
119
120 Ok(encoded)
121 }
122
123 /// Decodes a Base64-encoded string into a `HashMap<String, Value>`.
124 ///
125 /// This function:
126 /// 1. Base64 decodes the input string.
127 /// 2. Converts the resulting bytes into a UTF-8 string.
128 /// 3. Deserializes the string into a `HashMap`.
129 ///
130 /// # Arguments
131 ///
132 /// * `params` - A Base64-encoded string representing serialized `HashMap<String, Value>`.
133 ///
134 /// # Returns
135 ///
136 /// Returns the decoded `HashMap<String, Value>` as `Result<HashMap<String, Value>, Box<dyn Error>>`.
137 pub fn decrypt(&self, params: String) -> Result<HashMap<String, Value>, Box<dyn Error>> {
138 // Base64 decode the input string
139 let bytes = general_purpose::STANDARD.decode(¶ms).unwrap();
140
141 // Convert the decoded bytes into a UTF-8 string
142 let body = String::from_utf8(bytes).unwrap();
143
144 // Deserialize the string into a HashMap
145 let result = serde_json::from_str(&body).unwrap();
146 Ok(result)
147 }
148
149 /// Verifies the integrity of the provided signature.
150 ///
151 /// This function:
152 /// 1. Regenerates the signature based on the `params`.
153 /// 2. Compares the regenerated signature with the provided `sign`.
154 ///
155 /// # Arguments
156 ///
157 /// * `params` - A `HashMap<String, Value>` representing the parameters to verify.
158 /// * `sign` - A `String` representing the signature to verify.
159 ///
160 /// # Returns
161 ///
162 /// Returns `true` if the signature matches, otherwise `false`.
163 pub fn check_sign(&self, params: HashMap<String, Value>, sign: String) -> bool {
164 let value = self.generate_sign(params);
165 if value.is_err() {
166 return false;
167 }
168
169 value.unwrap() == sign
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use serde_json::Value;
177 use std::collections::HashMap;
178
179 #[test]
180 fn test_signature_generation_and_validation() {
181 // Create the signatory instance with a sample key
182 let key = "ds069ed4223ac1660f".to_string();
183 let signatory = Signatory::new(key);
184
185 // Prepare a sample HashMap
186 let mut params = HashMap::new();
187 params.insert(
188 "client_id".to_string(),
189 Value::String("16327128".to_string()),
190 );
191 params.insert(
192 "method".to_string(),
193 Value::String("android.shutdown".to_string()),
194 );
195 params.insert(
196 "timestamp".to_string(),
197 Value::String("1727494645".to_string()),
198 );
199
200 // Generate signature
201 let sign = signatory.generate_sign(params.clone()).unwrap();
202 println!("Generated sign: {}", sign);
203
204 // Manually provided expected signature (from the decoded JSON)
205 let expected_sign = "4D49FFFDE0DA4537160CFC258356277B";
206
207 // Assert that the generated signature matches the expected one
208 assert_eq!(
209 sign, expected_sign,
210 "The generated signature should match the expected signature"
211 );
212
213 // Insert the expected sign back into the params
214 params.insert("sign".to_string(), Value::String(sign.clone()));
215
216 // Now encode the parameters as base64
217 let base64_str = signatory.to_string(params.clone()).unwrap();
218 println!("Base64 encoded: {}", base64_str);
219
220 // Decode back to HashMap
221 let decoded_params = signatory.decrypt(base64_str).unwrap();
222 assert_eq!(
223 params, decoded_params,
224 "Decoded params should match the original params"
225 );
226
227 // Check if signature is valid
228 let is_valid = signatory.check_sign(decoded_params.clone(), sign.clone());
229 assert!(is_valid, "Signature should be valid");
230 }
231}