diff --git a/Lib/hmac.py b/Lib/hmac.py index 16022c9ceb5..2d6016cda11 100644 --- a/Lib/hmac.py +++ b/Lib/hmac.py @@ -159,6 +159,7 @@ def copy(self): # Call __new__ directly to avoid the expensive __init__. other = self.__class__.__new__(self.__class__) other.digest_size = self.digest_size + other.block_size = self.block_size if self._hmac: other._hmac = self._hmac.copy() other._inner = other._outer = None diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index d5023a6c1b3..1506bb7982a 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -1066,22 +1066,6 @@ def test_hmac_digest_digestmod_parameter(self): ): self.hmac_digest(b'key', b'msg', value) - @unittest.expectedFailure # TODO: RUSTPYTHON - def test_constructor(self): - return super().test_constructor() - - @unittest.expectedFailure # TODO: RUSTPYTHON - def test_constructor_missing_digestmod(self): - return super().test_constructor_missing_digestmod() - - @unittest.expectedFailure # TODO: RUSTPYTHON - def test_constructor_unknown_digestmod(self): - return super().test_constructor_unknown_digestmod() - - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module '_hashlib' has no attribute 'HMAC'. Did you mean: 'exc_type'? - def test_internal_types(self): - return super().test_internal_types() - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module '_hashlib' has no attribute 'hmac_digest' def test_digest(self): return super().test_digest() @@ -1137,6 +1121,15 @@ def test_properties(self): self.assertEqual(h.digest_size, self.digest_size) self.assertEqual(h.block_size, self.block_size) + def test_copy(self): + # Test a generic copy() and the attributes it exposes. + # See https://github.com/python/cpython/issues/142451. + h1 = self.hmac_new(b"my secret key", digestmod=self.digestname) + h2 = h1.copy() + self.assertEqual(h1.name, h2.name) + self.assertEqual(h1.digest_size, h2.digest_size) + self.assertEqual(h1.block_size, h2.block_size) + def test_repr(self): # HMAC object representation may differ across implementations raise NotImplementedError @@ -1160,7 +1153,6 @@ def test_repr(self): @hashlib_helper.requires_openssl_hashdigest('sha256') -@unittest.skip("TODO: RUSTPYTHON; AttributeError: module '_hashlib' has no attribute 'HMAC'") class OpenSSLSanityTestCase(ThroughOpenSSLAPIMixin, SanityTestCaseMixin, unittest.TestCase): @@ -1257,18 +1249,6 @@ def HMAC(self, key, msg=None): def gil_minsize(self): return _hashlib._GIL_MINSIZE - @unittest.expectedFailure # TODO: RUSTPYTHON - def test_update(self): - return super().test_update() - - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module '_hashlib' has no attribute '_GIL_MINSIZE' - def test_update_large(self): - return super().test_update_large() - - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'NoneType' - def test_update_exceptions(self): - return super().test_update_exceptions() - class BuiltinUpdateTestCase(BuiltinModuleMixin, UpdateTestCaseMixin, unittest.TestCase): @@ -1320,7 +1300,6 @@ def test_realcopy(self): self.assertNotEqual(id(h1._inner), id(h2._inner)) self.assertNotEqual(id(h1._outer), id(h2._outer)) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'NoneType' def test_equality(self): # Testing if the copy has the same digests. h1 = hmac.HMAC(b"key", digestmod="sha256") @@ -1329,7 +1308,6 @@ def test_equality(self): self.assertEqual(h1.digest(), h2.digest()) self.assertEqual(h1.hexdigest(), h2.hexdigest()) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'NoneType' def test_equality_new(self): # Testing if the copy has the same digests with hmac.new(). h1 = hmac.new(b"key", digestmod="sha256") @@ -1375,14 +1353,6 @@ class OpenSSLCopyTestCase(ExtensionCopyTestCase, unittest.TestCase): def init(self, h): h._init_openssl_hmac(b"key", b"msg", digestmod="sha256") - @unittest.expectedFailure # TODO: RUSTPYTHON; _hashlib.UnsupportedDigestmodError: unsupported hash type - def test_attributes(self): - return super().test_attributes() - - @unittest.expectedFailure # TODO: RUSTPYTHON; _hashlib.UnsupportedDigestmodError: unsupported hash type - def test_realcopy(self): - return super().test_realcopy() - @hashlib_helper.requires_builtin_hmac() class BuiltinCopyTestCase(ExtensionCopyTestCase, unittest.TestCase): diff --git a/crates/stdlib/src/hashlib.rs b/crates/stdlib/src/hashlib.rs index c66e4a8cd28..5097d804a79 100644 --- a/crates/stdlib/src/hashlib.rs +++ b/crates/stdlib/src/hashlib.rs @@ -19,7 +19,7 @@ pub mod _hashlib { types::{Constructor, Representable}, }; use blake2::{Blake2b512, Blake2s256}; - use digest::{DynDigest, core_api::BlockSizeUser}; + use digest::{DynDigest, OutputSizeUser, core_api::BlockSizeUser}; use digest::{ExtendableOutput, Update}; use dyn_clone::{DynClone, clone_trait_object}; use hmac::Mac; @@ -258,6 +258,105 @@ pub mod _hashlib { ) } + // Object-safe HMAC trait for type-erased dispatch + trait DynHmac: Send + Sync { + fn dyn_update(&mut self, data: &[u8]); + fn dyn_finalize(&self) -> Vec; + fn dyn_clone(&self) -> Box; + } + + struct TypedHmac(D); + + impl DynHmac for TypedHmac + where + D: Mac + Clone + Send + Sync + 'static, + { + fn dyn_update(&mut self, data: &[u8]) { + Mac::update(&mut self.0, data); + } + + fn dyn_finalize(&self) -> Vec { + self.0.clone().finalize().into_bytes().to_vec() + } + + fn dyn_clone(&self) -> Box { + Box::new(TypedHmac(self.0.clone())) + } + } + + #[pyattr] + #[pyclass(module = "_hashlib", name = "HMAC")] + #[derive(PyPayload)] + pub struct PyHmac { + algo_name: String, + digest_size: usize, + block_size: usize, + ctx: PyRwLock>, + } + + impl core::fmt::Debug for PyHmac { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "HMAC {}", self.algo_name) + } + } + + #[pyclass(with(Representable), flags(IMMUTABLETYPE))] + impl PyHmac { + #[pyslot] + fn slot_new(_cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult { + Err(vm.new_type_error("cannot create '_hashlib.HMAC' instances".to_owned())) + } + + #[pygetset] + fn name(&self) -> String { + format!("hmac-{}", self.algo_name) + } + + #[pygetset] + fn digest_size(&self) -> usize { + self.digest_size + } + + #[pygetset] + fn block_size(&self) -> usize { + self.block_size + } + + #[pymethod] + fn update(&self, msg: ArgBytesLike) { + msg.with_ref(|bytes| self.ctx.write().dyn_update(bytes)); + } + + #[pymethod] + fn digest(&self) -> PyBytes { + self.ctx.read().dyn_finalize().into() + } + + #[pymethod] + fn hexdigest(&self) -> String { + hex::encode(self.ctx.read().dyn_finalize()) + } + + #[pymethod] + fn copy(&self) -> Self { + Self { + algo_name: self.algo_name.clone(), + digest_size: self.digest_size, + block_size: self.block_size, + ctx: PyRwLock::new(self.ctx.read().dyn_clone()), + } + } + } + + impl Representable for PyHmac { + fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + Ok(format!( + "<{} HMAC object @ {:#x}>", + zelf.algo_name, zelf as *const _ as usize + )) + } + } + #[pyattr] #[pyclass(module = "_hashlib", name = "HASH")] #[derive(PyPayload)] @@ -646,18 +745,50 @@ pub mod _hashlib { #[pyarg(positional)] key: ArgBytesLike, #[pyarg(any, optional)] - msg: OptionalArg, + msg: OptionalArg>, #[pyarg(named, optional)] digestmod: OptionalArg, } #[pyfunction] - fn hmac_new(args: NewHMACHashArgs, vm: &VirtualMachine) -> PyResult { - let _ = args; - Err(vm.new_exception_msg( - UnsupportedDigestmodError::static_type().to_owned(), - "unsupported hash type".to_owned(), - )) + fn hmac_new(args: NewHMACHashArgs, vm: &VirtualMachine) -> PyResult { + let digestmod = args.digestmod.into_option().ok_or_else(|| { + vm.new_type_error("Missing required parameter 'digestmod'.".to_owned()) + })?; + let name = resolve_digestmod(&digestmod, vm)?; + + let key_buf = args.key.borrow_buf(); + let msg_data = args.msg.flatten(); + + macro_rules! make_hmac { + ($hash_ty:ty) => {{ + let mut mac = as Mac>::new_from_slice(&key_buf) + .map_err(|_| vm.new_value_error("invalid key length".to_owned()))?; + if let Some(ref m) = msg_data { + m.with_ref(|bytes| Mac::update(&mut mac, bytes)); + } + Ok(PyHmac { + algo_name: name, + digest_size: <$hash_ty as OutputSizeUser>::output_size(), + block_size: <$hash_ty as BlockSizeUser>::block_size(), + ctx: PyRwLock::new(Box::new(TypedHmac(mac))), + }) + }}; + } + + match name.as_str() { + "md5" => make_hmac!(Md5), + "sha1" => make_hmac!(Sha1), + "sha224" => make_hmac!(Sha224), + "sha256" => make_hmac!(Sha256), + "sha384" => make_hmac!(Sha384), + "sha512" => make_hmac!(Sha512), + "sha3_224" => make_hmac!(Sha3_224), + "sha3_256" => make_hmac!(Sha3_256), + "sha3_384" => make_hmac!(Sha3_384), + "sha3_512" => make_hmac!(Sha3_512), + _ => Err(unsupported_hash(&name, vm)), + } } #[pyfunction]