├── .coveragerc ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── AUTHORS ├── CHANGELOG ├── INSTALL ├── LICENSE ├── MANIFEST.in ├── README.rst ├── setup.cfg ├── setup.py ├── srptools ├── __init__.py ├── cli.py ├── client.py ├── common.py ├── constants.py ├── context.py ├── exceptions.py ├── server.py └── utils.py ├── tests ├── test_client_server.py └── test_context.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = srptools/ 3 | omit = srptools/cli.py 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | checks: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | python-version: 12 | - "3.10" 13 | - 3.11 14 | - 3.12 15 | os: 16 | - ubuntu-latest 17 | - macos-latest 18 | - windows-latest 19 | exclude: 20 | - os: macos-latest 21 | python-version: '3.10' 22 | runs-on: ${{ matrix.os }} 23 | name: ${{ matrix.python-version }} on ${{ matrix.os }} 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | architecture: x64 31 | - run: pip install setuptools pytest six 32 | - run: python setup.py install 33 | - run: pytest 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .pydevproject 3 | .idea 4 | .tox 5 | __pycache__ 6 | *.pyc 7 | *.pyo 8 | *.egg-info 9 | docs/_build/ 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | srptools authors 2 | ================ 3 | 4 | Created by Igor `idle sign` Starikov. 5 | 6 | 7 | Contributors 8 | ------------ 9 | 10 | Bouke Haarsma 11 | Michael Zimmermann 12 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | srptools changelog 2 | ================== 3 | 4 | 5 | v1.0.1 [2020-09-12] 6 | ------------------- 7 | ! Dropped QA for Python 2. 8 | * Now context.get_common_password_hash handles inner hashes with leading zeros correctly. 9 | 10 | 11 | v1.0.0 12 | ------ 13 | ! Dropped QA for Python 3.3 and 3.4. 14 | * No functional changes. Celebrating 1.0.0. 15 | 16 | 17 | v0.2.0 18 | ------ 19 | * Fixed unable to install into envs with 'six' unavailable 20 | + Added 'preset' CLI option 21 | * Improved compatibility with other implementations. 22 | 23 | 24 | v0.1.1 25 | ------ 26 | * Hex strings are now even-length. 27 | * Fixed shareable hashed values with leading zeros issue. 28 | 29 | 30 | v0.1.0 31 | ------ 32 | + Basic functionality. -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | srptools installation 2 | ===================== 3 | 4 | 5 | Python ``pip`` package is required to install ``srptools``. 6 | 7 | 8 | From sources 9 | ------------ 10 | 11 | Use the following command line to install ``srptools`` from sources directory (containing setup.py): 12 | 13 | pip install . 14 | 15 | or 16 | 17 | python setup.py install 18 | 19 | 20 | From PyPI 21 | --------- 22 | 23 | Alternatively you can install ``srptools`` from PyPI: 24 | 25 | pip install srptools 26 | 27 | 28 | Use `-U` flag for upgrade: 29 | 30 | pip install -U srptools 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2022, Igor `idle sign` Starikov 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the srptools nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include CHANGELOG 3 | include INSTALL 4 | include LICENSE 5 | include README.rst 6 | 7 | include docs/Makefile 8 | recursive-include docs *.rst 9 | recursive-include docs *.py 10 | 11 | recursive-include bin * 12 | 13 | recursive-include tests * 14 | 15 | recursive-exclude * __pycache__ 16 | recursive-exclude * *.py[co] 17 | recursive-exclude * empty 18 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | srptools 2 | ======== 3 | https://github.com/idlesign/srptools 4 | 5 | 6 | |release| |lic| |coverage| 7 | 8 | .. |release| image:: https://img.shields.io/pypi/v/srptools.svg 9 | :target: https://pypi.python.org/pypi/srptools 10 | 11 | .. |lic| image:: https://img.shields.io/pypi/l/srptools.svg 12 | :target: https://pypi.python.org/pypi/srptools 13 | 14 | .. |coverage| image:: https://img.shields.io/coveralls/idlesign/srptools/master.svg 15 | :target: https://coveralls.io/r/idlesign/srptools 16 | 17 | 18 | Description 19 | ----------- 20 | 21 | *Tools to implement Secure Remote Password (SRP) authentication* 22 | 23 | SRP is a secure password-based authentication and key-exchange protocol - 24 | a password-authenticated key agreement protocol (PAKE). 25 | 26 | This package contains protocol implementation for Python 2 and 3. 27 | 28 | You may import it into you applications and use its API or you may use 29 | ``srptools`` command-line utility (CLI): 30 | 31 | 32 | CLI usage 33 | --------- 34 | 35 | Command-line utility requires ``click`` package to be installed. 36 | 37 | Basic scenario: 38 | 39 | .. code-block:: 40 | 41 | > srptools get_user_data_triplet 42 | > srptools server get_private_and_public 43 | > srptools client get_private_and_public 44 | > srptools client get_session_data 45 | > srptools server get_session_data 46 | 47 | Help is available: 48 | 49 | .. code-block:: 50 | 51 | > srptools --help 52 | 53 | 54 | 55 | API usage 56 | --------- 57 | 58 | Preliminary step. Agree on communication details: 59 | 60 | .. code-block:: python 61 | 62 | from srptools import SRPContext 63 | 64 | context = SRPContext('alice', 'password123') 65 | username, password_verifier, salt = context.get_user_data_triplet() 66 | prime = context.prime 67 | gen = context.generator 68 | 69 | 70 | Simplified workflow: 71 | 72 | .. code-block:: python 73 | 74 | from srptools import SRPContext, SRPServerSession, SRPClientSession 75 | 76 | # Receive username from client and generate server public. 77 | server_session = SRPServerSession(SRPContext(username, prime=prime, generator=gen), password_verifier) 78 | server_public = server_session.public 79 | 80 | # Receive server public and salt and process them. 81 | client_session = SRPClientSession(SRPContext('alice', 'password123', prime=prime, generator=gen)) 82 | client_session.process(server_public, salt) 83 | # Generate client public and session key. 84 | client_public = client_session.public 85 | 86 | # Process client public and compare session keys. 87 | server_session.process(client_public, salt) 88 | 89 | assert server_session.key == client_session.key 90 | 91 | 92 | Extended workflow 93 | 94 | .. code-block:: python 95 | 96 | from srptools import SRPContext, SRPServerSession, SRPClientSession 97 | 98 | # Receive username from client and generate server public. 99 | server_session = SRPServerSession(SRPContext(username, prime=prime, generator=gen), password_verifier) 100 | server_public = server_session.public 101 | 102 | # Receive server public and salt and process them. 103 | client_session = SRPClientSession(SRPContext('alice', 'password123', prime=prime, generator=gen)) 104 | client_session.process(server_public, salt) 105 | # Generate client public and session key proof. 106 | client_public = client_session.public 107 | client_session_key_proof = client_session.key_proof 108 | 109 | # Process client public and verify session key proof. 110 | server_session.process(client_public, salt) 111 | assert server_session.verify_proof(client_session_key_proof) 112 | # Generate session key proof hash. 113 | server_session_key_proof_hash = client_session.key_proof_hash 114 | 115 | # Verify session key proof hash received from server. 116 | assert client_session.verify_proof(server_session_key_proof_hash) 117 | 118 | 119 | 120 | Usage hints 121 | ----------- 122 | 123 | * ``srptools.constants`` contains basic constants which can be used with ``SRPContext`` for server and client to agree 124 | upon communication details. 125 | * ``.process()`` methods of session classes may raise ``SRPException`` in certain circumstances. Auth process on 126 | such occasions must be stopped. 127 | * ``.private`` attribute of session classes may be used to restore sessions: 128 | .. code-block:: python 129 | 130 | server_private = server_session.private 131 | 132 | # Restore session on new request. 133 | server_session = SRPServerSession(context, password_verifier, private=server_private) 134 | 135 | * ``SRPContext`` is rather flexible, so you can implement some custom server/client session logic with its help. 136 | * Basic values are represented as hex strings but base64 encoded values are also supported: 137 | 138 | .. code-block:: python 139 | 140 | server_public = server_session.public_b64 141 | 142 | # Receive server public and salt and process them. 143 | client_session = SRPClientSession(SRPContext('alice', 'password123', prime=prime, generator=gen)) 144 | client_session.process(server_public, salt, base64=True) 145 | 146 | # Use srptools.hex_from_b64() to represent base64 value as hex. 147 | server_public_hex = hex_from_b64(server_public) 148 | 149 | 150 | Links 151 | ----- 152 | * RFC 2945 - The SRP Authentication and Key Exchange System 153 | https://tools.ietf.org/html/rfc2945 154 | 155 | * RFC 5054 - Using the Secure Remote Password (SRP) Protocol for TLS Authentication 156 | https://tools.ietf.org/html/rfc5054 157 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | release = clean --all sdist bdist_wheel upload 3 | test = pytest 4 | 5 | [wheel] 6 | universal = 1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import io 3 | import re 4 | import sys 5 | from setuptools import setup, find_packages 6 | 7 | 8 | PATH_BASE = os.path.dirname(__file__) 9 | PYTEST_RUNNER = ['pytest-runner'] if 'test' in sys.argv else [] 10 | 11 | 12 | def read(fpath): 13 | return io.open(fpath).read() 14 | 15 | 16 | def get_version(): 17 | """Reads version number. 18 | 19 | This workaround is required since __init__ is an entry point exposing 20 | stuff from other modules, which may use dependencies unavailable 21 | in current environment, which in turn will prevent this application 22 | from install. 23 | 24 | """ 25 | contents = read(os.path.join(PATH_BASE, 'srptools', '__init__.py')) 26 | version = re.search('VERSION = \(([^)]+)\)', contents) 27 | version = version.group(1).replace(', ', '.').strip() 28 | return version 29 | 30 | 31 | setup( 32 | name='srptools', 33 | version=get_version(), 34 | url='https://github.com/idlesign/srptools', 35 | 36 | description='Tools to implement Secure Remote Password (SRP) authentication', 37 | long_description=read(os.path.join(PATH_BASE, 'README.rst')), 38 | license='BSD 3-Clause License', 39 | 40 | author='Igor `idle sign` Starikov', 41 | author_email='idlesign@yandex.ru', 42 | 43 | packages=find_packages(), 44 | include_package_data=True, 45 | zip_safe=False, 46 | 47 | install_requires=['six'], 48 | setup_requires=[] + PYTEST_RUNNER, 49 | extras_require={'cli': ['click>=2.0']}, 50 | tests_require=['pytest'], 51 | 52 | entry_points={'console_scripts': ['srptools = srptools.cli:main']}, 53 | 54 | test_suite='tests', 55 | 56 | classifiers=[ 57 | # As in https://pypi.python.org/pypi?:action=list_classifiers 58 | 'Development Status :: 5 - Production/Stable', 59 | 'Operating System :: OS Independent', 60 | 'Programming Language :: Python', 61 | 'Programming Language :: Python :: 2', 62 | 'Programming Language :: Python :: 2.7', 63 | 'Programming Language :: Python :: 3', 64 | 'Programming Language :: Python :: 3.6', 65 | 'Programming Language :: Python :: 3.7', 66 | 'Programming Language :: Python :: 3.8', 67 | 'Programming Language :: Python :: 3.9', 68 | 'Programming Language :: Python :: 3.10', 69 | 'License :: OSI Approved :: BSD License' 70 | ], 71 | ) 72 | -------------------------------------------------------------------------------- /srptools/__init__.py: -------------------------------------------------------------------------------- 1 | from .context import SRPContext 2 | from .client import SRPClientSession 3 | from .server import SRPServerSession 4 | from .exceptions import SRPException 5 | from .utils import hex_from_b64 6 | 7 | 8 | VERSION = (1, 0, 1) -------------------------------------------------------------------------------- /srptools/cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from collections import OrderedDict 3 | 4 | import click 5 | 6 | from srptools import VERSION, SRPContext, SRPServerSession, SRPClientSession, hex_from_b64 7 | from srptools.constants import * 8 | 9 | 10 | PRESETS = OrderedDict([ 11 | ('1024', (PRIME_1024, PRIME_1024_GEN)), 12 | ('1536', (PRIME_1536, PRIME_1536_GEN)), 13 | ('2048', (PRIME_2048, PRIME_2048_GEN)), 14 | ('3072', (PRIME_3072, PRIME_3072_GEN)), 15 | ('4096', (PRIME_4096, PRIME_4096_GEN)), 16 | ('6144', (PRIME_6144, PRIME_6144_GEN)), 17 | ]) 18 | 19 | 20 | @click.group() 21 | @click.version_option(version='.'.join(map(str, VERSION))) 22 | def base(): 23 | """srptools command line utility. 24 | 25 | Tools to implement Secure Remote Password (SRP) authentication. 26 | 27 | Basic scenario: 28 | 29 | > srptools get_user_data_triplet 30 | 31 | > srptools server get_private_and_public 32 | 33 | > srptools client get_private_and_public 34 | 35 | > srptools client get_session_data 36 | 37 | > srptools server get_session_data 38 | 39 | """ 40 | 41 | def common_options(func): 42 | """Commonly used command options.""" 43 | 44 | def parse_preset(ctx, param, value): 45 | return PRESETS.get(value, (None, None)) 46 | 47 | def parse_private(ctx, param, value): 48 | return hex_from_b64(value) if value else None 49 | 50 | func = click.option('--private', default=None, help='Private.', callback=parse_private)(func) 51 | 52 | func = click.option( 53 | '--preset', 54 | default=None, help='Preset ID defining prime and generator pair.', 55 | type=click.Choice(PRESETS.keys()), callback=parse_preset 56 | )(func) 57 | 58 | return func 59 | 60 | 61 | @base.group() 62 | def server(): 63 | """Server session related commands.""" 64 | 65 | 66 | @base.group() 67 | def client(): 68 | """Client session related commands.""" 69 | 70 | 71 | @server.command() 72 | @click.argument('username') 73 | @click.argument('password_verifier') 74 | @common_options 75 | def get_private_and_public(username, password_verifier, private, preset): 76 | """Print out server public and private.""" 77 | session = SRPServerSession( 78 | SRPContext(username, prime=preset[0], generator=preset[1]), 79 | hex_from_b64(password_verifier), private=private) 80 | 81 | click.secho('Server private: %s' % session.private_b64) 82 | click.secho('Server public: %s' % session.public_b64) 83 | 84 | 85 | @server.command() 86 | @click.argument('username') 87 | @click.argument('password_verifier') 88 | @click.argument('salt') 89 | @click.argument('client_public') 90 | @common_options 91 | def get_session_data( username, password_verifier, salt, client_public, private, preset): 92 | """Print out server session data.""" 93 | session = SRPServerSession( 94 | SRPContext(username, prime=preset[0], generator=preset[1]), 95 | hex_from_b64(password_verifier), private=private) 96 | 97 | session.process(client_public, salt, base64=True) 98 | 99 | click.secho('Server session key: %s' % session.key_b64) 100 | click.secho('Server session key proof: %s' % session.key_proof_b64) 101 | click.secho('Server session key hash: %s' % session.key_proof_hash_b64) 102 | 103 | 104 | @client.command() 105 | @click.argument('username') 106 | @click.argument('password') 107 | @common_options 108 | def get_private_and_public(ctx, username, password, private, preset): 109 | """Print out server public and private.""" 110 | session = SRPClientSession( 111 | SRPContext(username, password, prime=preset[0], generator=preset[1]), 112 | private=private) 113 | 114 | click.secho('Client private: %s' % session.private_b64) 115 | click.secho('Client public: %s' % session.public_b64) 116 | 117 | 118 | @client.command() 119 | @click.argument('username') 120 | @click.argument('password') 121 | @click.argument('salt') 122 | @click.argument('server_public') 123 | @common_options 124 | def get_session_data(ctx, username, password, salt, server_public, private, preset): 125 | """Print out client session data.""" 126 | session = SRPClientSession( 127 | SRPContext(username, password, prime=preset[0], generator=preset[1]), 128 | private=private) 129 | 130 | session.process(server_public, salt, base64=True) 131 | 132 | click.secho('Client session key: %s' % session.key_b64) 133 | click.secho('Client session key proof: %s' % session.key_proof_b64) 134 | click.secho('Client session key hash: %s' % session.key_proof_hash_b64) 135 | 136 | 137 | @base.command() 138 | @click.argument('username') 139 | @click.argument('password') 140 | def get_user_data_triplet(username, password): 141 | """Print out user data triplet: username, password verifier, salt.""" 142 | context = SRPContext(username, password) 143 | username, password_verifier, salt = context.get_user_data_triplet(base64=True) 144 | 145 | click.secho('Username: %s' % username) 146 | click.secho('Password verifier: %s' % password_verifier) 147 | click.secho('Salt: %s' % salt) 148 | 149 | 150 | def main(): 151 | """ 152 | CLI entry point 153 | """ 154 | base(obj={}) 155 | -------------------------------------------------------------------------------- /srptools/client.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from .common import SRPSessionBase 3 | 4 | 5 | if False: # pragma: no cover 6 | from .context import SRPContext 7 | 8 | 9 | class SRPClientSession(SRPSessionBase): 10 | 11 | role = 'client' 12 | 13 | def __init__(self, srp_context, private=None): 14 | """ 15 | :param SRPContext srp_context: 16 | :param st|unicode private: 17 | """ 18 | super(SRPClientSession, self).__init__(srp_context, private) 19 | 20 | self._password_hash = None 21 | 22 | if not private: 23 | self._this_private = srp_context.generate_client_private() 24 | 25 | self._client_public = srp_context.get_client_public(self._this_private) 26 | 27 | def init_base(self, salt): 28 | super(SRPClientSession, self).init_base(salt) 29 | 30 | self._password_hash = self._context.get_common_password_hash(self._salt) 31 | 32 | def init_session_key(self): 33 | super(SRPClientSession, self).init_session_key() 34 | 35 | premaster_secret = self._context.get_client_premaster_secret( 36 | self._password_hash, self._server_public, self._this_private, self._common_secret) 37 | 38 | self._key = self._context.get_common_session_key(premaster_secret) 39 | 40 | def verify_proof(self, key_proof, base64=False): 41 | super(SRPClientSession, self).verify_proof(key_proof) 42 | 43 | return self._value_decode(key_proof, base64) == self.key_proof_hash 44 | -------------------------------------------------------------------------------- /srptools/common.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from binascii import unhexlify 4 | 5 | from .utils import hex_from, int_from_hex, hex_from_b64, value_encode, b64_from 6 | from .exceptions import SRPException 7 | 8 | 9 | if False: # pragma: no cover 10 | from .context import SRPContext 11 | 12 | 13 | class SRPSessionBase(object): 14 | """Base session class for server and client.""" 15 | 16 | role = None 17 | 18 | def __init__(self, srp_context, private=None): 19 | """ 20 | :param SRPContext srp_context: 21 | :param str|unicode private: 22 | """ 23 | self._context = srp_context 24 | 25 | self._salt = None # type: bytes 26 | self._common_secret = None # type: int 27 | self._key = None # type: bytes 28 | self._key_proof = None # type: bytes 29 | self._key_proof_hash = None # type: bytes 30 | 31 | self._server_public = None # type: int 32 | self._client_public = None # type: int 33 | 34 | self._this_private = None # type: int 35 | 36 | if private: 37 | self._this_private = int_from_hex(private) # type: int 38 | 39 | @property 40 | def _this_public(self): 41 | return getattr(self, '_%s_public' % self.role) 42 | 43 | def _other_public(self, val): 44 | other = ('server' if self.role == 'client' else 'client') 45 | setattr(self, '_%s_public' % other, val) 46 | 47 | _other_public = property(None, _other_public) 48 | 49 | @property 50 | def private(self): 51 | return hex_from(self._this_private) 52 | 53 | @property 54 | def private_b64(self): 55 | return b64_from(self._this_private) 56 | 57 | @property 58 | def public(self): 59 | return hex_from(self._this_public) 60 | 61 | @property 62 | def public_b64(self): 63 | return b64_from(self._this_public) 64 | 65 | @property 66 | def key(self): 67 | return hex_from(self._key) 68 | 69 | @property 70 | def key_b64(self): 71 | return b64_from(self._key) 72 | 73 | @property 74 | def key_proof(self): 75 | return hex_from(self._key_proof) 76 | 77 | @property 78 | def key_proof_b64(self): 79 | return b64_from(self._key_proof) 80 | 81 | @property 82 | def key_proof_hash(self): 83 | return hex_from(self._key_proof_hash) 84 | 85 | @property 86 | def key_proof_hash_b64(self): 87 | return b64_from(self._key_proof_hash) 88 | 89 | @classmethod 90 | def _value_decode(cls, value, base64=False): 91 | """Decodes value into hex optionally from base64""" 92 | return hex_from_b64(value) if base64 else value 93 | 94 | def process(self, other_public, salt, base64=False): 95 | salt = self._value_decode(salt, base64) 96 | other_public = self._value_decode(other_public, base64) 97 | 98 | self.init_base(salt) 99 | self.init_common_secret(other_public) 100 | self.init_session_key() 101 | self.init_session_key_proof() 102 | 103 | key = value_encode(self._key, base64) 104 | key_proof = value_encode(self._key_proof, base64) 105 | key_proof_hash = value_encode(self._key_proof_hash, base64) 106 | 107 | return key, key_proof, key_proof_hash 108 | 109 | def init_base(self, salt): 110 | salt = unhexlify(salt) 111 | self._salt = salt 112 | 113 | def init_session_key(self): 114 | """""" 115 | 116 | def verify_proof(self, key_prove, base64=False): 117 | """""" 118 | 119 | def init_common_secret(self, other_public): 120 | other_public = int_from_hex(other_public) 121 | 122 | if other_public % self._context._prime == 0: # A % N is zero | B % N is zero 123 | raise SRPException('Wrong public provided for %s.' % self.__class__.__name__) 124 | 125 | self._other_public = other_public 126 | 127 | self._common_secret = self._context.get_common_secret(self._server_public, self._client_public) 128 | 129 | def init_session_key_proof(self): 130 | proof = self._context.get_common_session_key_proof( 131 | self._key, self._salt, self._server_public, self._client_public) 132 | self._key_proof = proof 133 | 134 | self._key_proof_hash = self._context.get_common_session_key_proof_hash(self._key, proof, self._client_public) 135 | -------------------------------------------------------------------------------- /srptools/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import hashlib 3 | 4 | 5 | HASH_SHA_1 = hashlib.sha1 6 | HASH_SHA_256 = hashlib.sha256 7 | 8 | 9 | PRIME_1024_GEN = '2' 10 | PRIME_1024 = '''\ 11 | EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF74\ 12 | 96EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6\ 13 | CE8EF4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4\ 14 | 976EAA9AFD5138FE8376435B9FC61D2FC0EB06E3''' 15 | 16 | PRIME_1536_GEN = '2' 17 | PRIME_1536 = '''\ 18 | 9DEF3CAFB939277AB1F12A8617A47BBBDBA51DF499AC4C80BEEEA9614B19CC4D5F4F5F55\ 19 | 6E27CBDE51C6A94BE4607A291558903BA0D0F84380B655BB9A22E8DCDF028A7CEC67F0D0\ 20 | 8134B1C8B97989149B609E0BE3BAB63D47548381DBC5B1FC764E3F4B53DD9DA1158BFD3E\ 21 | 2B9C8CF56EDF019539349627DB2FD53D24B7C48665772E437D6C7F8CE442734AF7CCB7AE\ 22 | 837C264AE3A9BEB87F8A2FE9B8B5292E5A021FFF5E91479E8CE7A28C2442C6F315180F93\ 23 | 499A234DCF76E3FED135F9BB''' 24 | 25 | PRIME_2048_GEN = '2' 26 | PRIME_2048 = '''\ 27 | AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CB\ 28 | B4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0\ 29 | CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740A\ 30 | DBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481\ 31 | F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDB\ 32 | F52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C382\ 33 | 71AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F\ 34 | 9E4AFF73 35 | ''' 36 | 37 | PRIME_3072_GEN = '5' 38 | PRIME_3072 = '''\ 39 | FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA6\ 40 | 3B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245\ 41 | E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F2411\ 42 | 7C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F\ 43 | 83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08\ 44 | CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9\ 45 | DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\ 46 | 04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7\ 47 | ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D8760273\ 48 | 3EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB31\ 49 | 43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF''' 50 | 51 | PRIME_4096_GEN = '5' 52 | PRIME_4096 = '''\ 53 | FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA6\ 54 | 3B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245\ 55 | E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F2411\ 56 | 7C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F\ 57 | 83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08\ 58 | CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9\ 59 | DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\ 60 | 04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7\ 61 | ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D8760273\ 62 | 3EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB31\ 63 | 43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C32718\ 64 | 6AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6\ 65 | 287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD76\ 66 | 2170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199\ 67 | FFFFFFFFFFFFFFFF''' 68 | 69 | PRIME_6144_GEN = '5' 70 | PRIME_6144 = '''\ 71 | FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA6\ 72 | 3B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245\ 73 | E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F2411\ 74 | 7C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F\ 75 | 83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08\ 76 | CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9\ 77 | DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\ 78 | 04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7\ 79 | ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D8760273\ 80 | 3EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB31\ 81 | 43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C32718\ 82 | 6AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6\ 83 | 287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD76\ 84 | 2170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492\ 85 | 36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F\ 86 | 413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1B\ 87 | DB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15\ 88 | D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F3\ 89 | 23A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED2\ 90 | 0F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55C\ 91 | DA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E\ 92 | 6DCC4024FFFFFFFFFFFFFFFF''' 93 | -------------------------------------------------------------------------------- /srptools/context.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from random import SystemRandom as random 3 | 4 | from six import integer_types, PY3 5 | 6 | from .utils import int_from_hex, int_to_bytes, hex_from, value_encode, b64_from 7 | from .constants import PRIME_1024, PRIME_1024_GEN, HASH_SHA_1 8 | from .exceptions import SRPException 9 | 10 | 11 | class SRPContext(object): 12 | """ 13 | 14 | * The SRP Authentication and Key Exchange System 15 | https://tools.ietf.org/html/rfc2945 16 | 17 | * Using the Secure Remote Password (SRP) Protocol for TLS Authentication 18 | https://tools.ietf.org/html/rfc5054 19 | 20 | """ 21 | def __init__( 22 | self, username, password=None, prime=None, generator=None, hash_func=None, multiplier=None, 23 | bits_random=1024, bits_salt=64): 24 | """ 25 | 26 | :param str|unicode username: User name 27 | :param str|unicode password: User _password 28 | :param str|unicode|None prime: Prime hex string . Default: PRIME_1024 29 | :param str|unicode|None generator: Generator hex string. Default: PRIME_1024_GEN 30 | :param str|unicode hash_func: Function to calculate hash. Default: HASH_SHA_1 31 | :param str|unicode multiplier: Multiplier hex string. If not given will be calculated 32 | automatically using _prime and _gen. 33 | :param int bits_random: Random value bits. Default: 1024 34 | :param int bits_salt: Salt value bits. Default: 64 35 | """ 36 | self._hash_func = hash_func or HASH_SHA_1 # H 37 | self._user = username # I 38 | self._password = password # p 39 | 40 | self._gen = int_from_hex(generator or PRIME_1024_GEN) # g 41 | self._prime = int_from_hex(prime or PRIME_1024) # N 42 | self._mult = ( # k = H(N | PAD(g)) 43 | int_from_hex(multiplier) if multiplier else self.hash(self._prime, self.pad(self._gen))) 44 | 45 | self._bits_salt = bits_salt 46 | self._bits_random = bits_random 47 | 48 | @property 49 | def generator(self): 50 | return hex_from(self._gen) 51 | 52 | @property 53 | def generator_b64(self): 54 | return b64_from(self._gen) 55 | 56 | @property 57 | def prime(self): 58 | return hex_from(self._prime) 59 | 60 | @property 61 | def prime_b64(self): 62 | return b64_from(self._prime) 63 | 64 | def pad(self, val): 65 | """ 66 | :param val: 67 | :rtype: bytes 68 | """ 69 | padding = len(int_to_bytes(self._prime)) 70 | padded = int_to_bytes(val).rjust(padding, b'\x00') 71 | return padded 72 | 73 | def hash(self, *args, **kwargs): 74 | """ 75 | :param args: 76 | :param kwargs: 77 | joiner - string to join values (args) 78 | as_bytes - bool to return hash bytes instead of default int 79 | :rtype: int|bytes 80 | """ 81 | joiner = kwargs.get('joiner', '').encode('utf-8') 82 | as_bytes = kwargs.get('as_bytes', False) 83 | 84 | def conv(arg): 85 | if isinstance(arg, integer_types): 86 | arg = int_to_bytes(arg) 87 | 88 | if PY3: 89 | if isinstance(arg, str): 90 | arg = arg.encode('utf-8') 91 | return arg 92 | 93 | return str(arg) 94 | 95 | digest = joiner.join(map(conv, args)) 96 | 97 | hash_obj = self._hash_func(digest) 98 | 99 | if as_bytes: 100 | return hash_obj.digest() 101 | 102 | return int_from_hex(hash_obj.hexdigest()) 103 | 104 | def generate_random(self, bits_len=None): 105 | """Generates a random value. 106 | 107 | :param int bits_len: 108 | :rtype: int 109 | """ 110 | bits_len = bits_len or self._bits_random 111 | return random().getrandbits(bits_len) 112 | 113 | def generate_salt(self): 114 | """s = random 115 | 116 | :rtype: int 117 | """ 118 | return self.generate_random(self._bits_salt) 119 | 120 | def get_common_secret(self, server_public, client_public): 121 | """u = H(PAD(A) | PAD(B)) 122 | 123 | :param int server_public: 124 | :param int client_public: 125 | :rtype: int 126 | """ 127 | return self.hash(self.pad(client_public), self.pad(server_public)) 128 | 129 | def get_client_premaster_secret(self, password_hash, server_public, client_private, common_secret): 130 | """S = (B - (k * g^x)) ^ (a + (u * x)) % N 131 | 132 | :param int server_public: 133 | :param int password_hash: 134 | :param int client_private: 135 | :param int common_secret: 136 | :rtype: int 137 | """ 138 | password_verifier = self.get_common_password_verifier(password_hash) 139 | return pow( 140 | (server_public - (self._mult * password_verifier)), 141 | (client_private + (common_secret * password_hash)), self._prime) 142 | 143 | def get_common_session_key(self, premaster_secret): 144 | """K = H(S) 145 | 146 | :param int premaster_secret: 147 | :rtype: bytes 148 | """ 149 | return self.hash(premaster_secret, as_bytes=True) 150 | 151 | def get_server_premaster_secret(self, password_verifier, server_private, client_public, common_secret): 152 | """S = (A * v^u) ^ b % N 153 | 154 | :param int password_verifier: 155 | :param int server_private: 156 | :param int client_public: 157 | :param int common_secret: 158 | :rtype: int 159 | """ 160 | return pow((client_public * pow(password_verifier, common_secret, self._prime)), server_private, self._prime) 161 | 162 | def generate_client_private(self): 163 | """a = random() 164 | 165 | :rtype: int 166 | """ 167 | return self.generate_random() 168 | 169 | def generate_server_private(self): 170 | """b = random() 171 | 172 | :rtype: int 173 | """ 174 | return self.generate_random() 175 | 176 | def get_client_public(self, client_private): 177 | """A = g^a % N 178 | 179 | :param int client_private: 180 | :rtype: int 181 | """ 182 | return pow(self._gen, client_private, self._prime) 183 | 184 | def get_server_public(self, password_verifier, server_private): 185 | """B = (k*v + g^b) % N 186 | 187 | :param int password_verifier: 188 | :param int server_private: 189 | :rtype: int 190 | """ 191 | return ((self._mult * password_verifier) + pow(self._gen, server_private, self._prime)) % self._prime 192 | 193 | def get_common_password_hash(self, salt): 194 | """x = H(s | H(I | ":" | P)) 195 | 196 | :param int salt: 197 | :rtype: int 198 | """ 199 | password = self._password 200 | if password is None: 201 | raise SRPException('User password should be in context for this scenario.') 202 | 203 | return self.hash(salt, self.hash(self._user, password, joiner=':', as_bytes=True)) 204 | 205 | def get_common_password_verifier(self, password_hash): 206 | """v = g^x % N 207 | 208 | :param int password_hash: 209 | :rtype: int 210 | """ 211 | return pow(self._gen, password_hash, self._prime) 212 | 213 | def get_common_session_key_proof(self, session_key, salt, server_public, client_public): 214 | """M = H(H(N) XOR H(g) | H(U) | s | A | B | K) 215 | 216 | :param bytes session_key: 217 | :param int salt: 218 | :param int server_public: 219 | :param int client_public: 220 | :rtype: bytes 221 | """ 222 | h = self.hash 223 | prove = h( 224 | h(self._prime) ^ h(self._gen), 225 | h(self._user), 226 | salt, 227 | client_public, 228 | server_public, 229 | session_key, 230 | as_bytes=True 231 | ) 232 | return prove 233 | 234 | def get_common_session_key_proof_hash(self, session_key, session_key_proof, client_public): 235 | """H(A | M | K) 236 | 237 | :param bytes session_key: 238 | :param bytes session_key_proof: 239 | :param int client_public: 240 | :rtype: bytes 241 | """ 242 | return self.hash(client_public, session_key_proof, session_key, as_bytes=True) 243 | 244 | def get_user_data_triplet(self, base64=False): 245 | """( <_user>, <_password verifier>, ) 246 | 247 | :param base64: 248 | :rtype: tuple 249 | """ 250 | salt = self.generate_salt() 251 | verifier = self.get_common_password_verifier(self.get_common_password_hash(salt)) 252 | 253 | verifier = value_encode(verifier, base64) 254 | salt = value_encode(salt, base64) 255 | 256 | return self._user, verifier, salt 257 | -------------------------------------------------------------------------------- /srptools/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | 4 | class SRPException(Exception): 5 | """Base srptools exception class.""" 6 | -------------------------------------------------------------------------------- /srptools/server.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from .common import SRPSessionBase 3 | from .utils import int_from_hex 4 | 5 | 6 | if False: # pragma: no cover 7 | from .context import SRPContext 8 | 9 | 10 | class SRPServerSession(SRPSessionBase): 11 | 12 | role = 'server' 13 | 14 | def __init__(self, srp_context, password_verifier, private=None): 15 | """ 16 | :param SRPContext srp_context: 17 | :param st|unicode password_verifier: 18 | :param st|unicode private: 19 | """ 20 | super(SRPServerSession, self).__init__(srp_context, private) 21 | 22 | self._password_verifier = int_from_hex(password_verifier) 23 | 24 | if not private: 25 | self._this_private = srp_context.generate_server_private() 26 | 27 | self._server_public = srp_context.get_server_public(self._password_verifier, self._this_private) 28 | 29 | def init_session_key(self): 30 | super(SRPServerSession, self).init_session_key() 31 | 32 | premaster_secret = self._context.get_server_premaster_secret( 33 | self._password_verifier, self._this_private, self._client_public, self._common_secret) 34 | 35 | self._key = self._context.get_common_session_key(premaster_secret) 36 | 37 | def verify_proof(self, key_proof, base64=False): 38 | super(SRPServerSession, self).verify_proof(key_proof) 39 | 40 | return self._value_decode(key_proof, base64) == self.key_proof 41 | -------------------------------------------------------------------------------- /srptools/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from binascii import unhexlify, hexlify 3 | from base64 import b64encode, b64decode 4 | 5 | from six import integer_types 6 | 7 | 8 | def value_encode(val, base64=False): 9 | """Encodes int into hex or base64.""" 10 | return b64_from(val) if base64 else hex_from(val) 11 | 12 | 13 | def hex_from_b64(val): 14 | """Returns hex string representation for base64 encoded value. 15 | 16 | :param str val: 17 | :rtype: bytes|str 18 | """ 19 | return hex_from(b64decode(val)) 20 | 21 | 22 | def hex_from(val): 23 | """Returns hex string representation for a given value. 24 | 25 | :param bytes|str|unicode|int|long val: 26 | :rtype: bytes|str 27 | """ 28 | if isinstance(val, integer_types): 29 | hex_str = '%x' % val 30 | if len(hex_str) % 2: 31 | hex_str = '0' + hex_str 32 | return hex_str 33 | 34 | return hexlify(val) 35 | 36 | 37 | def int_from_hex(hexstr): 38 | """Returns int/long representation for a given hex string. 39 | 40 | :param bytes|str|unicode hexstr: 41 | :rtype: int|long 42 | """ 43 | return int(hexstr, 16) 44 | 45 | 46 | def int_to_bytes(val): 47 | """Returns bytes representation for a given int/long. 48 | 49 | :param int|long val: 50 | :rtype: bytes|str 51 | """ 52 | hex_str = hex_from(val) 53 | return unhexlify(hex_str) 54 | 55 | 56 | def b64_from(val): 57 | """Returns base64 encoded bytes for a given int/long/bytes value. 58 | 59 | :param int|long|bytes val: 60 | :rtype: bytes|str 61 | """ 62 | if isinstance(val, integer_types): 63 | val = int_to_bytes(val) 64 | return b64encode(val).decode('ascii') 65 | -------------------------------------------------------------------------------- /tests/test_client_server.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import pytest 3 | 4 | from srptools import SRPContext, SRPClientSession, SRPServerSession, SRPException 5 | from srptools.utils import int_from_hex, value_encode 6 | 7 | 8 | def test_extended(): 9 | 10 | # Preliminary steps. 11 | context = SRPContext('alice', 'password123') 12 | # Generate basic user auth data usually stored on server. 13 | username, password_verifier, salt = context.get_user_data_triplet() 14 | # And gather basic numbers for client and server to agree upon. 15 | prime = context.prime 16 | gen = context.generator 17 | 18 | salt_b64 = value_encode(int_from_hex(salt), base64=True) 19 | 20 | # Actual negotiation 21 | 22 | # Receive username from client and generate server public. 23 | server_session = SRPServerSession(SRPContext(username, prime=prime, generator=gen), password_verifier) 24 | server_public = server_session.public 25 | server_public_b64 = server_session.public_b64 26 | server_private = server_session.private 27 | assert server_session.private_b64 28 | 29 | # Receive server public and salt and process them. 30 | client_session = SRPClientSession(SRPContext(username, 'password123', prime=prime, generator=gen)) 31 | client_session.process(server_public, salt) 32 | # Generate client public and session key proof. 33 | client_public = client_session.public 34 | client_public_b64 = client_session.public_b64 35 | client_session_key_proof = client_session.key_proof 36 | client_private = client_session.private 37 | assert client_session.private_b64 38 | 39 | # Process client public and verify session key proof. 40 | server_session.process(client_public, salt) 41 | assert server_session.verify_proof(client_session_key_proof) 42 | # Generate session key proof hash. 43 | server_session_key_proof_hash = client_session.key_proof_hash 44 | 45 | # Verify session key proff hash received from server. 46 | assert client_session.verify_proof(server_session_key_proof_hash) 47 | 48 | assert client_session.key_b64 49 | assert client_session.key_proof_b64 50 | assert client_session.key_proof_hash_b64 51 | 52 | # Restore sessions from privates. 53 | server_session = SRPServerSession( 54 | SRPContext(username, prime=prime, generator=gen), password_verifier, 55 | private=server_private) 56 | client_session = SRPClientSession( 57 | SRPContext(username, 'password123', prime=prime, generator=gen), 58 | private=client_private) 59 | 60 | skey_cl, skey_proof_cl, skey_prove_hash_cl = client_session.process(server_public, salt) 61 | skey_srv, skey_proof_srv, skey_prove_hash_srv = server_session.process(client_public, salt) 62 | 63 | assert skey_cl == skey_srv 64 | assert skey_proof_cl == skey_proof_srv 65 | 66 | # Base 64 test 67 | skey_cl, skey_proof_cl, skey_prove_hash_cl = client_session.process(server_public_b64, salt_b64, base64=True) 68 | skey_srv, skey_proof_srv, skey_prove_hash_srv = server_session.process(client_public_b64, salt_b64, base64=True) 69 | 70 | assert skey_cl == skey_srv 71 | assert skey_proof_cl == skey_proof_srv 72 | 73 | 74 | def test_simple(): 75 | # Agree on communication details. 76 | context = SRPContext('alice', 'password123') 77 | username, password_verifier, salt = context.get_user_data_triplet() 78 | prime = context.prime 79 | gen = context.generator 80 | 81 | # Receive username from client and generate server public. 82 | server_session = SRPServerSession(SRPContext(username, prime=prime, generator=gen), password_verifier) 83 | server_public = server_session.public 84 | 85 | # Receive server public and salt and process them. 86 | client_session = SRPClientSession(SRPContext(username, 'password123', prime=prime, generator=gen)) 87 | client_session.process(server_public, salt) 88 | # Generate client public and session key. 89 | client_public = client_session.public 90 | client_session_key = client_session.key 91 | 92 | # Process client public and compare session keys. 93 | server_session.process(client_public, salt) 94 | server_session_key = server_session.key 95 | 96 | assert server_session_key == client_session_key 97 | 98 | 99 | def test_raises(): 100 | server_session = SRPServerSession(SRPContext('1', '2'), '1') 101 | server_session._context._prime = 1 # to trigger error 102 | with pytest.raises(SRPException): 103 | server_session.init_common_secret('1') 104 | 105 | client_session = SRPClientSession(SRPContext('1', '2')) 106 | client_session._context._prime = 1 # to trigger error 107 | with pytest.raises(SRPException): 108 | client_session.init_common_secret('1') 109 | -------------------------------------------------------------------------------- /tests/test_context.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import pytest 3 | 4 | from srptools import SRPContext, SRPException 5 | from srptools.utils import int_from_hex, hex_from 6 | 7 | 8 | def test_context(): 9 | 10 | def to_hex_u(val): 11 | return hex_from(val).upper() 12 | 13 | static_salt = int_from_hex('BEB25379D1A8581EB5A727673A2441EE') 14 | static_client_private = int_from_hex('60975527035CF2AD1989806F0407210BC81EDC04E2762A56AFD529DDDA2D4393') 15 | static_server_private = int_from_hex('E487CB59D31AC550471E81F00F6928E01DDA08E974A004F49E61F5D105284D20') 16 | 17 | context = SRPContext( 18 | 'alice', 19 | 'password123', 20 | multiplier='7556AA045AEF2CDD07ABAF0F665C3E818913186F', 21 | ) 22 | 23 | assert context.prime_b64 24 | assert context.generator_b64 25 | 26 | password_hash = context.get_common_password_hash(static_salt) 27 | assert to_hex_u(password_hash) == '94B7555AABE9127CC58CCF4993DB6CF84D16C124' 28 | 29 | password_verifier = context.get_common_password_verifier(password_hash) 30 | assert to_hex_u(password_verifier) == ( 31 | '7E273DE8696FFC4F4E337D05B4B375BEB0DDE1569E8FA00A9886D8129BADA1F1822223CA1A605B530E379BA4729FDC59' 32 | 'F105B4787E5186F5C671085A1447B52A48CF1970B4FB6F8400BBF4CEBFBB168152E08AB5EA53D15C1AFF87B2B9DA6E04' 33 | 'E058AD51CC72BFC9033B564E26480D78E955A5E29E7AB245DB2BE315E2099AFB') 34 | 35 | client_public = context.get_client_public(static_client_private) 36 | assert to_hex_u(client_public) == ( 37 | '61D5E490F6F1B79547B0704C436F523DD0E560F0C64115BB72557EC44352E8903211C04692272D8B2D1A5358A2CF1B6E' 38 | '0BFCF99F921530EC8E39356179EAE45E42BA92AEACED825171E1E8B9AF6D9C03E1327F44BE087EF06530E69F66615261' 39 | 'EEF54073CA11CF5858F0EDFDFE15EFEAB349EF5D76988A3672FAC47B0769447B') 40 | 41 | server_public = context.get_server_public(password_verifier, static_server_private) 42 | assert to_hex_u(server_public) == ( 43 | 'BD0C61512C692C0CB6D041FA01BB152D4916A1E77AF46AE105393011BAF38964DC46A0670DD125B95A981652236F99D9' 44 | 'B681CBF87837EC996C6DA04453728610D0C6DDB58B318885D7D82C7F8DEB75CE7BD4FBAA37089E6F9C6059F388838E7A' 45 | '00030B331EB76840910440B1B27AAEAEEB4012B7D7665238A8E3FB004B117B58') 46 | 47 | common_secret = context.get_common_secret(server_public, client_public) 48 | assert to_hex_u(common_secret) == 'CE38B9593487DA98554ED47D70A7AE5F462EF019' 49 | 50 | expected_premaster_secret = ( 51 | 'B0DC82BABCF30674AE450C0287745E7990A3381F63B387AAF271A10D233861E359B48220F7C4693C9AE12B0A6F67809F' 52 | '0876E2D013800D6C41BB59B6D5979B5C00A172B4A2A5903A0BDCAF8A709585EB2AFAFA8F3499B200210DCC1F10EB3394' 53 | '3CD67FC88A2F39A4BE5BEC4EC0A3212DC346D7E474B29EDE8A469FFECA686E5A') 54 | 55 | expected_session_key = b'017EEFA1CEFC5C2E626E21598987F31E0F1B11BB' 56 | 57 | server_premaster_secret = context.get_server_premaster_secret( 58 | password_verifier, static_server_private, client_public, common_secret) 59 | assert to_hex_u(server_premaster_secret) == expected_premaster_secret 60 | 61 | client_premaster_secret = context.get_client_premaster_secret( 62 | password_hash, server_public, static_client_private, common_secret) 63 | assert to_hex_u(client_premaster_secret) == expected_premaster_secret 64 | 65 | server_session_key = context.get_common_session_key(server_premaster_secret) 66 | assert to_hex_u(server_session_key) == expected_session_key 67 | 68 | client_session_key = context.get_common_session_key(client_premaster_secret) 69 | assert to_hex_u(client_session_key) == expected_session_key 70 | 71 | client_session_key_prove = context.get_common_session_key_proof( 72 | client_session_key, static_salt, server_public, client_public) 73 | assert to_hex_u(client_session_key_prove) == b'3F3BC67169EA71302599CF1B0F5D408B7B65D347' 74 | 75 | server_session_key_prove = context.get_common_session_key_proof_hash( 76 | server_session_key, client_session_key_prove, client_public) 77 | assert to_hex_u(server_session_key_prove) == b'9CAB3C575A11DE37D3AC1421A9F009236A48EB55' 78 | 79 | 80 | def test_context_raises(): 81 | context = SRPContext('alice') 82 | 83 | with pytest.raises(SRPException): 84 | context.get_common_password_hash(123) 85 | 86 | 87 | def test_byte_hashes(): 88 | static_salt = int_from_hex('99e50c9ad1bd2856') 89 | static_client_private = int_from_hex('2b557313c052bb0e24a3c7462e8f436769a54e8d325da794004cefab83ac8b71') 90 | static_server_private = int_from_hex( 91 | '57e997761d2aeb4c8dbfed9fde120c0ec730af1237e296f58649a6b3193ff21b36f5cfaed3049ee0051e5378f666f13d' 92 | '0c7c91040940a77a3ff1a461666c41e9aca3bd4747d74036e34941578553eb56d369638f796707425d0294809e81363f' 93 | 'ac90af29c7fde1ae142f8c280e3c2e17f9c4d68f644de5406aac7d378b812a34') 94 | 95 | context = SRPContext('bouke', 'test') 96 | 97 | password_hash = context.get_common_password_hash(static_salt) 98 | password_verifier = context.get_common_password_verifier(password_hash) 99 | assert hex_from(password_verifier) == ( 100 | '52e3ee0cde007d2e7cee87acca1c041999b528e56dec925112d30a63d8e814231c2cd3bac9ae40220c44d63029912f1f' 101 | '7dda878e938ab5bfe7b87b854bb8385020d765054d07424eb5749fcd90344dbc0372432f6db25ae12cca4584ea72270c' 102 | 'a61d831540b10919a31fde1b7b9e1cc7110429d8bbde1a6fe005896697b91436') 103 | 104 | client_public = context.get_client_public(static_client_private) 105 | assert hex_from(client_public) == ( 106 | 'e18b11cddbfa709020fa2c67344a20e6704dba3e5ca6c4ca864b94ff5442965c80dfa751a9404feb2234fcd02d7f179d' 107 | 'ca4e308d76af173ec4eacc13a8daf0237bf19d4ac0ae9a4db885fdb46d5107caea8f71a8db39eda96d594e216c632a0d' 108 | '9720d84e8abb82b3dfa67fad099e1c67b13081bb564b2369c6db5f10358680b2') 109 | 110 | server_public = context.get_server_public(password_verifier, static_server_private) 111 | assert hex_from(server_public) == ( 112 | '0b3cc73f40a5fbdee992995dc26bfc43558803689798731fd303cdf18fdecbb5544f5caf960910f1b9449c772032be38' 113 | '2b22d8763104781793553977bfdbd7cd3b05af0bf00deee22d76b477275e3294713711e3fe97f34724f9580bf2c055e7' 114 | '8ae138664dfecaa2fe353768e30c3cc395541a929dc2af6a66e118ca937cffe8') 115 | 116 | common_secret = context.get_common_secret(server_public, client_public) 117 | assert hex_from(common_secret) == 'cb709a3c8a6767fda651ad6543436e4da2c85268' 118 | 119 | expected_premaster_secret = ( 120 | '8c0ade0a5cc22507230bf092348a518fe9c29f1cbeb7a1a089ac070da5f5f7d540377fa30703164823017f421cc71237' 121 | '2cc2093228fc6b05a4c77f05216c7c911fbdc2ed63f48a1ecec9da8a1edda3c810c724d8c45f83acd48a6c05f33d36b4' 122 | '0ebca6db6f34a3f8e69289f7e49ef3492265d18488d447fb232b56306cb39a3a') 123 | 124 | server_premaster_secret = context.get_server_premaster_secret( 125 | password_verifier, static_server_private, client_public, common_secret) 126 | assert hex_from(server_premaster_secret) == expected_premaster_secret 127 | 128 | client_premaster_secret = context.get_client_premaster_secret( 129 | password_hash, server_public, static_client_private, common_secret) 130 | assert hex_from(client_premaster_secret) == expected_premaster_secret 131 | 132 | expected_session_key = b'86a5aff58ae7eca772b05bbb629f5b1c51677b14' 133 | 134 | server_session_key = context.get_common_session_key(server_premaster_secret) 135 | assert hex_from(server_session_key) == expected_session_key 136 | 137 | client_session_key = context.get_common_session_key(client_premaster_secret) 138 | assert hex_from(client_session_key) == expected_session_key 139 | 140 | client_session_key_prove = context.get_common_session_key_proof( 141 | client_session_key, static_salt, server_public, client_public) 142 | assert hex_from(client_session_key_prove) == b'001961fb3aa5c24c437df55a18a41cabce3d57b4' 143 | 144 | server_session_key_prove = context.get_common_session_key_proof_hash( 145 | server_session_key, client_session_key_prove, client_public) 146 | assert hex_from(server_session_key_prove) == b'f0a6d49e5037f34b770a8e2de9ec5e3c0880953b' 147 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # See https://tox.wiki/en/4.25.0/user_guide.html for samples. 2 | 3 | [tox] 4 | envlist = py{310,311,312} 5 | 6 | skip_missing_interpreters = True 7 | 8 | install_command = pip install {opts} {packages} 9 | 10 | [testenv] 11 | deps = 12 | pytest 13 | commands = pytest {posargs:tests} 14 | --------------------------------------------------------------------------------