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
2 changes: 2 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:

jobs:
build:
if: secrets.ENABLE_PUBLISH == 'true'
runs-on: ubuntu-latest
permissions:
id-token: write
Expand Down Expand Up @@ -50,6 +51,7 @@ jobs:
if: steps.check.outputs.secret_missing == 'false' # when secret is missing, skip this step
publish-github:
needs: build
if: secrets.ENABLE_PUBLISH == 'true'
permissions:
contents: write
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ jobs:
python-version: ${{ matrix.python-version }}
activate-environment: true
- name: Sync dependencies
run: uv sync --extra tests
run: uv sync --group tests
- name: Run tests
run: uv run pytest
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ __pycache__
docs/_build/
.venv
build
*.lock
6 changes: 0 additions & 6 deletions main.py

This file was deleted.

3 changes: 2 additions & 1 deletion src/srptools/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from secrets import compare_digest
from typing import TYPE_CHECKING

from .common import SRPSessionBase
Expand Down Expand Up @@ -43,6 +44,6 @@ def verify_proof(self, key_proof: str | bytes, *, base64: bool = False) -> bool:
super().verify_proof(key_proof)

if isinstance(key_proof, bytes):
return key_proof == self._key_proof_hash
return compare_digest(key_proof, self._key_proof_hash)

return self._value_decode(key_proof, base64=base64) == self.key_proof_hash
18 changes: 8 additions & 10 deletions src/srptools/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def _this_public(self) -> int:
return getattr(self, f'_{self.role}_public')

def _other_public(self, val: int) -> None:
other = ('server' if self.role == 'client' else 'client')
other = 'server' if self.role == 'client' else 'client'
setattr(self, f'_{other}_public', val)

_other_public = property(None, _other_public)
Expand Down Expand Up @@ -125,7 +125,9 @@ def process(
) -> tuple[str, str, str]:
if base64 and (isinstance(other_public, bytes) or isinstance(salt, bytes)):
raise SRPException(
'Cannot decode base64 from bytes. If the value is bytes, it is already decoded and should not be treated as base64.')
'Cannot decode base64 from bytes. '
'If the value is bytes, it is already decoded and should not be treated as base64.'
)
salt = self._value_decode(salt, base64=base64)
other_public = self._value_decode(other_public, base64=base64)

Expand Down Expand Up @@ -162,7 +164,8 @@ def init_common_secret(self, other_public: str | int | bytes) -> None:
other_public = int(other_public, 16)
except (ValueError, TypeError) as e:
raise SRPException(
'Wrong public provided for %s: cannot decode value: %s' % (self.__class__.__name__, e))
f'Wrong public provided for {self.__class__.__name__}: cannot decode value: {e}',
) from e

if other_public % self._context._prime == 0: # A % N is zero | B % N is zero
raise SRPException(f'Wrong public provided for {self.__class__.__name__}.')
Expand All @@ -173,16 +176,11 @@ def init_common_secret(self, other_public: str | int | bytes) -> None:

def init_session_key_proof(self) -> None:
proof = self._context.get_common_session_key_proof(
session_key=self._key,
salt=self._salt,
server_public=self._server_public,
client_public=self._client_public
session_key=self._key, salt=self._salt, server_public=self._server_public, client_public=self._client_public
)

self._key_proof = proof

self._key_proof_hash = self._context.get_common_session_key_proof_hash(
session_key=self._key,
session_key_proof=proof,
client_public=self._client_public
session_key=self._key, session_key_proof=proof, client_public=self._client_public
)
4 changes: 2 additions & 2 deletions src/srptools/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@ def get_common_session_key_proof(
h(self._prime) ^ h(self._gen),
h(self._user),
salt,
client_public,
server_public,
self.pad(client_public),
self.pad(server_public),
session_key,
as_bytes=True
)
Expand Down
17 changes: 11 additions & 6 deletions src/srptools/server.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from secrets import compare_digest
from typing import TYPE_CHECKING

from .common import SRPSessionBase
Expand All @@ -10,10 +11,15 @@


