Skip to content

Commit 9e2cee1

Browse files
committed
_hashlib.HMAC
1 parent 2a611c9 commit 9e2cee1

File tree

2 files changed

+139
-19
lines changed

2 files changed

+139
-19
lines changed

Lib/test/test_hmac.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,23 +1066,19 @@ def test_hmac_digest_digestmod_parameter(self):
10661066
):
10671067
self.hmac_digest(b'key', b'msg', value)
10681068

1069-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module '_hashlib' has no attribute 'HMAC'. Did you mean: 'exc_type'?
10701069
def test_internal_types(self):
10711070
return super().test_internal_types()
10721071

10731072
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module '_hashlib' has no attribute 'hmac_digest'
10741073
def test_digest(self):
10751074
return super().test_digest()
10761075

1077-
@unittest.expectedFailure # TODO: RUSTPYTHON
10781076
def test_constructor(self):
10791077
return super().test_constructor()
10801078

1081-
@unittest.expectedFailure # TODO: RUSTPYTHON
10821079
def test_constructor_missing_digestmod(self):
10831080
return super().test_constructor_missing_digestmod()
10841081

1085-
@unittest.expectedFailure # TODO: RUSTPYTHON
10861082
def test_constructor_unknown_digestmod(self):
10871083
return super().test_constructor_unknown_digestmod()
10881084

@@ -1265,15 +1261,12 @@ def HMAC(self, key, msg=None):
12651261
def gil_minsize(self):
12661262
return _hashlib._GIL_MINSIZE
12671263

1268-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module '_hashlib' has no attribute '_GIL_MINSIZE'
12691264
def test_update_large(self):
12701265
return super().test_update_large()
12711266

1272-
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'NoneType'
12731267
def test_update_exceptions(self):
12741268
return super().test_update_exceptions()
12751269

1276-
@unittest.expectedFailure # TODO: RUSTPYTHON
12771270
def test_update(self):
12781271
return super().test_update()
12791272

@@ -1328,7 +1321,6 @@ def test_realcopy(self):
13281321
self.assertNotEqual(id(h1._inner), id(h2._inner))
13291322
self.assertNotEqual(id(h1._outer), id(h2._outer))
13301323

1331-
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'NoneType'
13321324
def test_equality(self):
13331325
# Testing if the copy has the same digests.
13341326
h1 = hmac.HMAC(b"key", digestmod="sha256")
@@ -1337,7 +1329,6 @@ def test_equality(self):
13371329
self.assertEqual(h1.digest(), h2.digest())
13381330
self.assertEqual(h1.hexdigest(), h2.hexdigest())
13391331

1340-
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: a bytes-like object is required, not 'NoneType'
13411332
def test_equality_new(self):
13421333
# Testing if the copy has the same digests with hmac.new().
13431334
h1 = hmac.new(b"key", digestmod="sha256")
@@ -1383,11 +1374,9 @@ class OpenSSLCopyTestCase(ExtensionCopyTestCase, unittest.TestCase):
13831374
def init(self, h):
13841375
h._init_openssl_hmac(b"key", b"msg", digestmod="sha256")
13851376

1386-
@unittest.expectedFailure # TODO: RUSTPYTHON; _hashlib.UnsupportedDigestmodError: unsupported hash type
13871377
def test_attributes(self):
13881378
return super().test_attributes()
13891379

1390-
@unittest.expectedFailure # TODO: RUSTPYTHON; _hashlib.UnsupportedDigestmodError: unsupported hash type
13911380
def test_realcopy(self):
13921381
return super().test_realcopy()
13931382

crates/stdlib/src/hashlib.rs

Lines changed: 139 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub mod _hashlib {
1919
types::{Constructor, Representable},
2020
};
2121
use blake2::{Blake2b512, Blake2s256};
22-
use digest::{DynDigest, core_api::BlockSizeUser};
22+
use digest::{DynDigest, OutputSizeUser, core_api::BlockSizeUser};
2323
use digest::{ExtendableOutput, Update};
2424
use dyn_clone::{DynClone, clone_trait_object};
2525
use hmac::Mac;
@@ -258,6 +258,105 @@ pub mod _hashlib {
258258
)
259259
}
260260

