ccxt_core/
auth.rs

1//! API authentication and cryptographic signing utilities.
2//!
3//! Provides cryptographic functions for exchange API authentication:
4//! - HMAC signing (SHA256/SHA512/SHA384/MD5)
5//! - Hash functions (SHA256/SHA512/SHA384/SHA1/MD5/Keccak)
6//! - RSA signing (PKCS1v15)
7//! - EdDSA signing (Ed25519)
8//! - JWT token generation
9//! - Base64 encoding/decoding utilities
10
11use crate::error::{Error, Result};
12use base64::{Engine as _, engine::general_purpose};
13use hmac::{Hmac, Mac};
14// use rsa::{
15//     RsaPrivateKey,
16//     pkcs1::DecodeRsaPrivateKey,
17//     pkcs1v15::SigningKey,
18//     signature::{RandomizedSigner, SignatureEncoding},
19// };
20use sha1::Sha1;
21use sha2::{Digest, Sha256, Sha384, Sha512};
22use sha3::Keccak256;
23use std::fmt;
24
25/// Supported cryptographic hash algorithms.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum HashAlgorithm {
28    /// SHA-1 hash algorithm
29    Sha1,
30    /// SHA-256 hash algorithm
31    Sha256,
32    /// SHA-384 hash algorithm
33    Sha384,
34    /// SHA-512 hash algorithm
35    Sha512,
36    /// MD5 hash algorithm
37    Md5,
38    /// Keccak-256 (SHA-3) hash algorithm
39    Keccak,
40}
41
42impl HashAlgorithm {
43    /// Parses a hash algorithm from a string.
44    ///
45    /// # Arguments
46    /// * `s` - Algorithm name (case-insensitive)
47    ///
48    /// # Returns
49    /// Parsed [`HashAlgorithm`] or error if unsupported.
50    pub fn from_str(s: &str) -> Result<Self> {
51        match s.to_lowercase().as_str() {
52            "sha1" => Ok(HashAlgorithm::Sha1),
53            "sha256" => Ok(HashAlgorithm::Sha256),
54            "sha384" => Ok(HashAlgorithm::Sha384),
55            "sha512" => Ok(HashAlgorithm::Sha512),
56            "md5" => Ok(HashAlgorithm::Md5),
57            "keccak" | "sha3" => Ok(HashAlgorithm::Keccak),
58            _ => Err(Error::invalid_argument(format!(
59                "Unsupported hash algorithm: {}",
60                s
61            ))),
62        }
63    }
64}
65
66impl fmt::Display for HashAlgorithm {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        let s = match self {
69            HashAlgorithm::Sha1 => "sha1",
70            HashAlgorithm::Sha256 => "sha256",
71            HashAlgorithm::Sha384 => "sha384",
72            HashAlgorithm::Sha512 => "sha512",
73            HashAlgorithm::Md5 => "md5",
74            HashAlgorithm::Keccak => "keccak",
75        };
76        write!(f, "{}", s)
77    }
78}
79
80/// Output encoding format for cryptographic digests.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum DigestFormat {
83    /// Hexadecimal encoding
84    Hex,
85    /// Base64 encoding
86    Base64,
87    /// Raw binary format
88    Binary,
89}
90
91impl DigestFormat {
92    /// Parses a digest format from a string.
93    ///
94    /// # Arguments
95    /// * `s` - Format name (case-insensitive)
96    ///
97    /// # Returns
98    /// Parsed [`DigestFormat`], defaults to `Hex` if unrecognized.
99    pub fn from_str(s: &str) -> Self {
100        match s.to_lowercase().as_str() {
101            "hex" => DigestFormat::Hex,
102            "base64" => DigestFormat::Base64,
103            "binary" => DigestFormat::Binary,
104            _ => DigestFormat::Hex,
105        }
106    }
107}
108
109/// Generates an HMAC signature for a message.
110///
111/// # Arguments
112/// * `message` - Message to sign
113/// * `secret` - Secret key for HMAC
114/// * `algorithm` - Hash algorithm to use
115/// * `digest` - Output encoding format
116///
117/// # Returns
118/// Encoded signature string.
119///
120/// # Errors
121/// Returns error if algorithm is not supported for HMAC.
122///
123/// # Examples
124/// ```
125/// use ccxt_core::auth::{hmac_sign, HashAlgorithm, DigestFormat};
126///
127/// let signature = hmac_sign(
128///     "test message",
129///     "secret_key",
130///     HashAlgorithm::Sha256,
131///     DigestFormat::Hex
132/// ).unwrap();
133/// ```
134pub fn hmac_sign(
135    message: &str,
136    secret: &str,
137    algorithm: HashAlgorithm,
138    digest: DigestFormat,
139) -> Result<String> {
140    let signature = match algorithm {
141        HashAlgorithm::Sha256 => hmac_sha256(message.as_bytes(), secret.as_bytes()),
142        HashAlgorithm::Sha512 => hmac_sha512(message.as_bytes(), secret.as_bytes()),
143        HashAlgorithm::Sha384 => hmac_sha384(message.as_bytes(), secret.as_bytes()),
144        HashAlgorithm::Md5 => hmac_md5(message.as_bytes(), secret.as_bytes()),
145        _ => {
146            return Err(Error::invalid_argument(format!(
147                "HMAC does not support {} algorithm",
148                algorithm
149            )));
150        }
151    };
152
153    Ok(encode_bytes(&signature, digest))
154}
155
156/// Computes HMAC-SHA256 signature.
157///
158/// # Panics
159///
160/// This function will never panic. The `expect()` call is safe because HMAC algorithms
161/// accept keys of any length (including empty keys). The `InvalidLength` error from
162/// `new_from_slice` is only returned for algorithms with fixed key requirements,
163/// which does not apply to HMAC.
164fn hmac_sha256(data: &[u8], secret: &[u8]) -> Vec<u8> {
165    type HmacSha256 = Hmac<Sha256>;
166    // SAFETY: HMAC accepts keys of any length - this cannot fail
167    let mut mac = HmacSha256::new_from_slice(secret)
168        .expect("HMAC-SHA256 accepts keys of any length; this is an infallible operation");
169    mac.update(data);
170    mac.finalize().into_bytes().to_vec()
171}
172
173/// Computes HMAC-SHA512 signature.
174///
175/// # Panics
176///
177/// This function will never panic. See `hmac_sha256` for rationale.
178fn hmac_sha512(data: &[u8], secret: &[u8]) -> Vec<u8> {
179    type HmacSha512 = Hmac<Sha512>;
180    // SAFETY: HMAC accepts keys of any length - this cannot fail
181    let mut mac = HmacSha512::new_from_slice(secret)
182        .expect("HMAC-SHA512 accepts keys of any length; this is an infallible operation");
183    mac.update(data);
184    mac.finalize().into_bytes().to_vec()
185}
186
187/// Computes HMAC-SHA384 signature.
188///
189/// # Panics
190///
191/// This function will never panic. See `hmac_sha256` for rationale.
192fn hmac_sha384(data: &[u8], secret: &[u8]) -> Vec<u8> {
193    type HmacSha384 = Hmac<Sha384>;
194    // SAFETY: HMAC accepts keys of any length - this cannot fail
195    let mut mac = HmacSha384::new_from_slice(secret)
196        .expect("HMAC-SHA384 accepts keys of any length; this is an infallible operation");
197    mac.update(data);
198    mac.finalize().into_bytes().to_vec()
199}
200
201/// Computes HMAC-MD5 signature.
202///
203/// # Panics
204///
205/// This function will never panic. See `hmac_sha256` for rationale.
206fn hmac_md5(data: &[u8], secret: &[u8]) -> Vec<u8> {
207    use md5::Md5;
208    type HmacMd5 = Hmac<Md5>;
209    // SAFETY: HMAC accepts keys of any length - this cannot fail
210    let mut mac = HmacMd5::new_from_slice(secret)
211        .expect("HMAC-MD5 accepts keys of any length; this is an infallible operation");
212    mac.update(data);
213    mac.finalize().into_bytes().to_vec()
214}
215
216/// Computes a cryptographic hash (one-way, keyless).
217///
218/// # Arguments
219/// * `data` - Data to hash
220/// * `algorithm` - Hash algorithm to use
221/// * `digest` - Output encoding format
222///
223/// # Returns
224/// Encoded hash string.
225///
226/// # Examples
227/// ```
228/// use ccxt_core::auth::{hash, HashAlgorithm, DigestFormat};
229///
230/// let hashed = hash("test", HashAlgorithm::Sha256, DigestFormat::Hex).unwrap();
231/// ```
232pub fn hash(data: &str, algorithm: HashAlgorithm, digest: DigestFormat) -> Result<String> {
233    let hash_bytes = match algorithm {
234        HashAlgorithm::Sha256 => hash_sha256(data.as_bytes()),
235        HashAlgorithm::Sha512 => hash_sha512(data.as_bytes()),
236        HashAlgorithm::Sha384 => hash_sha384(data.as_bytes()),
237        HashAlgorithm::Sha1 => hash_sha1(data.as_bytes()),
238        HashAlgorithm::Md5 => hash_md5(data.as_bytes()),
239        HashAlgorithm::Keccak => hash_keccak(data.as_bytes()),
240    };
241
242    Ok(encode_bytes(&hash_bytes, digest))
243}
244
245/// Computes SHA-256 hash.
246fn hash_sha256(data: &[u8]) -> Vec<u8> {
247    let mut hasher = Sha256::new();
248    hasher.update(data);
249    hasher.finalize().to_vec()
250}
251
252/// Computes SHA-512 hash.
253fn hash_sha512(data: &[u8]) -> Vec<u8> {
254    let mut hasher = Sha512::new();
255    hasher.update(data);
256    hasher.finalize().to_vec()
257}
258
259/// Computes SHA-384 hash.
260fn hash_sha384(data: &[u8]) -> Vec<u8> {
261    let mut hasher = Sha384::new();
262    hasher.update(data);
263    hasher.finalize().to_vec()
264}
265
266/// Computes SHA-1 hash.
267fn hash_sha1(data: &[u8]) -> Vec<u8> {
268    let mut hasher = Sha1::new();
269    hasher.update(data);
270    hasher.finalize().to_vec()
271}
272
273/// Computes MD5 hash.
274fn hash_md5(data: &[u8]) -> Vec<u8> {
275    use md5::{Digest, Md5};
276    let mut hasher = Md5::new();
277    hasher.update(data);
278    hasher.finalize().to_vec()
279}
280
281/// Computes Keccak-256 hash (Ethereum-compatible).
282fn hash_keccak(data: &[u8]) -> Vec<u8> {
283    let mut hasher = Keccak256::new();
284    hasher.update(data);
285    hasher.finalize().to_vec()
286}
287
288// /// Generates an RSA signature using PKCS1v15 padding.
289// ///
290// /// # Arguments
291// /// * `data` - Data to sign
292// /// * `private_key_pem` - RSA private key in PEM format
293// /// * `algorithm` - Hash algorithm for digest
294// ///
295// /// # Returns
296// /// Base64-encoded signature.
297// ///
298// /// # Errors
299// /// Returns error if key is invalid or algorithm unsupported.
300// pub fn rsa_sign(data: &str, private_key_pem: &str, algorithm: HashAlgorithm) -> Result<String> {
301//     let private_key = RsaPrivateKey::from_pkcs1_pem(private_key_pem)
302//         .map_err(|e| Error::invalid_argument(format!("Invalid RSA private key: {}", e)))?;
303//
304//     let hashed = match algorithm {
305//         HashAlgorithm::Sha256 => hash_sha256(data.as_bytes()),
306//         HashAlgorithm::Sha384 => hash_sha384(data.as_bytes()),
307//         HashAlgorithm::Sha512 => hash_sha512(data.as_bytes()),
308//         HashAlgorithm::Sha1 => hash_sha1(data.as_bytes()),
309//         HashAlgorithm::Md5 => hash_md5(data.as_bytes()),
310//         _ => {
311//             return Err(Error::invalid_argument(format!(
312//                 "RSA does not support {} algorithm",
313//                 algorithm
314//             )));
315//         }
316//     };
317//
318//     let signature = match algorithm {
319//         HashAlgorithm::Sha256 => {
320//             let signing_key = SigningKey::<Sha256>::new_unprefixed(private_key);
321//             let mut rng = rand::rngs::ThreadRng::default();
322//             signing_key.sign_with_rng(&mut rng, &hashed).to_bytes()
323//         }
324//         HashAlgorithm::Sha384 => {
325//             let signing_key = SigningKey::<Sha384>::new_unprefixed(private_key);
326//             let mut rng = rand::rngs::ThreadRng::default();
327//             signing_key.sign_with_rng(&mut rng, &hashed).to_bytes()
328//         }
329//         HashAlgorithm::Sha512 => {
330//             let signing_key = SigningKey::<Sha512>::new_unprefixed(private_key);
331//             let mut rng = rand::rngs::ThreadRng::default();
332//             signing_key.sign_with_rng(&mut rng, &hashed).to_bytes()
333//         }
334//         _ => {
335//             return Err(Error::invalid_argument(format!(
336//                 "Unsupported RSA hash algorithm: {}",
337//                 algorithm
338//             )));
339//         }
340//     };
341//
342//     Ok(general_purpose::STANDARD.encode(signature.as_ref()))
343// }
344
345/// Generates an EdDSA signature using Ed25519.
346///
347/// # Arguments
348/// * `data` - Data to sign
349/// * `secret_key` - 32-byte Ed25519 seed key
350///
351/// # Returns
352/// Base64 URL-encoded signature (without padding).
353///
354/// # Errors
355/// Returns error if secret key is not exactly 32 bytes.
356pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result<String> {
357    use ed25519_dalek::{Signature, Signer, SigningKey};
358
359    if secret_key.len() != 32 {
360        return Err(Error::invalid_argument(format!(
361            "Ed25519 secret key must be 32 bytes, got {}",
362            secret_key.len()
363        )));
364    }
365
366    let signing_key = SigningKey::from_bytes(
367        secret_key
368            .try_into()
369            .map_err(|_| Error::invalid_argument("Invalid Ed25519 key".to_string()))?,
370    );
371
372    let signature: Signature = signing_key.sign(data.as_bytes());
373    let encoded = general_purpose::STANDARD.encode(signature.to_bytes());
374
375    Ok(base64_to_base64url(&encoded, true))
376}
377
378/// Generates a JWT (JSON Web Token).
379///
380/// # Arguments
381/// * `payload` - JWT payload as JSON object
382/// * `secret` - Secret key for signing
383/// * `algorithm` - Hash algorithm for HMAC
384/// * `header_options` - Optional additional header fields
385///
386/// # Returns
387/// Complete JWT string (header.payload.signature).
388///
389/// # Errors
390/// Returns error if JSON serialization or signing fails.
391///
392/// # Examples
393/// ```
394/// use ccxt_core::auth::{jwt_sign, HashAlgorithm};
395/// use serde_json::json;
396///
397/// let payload = json!({
398///     "user_id": "123",
399///     "exp": 1234567890
400/// });
401///
402/// let token = jwt_sign(&payload, "secret", HashAlgorithm::Sha256, None).unwrap();
403/// ```
404pub fn jwt_sign(
405    payload: &serde_json::Value,
406    secret: &str,
407    algorithm: HashAlgorithm,
408    header_options: Option<serde_json::Map<String, serde_json::Value>>,
409) -> Result<String> {
410    let mut header = serde_json::Map::new();
411    header.insert(
412        "alg".to_string(),
413        serde_json::Value::String("HS256".to_string()),
414    );
415    header.insert(
416        "typ".to_string(),
417        serde_json::Value::String("JWT".to_string()),
418    );
419
420    if let Some(options) = header_options {
421        for (key, value) in options {
422            header.insert(key, value);
423        }
424    }
425
426    let header_json = serde_json::to_string(&header)?;
427    let payload_json = serde_json::to_string(payload)?;
428
429    let encoded_header = general_purpose::URL_SAFE_NO_PAD.encode(header_json.as_bytes());
430    let encoded_payload = general_purpose::URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
431
432    let token = format!("{}.{}", encoded_header, encoded_payload);
433
434    let signature = hmac_sign(&token, secret, algorithm, DigestFormat::Base64)?;
435
436    let signature_url = base64_to_base64url(&signature, true);
437
438    Ok(format!("{}.{}", token, signature_url))
439}
440
441/// Encodes bytes to the specified format.
442fn encode_bytes(bytes: &[u8], format: DigestFormat) -> String {
443    match format {
444        DigestFormat::Hex => hex::encode(bytes),
445        DigestFormat::Base64 => general_purpose::STANDARD.encode(bytes),
446        DigestFormat::Binary => String::from_utf8_lossy(bytes).to_string(),
447    }
448}
449
450/// Converts standard Base64 to Base64 URL format.
451///
452/// # Arguments
453/// * `base64_str` - Standard Base64 string
454/// * `strip_padding` - Whether to remove padding (`=`)
455///
456/// # Returns
457/// Base64 URL-encoded string.
458pub fn base64_to_base64url(base64_str: &str, strip_padding: bool) -> String {
459    let mut result = base64_str.replace('+', "-").replace('/', "_");
460    if strip_padding {
461        result = result.trim_end_matches('=').to_string();
462    }
463    result
464}
465
466/// Decodes a Base64 URL-encoded string.
467///
468/// # Arguments
469/// * `base64url` - Base64 URL-encoded string
470///
471/// # Returns
472/// Decoded bytes.
473///
474/// # Errors
475/// Returns error if decoding fails.
476pub fn base64url_decode(base64url: &str) -> Result<Vec<u8>> {
477    let base64 = base64url.replace('-', "+").replace('_', "/");
478
479    let padding = match base64.len() % 4 {
480        2 => "==",
481        3 => "=",
482        _ => "",
483    };
484    let base64_padded = format!("{}{}", base64, padding);
485
486    general_purpose::STANDARD
487        .decode(base64_padded.as_bytes())
488        .map_err(|e| Error::invalid_argument(format!("Base64 decode error: {}", e)))
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    #[test]
496    fn test_hmac_sha256_hex() {
497        let result = hmac_sign("test", "secret", HashAlgorithm::Sha256, DigestFormat::Hex).unwrap();
498        assert_eq!(
499            result,
500            "0329a06b62cd16b33eb6792be8c60b158d89a2ee3a876fce9a881ebb488c0914"
501        );
502    }
503
504    #[test]
505    fn test_hmac_sha256_base64() {
506        let result = hmac_sign(
507            "test",
508            "secret",
509            HashAlgorithm::Sha256,
510            DigestFormat::Base64,
511        )
512        .unwrap();
513        assert!(!result.is_empty());
514    }
515
516    #[test]
517    fn test_hash_sha256() {
518        let result = hash("test", HashAlgorithm::Sha256, DigestFormat::Hex).unwrap();
519        assert_eq!(
520            result,
521            "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
522        );
523    }
524
525    #[test]
526    fn test_hash_keccak() {
527        let result = hash("test", HashAlgorithm::Keccak, DigestFormat::Hex).unwrap();
528        assert_eq!(result.len(), 64); // Keccak256输出32字节=64个hex字符
529    }
530
531    #[test]
532    fn test_base64_to_base64url() {
533        let base64 = "abc+def/ghi==";
534        let base64url = base64_to_base64url(base64, true);
535        assert_eq!(base64url, "abc-def_ghi");
536    }
537
538    #[test]
539    fn test_base64url_decode() {
540        let base64url = "abc-def_ghg";
541        let decoded = base64url_decode(base64url).unwrap();
542        assert!(!decoded.is_empty());
543    }
544
545    #[test]
546    fn test_jwt_sign() {
547        use serde_json::json;
548
549        let payload = json!({
550            "user_id": "123",
551            "exp": 1234567890
552        });
553
554        let token = jwt_sign(&payload, "secret", HashAlgorithm::Sha256, None).unwrap();
555
556        // JWT应该有3部分,用.分隔
557        let parts: Vec<&str> = token.split('.').collect();
558        assert_eq!(parts.len(), 3);
559    }
560
561    #[test]
562    fn test_hash_algorithm_from_str() {
563        assert_eq!(
564            HashAlgorithm::from_str("sha256").unwrap(),
565            HashAlgorithm::Sha256
566        );
567        assert_eq!(
568            HashAlgorithm::from_str("SHA256").unwrap(),
569            HashAlgorithm::Sha256
570        );
571        assert!(HashAlgorithm::from_str("invalid").is_err());
572    }
573
574    #[test]
575    fn test_digest_format_from_str() {
576        assert_eq!(DigestFormat::from_str("hex"), DigestFormat::Hex);
577        assert_eq!(DigestFormat::from_str("base64"), DigestFormat::Base64);
578        assert_eq!(DigestFormat::from_str("binary"), DigestFormat::Binary);
579        assert_eq!(DigestFormat::from_str("unknown"), DigestFormat::Hex); // 默认
580    }
581
582    #[test]
583    fn test_hmac_sha512() {
584        let result = hmac_sign("test", "secret", HashAlgorithm::Sha512, DigestFormat::Hex).unwrap();
585        assert_eq!(result.len(), 128); // SHA512输出64字节=128个hex字符
586    }
587
588    #[test]
589    fn test_hash_md5() {
590        let result = hash("test", HashAlgorithm::Md5, DigestFormat::Hex).unwrap();
591        assert_eq!(result.len(), 32); // MD5输出16字节=32个hex字符
592    }
593}