class SRPServerSession(SRPSessionBase):

role = 'server'

def __init__(self, srp_context: SRPContext, password_verifier: str | int | bytes, *, private: str | int | bytes = ''):
def __init__(
self,
srp_context: SRPContext,
*,
password_verifier: str | int | bytes,
private: str | int | bytes = '',
):
super().__init__(srp_context, private)

if isinstance(password_verifier, int):
Expand All @@ -27,8 +33,7 @@ def __init__(self, srp_context: SRPContext, password_verifier: str | int | bytes
self._this_private = srp_context.generate_server_private()

self._server_public = srp_context.get_server_public(
password_verifier=self._password_verifier,
server_private=self._this_private
password_verifier=self._password_verifier, server_private=self._this_private
)

def init_session_key(self) -> None:
Expand All @@ -38,7 +43,7 @@ def init_session_key(self) -> None:
password_verifier=self._password_verifier,
server_private=self._this_private,
client_public=self._client_public,
common_secret=self._common_secret
common_secret=self._common_secret,
)

self._key = self._context.get_common_session_key(premaster_secret)
Expand All @@ -47,6 +52,6 @@ def verify_proof(self, key_proof: str | bytes, *, base64: bool = False) -> bool:
super().verify_proof(key_proof)

if isinstance(key_proof, bytes):
return key_proof == self._key_proof
return compare_digest(key_proof, self._key_proof)

return self._value_decode(key_proof, base64=base64) == self.key_proof
60 changes: 26 additions & 34 deletions tests/test_binary.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from __future__ import unicode_literals
from binascii import unhexlify

import pytest

from srptools import SRPContext, SRPClientSession, SRPServerSession, SRPException
from srptools import SRPClientSession, SRPContext, SRPException, SRPServerSession
from srptools.utils import int_from_hex


def test_full_handshake_binary():
Expand All @@ -17,13 +17,11 @@ def test_full_handshake_binary():
salt_bin = unhexlify(salt)

# Server accepts bytes verifier.
server_session = SRPServerSession(
SRPContext(username, prime=prime, generator=gen), verifier_bin)
server_session = SRPServerSession(SRPContext(username, prime=prime, generator=gen), password_verifier=verifier_bin)
server_public_bin = server_session.public_bin

# Client processes bytes public + bytes salt.
client_session = SRPClientSession(
SRPContext(username, 'password123', prime=prime, generator=gen))
client_session = SRPClientSession(SRPContext(username, 'password123', prime=prime, generator=gen))
client_session.process(server_public_bin, salt_bin)
client_public_bin = client_session.public_bin

Expand All @@ -45,10 +43,8 @@ def test_verify_proof_binary():
salt_bin = unhexlify(salt)
verifier_bin = unhexlify(password_verifier)

server_session = SRPServerSession(
SRPContext(username, prime=prime, generator=gen), verifier_bin)
client_session = SRPClientSession(
SRPContext(username, 'password123', prime=prime, generator=gen))
server_session = SRPServerSession(SRPContext(username, prime=prime, generator=gen), password_verifier=verifier_bin)
client_session = SRPClientSession(SRPContext(username, 'password123', prime=prime, generator=gen))

client_session.process(server_session.public_bin, salt_bin)
server_session.process(client_session.public_bin, salt_bin)
Expand All @@ -62,41 +58,39 @@ def test_verify_proof_binary():
def test_session_restore_via_bytes_private():
"""Session restored with private=<bytes> reproduces the same public."""
context = SRPContext('alice', 'password123')
username, password_verifier, salt = context.get_user_data_triplet()
username, password_verifier, _ = context.get_user_data_triplet()
prime, gen = context.prime, context.generator

verifier_bin = unhexlify(password_verifier)

original_server = SRPServerSession(
SRPContext(username, prime=prime, generator=gen), password_verifier)
SRPContext(username, prime=prime, generator=gen), password_verifier=password_verifier
)
server_private_bin = original_server.private_bin

