Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for wasm32-pyodide #190

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
38 changes: 34 additions & 4 deletions src/argon2/_password_hasher.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,34 @@
from __future__ import annotations

import os
import platform
import sys

from typing import ClassVar, Literal

from ._utils import Parameters, _check_types, extract_parameters
from .exceptions import InvalidHashError
from .exceptions import InvalidHashError, UnsupportedParamsError
from .low_level import Type, hash_secret, verify_secret
from .profiles import RFC_9106_LOW_MEMORY


# this is a function because tests injects machine and platform
# in PasswordHasher class. A global variable will be populated once and
# we will need to import the file each time so that the mocking will be
# effective. The function is used during initialization so it will not be
# an overhead
def is_wasm() -> bool:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi sorry for the late reaction.

I think I instead of checking for was and polluting the class, I would prefer to have a validate_params_for_platform(params: Parameters) -> None that raises the error.

This can be made efficient by having the sys.platform check at module level and defining different functions. For non-wasm it would be a return None. You can also set DEFAULT_PARALLELISM in the same block

return sys.platform == "emscripten" or platform.machine() in [
"wasm32",
"wasm64",
]


DEFAULT_RANDOM_SALT_LENGTH = RFC_9106_LOW_MEMORY.salt_len
DEFAULT_HASH_LENGTH = RFC_9106_LOW_MEMORY.hash_len
DEFAULT_TIME_COST = RFC_9106_LOW_MEMORY.time_cost
DEFAULT_MEMORY_COST = RFC_9106_LOW_MEMORY.memory_cost
DEFAULT_PARALLELISM = RFC_9106_LOW_MEMORY.parallelism
DEFAULT_PARALLELISM = 1 if is_wasm() else RFC_9106_LOW_MEMORY.parallelism


def _ensure_bytes(s: bytes | str, encoding: str) -> bytes:
Expand Down Expand Up @@ -106,8 +120,7 @@ def __init__(
if e:
raise TypeError(e)

# Cache a Parameters object for check_needs_rehash.
self._parameters = Parameters(
params = Parameters(
type=type,
version=19,
salt_len=salt_len,
Expand All @@ -116,6 +129,16 @@ def __init__(
memory_cost=memory_cost,
parallelism=parallelism,
)

# verify params before accepting
if is_wasm() and parallelism != 1:
msg = (
"within wasm/wasi environments `parallelism` must be set to 1"
)
raise UnsupportedParamsError(msg)

# Cache a Parameters object for check_needs_rehash.
self._parameters = params
self.encoding = encoding

@classmethod
Expand All @@ -128,6 +151,13 @@ def from_parameters(cls, params: Parameters) -> PasswordHasher:

.. versionadded:: 21.2.0
"""
# verify params before accepting
if is_wasm() and params.parallelism != 1:
msg = (
"within wasm/wasi environments `parallelism` must be set to 1"
)
raise UnsupportedParamsError(msg)

ph = cls()
ph._parameters = params

Expand Down
17 changes: 17 additions & 0 deletions src/argon2/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ class InvalidHashError(ValueError):
"""


class UnsupportedParamsError(ValueError):
"""
Raised if the current platform doesn not support the parameters.

Eg. In Wasm32/64, parallelism must be set to 1.

Args:
(Opt) message: a description of the incompatibility
"""

def __init__(
self,
message: str = "Params are not compatible with the current platform",
) -> None:
super().__init__(message)


InvalidHash = InvalidHashError
"""
Deprecated alias for :class:`InvalidHashError`.
Expand Down
39 changes: 38 additions & 1 deletion tests/test_password_hasher.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
# SPDX-License-Identifier: MIT

from unittest import mock

import pytest

from argon2 import PasswordHasher, Type, extract_parameters, profiles
from argon2._password_hasher import _ensure_bytes
from argon2.exceptions import InvalidHash, InvalidHashError
from argon2._utils import Parameters
from argon2.exceptions import (
InvalidHash,
InvalidHashError,
UnsupportedParamsError,
)


class TestEnsureBytes:
Expand Down Expand Up @@ -151,3 +158,33 @@ def test_type_is_configurable(self):
assert Type.I is ph.type is ph._parameters.type
assert Type.I is extract_parameters(ph.hash("foo")).type
assert ph.check_needs_rehash(default_hash)

@mock.patch("sys.platform", "emscripten")
def test_params_on_wasm(self):
"""
Should fail if on wasm and parallelism > 1
"""
for machine in ["wasm32", "wasm64"]:
with mock.patch("platform.machine", return_value=machine):
with pytest.raises(UnsupportedParamsError) as exinfo:
PasswordHasher(parallelism=2)

assert (
str(exinfo.value)
== "within wasm/wasi environments `parallelism` must be set to 1"
)

# last param is parallelism so it should fail
params = Parameters(Type.I, 2, 8, 8, 3, 256, 8)
with pytest.raises(UnsupportedParamsError) as exinfo:
ph = PasswordHasher.from_parameters(params)

assert (
str(exinfo.value)
== "within wasm/wasi environments `parallelism` must be set to 1"
)

# test normal execution
ph = PasswordHasher(parallelism=1)
hash = ph.hash("hello")
assert ph.verify(hash, "hello") is True
Loading