Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions Lib/hmac.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 9 additions & 39 deletions Lib/test/test_hmac.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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):

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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):
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