@@ -942,8 +942,236 @@ impl X963Kdf {
942942 }
943943}
944944
945+ // NO-COVERAGE-START
946+ #[ pyo3:: pyclass(
947+ module = "cryptography.hazmat.primitives.kdf.concatkdf" ,
948+ name = "ConcatKDFHash"
949+ ) ]
950+ // NO-COVERAGE-END
951+ struct ConcatKdfHash {
952+ algorithm : pyo3:: Py < pyo3:: PyAny > ,
953+ length : usize ,
954+ otherinfo : pyo3:: Py < pyo3:: types:: PyBytes > ,
955+ used : bool ,
956+ }
957+
958+ #[ pyo3:: pymethods]
959+ impl ConcatKdfHash {
960+ #[ new]
961+ #[ pyo3( signature = ( algorithm, length, otherinfo, backend=None ) ) ]
962+ fn new (
963+ py : pyo3:: Python < ' _ > ,
964+ algorithm : pyo3:: Py < pyo3:: PyAny > ,
965+ length : usize ,
966+ otherinfo : Option < pyo3:: Py < pyo3:: types:: PyBytes > > ,
967+ backend : Option < pyo3:: Bound < ' _ , pyo3:: PyAny > > ,
968+ ) -> CryptographyResult < Self > {
969+ _ = backend;
970+
971+ let algorithm_bound = algorithm. bind ( py) ;
972+ let digest_size = algorithm_bound
973+ . getattr ( pyo3:: intern!( py, "digest_size" ) ) ?
974+ . extract :: < usize > ( ) ?;
975+
976+ let max_len = digest_size. saturating_mul ( u32:: MAX as usize ) ;
977+ if length > max_len {
978+ return Err ( CryptographyError :: from (
979+ pyo3:: exceptions:: PyValueError :: new_err ( format ! (
980+ "Cannot derive keys larger than {max_len} bits."
981+ ) ) ,
982+ ) ) ;
983+ }
984+
985+ let otherinfo_bytes =
986+ otherinfo. unwrap_or_else ( || pyo3:: types:: PyBytes :: new ( py, b"" ) . into ( ) ) ;
987+
988+ Ok ( ConcatKdfHash {
989+ algorithm,
990+ length,
991+ otherinfo : otherinfo_bytes,
992+ used : false ,
993+ } )
994+ }
995+
996+ fn derive < ' p > (
997+ & mut self ,
998+ py : pyo3:: Python < ' p > ,
999+ key_material : CffiBuf < ' _ > ,
1000+ ) -> CryptographyResult < pyo3:: Bound < ' p , pyo3:: types:: PyBytes > > {
1001+ if self . used {
1002+ return Err ( exceptions:: already_finalized_error ( ) ) ;
1003+ }
1004+ self . used = true ;
1005+
1006+ let algorithm_bound = self . algorithm . bind ( py) ;
1007+
1008+ let mut output = Vec :: with_capacity ( self . length ) ;
1009+ let mut counter = 1u32 ;
1010+
1011+ while output. len ( ) < self . length {
1012+ let mut hash_obj = hashes:: Hash :: new ( py, algorithm_bound, None ) ?;
1013+ hash_obj. update_bytes ( & counter. to_be_bytes ( ) ) ?;
1014+ hash_obj. update_bytes ( key_material. as_bytes ( ) ) ?;
1015+ hash_obj. update_bytes ( self . otherinfo . as_bytes ( py) ) ?;
1016+ let block = hash_obj. finalize ( py) ?;
1017+ output. extend_from_slice ( block. as_bytes ( ) ) ;
1018+ counter += 1 ;
1019+ }
1020+
1021+ output. truncate ( self . length ) ;
1022+ Ok ( pyo3:: types:: PyBytes :: new ( py, & output) )
1023+ }
1024+
1025+ fn verify (
1026+ & mut self ,
1027+ py : pyo3:: Python < ' _ > ,
1028+ key_material : CffiBuf < ' _ > ,
1029+ expected_key : CffiBuf < ' _ > ,
1030+ ) -> CryptographyResult < ( ) > {
1031+ let actual = self . derive ( py, key_material) ?;
1032+ let actual_bytes = actual. as_bytes ( ) ;
1033+ let expected_bytes = expected_key. as_bytes ( ) ;
1034+
1035+ if !constant_time:: bytes_eq ( actual_bytes, expected_bytes) {
1036+ return Err ( CryptographyError :: from ( exceptions:: InvalidKey :: new_err (
1037+ "Keys do not match." ,
1038+ ) ) ) ;
1039+ }
1040+
1041+ Ok ( ( ) )
1042+ }
1043+ }
1044+
1045+ // NO-COVERAGE-START
1046+ #[ pyo3:: pyclass(
1047+ module = "cryptography.hazmat.primitives.kdf.concatkdf" ,
1048+ name = "ConcatKDFHMAC"
1049+ ) ]
1050+ // NO-COVERAGE-END
1051+ struct ConcatKdfHmac {
1052+ algorithm : pyo3:: Py < pyo3:: PyAny > ,
1053+ length : usize ,
1054+ salt : pyo3:: Py < pyo3:: types:: PyBytes > ,
1055+ otherinfo : pyo3:: Py < pyo3:: types:: PyBytes > ,
1056+ used : bool ,
1057+ }
1058+
1059+ #[ pyo3:: pymethods]
1060+ impl ConcatKdfHmac {
1061+ #[ new]
1062+ #[ pyo3( signature = ( algorithm, length, salt, otherinfo, backend=None ) ) ]
1063+ fn new (
1064+ py : pyo3:: Python < ' _ > ,
1065+ algorithm : pyo3:: Py < pyo3:: PyAny > ,
1066+ length : usize ,
1067+ salt : Option < pyo3:: Py < pyo3:: types:: PyBytes > > ,
1068+ otherinfo : Option < pyo3:: Py < pyo3:: types:: PyBytes > > ,
1069+ backend : Option < pyo3:: Bound < ' _ , pyo3:: PyAny > > ,
1070+ ) -> CryptographyResult < Self > {
1071+ _ = backend;
1072+
1073+ let algorithm_bound = algorithm. bind ( py) ;
1074+ let digest_size = algorithm_bound
1075+ . getattr ( pyo3:: intern!( py, "digest_size" ) ) ?
1076+ . extract :: < usize > ( ) ?;
1077+
1078+ let max_len = digest_size. saturating_mul ( u32:: MAX as usize ) ;
1079+ if length > max_len {
1080+ return Err ( CryptographyError :: from (
1081+ pyo3:: exceptions:: PyValueError :: new_err ( format ! (
1082+ "Cannot derive keys larger than {max_len} bits."
1083+ ) ) ,
1084+ ) ) ;
1085+ }
1086+
1087+ // Check for block_size (required for HMAC)
1088+ let block_size = algorithm_bound. getattr ( pyo3:: intern!( py, "block_size" ) ) ?;
1089+
1090+ if block_size. is_none ( ) {
1091+ let name = algorithm_bound
1092+ . getattr ( pyo3:: intern!( py, "name" ) ) ?
1093+ . extract :: < String > ( ) ?;
1094+ return Err ( CryptographyError :: from (
1095+ pyo3:: exceptions:: PyTypeError :: new_err ( format ! (
1096+ "{name} is unsupported for ConcatKDF"
1097+ ) ) ,
1098+ ) ) ;
1099+ }
1100+
1101+ let block_size_val = block_size. extract :: < usize > ( ) ?;
1102+
1103+ // Default salt to zeros of block_size length
1104+ let salt_bytes = if let Some ( s) = salt {
1105+ s
1106+ } else {
1107+ pyo3:: types:: PyBytes :: new ( py, & vec ! [ 0u8 ; block_size_val] ) . into ( )
1108+ } ;
1109+
1110+ let otherinfo_bytes =
1111+ otherinfo. unwrap_or_else ( || pyo3:: types:: PyBytes :: new ( py, b"" ) . into ( ) ) ;
1112+
1113+ Ok ( ConcatKdfHmac {
1114+ algorithm,
1115+ length,
1116+ salt : salt_bytes,
1117+ otherinfo : otherinfo_bytes,
1118+ used : false ,
1119+ } )
1120+ }
1121+
1122+ fn derive < ' p > (
1123+ & mut self ,
1124+ py : pyo3:: Python < ' p > ,
1125+ key_material : CffiBuf < ' _ > ,
1126+ ) -> CryptographyResult < pyo3:: Bound < ' p , pyo3:: types:: PyBytes > > {
1127+ if self . used {
1128+ return Err ( exceptions:: already_finalized_error ( ) ) ;
1129+ }
1130+ self . used = true ;
1131+
1132+ let algorithm_bound = self . algorithm . bind ( py) ;
1133+
1134+ let mut output = Vec :: with_capacity ( self . length ) ;
1135+ let mut counter = 1u32 ;
1136+
1137+ while output. len ( ) < self . length {
1138+ let mut hmac = Hmac :: new_bytes ( py, self . salt . as_bytes ( py) , algorithm_bound) ?;
1139+ hmac. update_bytes ( & counter. to_be_bytes ( ) ) ?;
1140+ hmac. update_bytes ( key_material. as_bytes ( ) ) ?;
1141+ hmac. update_bytes ( self . otherinfo . as_bytes ( py) ) ?;
1142+ let result = hmac. finalize_bytes ( ) ?;
1143+ output. extend_from_slice ( & result) ;
1144+ counter += 1 ;
1145+ }
1146+
1147+ output. truncate ( self . length ) ;
1148+ Ok ( pyo3:: types:: PyBytes :: new ( py, & output) )
1149+ }
1150+
1151+ fn verify (
1152+ & mut self ,
1153+ py : pyo3:: Python < ' _ > ,
1154+ key_material : CffiBuf < ' _ > ,
1155+ expected_key : CffiBuf < ' _ > ,
1156+ ) -> CryptographyResult < ( ) > {
1157+ let actual = self . derive ( py, key_material) ?;
1158+ let actual_bytes = actual. as_bytes ( ) ;
1159+ let expected_bytes = expected_key. as_bytes ( ) ;
1160+
1161+ if !constant_time:: bytes_eq ( actual_bytes, expected_bytes) {
1162+ return Err ( CryptographyError :: from ( exceptions:: InvalidKey :: new_err (
1163+ "Keys do not match." ,
1164+ ) ) ) ;
1165+ }
1166+
1167+ Ok ( ( ) )
1168+ }
1169+ }
1170+
9451171#[ pyo3:: pymodule( gil_used = false ) ]
9461172pub ( crate ) mod kdf {
9471173 #[ pymodule_export]
948- use super :: { Argon2id , Hkdf , HkdfExpand , Pbkdf2Hmac , Scrypt , X963Kdf } ;
1174+ use super :: {
1175+ Argon2id , ConcatKdfHash , ConcatKdfHmac , Hkdf , HkdfExpand , Pbkdf2Hmac , Scrypt , X963Kdf ,
1176+ } ;
9491177}
0 commit comments