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