Skip to main content

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///
27/// # Security Warning
28///
29/// **MD5 and SHA-1 are deprecated** due to known cryptographic vulnerabilities.
30/// Use [Sha256](Self::Sha256) or stronger algorithms for new implementations.
31/// The deprecated variants are maintained only for backward compatibility with
32/// legacy exchanges that still require them.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum HashAlgorithm {
35    /// SHA-1 hash algorithm
36    ///
37    /// # Security Warning
38    ///
39    /// **DEPRECATED** - SHA-1 has been cryptographically broken since 2017.
40    /// Collision attacks are practical. Do NOT use for new implementations.
41    /// Use [Sha256](Self::Sha256) instead.
42    #[deprecated(
43        since = "0.1.3",
44        note = "SHA-1 is cryptographically broken, use Sha256 instead"
45    )]
46    Sha1,
47
48    /// SHA-256 hash algorithm (recommended)
49    Sha256,
50
51    /// SHA-384 hash algorithm
52    Sha384,
53
54    /// SHA-512 hash algorithm
55    Sha512,
56
57    /// MD5 hash algorithm
58    ///
59    /// # Security Warning
60    ///
61    /// **DEPRECATED** - MD5 has been cryptographically broken since 2004.
62    /// Collision attacks are practical. Do NOT use for new implementations.
63    /// Use [Sha256](Self::Sha256) instead.
64    #[deprecated(
65        since = "0.1.3",
66        note = "MD5 is cryptographically broken, use Sha256 instead"
67    )]
68    Md5,
69
70    /// Keccak-256 (SHA-3) hash algorithm
71    Keccak,
72}
73
74impl HashAlgorithm {
75    /// Parses a hash algorithm from a string.
76    ///
77    /// # Arguments
78    /// * `s` - Algorithm name (case-insensitive)
79    ///
80    /// # Returns
81    /// Parsed [`HashAlgorithm`] or error if unsupported.
82    // Lint: should_implement_trait
83    // Reason: This method returns Result<Self> with custom error type, not compatible with FromStr trait
84    #[allow(clippy::should_implement_trait)]
85    #[allow(deprecated)]
86    pub fn from_str(s: &str) -> Result<Self> {
87        match s.to_lowercase().as_str() {
88            "sha1" => Ok(HashAlgorithm::Sha1),
89            "sha256" => Ok(HashAlgorithm::Sha256),
90            "sha384" => Ok(HashAlgorithm::Sha384),
91            "sha512" => Ok(HashAlgorithm::Sha512),
92            "md5" => Ok(HashAlgorithm::Md5),
93            "keccak" | "sha3" => Ok(HashAlgorithm::Keccak),
94            _ => Err(Error::invalid_argument(format!(
95                "Unsupported hash algorithm: {s}"
96            ))),
97        }
98    }
99}
100
101impl fmt::Display for HashAlgorithm {
102    #[allow(deprecated)]
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        let s = match self {
105            HashAlgorithm::Sha1 => "sha1",
106            HashAlgorithm::Sha256 => "sha256",
107            HashAlgorithm::Sha384 => "sha384",
108            HashAlgorithm::Sha512 => "sha512",
109            HashAlgorithm::Md5 => "md5",
110            HashAlgorithm::Keccak => "keccak",
111        };
112        write!(f, "{s}")
113    }
114}
115
116/// Output encoding format for cryptographic digests.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum DigestFormat {
119    /// Hexadecimal encoding
120    Hex,
121    /// Base64 encoding
122    Base64,
123    /// Raw binary format
124    Binary,
125}
126
127impl DigestFormat {
128    /// Parses a digest format from a string.
129    ///
130    /// # Arguments
131    /// * `s` - Format name (case-insensitive)
132    ///
133    /// # Returns
134    /// Parsed [`DigestFormat`], defaults to `Hex` if unrecognized.
135    // Lint: should_implement_trait
136    // Reason: This method has different semantics than FromStr - it never fails and has a default
137    #[allow(clippy::should_implement_trait)]
138    // Lint: match_same_arms
139    // Reason: Explicit "hex" match documents the supported format, wildcard is the fallback default
140    #[allow(clippy::match_same_arms)]
141    pub fn from_str(s: &str) -> Self {
142        match s.to_lowercase().as_str() {
143            "hex" => DigestFormat::Hex,
144            "base64" => DigestFormat::Base64,
145            "binary" => DigestFormat::Binary,
146            _ => DigestFormat::Hex,
147        }
148    }
149}
150
151/// Generates an HMAC signature for a message.
152///
153/// # Arguments
154/// * `message` - Message to sign
155/// * `secret` - Secret key for HMAC
156/// * `algorithm` - Hash algorithm to use
157/// * `digest` - Output encoding format
158///
159/// # Returns
160/// Encoded signature string.
161///
162/// # Errors
163/// Returns error if algorithm is not supported for HMAC.
164///
165/// # Examples
166/// ```
167/// use ccxt_core::auth::{hmac_sign, HashAlgorithm, DigestFormat};
168///
169/// let signature = hmac_sign(
170///     "test message",
171///     "secret_key",
172///     HashAlgorithm::Sha256,
173///     DigestFormat::Hex
174/// ).unwrap();
175/// ```
176pub fn hmac_sign(
177    message: &str,
178    secret: &str,
179    algorithm: HashAlgorithm,
180    digest: DigestFormat,
181) -> Result<String> {
182    #[allow(deprecated)]
183    let signature = match algorithm {
184        HashAlgorithm::Sha256 => hmac_sha256(message.as_bytes(), secret.as_bytes()),
185        HashAlgorithm::Sha512 => hmac_sha512(message.as_bytes(), secret.as_bytes()),
186        HashAlgorithm::Sha384 => hmac_sha384(message.as_bytes(), secret.as_bytes()),
187        HashAlgorithm::Md5 => hmac_md5(message.as_bytes(), secret.as_bytes()),
188        _ => {
189            return Err(Error::invalid_argument(format!(
190                "HMAC does not support {algorithm} algorithm"
191            )));
192        }
193    };
194
195    Ok(encode_bytes(&signature, digest))
196}
197
198/// Computes HMAC-SHA256 signature.
199///
200/// # Panics
201///
202/// This function will never panic. The `expect()` call is safe because HMAC algorithms
203/// accept keys of any length (including empty keys). The `InvalidLength` error from
204/// `new_from_slice` is only returned for algorithms with fixed key requirements,
205/// which does not apply to HMAC.
206fn hmac_sha256(data: &[u8], secret: &[u8]) -> Vec<u8> {
207    type HmacSha256 = Hmac<Sha256>;
208    // SAFETY: HMAC accepts keys of any length - this cannot fail
209    let mut mac = HmacSha256::new_from_slice(secret)
210        .expect("HMAC-SHA256 accepts keys of any length; this is an infallible operation");
211    mac.update(data);
212    mac.finalize().into_bytes().to_vec()
213}
214
215/// Computes HMAC-SHA512 signature.
216///
217/// # Panics
218///
219/// This function will never panic. See `hmac_sha256` for rationale.
220fn hmac_sha512(data: &[u8], secret: &[u8]) -> Vec<u8> {
221    type HmacSha512 = Hmac<Sha512>;
222    // SAFETY: HMAC accepts keys of any length - this cannot fail
223    let mut mac = HmacSha512::new_from_slice(secret)
224        .expect("HMAC-SHA512 accepts keys of any length; this is an infallible operation");
225    mac.update(data);
226    mac.finalize().into_bytes().to_vec()
227}
228
229/// Computes HMAC-SHA384 signature.
230///
231/// # Panics
232///
233/// This function will never panic. See `hmac_sha256` for rationale.
234fn hmac_sha384(data: &[u8], secret: &[u8]) -> Vec<u8> {
235    type HmacSha384 = Hmac<Sha384>;
236    // SAFETY: HMAC accepts keys of any length - this cannot fail
237    let mut mac = HmacSha384::new_from_slice(secret)
238        .expect("HMAC-SHA384 accepts keys of any length; this is an infallible operation");
239    mac.update(data);
240    mac.finalize().into_bytes().to_vec()
241}
242
243/// Computes HMAC-MD5 signature.
244///
245/// # Panics
246///
247/// This function will never panic. See `hmac_sha256` for rationale.
248fn hmac_md5(data: &[u8], secret: &[u8]) -> Vec<u8> {
249    use md5::Md5;
250    type HmacMd5 = Hmac<Md5>;
251    // SAFETY: HMAC accepts keys of any length - this cannot fail
252    let mut mac = HmacMd5::new_from_slice(secret)
253        .expect("HMAC-MD5 accepts keys of any length; this is an infallible operation");
254    mac.update(data);
255    mac.finalize().into_bytes().to_vec()
256}
257
258/// Computes a cryptographic hash (one-way, keyless).
259///
260/// # Arguments
261/// * `data` - Data to hash
262/// * `algorithm` - Hash algorithm to use
263/// * `digest` - Output encoding format
264///
265/// # Returns
266/// Encoded hash string.
267///
268/// # Examples
269/// ```
270/// use ccxt_core::auth::{hash, HashAlgorithm, DigestFormat};
271///
272/// let hashed = hash("test", HashAlgorithm::Sha256, DigestFormat::Hex).unwrap();
273/// ```
274pub fn hash(data: &str, algorithm: HashAlgorithm, digest: DigestFormat) -> Result<String> {
275    #[allow(deprecated)]
276    let hash_bytes = match algorithm {
277        HashAlgorithm::Sha256 => hash_sha256(data.as_bytes()),
278        HashAlgorithm::Sha512 => hash_sha512(data.as_bytes()),
279        HashAlgorithm::Sha384 => hash_sha384(data.as_bytes()),
280        HashAlgorithm::Sha1 => hash_sha1(data.as_bytes()),
281        HashAlgorithm::Md5 => hash_md5(data.as_bytes()),
282        HashAlgorithm::Keccak => hash_keccak(data.as_bytes()),
283    };
284
285    Ok(encode_bytes(&hash_bytes, digest))
286}
287
288/// Computes SHA-256 hash.
289fn hash_sha256(data: &[u8]) -> Vec<u8> {
290    let mut hasher = Sha256::new();
291    hasher.update(data);
292    hasher.finalize().to_vec()
293}
294
295/// Computes SHA-512 hash.
296fn hash_sha512(data: &[u8]) -> Vec<u8> {
297    let mut hasher = Sha512::new();
298    hasher.update(data);
299    hasher.finalize().to_vec()
300}
301
302/// Computes SHA-384 hash.
303fn hash_sha384(data: &[u8]) -> Vec<u8> {
304    let mut hasher = Sha384::new();
305    hasher.update(data);
306    hasher.finalize().to_vec()
307}
308
309/// Computes SHA-1 hash.
310fn hash_sha1(data: &[u8]) -> Vec<u8> {
311    let mut hasher = Sha1::new();
312    hasher.update(data);
313    hasher.finalize().to_vec()
314}
315
316/// Computes MD5 hash.
317fn hash_md5(data: &[u8]) -> Vec<u8> {
318    use md5::{Digest, Md5};
319    let mut hasher = Md5::new();
320    hasher.update(data);
321    hasher.finalize().to_vec()
322}
323
324/// Computes Keccak-256 hash (Ethereum-compatible).
325fn hash_keccak(data: &[u8]) -> Vec<u8> {
326    let mut hasher = Keccak256::new();
327    hasher.update(data);
328    hasher.finalize().to_vec()
329}
330
331// /// Generates an RSA signature using PKCS1v15 padding.
332// ///
333// /// # Arguments
334// /// * `data` - Data to sign
335// /// * `private_key_pem` - RSA private key in PEM format
336// /// * `algorithm` - Hash algorithm for digest
337// ///
338// /// # Returns
339// /// Base64-encoded signature.
340// ///
341// /// # Errors
342// /// Returns error if key is invalid or algorithm unsupported.
343// pub fn rsa_sign(data: &str, private_key_pem: &str, algorithm: HashAlgorithm) -> Result<String> {
344//     let private_key = RsaPrivateKey::from_pkcs1_pem(private_key_pem)
345//         .map_err(|e| Error::invalid_argument(format!("Invalid RSA private key: {}", e)))?;
346//
347//     let hashed = match algorithm {
348//         HashAlgorithm::Sha256 => hash_sha256(data.as_bytes()),
349//         HashAlgorithm::Sha384 => hash_sha384(data.as_bytes()),
350//         HashAlgorithm::Sha512 => hash_sha512(data.as_bytes()),
351//         HashAlgorithm::Sha1 => hash_sha1(data.as_bytes()),
352//         HashAlgorithm::Md5 => hash_md5(data.as_bytes()),
353//         _ => {
354//             return Err(Error::invalid_argument(format!(
355//                 "RSA does not support {} algorithm",
356//                 algorithm
357//             )));
358//         }
359//     };
360//
361//     let signature = match algorithm {
362//         HashAlgorithm::Sha256 => {
363//             let signing_key = SigningKey::<Sha256>::new_unprefixed(private_key);
364//             let mut rng = rand::rngs::ThreadRng::default();
365//             signing_key.sign_with_rng(&mut rng, &hashed).to_bytes()
366//         }
367//         HashAlgorithm::Sha384 => {
368//             let signing_key = SigningKey::<Sha384>::new_unprefixed(private_key);
369//             let mut rng = rand::rngs::ThreadRng::default();
370//             signing_key.sign_with_rng(&mut rng, &hashed).to_bytes()
371//         }
372//         HashAlgorithm::Sha512 => {
373//             let signing_key = SigningKey::<Sha512>::new_unprefixed(private_key);
374//             let mut rng = rand::rngs::ThreadRng::default();
375//             signing_key.sign_with_rng(&mut rng, &hashed).to_bytes()
376//         }
377//         _ => {
378//             return Err(Error::invalid_argument(format!(
379//                 "Unsupported RSA hash algorithm: {}",
380//                 algorithm
381//             )));
382//         }
383//     };
384//
385//     Ok(general_purpose::STANDARD.encode(signature.as_ref()))
386// }
387
388/// Generates an EdDSA signature using Ed25519.
389///
390/// # Arguments
391/// * `data` - Data to sign
392/// * `secret_key` - 32-byte Ed25519 seed key
393///
394/// # Returns
395/// Base64 URL-encoded signature (without padding).
396///
397/// # Errors
398/// Returns error if secret key is not exactly 32 bytes.
399pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result<String> {
400    use ed25519_dalek::{Signature, Signer, SigningKey};
401
402    if secret_key.len() != 32 {
403        return Err(Error::invalid_argument(format!(
404            "Ed25519 secret key must be 32 bytes, got {}",
405            secret_key.len()
406        )));
407    }
408
409    let signing_key = SigningKey::from_bytes(
410        secret_key
411            .try_into()
412            .map_err(|_| Error::invalid_argument("Invalid Ed25519 key".to_string()))?,
413    );
414
415    let signature: Signature = signing_key.sign(data.as_bytes());
416    let encoded = general_purpose::STANDARD.encode(signature.to_bytes());
417
418    Ok(base64_to_base64url(&encoded, true))
419}
420
421/// Generates a JWT (JSON Web Token).
422///
423/// # Arguments
424/// * `payload` - JWT payload as JSON object
425/// * `secret` - Secret key for signing (must be at least 32 characters for security)
426/// * `algorithm` - Hash algorithm for HMAC (supports Sha256, Sha384, Sha512)
427/// * `header_options` - Optional additional header fields
428///
429/// # Returns
430/// Complete JWT string (header.payload.signature).
431///
432/// # Errors
433/// Returns error if:
434/// - JSON serialization fails
435/// - Signing fails
436/// - Unsupported algorithm is provided (only HS256, HS384, HS512 are supported)
437/// - Secret key is less than 32 characters (security requirement)
438///
439/// # Security
440///
441/// **Minimum secret length: 32 characters**
442///
443/// Short secrets are vulnerable to brute-force attacks. The minimum 32-character
444/// requirement ensures sufficient entropy for secure HMAC signatures.
445///
446/// # Examples
447/// ```
448/// use ccxt_core::auth::{jwt_sign, HashAlgorithm};
449/// use serde_json::json;
450///
451/// let payload = json!({
452///     "user_id": "123",
453///     "exp": 1234567890
454/// });
455///
456/// // Using HS256 with a strong secret (at least 32 characters)
457/// let token = jwt_sign(
458///     &payload,
459///     "my-very-secure-secret-key-at-least-32-chars",
460///     HashAlgorithm::Sha256,
461///     None
462/// ).unwrap();
463///
464/// // Using HS512
465/// let token_512 = jwt_sign(
466///     &payload,
467///     "my-very-secure-secret-key-at-least-32-chars",
468///     HashAlgorithm::Sha512,
469///     None
470/// ).unwrap();
471/// ```
472pub fn jwt_sign(
473    payload: &serde_json::Value,
474    secret: &str,
475    algorithm: HashAlgorithm,
476    header_options: Option<serde_json::Map<String, serde_json::Value>>,
477) -> Result<String> {
478    // Validate secret key strength (minimum 32 characters for security)
479    const MIN_SECRET_LENGTH: usize = 32;
480
481    if secret.len() < MIN_SECRET_LENGTH {
482        return Err(Error::invalid_argument(format!(
483            "JWT secret must be at least {MIN_SECRET_LENGTH} characters for security. \
484            Provided: {} characters. \
485            Use a longer secret to protect against brute-force attacks.",
486            secret.len()
487        )));
488    }
489
490    // Map HashAlgorithm to JWT algorithm string
491    let alg_str = match algorithm {
492        HashAlgorithm::Sha256 => "HS256",
493        HashAlgorithm::Sha384 => "HS384",
494        HashAlgorithm::Sha512 => "HS512",
495        _ => {
496            return Err(Error::invalid_argument(format!(
497                "JWT does not support {algorithm} algorithm. Supported algorithms: HS256, HS384, HS512"
498            )));
499        }
500    };
501
502    let mut header = serde_json::Map::new();
503    header.insert(
504        "alg".to_string(),
505        serde_json::Value::String(alg_str.to_string()),
506    );
507    header.insert(
508        "typ".to_string(),
509        serde_json::Value::String("JWT".to_string()),
510    );
511
512    if let Some(options) = header_options {
513        for (key, value) in options {
514            header.insert(key, value);
515        }
516    }
517
518    let header_json = serde_json::to_string(&header)?;
519    let payload_json = serde_json::to_string(payload)?;
520
521    let encoded_header = general_purpose::URL_SAFE_NO_PAD.encode(header_json.as_bytes());
522    let encoded_payload = general_purpose::URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
523
524    let token = format!("{encoded_header}.{encoded_payload}");
525
526    let signature = hmac_sign(&token, secret, algorithm, DigestFormat::Base64)?;
527
528    let signature_url = base64_to_base64url(&signature, true);
529
530    Ok(format!("{token}.{signature_url}"))
531}
532
533/// Encodes bytes to the specified format.
534fn encode_bytes(bytes: &[u8], format: DigestFormat) -> String {
535    match format {
536        DigestFormat::Hex => hex::encode(bytes),
537        DigestFormat::Base64 => general_purpose::STANDARD.encode(bytes),
538        DigestFormat::Binary => String::from_utf8_lossy(bytes).to_string(),
539    }
540}
541
542/// Converts standard Base64 to Base64 URL format.
543///
544/// # Arguments
545/// * `base64_str` - Standard Base64 string
546/// * `strip_padding` - Whether to remove padding (`=`)
547///
548/// # Returns
549/// Base64 URL-encoded string.
550pub fn base64_to_base64url(base64_str: &str, strip_padding: bool) -> String {
551    let mut result = base64_str.replace('+', "-").replace('/', "_");
552    if strip_padding {
553        result = result.trim_end_matches('=').to_string();
554    }
555    result
556}
557
558/// Decodes a Base64 URL-encoded string.
559///
560/// # Arguments
561/// * `base64url` - Base64 URL-encoded string
562///
563/// # Returns
564/// Decoded bytes.
565///
566/// # Errors
567/// Returns error if decoding fails.
568pub fn base64url_decode(base64url: &str) -> Result<Vec<u8>> {
569    let base64 = base64url.replace('-', "+").replace('_', "/");
570
571    let padding = match base64.len() % 4 {
572        2 => "==",
573        3 => "=",
574        _ => "",
575    };
576    let base64_padded = format!("{base64}{padding}");
577
578    general_purpose::STANDARD
579        .decode(base64_padded.as_bytes())
580        .map_err(|e| Error::invalid_argument(format!("Base64 decode error: {e}")))
581}
582
583#[cfg(test)]
584#[allow(clippy::disallowed_methods)] // unwrap() is acceptable in tests
585mod tests {
586    use super::*;
587
588    #[test]
589    fn test_hmac_sha256_hex() {
590        let result = hmac_sign("test", "secret", HashAlgorithm::Sha256, DigestFormat::Hex).unwrap();
591        assert_eq!(
592            result,
593            "0329a06b62cd16b33eb6792be8c60b158d89a2ee3a876fce9a881ebb488c0914"
594        );
595    }
596
597    #[test]
598    fn test_hmac_sha256_base64() {
599        let result = hmac_sign(
600            "test",
601            "secret",
602            HashAlgorithm::Sha256,
603            DigestFormat::Base64,
604        )
605        .unwrap();
606        assert!(!result.is_empty());
607    }
608
609    #[test]
610    fn test_hash_sha256() {
611        let result = hash("test", HashAlgorithm::Sha256, DigestFormat::Hex).unwrap();
612        assert_eq!(
613            result,
614            "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
615        );
616    }
617
618    #[test]
619    fn test_hash_keccak() {
620        let result = hash("test", HashAlgorithm::Keccak, DigestFormat::Hex).unwrap();
621        assert_eq!(result.len(), 64); // Keccak256 outputs 32 bytes = 64 hex characters
622    }
623
624    #[test]
625    fn test_base64_to_base64url() {
626        let base64 = "abc+def/ghi==";
627        let base64url = base64_to_base64url(base64, true);
628        assert_eq!(base64url, "abc-def_ghi");
629    }
630
631    #[test]
632    fn test_base64url_decode() {
633        let base64url = "abc-def_ghg";
634        let decoded = base64url_decode(base64url).unwrap();
635        assert!(!decoded.is_empty());
636    }
637
638    #[test]
639    fn test_jwt_sign() {
640        use serde_json::json;
641
642        let payload = json!({
643            "user_id": "123",
644            "exp": 1234567890
645        });
646
647        // Use a strong secret (at least 32 characters)
648        let token = jwt_sign(
649            &payload,
650            "my-very-secure-secret-key-at-least-32-chars",
651            HashAlgorithm::Sha256,
652            None,
653        )
654        .unwrap();
655
656        // JWT should have 3 parts separated by .
657        let parts: Vec<&str> = token.split('.').collect();
658        assert_eq!(parts.len(), 3);
659
660        // Verify header contains correct algorithm
661        let header_bytes = base64url_decode(parts[0]).unwrap();
662        let header_str = String::from_utf8(header_bytes).unwrap();
663        assert!(header_str.contains("HS256"));
664    }
665
666    #[test]
667    fn test_jwt_sign_with_different_algorithms() {
668        use serde_json::json;
669
670        let payload = json!({
671            "user_id": "123",
672            "exp": 1234567890
673        });
674
675        let strong_secret = "my-very-secure-secret-key-at-least-32-chars";
676
677        // Test HS256
678        let token_256 = jwt_sign(&payload, strong_secret, HashAlgorithm::Sha256, None).unwrap();
679        let parts_256: Vec<&str> = token_256.split('.').collect();
680        let header_256 = String::from_utf8(base64url_decode(parts_256[0]).unwrap()).unwrap();
681        assert!(header_256.contains("HS256"));
682
683        // Test HS384
684        let token_384 = jwt_sign(&payload, strong_secret, HashAlgorithm::Sha384, None).unwrap();
685        let parts_384: Vec<&str> = token_384.split('.').collect();
686        let header_384 = String::from_utf8(base64url_decode(parts_384[0]).unwrap()).unwrap();
687        assert!(header_384.contains("HS384"));
688
689        // Test HS512
690        let token_512 = jwt_sign(&payload, strong_secret, HashAlgorithm::Sha512, None).unwrap();
691        let parts_512: Vec<&str> = token_512.split('.').collect();
692        let header_512 = String::from_utf8(base64url_decode(parts_512[0]).unwrap()).unwrap();
693        assert!(header_512.contains("HS512"));
694    }
695
696    #[test]
697    fn test_jwt_sign_unsupported_algorithm() {
698        use serde_json::json;
699
700        let payload = json!({
701            "user_id": "123",
702            "exp": 1234567890
703        });
704
705        // SHA1 is not supported for JWT
706        let result = jwt_sign(
707            &payload,
708            "my-very-secure-secret-key-at-least-32-chars",
709            HashAlgorithm::Sha1,
710            None,
711        );
712        assert!(result.is_err());
713        assert!(result.unwrap_err().to_string().contains("does not support"));
714
715        // MD5 is not supported for JWT
716        let result = jwt_sign(
717            &payload,
718            "my-very-secure-secret-key-at-least-32-chars",
719            HashAlgorithm::Md5,
720            None,
721        );
722        assert!(result.is_err());
723
724        // Keccak is not supported for JWT
725        let result = jwt_sign(
726            &payload,
727            "my-very-secure-secret-key-at-least-32-chars",
728            HashAlgorithm::Keccak,
729            None,
730        );
731        assert!(result.is_err());
732    }
733
734    #[test]
735    fn test_hash_algorithm_from_str() {
736        assert_eq!(
737            HashAlgorithm::from_str("sha256").unwrap(),
738            HashAlgorithm::Sha256
739        );
740        assert_eq!(
741            HashAlgorithm::from_str("SHA256").unwrap(),
742            HashAlgorithm::Sha256
743        );
744        assert!(HashAlgorithm::from_str("invalid").is_err());
745    }
746
747    #[test]
748    fn test_digest_format_from_str() {
749        assert_eq!(DigestFormat::from_str("hex"), DigestFormat::Hex);
750        assert_eq!(DigestFormat::from_str("base64"), DigestFormat::Base64);
751        assert_eq!(DigestFormat::from_str("binary"), DigestFormat::Binary);
752        assert_eq!(DigestFormat::from_str("unknown"), DigestFormat::Hex); // defaults to Hex
753    }
754
755    #[test]
756    fn test_hmac_sha512() {
757        let result = hmac_sign("test", "secret", HashAlgorithm::Sha512, DigestFormat::Hex).unwrap();
758        assert_eq!(result.len(), 128); // SHA512 outputs 64 bytes = 128 hex characters
759    }
760
761    #[test]
762    fn test_hash_md5() {
763        let result = hash("test", HashAlgorithm::Md5, DigestFormat::Hex).unwrap();
764        assert_eq!(result.len(), 32); // MD5 outputs 16 bytes = 32 hex characters
765    }
766
767    #[test]
768    fn test_jwt_sign_weak_secret_rejected() {
769        use serde_json::json;
770
771        let payload = json!({
772            "user_id": "123",
773            "exp": 1234567890
774        });
775
776        // Test various weak secrets (less than 32 characters)
777        let weak_secrets = vec![
778            "",                            // empty
779            "a",                           // 1 character
780            "short",                       // 5 characters
781            "this-is-still-too-short-123", // 31 characters (just below threshold)
782        ];
783
784        for weak_secret in weak_secrets {
785            let result = jwt_sign(&payload, weak_secret, HashAlgorithm::Sha256, None);
786            assert!(
787                result.is_err(),
788                "Secret with {} characters should be rejected",
789                weak_secret.len()
790            );
791
792            if let Err(e) = result {
793                let error_msg = e.to_string();
794                assert!(
795                    error_msg.contains("32 characters"),
796                    "Error message should mention 32 character requirement"
797                );
798                assert!(
799                    error_msg.contains("security"),
800                    "Error message should mention security"
801                );
802            }
803        }
804    }
805
806    #[test]
807    fn test_jwt_sign_minimum_valid_secret() {
808        use serde_json::json;
809
810        let payload = json!({
811            "user_id": "123",
812            "exp": 1234567890
813        });
814
815        // Test exactly 32 characters (should succeed)
816        let exactly_32_chars = "12345678901234567890123456789012"; // exactly 32 chars
817        let result = jwt_sign(&payload, exactly_32_chars, HashAlgorithm::Sha256, None);
818        assert!(
819            result.is_ok(),
820            "Secret with exactly 32 characters should be accepted"
821        );
822
823        // Test 33 characters (should succeed)
824        let exactly_33_chars = "123456789012345678901234567890123"; // exactly 33 chars
825        let result = jwt_sign(&payload, exactly_33_chars, HashAlgorithm::Sha256, None);
826        assert!(
827            result.is_ok(),
828            "Secret with 33 characters should be accepted"
829        );
830    }
831
832    #[test]
833    fn test_jwt_sign_with_custom_header_and_strong_secret() {
834        use serde_json::json;
835
836        let payload = json!({
837            "user_id": "123",
838            "exp": 1234567890
839        });
840
841        let mut custom_header = serde_json::Map::new();
842        custom_header.insert(
843            "kid".to_string(),
844            serde_json::Value::String("key-123".to_string()),
845        );
846
847        let strong_secret = "my-very-secure-secret-key-at-least-32-chars";
848        let token = jwt_sign(
849            &payload,
850            strong_secret,
851            HashAlgorithm::Sha256,
852            Some(custom_header),
853        )
854        .unwrap();
855
856        let parts: Vec<&str> = token.split('.').collect();
857        assert_eq!(parts.len(), 3);
858
859        let header_bytes = base64url_decode(parts[0]).unwrap();
860        let header_str = String::from_utf8(header_bytes).unwrap();
861        assert!(header_str.contains("\"kid\":\"key-123\""));
862    }
863}