@@ -23,18 +23,50 @@ use sha3::Keccak256;
2323use std:: fmt;
2424
2525/// 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.
2633#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
2734pub enum HashAlgorithm {
2835 /// 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+ ) ]
2946 Sha1 ,
30- /// SHA-256 hash algorithm
47+
48+ /// SHA-256 hash algorithm (recommended)
3149 Sha256 ,
50+
3251 /// SHA-384 hash algorithm
3352 Sha384 ,
53+
3454 /// SHA-512 hash algorithm
3555 Sha512 ,
56+
3657 /// 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+ ) ]
3768 Md5 ,
69+
3870 /// Keccak-256 (SHA-3) hash algorithm
3971 Keccak ,
4072}
@@ -50,6 +82,7 @@ impl HashAlgorithm {
5082 // Lint: should_implement_trait
5183 // Reason: This method returns Result<Self> with custom error type, not compatible with FromStr trait
5284 #[ allow( clippy:: should_implement_trait) ]
85+ #[ allow( deprecated) ]
5386 pub fn from_str ( s : & str ) -> Result < Self > {
5487 match s. to_lowercase ( ) . as_str ( ) {
5588 "sha1" => Ok ( HashAlgorithm :: Sha1 ) ,
@@ -66,6 +99,7 @@ impl HashAlgorithm {
6699}
67100
68101impl fmt:: Display for HashAlgorithm {
102+ #[ allow( deprecated) ]
69103 fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
70104 let s = match self {
71105 HashAlgorithm :: Sha1 => "sha1" ,
@@ -145,6 +179,7 @@ pub fn hmac_sign(
145179 algorithm : HashAlgorithm ,
146180 digest : DigestFormat ,
147181) -> Result < String > {
182+ #[ allow( deprecated) ]
148183 let signature = match algorithm {
149184 HashAlgorithm :: Sha256 => hmac_sha256 ( message. as_bytes ( ) , secret. as_bytes ( ) ) ,
150185 HashAlgorithm :: Sha512 => hmac_sha512 ( message. as_bytes ( ) , secret. as_bytes ( ) ) ,
@@ -237,6 +272,7 @@ fn hmac_md5(data: &[u8], secret: &[u8]) -> Vec<u8> {
237272/// let hashed = hash("test", HashAlgorithm::Sha256, DigestFormat::Hex).unwrap();
238273/// ```
239274pub fn hash ( data : & str , algorithm : HashAlgorithm , digest : DigestFormat ) -> Result < String > {
275+ #[ allow( deprecated) ]
240276 let hash_bytes = match algorithm {
241277 HashAlgorithm :: Sha256 => hash_sha256 ( data. as_bytes ( ) ) ,
242278 HashAlgorithm :: Sha512 => hash_sha512 ( data. as_bytes ( ) ) ,
@@ -386,7 +422,7 @@ pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result<String> {
386422///
387423/// # Arguments
388424/// * `payload` - JWT payload as JSON object
389- /// * `secret` - Secret key for signing
425+ /// * `secret` - Secret key for signing (must be at least 32 characters for security)
390426/// * `algorithm` - Hash algorithm for HMAC (supports Sha256, Sha384, Sha512)
391427/// * `header_options` - Optional additional header fields
392428///
@@ -398,6 +434,14 @@ pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result<String> {
398434/// - JSON serialization fails
399435/// - Signing fails
400436/// - 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.
401445///
402446/// # Examples
403447/// ```
@@ -409,18 +453,40 @@ pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result<String> {
409453/// "exp": 1234567890
410454/// });
411455///
412- /// // Using HS256
413- /// let token = jwt_sign(&payload, "secret", HashAlgorithm::Sha256, None).unwrap();
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();
414463///
415464/// // Using HS512
416- /// let token_512 = jwt_sign(&payload, "secret", HashAlgorithm::Sha512, None).unwrap();
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();
417471/// ```
418472pub fn jwt_sign (
419473 payload : & serde_json:: Value ,
420474 secret : & str ,
421475 algorithm : HashAlgorithm ,
422476 header_options : Option < serde_json:: Map < String , serde_json:: Value > > ,
423477) -> 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+
424490 // Map HashAlgorithm to JWT algorithm string
425491 let alg_str = match algorithm {
426492 HashAlgorithm :: Sha256 => "HS256" ,
@@ -552,7 +618,7 @@ mod tests {
552618 #[ test]
553619 fn test_hash_keccak ( ) {
554620 let result = hash ( "test" , HashAlgorithm :: Keccak , DigestFormat :: Hex ) . unwrap ( ) ;
555- assert_eq ! ( result. len( ) , 64 ) ; // Keccak256输出32字节=64个hex字符
621+ assert_eq ! ( result. len( ) , 64 ) ; // Keccak256 outputs 32 bytes = 64 hex characters
556622 }
557623
558624 #[ test]
@@ -578,9 +644,16 @@ mod tests {
578644 "exp" : 1234567890
579645 } ) ;
580646
581- let token = jwt_sign ( & payload, "secret" , HashAlgorithm :: Sha256 , None ) . unwrap ( ) ;
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 ( ) ;
582655
583- // JWT应该有3部分,用.分隔
656+ // JWT should have 3 parts separated by .
584657 let parts: Vec < & str > = token. split ( '.' ) . collect ( ) ;
585658 assert_eq ! ( parts. len( ) , 3 ) ;
586659
@@ -599,20 +672,22 @@ mod tests {
599672 "exp" : 1234567890
600673 } ) ;
601674
675+ let strong_secret = "my-very-secure-secret-key-at-least-32-chars" ;
676+
602677 // Test HS256
603- let token_256 = jwt_sign ( & payload, "secret" , HashAlgorithm :: Sha256 , None ) . unwrap ( ) ;
678+ let token_256 = jwt_sign ( & payload, strong_secret , HashAlgorithm :: Sha256 , None ) . unwrap ( ) ;
604679 let parts_256: Vec < & str > = token_256. split ( '.' ) . collect ( ) ;
605680 let header_256 = String :: from_utf8 ( base64url_decode ( parts_256[ 0 ] ) . unwrap ( ) ) . unwrap ( ) ;
606681 assert ! ( header_256. contains( "HS256" ) ) ;
607682
608683 // Test HS384
609- let token_384 = jwt_sign ( & payload, "secret" , HashAlgorithm :: Sha384 , None ) . unwrap ( ) ;
684+ let token_384 = jwt_sign ( & payload, strong_secret , HashAlgorithm :: Sha384 , None ) . unwrap ( ) ;
610685 let parts_384: Vec < & str > = token_384. split ( '.' ) . collect ( ) ;
611686 let header_384 = String :: from_utf8 ( base64url_decode ( parts_384[ 0 ] ) . unwrap ( ) ) . unwrap ( ) ;
612687 assert ! ( header_384. contains( "HS384" ) ) ;
613688
614689 // Test HS512
615- let token_512 = jwt_sign ( & payload, "secret" , HashAlgorithm :: Sha512 , None ) . unwrap ( ) ;
690+ let token_512 = jwt_sign ( & payload, strong_secret , HashAlgorithm :: Sha512 , None ) . unwrap ( ) ;
616691 let parts_512: Vec < & str > = token_512. split ( '.' ) . collect ( ) ;
617692 let header_512 = String :: from_utf8 ( base64url_decode ( parts_512[ 0 ] ) . unwrap ( ) ) . unwrap ( ) ;
618693 assert ! ( header_512. contains( "HS512" ) ) ;
@@ -628,16 +703,31 @@ mod tests {
628703 } ) ;
629704
630705 // SHA1 is not supported for JWT
631- let result = jwt_sign ( & payload, "secret" , HashAlgorithm :: Sha1 , None ) ;
706+ let result = jwt_sign (
707+ & payload,
708+ "my-very-secure-secret-key-at-least-32-chars" ,
709+ HashAlgorithm :: Sha1 ,
710+ None ,
711+ ) ;
632712 assert ! ( result. is_err( ) ) ;
633713 assert ! ( result. unwrap_err( ) . to_string( ) . contains( "does not support" ) ) ;
634714
635715 // MD5 is not supported for JWT
636- let result = jwt_sign ( & payload, "secret" , HashAlgorithm :: Md5 , None ) ;
716+ let result = jwt_sign (
717+ & payload,
718+ "my-very-secure-secret-key-at-least-32-chars" ,
719+ HashAlgorithm :: Md5 ,
720+ None ,
721+ ) ;
637722 assert ! ( result. is_err( ) ) ;
638723
639724 // Keccak is not supported for JWT
640- let result = jwt_sign ( & payload, "secret" , HashAlgorithm :: Keccak , None ) ;
725+ let result = jwt_sign (
726+ & payload,
727+ "my-very-secure-secret-key-at-least-32-chars" ,
728+ HashAlgorithm :: Keccak ,
729+ None ,
730+ ) ;
641731 assert ! ( result. is_err( ) ) ;
642732 }
643733
@@ -659,18 +749,115 @@ mod tests {
659749 assert_eq ! ( DigestFormat :: from_str( "hex" ) , DigestFormat :: Hex ) ;
660750 assert_eq ! ( DigestFormat :: from_str( "base64" ) , DigestFormat :: Base64 ) ;
661751 assert_eq ! ( DigestFormat :: from_str( "binary" ) , DigestFormat :: Binary ) ;
662- assert_eq ! ( DigestFormat :: from_str( "unknown" ) , DigestFormat :: Hex ) ; // 默认
752+ assert_eq ! ( DigestFormat :: from_str( "unknown" ) , DigestFormat :: Hex ) ; // defaults to Hex
663753 }
664754
665755 #[ test]
666756 fn test_hmac_sha512 ( ) {
667757 let result = hmac_sign ( "test" , "secret" , HashAlgorithm :: Sha512 , DigestFormat :: Hex ) . unwrap ( ) ;
668- assert_eq ! ( result. len( ) , 128 ) ; // SHA512输出64字节=128个hex字符
758+ assert_eq ! ( result. len( ) , 128 ) ; // SHA512 outputs 64 bytes = 128 hex characters
669759 }
670760
671761 #[ test]
672762 fn test_hash_md5 ( ) {
673763 let result = hash ( "test" , HashAlgorithm :: Md5 , DigestFormat :: Hex ) . unwrap ( ) ;
674- assert_eq ! ( result. len( ) , 32 ) ; // MD5输出16字节=32个hex字符
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\" " ) ) ;
675862 }
676863}
0 commit comments