Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
_hashlib.HMAC
  • Loading branch information
youknowone committed Feb 15, 2026
commit 10c393f06392b1dd04169f2bf6521e0fc46e89d4
38 changes: 0 additions & 38 deletions Lib/test/test_hmac.py
Original file line number Diff line number Diff line change
Expand Up @@ -1066,26 +1066,10 @@ def test_hmac_digest_digestmod_parameter(self):
):
self.hmac_digest(b'key', b'msg', value)

@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()

@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()


class BuiltinConstructorTestCase(ThroughBuiltinAPIMixin,
ExtensionConstructorTestCaseMixin,
Expand Down Expand Up @@ -1265,18 +1249,6 @@ def HMAC(self, key, msg=None):
def gil_minsize(self):
return _hashlib._GIL_MINSIZE

@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()

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_update(self):
return super().test_update()


class BuiltinUpdateTestCase(BuiltinModuleMixin,
UpdateTestCaseMixin, unittest.TestCase):
Expand Down Expand Up @@ -1328,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")
Expand All @@ -1337,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")
Expand Down Expand Up @@ -1383,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):
Expand Down
147 changes: 139 additions & 8 deletions crates/stdlib/src/hashlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<u8>;
fn dyn_clone(&self) -> Box<dyn DynHmac>;
}

struct TypedHmac<D>(D);

impl<D> DynHmac for TypedHmac<D>
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<u8> {
self.0.clone().finalize().into_bytes().to_vec()
}

fn dyn_clone(&self) -> Box<dyn DynHmac> {
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<Box<dyn DynHmac>>,
}

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<Self>, _vm: &VirtualMachine) -> PyResult<String> {
Ok(format!(
"<{} HMAC object @ {:#x}>",
zelf.algo_name, zelf as *const _ as usize
))
}
}

#[pyattr]
#[pyclass(module = "_hashlib", name = "HASH")]
#[derive(PyPayload)]
Expand Down Expand Up @@ -646,18 +745,50 @@ pub mod _hashlib {
#[pyarg(positional)]
key: ArgBytesLike,
#[pyarg(any, optional)]
msg: OptionalArg<ArgBytesLike>,
msg: OptionalArg<Option<ArgBytesLike>>,
#[pyarg(named, optional)]
digestmod: OptionalArg<PyObjectRef>,
}

#[pyfunction]
fn hmac_new(args: NewHMACHashArgs, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
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<PyHmac> {
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 = <hmac::Hmac<$hash_ty> 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]
Expand Down
Loading