Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-altcha for openSUSE:Factory checked in at 2026-06-30 15:12:27 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-altcha (Old) and /work/SRC/openSUSE:Factory/.python-altcha.new.11887 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-altcha" Tue Jun 30 15:12:27 2026 rev:7 rq:1362529 version:2.0.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-altcha/python-altcha.changes 2026-04-18 21:35:25.895256682 +0200 +++ /work/SRC/openSUSE:Factory/.python-altcha.new.11887/python-altcha.changes 2026-06-30 15:12:58.559112544 +0200 @@ -1,0 +2,7 @@ +Mon Jun 29 20:33:23 UTC 2026 - Dirk Müller <[email protected]> + +- update to 2.0.2: + * fix: support bytes as hmac keys + * feat: payload.from_base64 and from_dict + +------------------------------------------------------------------- Old: ---- altcha-2.0.0.tar.gz New: ---- altcha-2.0.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-altcha.spec ++++++ --- /var/tmp/diff_new_pack.VB8d31/_old 2026-06-30 15:12:59.543145844 +0200 +++ /var/tmp/diff_new_pack.VB8d31/_new 2026-06-30 15:12:59.543145844 +0200 @@ -17,7 +17,7 @@ Name: python-altcha -Version: 2.0.0 +Version: 2.0.2 Release: 0 Summary: A library for creating and verifying challenges for ALTCHA License: MIT ++++++ altcha-2.0.0.tar.gz -> altcha-2.0.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/altcha-2.0.0/PKG-INFO new/altcha-2.0.2/PKG-INFO --- old/altcha-2.0.0/PKG-INFO 2026-04-07 13:56:00.222773300 +0200 +++ new/altcha-2.0.2/PKG-INFO 2026-06-20 12:15:40.391074000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: altcha -Version: 2.0.0 +Version: 2.0.2 Summary: A library for creating and verifying challenges for ALTCHA. Home-page: https://github.com/altcha-org/altcha-lib-py Author: Daniel Regeci diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/altcha-2.0.0/altcha/v1.py new/altcha-2.0.2/altcha/v1.py --- old/altcha-2.0.0/altcha/v1.py 2026-04-07 13:55:56.000000000 +0200 +++ new/altcha-2.0.2/altcha/v1.py 2026-06-20 12:15:36.000000000 +0200 @@ -18,12 +18,15 @@ AlgoType = Literal["SHA-1", "SHA-256", "SHA-512"] -class PayloadType(TypedDict, total=False): +class _PayloadTypeRequired(TypedDict): algorithm: AlgoType challenge: str number: int salt: str signature: str + + +class PayloadType(_PayloadTypeRequired, total=False): verificationData: str verified: bool @@ -53,7 +56,7 @@ algorithm: AlgoType = DEFAULT_ALGORITHM, max_number: int = DEFAULT_MAX_NUMBER, salt_length: int = DEFAULT_SALT_LENGTH, - hmac_key: str = "", + hmac_key: str | bytes = "", salt: str = "", number: int | None = None, expires: datetime.datetime | None = None, @@ -149,6 +152,41 @@ """Convert the Payload to a base64 encoded JSON string.""" return base64.b64encode(json.dumps(self.to_dict()).encode()).decode() + @classmethod + def from_dict(cls, data: PayloadType) -> "Payload": + """Create a Payload from a dictionary.""" + return cls( + algorithm=cast(AlgoType, data["algorithm"]), + challenge=data["challenge"], + number=data["number"], + salt=data["salt"], + signature=data["signature"], + ) + + @classmethod + def from_base64(cls, encoded: str) -> "Payload": + """Create a Payload from a base64 encoded JSON string.""" + data = cast(PayloadType, json.loads(base64.b64decode(encoded).decode())) + return cls.from_dict(data) + + def parse_salt(self) -> tuple[str, float | None, dict[str, list[str]]]: + """Parse the salt into (hex_nonce, expires_timestamp, params). + + Returns: + tuple: (hex_nonce, expires or None, dict of query params from salt) + """ + parts = self.salt.split("?", 1) + hex_nonce = parts[0] + params = urllib.parse.parse_qs(parts[1]) if len(parts) > 1 else {} + expires_list = params.get("expires") + expires: float | None = None + if expires_list: + try: + expires = float(expires_list[0]) + except ValueError: + pass + return hex_nonce, expires, params + class ServerSignaturePayload: """ @@ -333,7 +371,7 @@ raise ValueError(f"Unsupported algorithm: {algorithm}") -def hmac_hex(algorithm: AlgoType, data: bytes, key: str) -> str: +def hmac_hex(algorithm: AlgoType, data: bytes, key: str | bytes) -> str: """ Computes the HMAC hexadecimal digest of the given data using the specified algorithm and key. @@ -345,9 +383,9 @@ Returns: str: Hexadecimal HMAC digest of the data. """ - hmac_obj = hmac.new( - key.encode(), data, getattr(hashlib, algorithm.replace("-", "").lower()) - ) + if isinstance(key, str): + key = key.encode() + hmac_obj = hmac.new(key, data, getattr(hashlib, algorithm.replace("-", "").lower())) return hmac_obj.hexdigest() @@ -359,7 +397,7 @@ algorithm: AlgoType, max_number: int, salt_length: int, - hmac_key: str, + hmac_key: str | bytes, salt: str, number: int | None, expires: datetime.datetime | None, @@ -373,7 +411,7 @@ algorithm: AlgoType = DEFAULT_ALGORITHM, max_number: int = DEFAULT_MAX_NUMBER, salt_length: int = DEFAULT_SALT_LENGTH, - hmac_key: str = "", + hmac_key: str | bytes = "", salt: str = "", number: int | None = None, expires: datetime.datetime | None = None, @@ -459,26 +497,26 @@ @overload def verify_solution( payload: Payload, - hmac_key: str, + hmac_key: str | bytes, check_expires: bool = True, ) -> tuple[bool, str | None]: ... @overload def verify_solution( payload: str, - hmac_key: str, + hmac_key: str | bytes, check_expires: bool = True, ) -> tuple[bool, str | None]: ... @overload def verify_solution( payload: PayloadType, - hmac_key: str, + hmac_key: str | bytes, check_expires: bool = True, ) -> tuple[bool, str | None]: ... def verify_solution( payload: str | Payload | PayloadType, - hmac_key: str, + hmac_key: str | bytes, check_expires: bool = True, ) -> tuple[bool, str | None]: """ @@ -577,23 +615,23 @@ @overload def verify_server_signature( payload: str, - hmac_key: str, + hmac_key: str | bytes, ) -> tuple[bool, ServerSignatureVerificationData | None, str | None]: ... @overload def verify_server_signature( payload: ServerSignaturePayload, - hmac_key: str, + hmac_key: str | bytes, ) -> tuple[bool, ServerSignatureVerificationData | None, str | None]: ... @overload def verify_server_signature( payload: PayloadType, - hmac_key: str, + hmac_key: str | bytes, ) -> tuple[bool, ServerSignatureVerificationData | None, str | None]: ... def verify_server_signature( payload: str | ServerSignaturePayload | PayloadType, - hmac_key: str, + hmac_key: str | bytes, ) -> tuple[bool, ServerSignatureVerificationData | None, str | None]: """ Verifies the server signature in the payload. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/altcha-2.0.0/altcha/v2.py new/altcha-2.0.2/altcha/v2.py --- old/altcha-2.0.0/altcha/v2.py 2026-04-07 13:55:56.000000000 +0200 +++ new/altcha-2.0.2/altcha/v2.py 2026-06-20 12:15:36.000000000 +0200 @@ -197,13 +197,16 @@ return base64.b64encode(json.dumps(self.to_dict()).encode()).decode() @classmethod - def from_base64(cls, data: str) -> Payload: - d = json.loads(base64.b64decode(data).decode()) + def from_dict(cls, d: dict) -> Payload: return cls( challenge=Challenge.from_dict(d["challenge"]), solution=Solution.from_dict(d["solution"]), ) + @classmethod + def from_base64(cls, data: str) -> Payload: + return cls.from_dict(json.loads(base64.b64decode(data).decode())) + class VerifySolutionResult: """ @@ -265,12 +268,14 @@ return json.dumps(_sort_keys(obj), separators=(",", ":"), ensure_ascii=False) -def _hmac_v2(algorithm: str, data: str | bytes, key: str) -> bytes: +def _hmac_v2(algorithm: str, data: str | bytes, key: str | bytes) -> bytes: """Compute HMAC and return raw bytes.""" if isinstance(data, str): data = data.encode() + if isinstance(key, str): + key = key.encode() hash_name = algorithm.lower().replace("-", "") # 'sha256', 'sha384', 'sha512' - return _hmac_module.new(key.encode(), data, getattr(hashlib, hash_name)).digest() + return _hmac_module.new(key, data, getattr(hashlib, hash_name)).digest() def _constant_time_equal(a: str, b: str) -> bool: @@ -281,8 +286,8 @@ hmac_algorithm: str, parameters: ChallengeParameters, derived_key: bytes | None, - hmac_secret: str, - hmac_key_secret: str | None = None, + hmac_secret: str | bytes, + hmac_key_secret: str | bytes | None = None, ) -> Challenge: """Sign challenge parameters with HMAC, optionally also signing the derived key.""" if derived_key is not None and hmac_key_secret is not None: @@ -420,8 +425,8 @@ parallelism: int | None = None, expires_at: int | datetime.datetime | None = None, data: dict | None = None, - hmac_secret: str | None = None, - hmac_key_secret: str | None = None, + hmac_secret: str | bytes | None = None, + hmac_key_secret: str | bytes | None = None, hmac_algorithm: HmacAlgorithmV2 = DEFAULT_HMAC_ALGORITHM, ) -> Challenge: """ @@ -556,10 +561,10 @@ def verify_solution( payload: str | Payload, - hmac_secret: str, + hmac_secret: str | bytes, derive_key: DeriveKeyFunctionV2 | None = None, *, - hmac_key_secret: str | None = None, + hmac_key_secret: str | bytes | None = None, hmac_algorithm: HmacAlgorithmV2 = DEFAULT_HMAC_ALGORITHM, ) -> VerifySolutionResult: """ @@ -794,7 +799,7 @@ def verify_server_signature( payload: str | ServerSignaturePayload, - hmac_secret: str, + hmac_secret: str | bytes, *, hmac_algorithm: HmacAlgorithmV2 = DEFAULT_HMAC_ALGORITHM, ) -> VerifyServerSignatureResult: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/altcha-2.0.0/altcha.egg-info/PKG-INFO new/altcha-2.0.2/altcha.egg-info/PKG-INFO --- old/altcha-2.0.0/altcha.egg-info/PKG-INFO 2026-04-07 13:56:00.000000000 +0200 +++ new/altcha-2.0.2/altcha.egg-info/PKG-INFO 2026-06-20 12:15:40.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: altcha -Version: 2.0.0 +Version: 2.0.2 Summary: A library for creating and verifying challenges for ALTCHA. Home-page: https://github.com/altcha-org/altcha-lib-py Author: Daniel Regeci diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/altcha-2.0.0/setup.py new/altcha-2.0.2/setup.py --- old/altcha-2.0.0/setup.py 2026-04-07 13:55:56.000000000 +0200 +++ new/altcha-2.0.2/setup.py 2026-06-20 12:15:36.000000000 +0200 @@ -2,7 +2,7 @@ setup( name="altcha", - version="2.0.0", + version="2.0.2", description="A library for creating and verifying challenges for ALTCHA.", long_description=open("README.md").read(), long_description_content_type="text/markdown", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/altcha-2.0.0/tests/test_altcha.py new/altcha-2.0.2/tests/test_altcha.py --- old/altcha-2.0.0/tests/test_altcha.py 2026-04-07 13:55:56.000000000 +0200 +++ new/altcha-2.0.2/tests/test_altcha.py 2026-06-20 12:15:36.000000000 +0200 @@ -58,6 +58,31 @@ result, _ = verify_solution(payload_encoded, self.hmac_key, check_expires=False) self.assertTrue(result) + def test_verify_solution_bytes_hmac_key(self): + # Non-UTF-8 binary key must be accepted (issue #20) + key = bytes(range(256)) + options = ChallengeOptions( + algorithm="SHA-256", + max_number=1000, + salt_length=16, + hmac_key=key, + salt="somesalt", + number=123, + ) + challenge = create_challenge(options) + payload = Payload( + algorithm="SHA-256", + challenge=challenge.challenge, + number=123, + salt="somesalt", + signature=challenge.signature, + ) + payload_encoded = base64.b64encode( + json.dumps(payload.__dict__).encode() + ).decode() + result, _ = verify_solution(payload_encoded, key, check_expires=False) + self.assertTrue(result) + def test_verify_solution_failure(self): options = ChallengeOptions( algorithm="SHA-256", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/altcha-2.0.0/tests/test_altcha_v2.py new/altcha-2.0.2/tests/test_altcha_v2.py --- old/altcha-2.0.0/tests/test_altcha_v2.py 2026-04-07 13:55:56.000000000 +0200 +++ new/altcha-2.0.2/tests/test_altcha_v2.py 2026-06-20 12:15:36.000000000 +0200 @@ -144,6 +144,18 @@ assert ch.signature is not None self.assertGreater(len(ch.signature), 0) + def test_bytes_hmac_secret(self): + # Non-UTF-8 binary key must be accepted (issue #20) + key = bytes(range(256)) + ch = create_challenge("SHA-256", cost=1, counter=5, hmac_secret=key) + assert ch.signature is not None + sol = solve_challenge(ch) + assert sol is not None + payload = Payload(ch, sol).to_base64() + result = verify_solution(payload, key) + self.assertTrue(result.verified) + self.assertFalse(result.invalid_signature) + def test_deterministic_counter(self): ch = create_challenge("SHA-256", cost=1, counter=42, hmac_secret=HMAC_KEY) # key_prefix should be the first 16 bytes (key_length//2) of the derived key
