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(&params)
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(&params).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}