1use crate::error::{Error, Result};
12use base64::{Engine as _, engine::general_purpose};
13use hmac::{Hmac, Mac};
14use sha1::Sha1;
21use sha2::{Digest, Sha256, Sha384, Sha512};
22use sha3::Keccak256;
23use std::fmt;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum HashAlgorithm {
35 #[deprecated(
43 since = "0.1.3",
44 note = "SHA-1 is cryptographically broken, use Sha256 instead"
45 )]
46 Sha1,
47
48 Sha256,
50
51 Sha384,
53
54 Sha512,
56
57 #[deprecated(
65 since = "0.1.3",
66 note = "MD5 is cryptographically broken, use Sha256 instead"
67 )]
68 Md5,
69
70 Keccak,
72}
73
74impl HashAlgorithm {
75 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum DigestFormat {
119 Hex,
121 Base64,
123 Binary,
125}
126
127impl DigestFormat {
128 #[allow(clippy::should_implement_trait)]
138 #[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
151pub 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
198fn hmac_sha256(data: &[u8], secret: &[u8]) -> Vec<u8> {
207 type HmacSha256 = Hmac<Sha256>;
208 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
215fn hmac_sha512(data: &[u8], secret: &[u8]) -> Vec<u8> {
221 type HmacSha512 = Hmac<Sha512>;
222 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
229fn hmac_sha384(data: &[u8], secret: &[u8]) -> Vec<u8> {
235 type HmacSha384 = Hmac<Sha384>;
236 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
243fn hmac_md5(data: &[u8], secret: &[u8]) -> Vec<u8> {
249 use md5::Md5;
250 type HmacMd5 = Hmac<Md5>;
251 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
258pub 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
288fn hash_sha256(data: &[u8]) -> Vec<u8> {
290 let mut hasher = Sha256::new();
291 hasher.update(data);
292 hasher.finalize().to_vec()
293}
294
295fn hash_sha512(data: &[u8]) -> Vec<u8> {
297 let mut hasher = Sha512::new();
298 hasher.update(data);
299 hasher.finalize().to_vec()
300}
301
302fn hash_sha384(data: &[u8]) -> Vec<u8> {
304 let mut hasher = Sha384::new();
305 hasher.update(data);
306 hasher.finalize().to_vec()
307}
308
309fn hash_sha1(data: &[u8]) -> Vec<u8> {
311 let mut hasher = Sha1::new();
312 hasher.update(data);
313 hasher.finalize().to_vec()
314}
315
316fn 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
324fn hash_keccak(data: &[u8]) -> Vec<u8> {
326 let mut hasher = Keccak256::new();
327 hasher.update(data);
328 hasher.finalize().to_vec()
329}
330
331pub 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
421pub 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 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 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
533fn 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
542pub 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
558pub 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)] mod 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); }
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 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 let parts: Vec<&str> = token.split('.').collect();
658 assert_eq!(parts.len(), 3);
659
660 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 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 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 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 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 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 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); }
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); }
760
761 #[test]
762 fn test_hash_md5() {
763 let result = hash("test", HashAlgorithm::Md5, DigestFormat::Hex).unwrap();
764 assert_eq!(result.len(), 32); }
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 let weak_secrets = vec![
778 "", "a", "short", "this-is-still-too-short-123", ];
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 let exactly_32_chars = "12345678901234567890123456789012"; 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 let exactly_33_chars = "123456789012345678901234567890123"; 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}