261+
// Object-safe HMAC trait for type-erased dispatch
262+
trait DynHmac: Send + Sync {
263+
fn dyn_update(&mut self, data: &[u8]);
264+
fn dyn_finalize(&self) -> Vec<u8>;
265+
fn dyn_clone(&self) -> Box<dyn DynHmac>;
266+
}
267+
268+
struct TypedHmac<D>(D);
269+
270+
impl<D> DynHmac for TypedHmac<D>
271+
where
272+
D: Mac + Clone + Send + Sync + 'static,
273+
{
274+
fn dyn_update(&mut self, data: &[u8]) {
275+
Mac::update(&mut self.0, data);
276+
}
277+
278+
fn dyn_finalize(&self) -> Vec<u8> {
279+
self.0.clone().finalize().into_bytes().to_vec()
280+
}
281+
282+
fn dyn_clone(&self) -> Box<dyn DynHmac> {
283+
Box::new(TypedHmac(self.0.clone()))
284+
}
285+
}
286+
287+
#[pyattr]
288+
#[pyclass(module = "_hashlib", name = "HMAC")]
289+
#[derive(PyPayload)]
290+
pub struct PyHmac {
291+
algo_name: String,
292+
digest_size: usize,
293+
block_size: usize,
294+
ctx: PyRwLock<Box<dyn DynHmac>>,
295+
}
296+
297+
impl core::fmt::Debug for PyHmac {
298+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
299+
write!(f, "HMAC {}", self.algo_name)
300+
}
301+
}
302+
303+
#[pyclass(with(Representable), flags(IMMUTABLETYPE))]
304+
impl PyHmac {
305+
#[pyslot]
306+
fn slot_new(_cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
307+
Err(vm.new_type_error("cannot create '_hashlib.HMAC' instances".to_owned()))
308+
}
309+
310+
#[pygetset]
311+
fn name(&self) -> String {
312+
format!("hmac-{}", self.algo_name)
313+
}
314+
315+
#[pygetset]
316+
fn digest_size(&self) -> usize {
317+
self.digest_size
318+
}
319+
320+
#[pygetset]
321+
fn block_size(&self) -> usize {
322+
self.block_size
323+
}
324+
325+
#[pymethod]
326+
fn update(&self, msg: ArgBytesLike) {
327+
msg.with_ref(|bytes| self.ctx.write().dyn_update(bytes));
328+
}
329+
330+
#[pymethod]
331+
fn digest(&self) -> PyBytes {
332+
self.ctx.read().dyn_finalize().into()
333+
}
334+
335+
#[pymethod]
336+
fn hexdigest(&self) -> String {
337+
hex::encode(self.ctx.read().dyn_finalize())
338+
}
339+
340+
#[pymethod]
341+
fn copy(&self) -> Self {
342+
Self {
343+
algo_name: self.algo_name.clone(),
344+
digest_size: self.digest_size,
345+
block_size: self.block_size,
346+
ctx: PyRwLock::new(self.ctx.read().dyn_clone()),
347+
}
348+
}
349+
}
350+
351+
impl Representable for PyHmac {
352+
fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
353+
Ok(format!(
354+
"<{} HMAC object @ {:#x}>",
355+
zelf.algo_name, zelf as *const _ as usize
356+
))
357+
}
358+
}
359+
261360
#[pyattr]
262361
#[pyclass(module = "_hashlib", name = "HASH")]
263362
#[derive(PyPayload)]
@@ -646,18 +745,50 @@ pub mod _hashlib {
646745
#[pyarg(positional)]
647746
key: ArgBytesLike,
648747
#[pyarg(any, optional)]
649-
msg: OptionalArg<ArgBytesLike>,
748+
msg: OptionalArg<Option<ArgBytesLike>>,
650749
#[pyarg(named, optional)]
651750
digestmod: OptionalArg<PyObjectRef>,
652751
}
653752

654753
#[pyfunction]
655-
fn hmac_new(args: NewHMACHashArgs, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
656-
let _ = args;
657-
Err(vm.new_exception_msg(
658-
UnsupportedDigestmodError::static_type().to_owned(),
659-
"unsupported hash type".to_owned(),
660-
))
754+
fn hmac_new(args: NewHMACHashArgs, vm: &VirtualMachine) -> PyResult<PyHmac> {
755+
let digestmod = args.digestmod.into_option().ok_or_else(|| {
756+
vm.new_type_error("Missing required parameter 'digestmod'.".to_owned())
757+
})?;
758+
let name = resolve_digestmod(&digestmod, vm)?;
759+
760+
let key_buf = args.key.borrow_buf();
761+
let msg_data = args.msg.flatten();
762+
763+
macro_rules! make_hmac {
764+
($hash_ty:ty) => {{
765+
let mut mac = <hmac::Hmac<$hash_ty> as Mac>::new_from_slice(&key_buf)
766+
.map_err(|_| vm.new_value_error("invalid key length".to_owned()))?;
767+
if let Some(ref m) = msg_data {
768+
m.with_ref(|bytes| Mac::update(&mut mac, bytes));
769+
}
770+
Ok(PyHmac {
771+
algo_name: name,
772+
digest_size: <$hash_ty as OutputSizeUser>::output_size(),
773+
block_size: <$hash_ty as BlockSizeUser>::block_size(),
774+
ctx: PyRwLock::new(Box::new(TypedHmac(mac))),
775+
})
776+
}};
777+
}
778+
779+
match name.as_str() {
780+
"md5" => make_hmac!(Md5),
781+
"sha1" => make_hmac!(Sha1),
782+
"sha224" => make_hmac!(Sha224),
783+
"sha256" => make_hmac!(Sha256),
784+
"sha384" => make_hmac!(Sha384),
785+
"sha512" => make_hmac!(Sha512),
786+
"sha3_224" => make_hmac!(Sha3_224),
787+
"sha3_256" => make_hmac!(Sha3_256),
788+
"sha3_384" => make_hmac!(Sha3_384),
789+
"sha3_512" => make_hmac!(Sha3_512),
790+
_ => Err(unsupported_hash(&name, vm)),
791+
}
661792
}
662793

663794
#[pyfunction]

0 commit comments

Comments
 (0)