restored_server = SRPServerSession(
SRPContext(username, prime=prime, generator=gen), verifier_bin,
private=server_private_bin)
SRPContext(username, prime=prime, generator=gen), password_verifier=verifier_bin, private=server_private_bin
)
assert restored_server.public_bin == original_server.public_bin

original_client = SRPClientSession(
SRPContext(username, 'password123', prime=prime, generator=gen))
original_client = SRPClientSession(SRPContext(username, 'password123', prime=prime, generator=gen))
client_private_bin = original_client.private_bin

restored_client = SRPClientSession(
SRPContext(username, 'password123', prime=prime, generator=gen),
private=client_private_bin)
SRPContext(username, 'password123', prime=prime, generator=gen), private=client_private_bin
)
assert restored_client.public_bin == original_client.public_bin


def test_server_accepts_int_verifier():
"""SRPServerSession accepts int password_verifier directly."""
context = SRPContext('alice', 'password123')
username, password_verifier, salt = context.get_user_data_triplet()
username, password_verifier, _ = context.get_user_data_triplet()
prime, gen = context.prime, context.generator

from srptools.utils import int_from_hex
verifier_int = int_from_hex(password_verifier)

server_session = SRPServerSession(
SRPContext(username, prime=prime, generator=gen), verifier_int)
server_session = SRPServerSession(SRPContext(username, prime=prime, generator=gen), password_verifier=verifier_int)
assert server_session.public_bin


Expand Down Expand Up @@ -130,9 +124,9 @@ def test_process_bytes_base64_mutual_exclusion():

salt_bin = unhexlify(salt)
server_session = SRPServerSession(
SRPContext(username, prime=prime, generator=gen), password_verifier)
client_session = SRPClientSession(
SRPContext(username, 'password123', prime=prime, generator=gen))
SRPContext(username, prime=prime, generator=gen), password_verifier=password_verifier
)
client_session = SRPClientSession(SRPContext(username, 'password123', prime=prime, generator=gen))

with pytest.raises(SRPException):
client_session.process(server_session.public_bin, salt_bin, base64=True)
Expand All @@ -141,7 +135,7 @@ def test_process_bytes_base64_mutual_exclusion():
def test_init_common_secret_rejects_garbage_str():
"""init_common_secret raises SRPException on non-hex str."""
context = SRPContext('alice', 'password123')
server_session = SRPServerSession(context, '1')
server_session = SRPServerSession(context, password_verifier='1')
with pytest.raises(SRPException):
server_session.init_common_secret('not-hex-at-all')

Expand All @@ -158,12 +152,10 @@ def test_binary_path_matches_hex_path():
verifier_bin = unhexlify(password_verifier)

# Hex path: capture privates to reuse in binary path.
server_hex = SRPServerSession(
SRPContext(username, prime=prime, generator=gen), password_verifier)
server_hex = SRPServerSession(SRPContext(username, prime=prime, generator=gen), password_verifier=password_verifier)
server_private_bin = server_hex.private_bin

client_hex = SRPClientSession(
SRPContext(username, 'password123', prime=prime, generator=gen))
client_hex = SRPClientSession(SRPContext(username, 'password123', prime=prime, generator=gen))
client_private_bin = client_hex.private_bin

client_hex.process(server_hex.public, salt)
Expand All @@ -172,11 +164,11 @@ def test_binary_path_matches_hex_path():

# Binary path: restore sessions with the same privates, feed bytes.
server_bin = SRPServerSession(
SRPContext(username, prime=prime, generator=gen), verifier_bin,
private=server_private_bin)
SRPContext(username, prime=prime, generator=gen), password_verifier=verifier_bin, private=server_private_bin
)
client_bin = SRPClientSession(
SRPContext(username, 'password123', prime=prime, generator=gen),
private=client_private_bin)
SRPContext(username, 'password123', prime=prime, generator=gen), private=client_private_bin
)

client_bin.process(server_bin.public_bin, salt_bin)
server_bin.process(client_bin.public_bin, salt_bin)
Expand Down
Loading
Loading