├── .appveyor.yml ├── .coveragerc ├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── NEWS ├── README.md ├── misc └── windows-build.cmd ├── setup.cfg ├── setup.py ├── src └── spake2 │ ├── __init__.py │ ├── _version.py │ ├── ed25519_basic.py │ ├── ed25519_group.py │ ├── groups.py │ ├── parameters │ ├── __init__.py │ ├── all.py │ ├── ed25519.py │ ├── i1024.py │ ├── i2048.py │ └── i3072.py │ ├── params.py │ ├── spake2.py │ ├── test │ ├── __init__.py │ ├── common.py │ ├── myhkdf.py │ ├── test_compat.py │ ├── test_group.py │ ├── test_spake2.py │ └── test_utils.py │ └── util.py ├── tox.ini └── versioneer.py /.appveyor.yml: -------------------------------------------------------------------------------- 1 | # adapted from https://packaging.python.org/en/latest/appveyor/ 2 | 3 | image: 4 | - Visual Studio 2019 5 | 6 | # support status of various Python releases 7 | # https://devguide.python.org/versions/ 8 | 9 | environment: 10 | matrix: 11 | # For Python versions available on Appveyor, see 12 | # https://www.appveyor.com/docs/windows-images-software/#python 13 | - PYTHON: "C:\\Python39" 14 | TOXENV: py39 15 | - PYTHON: "C:\\Python39-x64" 16 | TOXENV: py39 17 | - PYTHON: "C:\\Python311" 18 | TOXENV: py311 19 | - PYTHON: "C:\\Python311-x64" 20 | TOXENV: py311 21 | - PYTHON: "C:\\Python312" 22 | TOXENV: py312 23 | - PYTHON: "C:\\Python312-x64" 24 | TOXENV: py312 25 | 26 | install: 27 | - | 28 | %PYTHON%\python.exe --version 29 | %PYTHON%\python.exe -m pip install tox 30 | 31 | # note: 32 | # %PYTHON% has: python.exe 33 | # %PYTHON%\Scripts has: pip.exe, tox.exe (and others installed by bare pip) 34 | 35 | 36 | build: off 37 | 38 | test_script: 39 | - | 40 | %PYTHON%\python.exe -m tox 41 | 42 | after_test: 43 | # This step builds your wheels. 44 | # Again, you only need build.cmd if you're building C extensions for 45 | # 64-bit Python 3.3/3.4. And you need to use %PYTHON% to get the correct 46 | # interpreter 47 | - | 48 | %PYTHON%\python.exe -m pip install setuptools wheel 49 | %PYTHON%\python.exe setup.py bdist_wheel 50 | 51 | artifacts: 52 | # bdist_wheel puts your built wheel in the dist directory 53 | - path: dist\* 54 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # -*- conf -*- 2 | 3 | [run] 4 | include = 5 | src/spake2/* 6 | omit = 7 | */spake2/_version.py 8 | */spake2/test/* 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/spake2/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | testing: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip tox codecov 27 | 28 | - name: Test 29 | run: | 30 | python --version 31 | tox -e coverage 32 | 33 | - name: Upload Coverage 34 | run: codecov 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /MANIFEST 2 | /dist/ 3 | /build/ 4 | /.coverage 5 | /.coverage.el 6 | /.tox/ 7 | /src/spake2.egg-info/ 8 | /.cache/ 9 | /coverage.xml 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | cache: pip 4 | before_cache: 5 | - rm -f $HOME/.cache/pip/log/debug.log 6 | python: 7 | - "2.6" 8 | - "2.7" 9 | - "3.3" 10 | - "3.4" 11 | - "3.5" 12 | - "3.6" 13 | - "pypy" 14 | install: 15 | - pip install -U tox virtualenv coverage python-coveralls 16 | script: 17 | - tox -e coverage 18 | - tox -e speed 19 | after_success: 20 | - coveralls 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | "python-spake2" Copyright (c) 2015 Brian Warner 2 | 3 | The MIT License (MIT) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include src/spake2/_version.py 3 | include README.md NEWS LICENSE 4 | include tox.ini .travis.yml .coveragerc 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # How to Make a Release 2 | # --------------------- 3 | # 4 | # This file answers the question "how to make a release" hopefully 5 | # better than a document does (only meejah and warner may currently do 6 | # the "upload to PyPI" part anyway) 7 | # 8 | 9 | default: 10 | echo "see Makefile" 11 | echo "Make a new tag, then 'make release'" 12 | echo "git tag 13 | 14 | release: 15 | @echo "Is checkout clean?" 16 | git diff-files --quiet 17 | git diff-index --quiet --cached HEAD -- 18 | 19 | @echo "Install required build software" 20 | python3 -m pip install docutils 21 | 22 | @echo "Test README" 23 | python3 setup.py check -s 24 | 25 | @echo "Is GPG Agent running, and has key?" 26 | gpg --pinentry=loopback -u meejah@meejah.ca --armor --clear-sign NEWS 27 | 28 | @echo "Build and sign wheel" 29 | python3 setup.py bdist_wheel 30 | gpg --pinentry=loopback -u meejah@meejah.ca --armor --detach-sign dist/spake2-`git describe --abbrev=0 | tail -c +2`-py3-none-any.whl 31 | ls dist/*`git describe --abbrev=0 | tail -c +2`* 32 | 33 | @echo "Build and sign source-dist" 34 | python3 setup.py sdist 35 | gpg --pinentry=loopback -u meejah@meejah.ca --armor --detach-sign dist/spake2-`git describe --abbrev=0 | tail -c +2`.tar.gz 36 | ls dist/*`git describe --abbrev=0 | tail -c +2`* 37 | 38 | release-test: 39 | gpg --verify dist/spake2-`git describe --abbrev=0 | tail -c +2`.tar.gz.asc 40 | gpg --verify dist/spake2-`git describe --abbrev=0 | tail -c +2`-py3-none-any.whl.asc 41 | python -m venv test_spake2_venv 42 | test_spake2_venv/bin/pip install --upgrade pip 43 | test_spake2_venv/bin/pip install dist/spake2-`git describe --abbrev=0 | tail -c +2`-py3-none-any.whl 44 | test_spake2_venv/bin/python -c "import spake2" 45 | test_spake2_venv/bin/pip uninstall -y spake2 46 | test_spake2_venv/bin/pip install dist/spake2-`git describe --abbrev=0 | tail -c +2`.tar.gz 47 | test_spake2_venv/bin/python -c "import spake2" 48 | rm -rf test_spake2_venv 49 | 50 | release-upload: 51 | twine upload --username __token__ --password `cat PRIVATE-release-token` dist/spake2-`git describe --abbrev=0 | tail -c +2`-py3-none-any.whl dist/spake2-`git describe --abbrev=0 | tail -c +2`-py3-none-any.whl.asc dist/spake2-`git describe --abbrev=0 | tail -c +2`.tar.gz dist/spake2-`git describe --abbrev=0 | tail -c +2`.tar.gz.asc 52 | mv dist/*-`git describe --abbrev=0 | tail -c +2`.tar.gz.asc signatures/ 53 | mv dist/*-`git describe --abbrev=0 | tail -c +2`-py3-none-any.whl.asc signatures/ 54 | git push origin-push `git describe --abbrev=0 | tail -c +2` 55 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 2 | User-Visible Changes in python-spake2 3 | 4 | * Upcoming Release 5 | 6 | (Put notes about merged features here). 7 | 8 | 9 | * Release 0.9 (24-Sep-2024) 10 | 11 | This release mainly deals with packaging simplifications: two dependencies ("six" and "hkdf") are removed. 12 | The "cryptography" library takes the place of "hkdf" for key derivation. 13 | 14 | There is no longer a vendored version of "six" (nor internal use of it, thanks a-dieste). 15 | 16 | The "versioneer" library is updated to 0.29. 17 | 18 | 19 | * Release 0.8 (14-Feb-2018) 20 | 21 | API BREAK (but not a compatibility break) 22 | 23 | Applications using this release will start up faster. The library includes 24 | multiple groups (with different message sizes, performance, and security 25 | levels), and each group uses a different blinding factor. These factors take 26 | a relatively long time to compute. The previous release did this computation 27 | for all groups, even ones that the application never imported. This release 28 | changes the import API to avoid the unnecessary calculations, which saves 29 | about 400ms at import time on my 2016-era laptop (and several seconds on a 30 | Raspberry Pi). 31 | 32 | Applications must use different "import" statements when upgrading to this 33 | release ("from spake2.parameters.ed25519 import ParamsEd25519" instead of 34 | "from spake2 import ParamsEd25519"). However this release retains message 35 | compatibility with spake2-0.7: apps using 0.8 can interoperate with apps 36 | using 0.7 without problems. 37 | 38 | 39 | * Release 0.7 (12-May-2016) 40 | 41 | COMPATIBILITY BREAK 42 | 43 | This release changes the way passwords are turned into scalars, and the way 44 | the final transcript hash is formatted. Hopefully this will be compatible 45 | with the proposed SJCL (Javascript) implementation described in the comments 46 | of https://github.com/bitwiseshiftleft/sjcl/pull/273 . Applications which use 47 | python-spake2-0.3 or earlier will not interoperate with those which use 0.7 48 | or later: the session keys will never match. 49 | 50 | pypy3 support has been dropped, until pypy3 handles python3.3 or later (it 51 | currently implements the equivalent of python3.2). 52 | 53 | python-spake2 now depends on the "hkdf" package. Tox and py.test are now used 54 | for running tests. setup.py has been switched from distutils to setuptools. 55 | 56 | 57 | * Release 0.3 (22-Sep-2015) 58 | 59 | Use the faster "M=N" blinding factors for SPAKE2_Symmetric, instead of 60 | running two sessions in parallel and combining the results. This gets the 61 | same speed and message size as the asymmetric (SPAKE2_A/SPAKE2_B) approach, 62 | and is probably safe (see README for the security proofs). 63 | 64 | 65 | * Release 0.2 (08-Apr-2015) 66 | 67 | Use Ed25519 group/parameters by default (improves speed, security, and 68 | message size). Note that both sides must use the same parameter set for 69 | compatibility. 70 | 71 | 72 | * Release 0.1 (13-Feb-2015) 73 | 74 | Initial release. Includes SPAKE2_A/SPAKE2_B, and SPAKE2_Symmetric. Provides 75 | three integer-group parameter sets (Params1024, Params2048, Params3072). 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Pure-Python SPAKE2 3 | 4 | * License: MIT 5 | * Dependencies: "cryptography" (for hkdf) 6 | * Compatible With: Python 3.9, 3.10, 3.11, 3.12, PyPy3 7 | * [![Build Status](https://travis-ci.org/warner/python-spake2.png?branch=master)](https://travis-ci.org/warner/python-spake2) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/j2q57qee3xwbqp5l/branch/master?svg=true)](https://ci.appveyor.com/project/warner/python-spake2) [![Coverage Status](https://coveralls.io/repos/warner/python-spake2/badge.svg)](https://coveralls.io/r/warner/python-spake2) 8 | 9 | This library implements the SPAKE2 password-authenticated key exchange 10 | ("PAKE") algorithm. This allows two parties, who share a weak password, to 11 | safely derive a strong shared secret (and therefore build an 12 | encrypted+authenticated channel). 13 | 14 | A passive attacker who eavesdrops on the connection learns no information 15 | about the password or the generated secret. An active attacker 16 | (man-in-the-middle) gets exactly one guess at the password, and unless they 17 | get it right, they learn no information about the password or the generated 18 | secret. Each execution of the protocol enables one guess. The use of a weak 19 | password is made safer by the rate-limiting of guesses: no off-line 20 | dictionary attack is available to the network-level attacker, and the 21 | protocol does not depend upon having previously-established confidentiality 22 | of the network (unlike e.g. sending a plaintext password over TLS). 23 | 24 | The protocol requires the exchange of one pair of messages, so only one round 25 | trip is necessary to establish the session key. If key-confirmation is 26 | necessary, that will require a second round trip. 27 | 28 | All messages are bytestrings. For the default security level (using the 29 | Ed25519 elliptic curve, roughly equivalent to an 128-bit symmetric key), the 30 | message is 33 bytes long. 31 | 32 | ## What Is It Good For? 33 | 34 | PAKE can be used in a pairing protocol, like the initial version of Firefox 35 | Sync (the one with J-PAKE), to introduce one device to another and help them 36 | share secrets. In this mode, one device creates a random code, the user 37 | copies that code to the second device, then both devices use the code as a 38 | one-time password and run the PAKE protocol. Once both devices have a shared 39 | strong key, they can exchange other secrets safely. 40 | 41 | PAKE can also be used (carefully) in a login protocol, where SRP is perhaps 42 | the best-known approach. Traditional non-PAKE login consists of sending a 43 | plaintext password through a TLS-encrypted channel, to a server which then 44 | checks it (by hashing/stretching and comparing against a stored verifier). In 45 | a PAKE login, both sides put the password into their PAKE protocol, and then 46 | confirm that their generated key is the same. This nominally does not require 47 | the initial TLS-protected channel. However note that it requires other, 48 | deeper design considerations (the PAKE protocol must be bound to whatever 49 | protected channel you end up using, else the attacker can wait for PAKE to 50 | complete normally and then steal the channel), and is not simply a drop-in 51 | replacement. In addition, the server cannot hash/stretch the password very 52 | much (see the note on "Augmented PAKE" below), so unless the client is 53 | willing to perform key-stretching before running PAKE, the server's stored 54 | verifier will be vulnerable to a low-cost dictionary attack. 55 | 56 | ## Usage 57 | 58 | Alice and Bob both initialize their SPAKE2 instances with the same (weak) 59 | password. They will exchange messages to (hopefully) derive a shared secret 60 | key. The protocol is symmetric: for each operation that Alice does, Bob will 61 | do the same. 62 | 63 | However, there are two roles in the SPAKE2 protocol, "A" and "B". The two 64 | sides must agree ahead of time which one will play which role (the messages 65 | they generate depend upon which side they play). There are two separate 66 | classes, `SPAKE2_A` and `SPAKE2_B`, and a complete interaction will use one 67 | of each (one `SPAKE2_A` on one computer, and one `SPAKE2_B` on the other 68 | computer). 69 | 70 | Each instance of a SPAKE2 protocol uses a set of shared parameters. These 71 | include a group, a generator, and a pair of arbitrary group elements. This 72 | library comes with several pre-generated parameter sets, with various 73 | security levels. 74 | 75 | You start by creating an instance (either `SPAKE2_A` or `SPAKE2_B`) with the 76 | password. Then you ask the instance for the outbound message by calling 77 | `msg_out=s.start()`, and send it to your partner. Once you receive the 78 | corresponding inbound message, you pass it into the instance and extract the 79 | (shared) key bytestring with `key=s.finish(msg_in)`. For example, the 80 | client-side might do: 81 | 82 | ```python 83 | from spake2 import SPAKE2_A 84 | s = SPAKE2_A(b"our password") 85 | msg_out = s.start() 86 | send(msg_out) # this is message A->B 87 | msg_in = receive() 88 | key = s.finish(msg_in) 89 | ``` 90 | 91 | while the server-side might do: 92 | 93 | ```python 94 | from spake2 import SPAKE2_B 95 | q = SPAKE2_B(b"our password") 96 | msg_out = q.start() 97 | send(msg_out) 98 | msg_in = receive() # this is message A->B 99 | key = q.finish(msg_in) 100 | ``` 101 | 102 | If both sides used the same password, and there is no man-in-the-middle, then 103 | both sides will obtain the same `key`. If not, the two sides will get 104 | different keys, so using "key" for data encryption will result in garbled 105 | data. 106 | 107 | The shared "key" can be used as an HMAC key to provide data integrity on 108 | subsequent messages, or as an authenticated-encryption key (e.g. 109 | nacl.secretbox). It can also be fed into [HKDF] [1] to derive other session 110 | keys as necessary. 111 | 112 | The `SPAKE2` instances, and the messages they create, are single-use. Create 113 | a new one for each new session. 114 | 115 | ### Key Confirmation 116 | 117 | To safely test for identical keys before use, you can perform a second 118 | message exchange at the end of the protocol, before actually using the key 119 | (be careful to not simply send the shared key over the wire: this would allow 120 | a MitM to learn the key that they could otherwise not guess). 121 | 122 | Alice does this: 123 | 124 | ```python 125 | ... 126 | key = s.finish(msg_in) 127 | confirm_A = HKDF(key, info="confirm_A", length=32) 128 | expected_confirm_B = HKDF(key, info="confirm_B", length=32) 129 | send(confirm_A) 130 | confirm_B = receive() 131 | assert confirm_B == expected_confirm_B 132 | ``` 133 | 134 | And Bob does this: 135 | ```python 136 | ... 137 | key = q.finish(msg_in) 138 | expected_confirm_A = HKDF(key, info="confirm_A", length=32) 139 | confirm_B = HKDF(key, info="confirm_B", length=32) 140 | send(confirm_B) 141 | confirm_A = receive() 142 | assert confirm_A == expected_confirm_A 143 | ``` 144 | 145 | ## Symmetric Usage 146 | 147 | A single SPAKE2 instance must be used asymmetrically: the two sides must 148 | somehow decide (ahead of time) which role they will each play. The 149 | implementation includes the side identifier in the exchanged message to guard 150 | against an `SPAKE2_A` talking to another `SPAKE2_A`. Typically a "client" 151 | will take on the `A` role, and the "server" will be `B`. 152 | 153 | This is a nuisance for more egalitarian protocols, where there's no clear way 154 | to assign these roles ahead of time. In this case, use `SPAKE2_Symmetric` on 155 | both sides. This uses a different set of parameters (so it is not 156 | interoperable with `SPAKE2_A` or `SPAKE2_B`, but should otherwise behave the 157 | same way. 158 | 159 | Carol does: 160 | 161 | ```python 162 | s1 = SPAKE2_Symmetric(pw) 163 | outmsg1 = s1.start() 164 | send(outmsg1) 165 | ``` 166 | 167 | Dave does the same: 168 | ```python 169 | s2 = SPAKE2_Symmetric(pw) 170 | outmsg2 = s2.start() 171 | send(outmsg2) 172 | ``` 173 | 174 | Carol then processes Dave's incoming message: 175 | ```python 176 | inmsg2 = receive() # this is outmsg1 177 | key = s1.finish(inmsg2) 178 | ``` 179 | 180 | And Dave does the same: 181 | ```python 182 | inmsg1 = receive() # this is outmsg2 183 | key = s2.finish(inmsg1) 184 | ``` 185 | 186 | ## Identifier Strings 187 | 188 | The SPAKE2 protocol includes a pair of "identity strings" `idA` and `idB` 189 | that are included in the final key-derivation hash. This binds the key to a 190 | single pair of parties, or for some specific purpose. 191 | 192 | For example, when user "alice" logs into "example.com", both sides should set 193 | `idA = b"alice"` and `idB = b"example.com"`. This prevents an attacker from 194 | substituting messages from unrelated login sessions (other users on the same 195 | server, or other servers for the same user). 196 | 197 | This also makes sure the session is established with the correct service. If 198 | Alice has one password for "example.com" but uses it for both login and 199 | file-transfer services, `idB` should be different for the two services. 200 | Otherwise if Alice is simultaneously connecting to both services, and 201 | attacker could rearrange the messages and cause her login client to connect 202 | to the file-transfer server, and vice versa. 203 | 204 | If provided, `idA` and `idB` must be bytestrings. They default to an empty 205 | string. 206 | 207 | `SPAKE2_Symmetric` uses a single `idSymmetric=` string, instead of `idA` and 208 | `idB`. Both sides must provide the same `idSymmetric=`, or leave it empty. 209 | 210 | ## Serialization 211 | 212 | Sometimes, you can't hold the SPAKE2 instance in memory for the whole 213 | negotiation: perhaps all your program state is stored in a database, and 214 | nothing lives in RAM for more than a few moments. You can persist the data 215 | from a SPAKE2 instance with `data = p.serialize()`, after the call to 216 | `start`. Then later, when the inbound message is received, you can 217 | reconstruct the instance with `p = SPAKE2_A.from_serialized(data)` before 218 | calling `p.finish(msg)`. 219 | 220 | ```python 221 | def first(): 222 | p = SPAKE2_A(password) 223 | send(p.start()) 224 | open('saved','w').write(p.serialize()) 225 | 226 | def second(inbound_message): 227 | p = SPAKE2_A.from_serialized(open('saved').read()) 228 | key = p.finish(inbound_message) 229 | return key 230 | ``` 231 | 232 | The instance data is highly sensitive and includes the password: protect it 233 | carefully. An eavesdropper who learns the instance state from just one side 234 | will be able to reconstruct the shared key. `data` is a printable ASCII 235 | bytestring (the JSON-encoding of a small dictionary). For `ParamsEd25519`, 236 | the serialized data requires 221 bytes. 237 | 238 | Note that you must restore the instance with the same side (`SPAKE2_A` vs 239 | `SPAKE2_B`) and `params=` (if overridden) as you used when first creating it. 240 | Otherwise `from_serialized()` will throw an exception. If you use non-default 241 | parameters, you might want to store an indicator along with the serialized 242 | state. 243 | 244 | Also remember that you must never re-use a SPAKE2 instance for multiple key 245 | agreements: that would reveal the key and/or password. Never use 246 | `.from_serialized()` more than once on the same saved state, and delete the 247 | state as soon as the incoming message is processed. SPAKE2 has internal 248 | checks to throw exceptions when instances are used multiple times, but the 249 | serialize/restore process can bypass those checks, so use with care. 250 | 251 | Database-backed applications should store the outbound message (`p.start()`) 252 | in the DB next to the serialized SPAKE2 state, so they can re-send the same 253 | message if the application crashes before it has been successfully delivered. 254 | `p.start()` cannot be called on the instance that `.from_serialized()` 255 | produces. 256 | 257 | ## Security 258 | 259 | SPAKE2's strength against cryptographic attacks depends upon the parameters 260 | you use, which also influence the execution speed. Use the strongest 261 | parameters your time budget can afford. 262 | 263 | The library defaults to the fast and secure Ed25519 elliptic-curve group 264 | through the `ParamsEd25519` parameter set. This offers a 128-bit security 265 | level, small messages, and fairly fast execution speed. 266 | 267 | If for some reason you don't care for elliptic curves, the `spake2.params` 268 | module includes three integer-group parameter sets: `Params1024`, 269 | `Params2048`, `Params3072`, offering 80-bit, 112-bit, and 128-bit security 270 | levels respectively. 271 | 272 | To override the default parameters, include a `params=` value when you create 273 | the SPAKE2 instance. Both sides must use the same parameters. 274 | 275 | ```python 276 | from spake2 import SPAKE2_A 277 | from spake2.parameters.i3072 import Params3072 278 | s = SPAKE2_A(b"password", params=Params3072) 279 | ``` 280 | 281 | Note that if you serialize an instance with non-default `params=`, you must 282 | restore it with the same parameters, otherwise you will get an exception: 283 | 284 | ```python 285 | s = SPAKE2_A.from_serialized(data, params=Params3072) 286 | ``` 287 | 288 | This library is very much *not* constant-time, and does not protect against 289 | timing attacks. Do not allow attackers to measure how long it takes you to 290 | create or respond to a message. 291 | 292 | This library depends upon a strong source of random numbers. Do not use it on 293 | a system where os.urandom() is weak. 294 | 295 | ## Speed 296 | 297 | To run the built-in speed tests, just run `python setup.py speed`. 298 | 299 | SPAKE2 consists of two phases, separated by a single message exchange. The 300 | time these phases take is split roughly 40/60. On my 2012 Mac Mini (2.6GHz 301 | Core-i7), the default `ParamsEd25519` security level takes about 14ms to 302 | complete both phases. For the integer groups, larger groups are slower and 303 | require larger messages (and their serialized state is larger), but are more 304 | secure. The complete output of `python setup.py speed` is: 305 | 306 | ParamsEd25519: msglen= 33, statelen=221, full=13.9ms, start= 5.5ms 307 | Params1024 : msglen=129, statelen=197, full= 4.3ms, start= 1.8ms 308 | Params2048 : msglen=257, statelen=213, full=20.8ms, start= 8.5ms 309 | Params3072 : msglen=385, statelen=221, full=41.5ms, start=16.5ms 310 | 311 | A slower CPU (1.8GHz Intel Atom) takes about 8x as long (76/32/157/322ms). 312 | 313 | This library uses only Python. A version which used C speedups for the large 314 | modular multiplication operations would probably be an order of magnitude 315 | faster. 316 | 317 | ## Testing 318 | 319 | To run the built-in test suite from a source directory, for all supported 320 | python versions, do: 321 | 322 | tox 323 | 324 | On my computer, the tests take approximately two seconds (per version). 325 | 326 | ## History 327 | 328 | The protocol was described as "PAKE2" in ["cryptobook"] [2] from Dan Boneh 329 | and Victor Shoup. This is a form of "SPAKE2", defined by Abdalla and 330 | Pointcheval at [RSA 2005] [3]. Additional recommendations for groups and 331 | distinguished elements were published in [Ladd's IETF draft] [4]. 332 | 333 | The Ed25519 implementation uses code adapted from Daniel Bernstein (djb), 334 | Matthew Dempsky, Daniel Holth, Ron Garret, with further optimizations by 335 | Brian Warner[5]. The "arbitrary element" computation, which must be the same 336 | for both participants, is from python-pure25519 version 0.5. 337 | 338 | The Boneh/Shoup chapter that defines PAKE2 also defines an augmented variant 339 | named "PAKE2+", which changes one side (typically a server) to record a 340 | derivative of the password instead of the actual password. In PAKE2+, a 341 | server compromise does not immediately give access to the passwords: instead, 342 | the attacker must perform an offline dictionary attack against the stolen 343 | data before they can learn the passwords. PAKE2+ support is planned, but not 344 | yet implemented. 345 | 346 | The security of the symmetric case was proved by Kobara/Imai[6] in 2003, and 347 | uses different (slightly weaker?) reductions than that of the asymmetric 348 | form. See also Mike Hamburg's analysis[7] from 2015. 349 | 350 | Brian Warner first wrote this Python version in July 2010. 351 | 352 | #### footnotes 353 | 354 | [1]: https://tools.ietf.org/html/rfc5869 "HKDF" 355 | [2]: http://crypto.stanford.edu/~dabo/cryptobook/ "cryptobook" 356 | [3]: http://www.di.ens.fr/~pointche/Documents/Papers/2005_rsa.pdf "RSA 2005" 357 | [4]: https://tools.ietf.org/html/draft-ladd-spake2-01 "Ladd's IETF draft" 358 | [5]: https://github.com/warner/python-pure25519 359 | [6]: http://eprint.iacr.org/2003/038.pdf "Pretty-Simple Password-Authenticated Key-Exchange Under Standard Assumptions" 360 | [7]: https://moderncrypto.org/mail-archive/curves/2015/000419.html "PAKE questions" 361 | -------------------------------------------------------------------------------- /misc/windows-build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | :: To build extensions for 64 bit Python 3, we need to configure environment 3 | :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: 4 | :: MS Windows SDK for Windows 7 and .NET Framework 4 5 | :: 6 | :: More details at: 7 | :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows 8 | 9 | IF "%DISTUTILS_USE_SDK%"=="1" ( 10 | ECHO Configuring environment to build with MSVC on a 64bit architecture 11 | ECHO Using Windows SDK 7.1 12 | "C:\Program Files\Microsoft SDKs\Windows\v7.1\Setup\WindowsSdkVer.exe" -q -version:v7.1 13 | CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 /release 14 | SET MSSdk=1 15 | REM Need the following to allow tox to see the SDK compiler 16 | SET TOX_TESTENV_PASSENV=DISTUTILS_USE_SDK MSSdk INCLUDE LIB 17 | ) ELSE ( 18 | ECHO Using default MSVC build environment 19 | ) 20 | 21 | CALL %* 22 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # See the docstring in versioneer.py for instructions. Note that you must 2 | # re-run 'versioneer.py setup' after changing this section, and commit the 3 | # resulting files. 4 | 5 | [versioneer] 6 | VCS = git 7 | style = pep440 8 | versionfile_source = src/spake2/_version.py 9 | versionfile_build = spake2/_version.py 10 | tag_prefix = v 11 | parentdir_prefix = python-spake2- 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import timeit 4 | from setuptools import setup, Command 5 | import versioneer 6 | 7 | cmdclass = {} 8 | cmdclass.update(versioneer.get_cmdclass()) 9 | 10 | class Speed(Command): 11 | description = "run speed benchmarks" 12 | user_options = [] 13 | boolean_options = [] 14 | def initialize_options(self): 15 | pass 16 | def finalize_options(self): 17 | pass 18 | def run(self): 19 | def do(setup_statements, statement): 20 | # extracted from timeit.py 21 | t = timeit.Timer(stmt=statement, 22 | setup="\n".join(setup_statements)) 23 | # determine number so that 0.2 <= total time < 2.0 24 | for i in range(1, 10): 25 | number = 10**i 26 | x = t.timeit(number) 27 | if x >= 0.2: 28 | break 29 | return x / number 30 | 31 | def abbrev(t): 32 | if t > 1.0: 33 | return "%.3fs" % t 34 | if t > 1e-3: 35 | return "%.1fms" % (t*1e3) 36 | return "%.1fus" % (t*1e6) 37 | 38 | for params in ["ParamsEd25519", 39 | "Params1024", "Params2048", "Params3072"]: 40 | S1 = "from spake2 import SPAKE2_A, SPAKE2_B; from spake2.parameters.all import %s" % params 41 | S2 = "sB = SPAKE2_B(b'password', params=%s)" % params 42 | S3 = "mB = sB.start()" 43 | S4 = "sA = SPAKE2_A(b'password', params=%s)" % params 44 | S5 = "mA = sA.start()" 45 | S8 = "key = sA.finish(mB)" 46 | 47 | full = do([S1, S2, S3], ";".join([S4, S5, S8])) 48 | start = do([S1], ";".join([S4, S5])) 49 | # how large is the generated message? 50 | from spake2.parameters import all as all_params 51 | from spake2 import SPAKE2_A 52 | p = getattr(all_params, params) 53 | s = SPAKE2_A(b"pw", params=p) 54 | msglen = len(s.start()) 55 | statelen = len(s.serialize()) 56 | print("%-13s: msglen=%3d, statelen=%3d, full=%6s, start=%6s" 57 | % (params, msglen, statelen, abbrev(full), abbrev(start))) 58 | cmdclass["speed"] = Speed 59 | 60 | setup(name="spake2", 61 | version=versioneer.get_version(), 62 | description="SPAKE2 password-authenticated key exchange (pure python)", 63 | author="Brian Warner", 64 | author_email="warner-pyspake2@lothar.com", 65 | url="https://github.com/warner/python-spake2", 66 | package_dir={"": "src"}, 67 | packages=["spake2", "spake2.parameters", "spake2.test"], 68 | license="MIT", 69 | cmdclass=cmdclass, 70 | classifiers=[ 71 | "Intended Audience :: Developers", 72 | "License :: OSI Approved :: MIT License", 73 | "Programming Language :: Python", 74 | "Programming Language :: Python :: 3", 75 | "Topic :: Security :: Cryptography", 76 | ], 77 | install_requires=["cryptography"], 78 | ) 79 | -------------------------------------------------------------------------------- /src/spake2/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .spake2 import SPAKE2_A, SPAKE2_B, SPAKE2_Symmetric, SPAKEError 3 | SPAKE2_A, SPAKE2_B, SPAKE2_Symmetric, SPAKEError # hush pyflakes 4 | 5 | from . import _version 6 | __version__ = _version.get_versions()['version'] 7 | -------------------------------------------------------------------------------- /src/spake2/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. 9 | # Generated by versioneer-0.29 10 | # https://github.com/python-versioneer/python-versioneer 11 | 12 | """Git implementation of _version.py.""" 13 | 14 | import errno 15 | import os 16 | import re 17 | import subprocess 18 | import sys 19 | from typing import Any, Callable, Dict, List, Optional, Tuple 20 | import functools 21 | 22 | 23 | def get_keywords() -> Dict[str, str]: 24 | """Get the keywords needed to look up the version information.""" 25 | # these strings will be replaced by git during git-archive. 26 | # setup.py/versioneer.py will grep for the variable names, so they must 27 | # each be defined on a line of their own. _version.py will just call 28 | # get_keywords(). 29 | git_refnames = " (HEAD -> master)" 30 | git_full = "a13d32d2b6cc5945eca182c4f89b6b4bafb2d78a" 31 | git_date = "2024-09-25 11:00:40 -0600" 32 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 33 | return keywords 34 | 35 | 36 | class VersioneerConfig: 37 | """Container for Versioneer configuration parameters.""" 38 | 39 | VCS: str 40 | style: str 41 | tag_prefix: str 42 | parentdir_prefix: str 43 | versionfile_source: str 44 | verbose: bool 45 | 46 | 47 | def get_config() -> VersioneerConfig: 48 | """Create, populate and return the VersioneerConfig() object.""" 49 | # these strings are filled in when 'setup.py versioneer' creates 50 | # _version.py 51 | cfg = VersioneerConfig() 52 | cfg.VCS = "git" 53 | cfg.style = "pep440" 54 | cfg.tag_prefix = "v" 55 | cfg.parentdir_prefix = "python-spake2-" 56 | cfg.versionfile_source = "src/spake2/_version.py" 57 | cfg.verbose = False 58 | return cfg 59 | 60 | 61 | class NotThisMethod(Exception): 62 | """Exception raised if a method is not valid for the current scenario.""" 63 | 64 | 65 | LONG_VERSION_PY: Dict[str, str] = {} 66 | HANDLERS: Dict[str, Dict[str, Callable]] = {} 67 | 68 | 69 | def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator 70 | """Create decorator to mark a method as the handler of a VCS.""" 71 | def decorate(f: Callable) -> Callable: 72 | """Store f in HANDLERS[vcs][method].""" 73 | if vcs not in HANDLERS: 74 | HANDLERS[vcs] = {} 75 | HANDLERS[vcs][method] = f 76 | return f 77 | return decorate 78 | 79 | 80 | def run_command( 81 | commands: List[str], 82 | args: List[str], 83 | cwd: Optional[str] = None, 84 | verbose: bool = False, 85 | hide_stderr: bool = False, 86 | env: Optional[Dict[str, str]] = None, 87 | ) -> Tuple[Optional[str], Optional[int]]: 88 | """Call the given command(s).""" 89 | assert isinstance(commands, list) 90 | process = None 91 | 92 | popen_kwargs: Dict[str, Any] = {} 93 | if sys.platform == "win32": 94 | # This hides the console window if pythonw.exe is used 95 | startupinfo = subprocess.STARTUPINFO() 96 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 97 | popen_kwargs["startupinfo"] = startupinfo 98 | 99 | for command in commands: 100 | try: 101 | dispcmd = str([command] + args) 102 | # remember shell=False, so use git.cmd on windows, not just git 103 | process = subprocess.Popen([command] + args, cwd=cwd, env=env, 104 | stdout=subprocess.PIPE, 105 | stderr=(subprocess.PIPE if hide_stderr 106 | else None), **popen_kwargs) 107 | break 108 | except OSError as e: 109 | if e.errno == errno.ENOENT: 110 | continue 111 | if verbose: 112 | print("unable to run %s" % dispcmd) 113 | print(e) 114 | return None, None 115 | else: 116 | if verbose: 117 | print("unable to find command, tried %s" % (commands,)) 118 | return None, None 119 | stdout = process.communicate()[0].strip().decode() 120 | if process.returncode != 0: 121 | if verbose: 122 | print("unable to run %s (error)" % dispcmd) 123 | print("stdout was %s" % stdout) 124 | return None, process.returncode 125 | return stdout, process.returncode 126 | 127 | 128 | def versions_from_parentdir( 129 | parentdir_prefix: str, 130 | root: str, 131 | verbose: bool, 132 | ) -> Dict[str, Any]: 133 | """Try to determine the version from the parent directory name. 134 | 135 | Source tarballs conventionally unpack into a directory that includes both 136 | the project name and a version string. We will also support searching up 137 | two directory levels for an appropriately named parent directory 138 | """ 139 | rootdirs = [] 140 | 141 | for _ in range(3): 142 | dirname = os.path.basename(root) 143 | if dirname.startswith(parentdir_prefix): 144 | return {"version": dirname[len(parentdir_prefix):], 145 | "full-revisionid": None, 146 | "dirty": False, "error": None, "date": None} 147 | rootdirs.append(root) 148 | root = os.path.dirname(root) # up a level 149 | 150 | if verbose: 151 | print("Tried directories %s but none started with prefix %s" % 152 | (str(rootdirs), parentdir_prefix)) 153 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 154 | 155 | 156 | @register_vcs_handler("git", "get_keywords") 157 | def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: 158 | """Extract version information from the given file.""" 159 | # the code embedded in _version.py can just fetch the value of these 160 | # keywords. When used from setup.py, we don't want to import _version.py, 161 | # so we do it with a regexp instead. This function is not used from 162 | # _version.py. 163 | keywords: Dict[str, str] = {} 164 | try: 165 | with open(versionfile_abs, "r") as fobj: 166 | for line in fobj: 167 | if line.strip().startswith("git_refnames ="): 168 | mo = re.search(r'=\s*"(.*)"', line) 169 | if mo: 170 | keywords["refnames"] = mo.group(1) 171 | if line.strip().startswith("git_full ="): 172 | mo = re.search(r'=\s*"(.*)"', line) 173 | if mo: 174 | keywords["full"] = mo.group(1) 175 | if line.strip().startswith("git_date ="): 176 | mo = re.search(r'=\s*"(.*)"', line) 177 | if mo: 178 | keywords["date"] = mo.group(1) 179 | except OSError: 180 | pass 181 | return keywords 182 | 183 | 184 | @register_vcs_handler("git", "keywords") 185 | def git_versions_from_keywords( 186 | keywords: Dict[str, str], 187 | tag_prefix: str, 188 | verbose: bool, 189 | ) -> Dict[str, Any]: 190 | """Get version information from git keywords.""" 191 | if "refnames" not in keywords: 192 | raise NotThisMethod("Short version file found") 193 | date = keywords.get("date") 194 | if date is not None: 195 | # Use only the last line. Previous lines may contain GPG signature 196 | # information. 197 | date = date.splitlines()[-1] 198 | 199 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 200 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 201 | # -like" string, which we must then edit to make compliant), because 202 | # it's been around since git-1.5.3, and it's too difficult to 203 | # discover which version we're using, or to work around using an 204 | # older one. 205 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 206 | refnames = keywords["refnames"].strip() 207 | if refnames.startswith("$Format"): 208 | if verbose: 209 | print("keywords are unexpanded, not using") 210 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 211 | refs = {r.strip() for r in refnames.strip("()").split(",")} 212 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 213 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 214 | TAG = "tag: " 215 | tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} 216 | if not tags: 217 | # Either we're using git < 1.8.3, or there really are no tags. We use 218 | # a heuristic: assume all version tags have a digit. The old git %d 219 | # expansion behaves like git log --decorate=short and strips out the 220 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 221 | # between branches and tags. By ignoring refnames without digits, we 222 | # filter out many common branch names like "release" and 223 | # "stabilization", as well as "HEAD" and "master". 224 | tags = {r for r in refs if re.search(r'\d', r)} 225 | if verbose: 226 | print("discarding '%s', no digits" % ",".join(refs - tags)) 227 | if verbose: 228 | print("likely tags: %s" % ",".join(sorted(tags))) 229 | for ref in sorted(tags): 230 | # sorting will prefer e.g. "2.0" over "2.0rc1" 231 | if ref.startswith(tag_prefix): 232 | r = ref[len(tag_prefix):] 233 | # Filter out refs that exactly match prefix or that don't start 234 | # with a number once the prefix is stripped (mostly a concern 235 | # when prefix is '') 236 | if not re.match(r'\d', r): 237 | continue 238 | if verbose: 239 | print("picking %s" % r) 240 | return {"version": r, 241 | "full-revisionid": keywords["full"].strip(), 242 | "dirty": False, "error": None, 243 | "date": date} 244 | # no suitable tags, so version is "0+unknown", but full hex is still there 245 | if verbose: 246 | print("no suitable tags, using unknown + full revision id") 247 | return {"version": "0+unknown", 248 | "full-revisionid": keywords["full"].strip(), 249 | "dirty": False, "error": "no suitable tags", "date": None} 250 | 251 | 252 | @register_vcs_handler("git", "pieces_from_vcs") 253 | def git_pieces_from_vcs( 254 | tag_prefix: str, 255 | root: str, 256 | verbose: bool, 257 | runner: Callable = run_command 258 | ) -> Dict[str, Any]: 259 | """Get version from 'git describe' in the root of the source tree. 260 | 261 | This only gets called if the git-archive 'subst' keywords were *not* 262 | expanded, and _version.py hasn't already been rewritten with a short 263 | version string, meaning we're inside a checked out source tree. 264 | """ 265 | GITS = ["git"] 266 | if sys.platform == "win32": 267 | GITS = ["git.cmd", "git.exe"] 268 | 269 | # GIT_DIR can interfere with correct operation of Versioneer. 270 | # It may be intended to be passed to the Versioneer-versioned project, 271 | # but that should not change where we get our version from. 272 | env = os.environ.copy() 273 | env.pop("GIT_DIR", None) 274 | runner = functools.partial(runner, env=env) 275 | 276 | _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, 277 | hide_stderr=not verbose) 278 | if rc != 0: 279 | if verbose: 280 | print("Directory %s not under git control" % root) 281 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 282 | 283 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 284 | # if there isn't one, this yields HEX[-dirty] (no NUM) 285 | describe_out, rc = runner(GITS, [ 286 | "describe", "--tags", "--dirty", "--always", "--long", 287 | "--match", f"{tag_prefix}[[:digit:]]*" 288 | ], cwd=root) 289 | # --long was added in git-1.5.5 290 | if describe_out is None: 291 | raise NotThisMethod("'git describe' failed") 292 | describe_out = describe_out.strip() 293 | full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) 294 | if full_out is None: 295 | raise NotThisMethod("'git rev-parse' failed") 296 | full_out = full_out.strip() 297 | 298 | pieces: Dict[str, Any] = {} 299 | pieces["long"] = full_out 300 | pieces["short"] = full_out[:7] # maybe improved later 301 | pieces["error"] = None 302 | 303 | branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], 304 | cwd=root) 305 | # --abbrev-ref was added in git-1.6.3 306 | if rc != 0 or branch_name is None: 307 | raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") 308 | branch_name = branch_name.strip() 309 | 310 | if branch_name == "HEAD": 311 | # If we aren't exactly on a branch, pick a branch which represents 312 | # the current commit. If all else fails, we are on a branchless 313 | # commit. 314 | branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) 315 | # --contains was added in git-1.5.4 316 | if rc != 0 or branches is None: 317 | raise NotThisMethod("'git branch --contains' returned error") 318 | branches = branches.split("\n") 319 | 320 | # Remove the first line if we're running detached 321 | if "(" in branches[0]: 322 | branches.pop(0) 323 | 324 | # Strip off the leading "* " from the list of branches. 325 | branches = [branch[2:] for branch in branches] 326 | if "master" in branches: 327 | branch_name = "master" 328 | elif not branches: 329 | branch_name = None 330 | else: 331 | # Pick the first branch that is returned. Good or bad. 332 | branch_name = branches[0] 333 | 334 | pieces["branch"] = branch_name 335 | 336 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 337 | # TAG might have hyphens. 338 | git_describe = describe_out 339 | 340 | # look for -dirty suffix 341 | dirty = git_describe.endswith("-dirty") 342 | pieces["dirty"] = dirty 343 | if dirty: 344 | git_describe = git_describe[:git_describe.rindex("-dirty")] 345 | 346 | # now we have TAG-NUM-gHEX or HEX 347 | 348 | if "-" in git_describe: 349 | # TAG-NUM-gHEX 350 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 351 | if not mo: 352 | # unparsable. Maybe git-describe is misbehaving? 353 | pieces["error"] = ("unable to parse git-describe output: '%s'" 354 | % describe_out) 355 | return pieces 356 | 357 | # tag 358 | full_tag = mo.group(1) 359 | if not full_tag.startswith(tag_prefix): 360 | if verbose: 361 | fmt = "tag '%s' doesn't start with prefix '%s'" 362 | print(fmt % (full_tag, tag_prefix)) 363 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 364 | % (full_tag, tag_prefix)) 365 | return pieces 366 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 367 | 368 | # distance: number of commits since tag 369 | pieces["distance"] = int(mo.group(2)) 370 | 371 | # commit: short hex revision ID 372 | pieces["short"] = mo.group(3) 373 | 374 | else: 375 | # HEX: no tags 376 | pieces["closest-tag"] = None 377 | out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) 378 | pieces["distance"] = len(out.split()) # total number of commits 379 | 380 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 381 | date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() 382 | # Use only the last line. Previous lines may contain GPG signature 383 | # information. 384 | date = date.splitlines()[-1] 385 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 386 | 387 | return pieces 388 | 389 | 390 | def plus_or_dot(pieces: Dict[str, Any]) -> str: 391 | """Return a + if we don't already have one, else return a .""" 392 | if "+" in pieces.get("closest-tag", ""): 393 | return "." 394 | return "+" 395 | 396 | 397 | def render_pep440(pieces: Dict[str, Any]) -> str: 398 | """Build up version string, with post-release "local version identifier". 399 | 400 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 401 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 402 | 403 | Exceptions: 404 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 405 | """ 406 | if pieces["closest-tag"]: 407 | rendered = pieces["closest-tag"] 408 | if pieces["distance"] or pieces["dirty"]: 409 | rendered += plus_or_dot(pieces) 410 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 411 | if pieces["dirty"]: 412 | rendered += ".dirty" 413 | else: 414 | # exception #1 415 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 416 | pieces["short"]) 417 | if pieces["dirty"]: 418 | rendered += ".dirty" 419 | return rendered 420 | 421 | 422 | def render_pep440_branch(pieces: Dict[str, Any]) -> str: 423 | """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . 424 | 425 | The ".dev0" means not master branch. Note that .dev0 sorts backwards 426 | (a feature branch will appear "older" than the master branch). 427 | 428 | Exceptions: 429 | 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] 430 | """ 431 | if pieces["closest-tag"]: 432 | rendered = pieces["closest-tag"] 433 | if pieces["distance"] or pieces["dirty"]: 434 | if pieces["branch"] != "master": 435 | rendered += ".dev0" 436 | rendered += plus_or_dot(pieces) 437 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 438 | if pieces["dirty"]: 439 | rendered += ".dirty" 440 | else: 441 | # exception #1 442 | rendered = "0" 443 | if pieces["branch"] != "master": 444 | rendered += ".dev0" 445 | rendered += "+untagged.%d.g%s" % (pieces["distance"], 446 | pieces["short"]) 447 | if pieces["dirty"]: 448 | rendered += ".dirty" 449 | return rendered 450 | 451 | 452 | def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: 453 | """Split pep440 version string at the post-release segment. 454 | 455 | Returns the release segments before the post-release and the 456 | post-release version number (or -1 if no post-release segment is present). 457 | """ 458 | vc = str.split(ver, ".post") 459 | return vc[0], int(vc[1] or 0) if len(vc) == 2 else None 460 | 461 | 462 | def render_pep440_pre(pieces: Dict[str, Any]) -> str: 463 | """TAG[.postN.devDISTANCE] -- No -dirty. 464 | 465 | Exceptions: 466 | 1: no tags. 0.post0.devDISTANCE 467 | """ 468 | if pieces["closest-tag"]: 469 | if pieces["distance"]: 470 | # update the post release segment 471 | tag_version, post_version = pep440_split_post(pieces["closest-tag"]) 472 | rendered = tag_version 473 | if post_version is not None: 474 | rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) 475 | else: 476 | rendered += ".post0.dev%d" % (pieces["distance"]) 477 | else: 478 | # no commits, use the tag as the version 479 | rendered = pieces["closest-tag"] 480 | else: 481 | # exception #1 482 | rendered = "0.post0.dev%d" % pieces["distance"] 483 | return rendered 484 | 485 | 486 | def render_pep440_post(pieces: Dict[str, Any]) -> str: 487 | """TAG[.postDISTANCE[.dev0]+gHEX] . 488 | 489 | The ".dev0" means dirty. Note that .dev0 sorts backwards 490 | (a dirty tree will appear "older" than the corresponding clean one), 491 | but you shouldn't be releasing software with -dirty anyways. 492 | 493 | Exceptions: 494 | 1: no tags. 0.postDISTANCE[.dev0] 495 | """ 496 | if pieces["closest-tag"]: 497 | rendered = pieces["closest-tag"] 498 | if pieces["distance"] or pieces["dirty"]: 499 | rendered += ".post%d" % pieces["distance"] 500 | if pieces["dirty"]: 501 | rendered += ".dev0" 502 | rendered += plus_or_dot(pieces) 503 | rendered += "g%s" % pieces["short"] 504 | else: 505 | # exception #1 506 | rendered = "0.post%d" % pieces["distance"] 507 | if pieces["dirty"]: 508 | rendered += ".dev0" 509 | rendered += "+g%s" % pieces["short"] 510 | return rendered 511 | 512 | 513 | def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: 514 | """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . 515 | 516 | The ".dev0" means not master branch. 517 | 518 | Exceptions: 519 | 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] 520 | """ 521 | if pieces["closest-tag"]: 522 | rendered = pieces["closest-tag"] 523 | if pieces["distance"] or pieces["dirty"]: 524 | rendered += ".post%d" % pieces["distance"] 525 | if pieces["branch"] != "master": 526 | rendered += ".dev0" 527 | rendered += plus_or_dot(pieces) 528 | rendered += "g%s" % pieces["short"] 529 | if pieces["dirty"]: 530 | rendered += ".dirty" 531 | else: 532 | # exception #1 533 | rendered = "0.post%d" % pieces["distance"] 534 | if pieces["branch"] != "master": 535 | rendered += ".dev0" 536 | rendered += "+g%s" % pieces["short"] 537 | if pieces["dirty"]: 538 | rendered += ".dirty" 539 | return rendered 540 | 541 | 542 | def render_pep440_old(pieces: Dict[str, Any]) -> str: 543 | """TAG[.postDISTANCE[.dev0]] . 544 | 545 | The ".dev0" means dirty. 546 | 547 | Exceptions: 548 | 1: no tags. 0.postDISTANCE[.dev0] 549 | """ 550 | if pieces["closest-tag"]: 551 | rendered = pieces["closest-tag"] 552 | if pieces["distance"] or pieces["dirty"]: 553 | rendered += ".post%d" % pieces["distance"] 554 | if pieces["dirty"]: 555 | rendered += ".dev0" 556 | else: 557 | # exception #1 558 | rendered = "0.post%d" % pieces["distance"] 559 | if pieces["dirty"]: 560 | rendered += ".dev0" 561 | return rendered 562 | 563 | 564 | def render_git_describe(pieces: Dict[str, Any]) -> str: 565 | """TAG[-DISTANCE-gHEX][-dirty]. 566 | 567 | Like 'git describe --tags --dirty --always'. 568 | 569 | Exceptions: 570 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 571 | """ 572 | if pieces["closest-tag"]: 573 | rendered = pieces["closest-tag"] 574 | if pieces["distance"]: 575 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 576 | else: 577 | # exception #1 578 | rendered = pieces["short"] 579 | if pieces["dirty"]: 580 | rendered += "-dirty" 581 | return rendered 582 | 583 | 584 | def render_git_describe_long(pieces: Dict[str, Any]) -> str: 585 | """TAG-DISTANCE-gHEX[-dirty]. 586 | 587 | Like 'git describe --tags --dirty --always -long'. 588 | The distance/hash is unconditional. 589 | 590 | Exceptions: 591 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 592 | """ 593 | if pieces["closest-tag"]: 594 | rendered = pieces["closest-tag"] 595 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 596 | else: 597 | # exception #1 598 | rendered = pieces["short"] 599 | if pieces["dirty"]: 600 | rendered += "-dirty" 601 | return rendered 602 | 603 | 604 | def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: 605 | """Render the given version pieces into the requested style.""" 606 | if pieces["error"]: 607 | return {"version": "unknown", 608 | "full-revisionid": pieces.get("long"), 609 | "dirty": None, 610 | "error": pieces["error"], 611 | "date": None} 612 | 613 | if not style or style == "default": 614 | style = "pep440" # the default 615 | 616 | if style == "pep440": 617 | rendered = render_pep440(pieces) 618 | elif style == "pep440-branch": 619 | rendered = render_pep440_branch(pieces) 620 | elif style == "pep440-pre": 621 | rendered = render_pep440_pre(pieces) 622 | elif style == "pep440-post": 623 | rendered = render_pep440_post(pieces) 624 | elif style == "pep440-post-branch": 625 | rendered = render_pep440_post_branch(pieces) 626 | elif style == "pep440-old": 627 | rendered = render_pep440_old(pieces) 628 | elif style == "git-describe": 629 | rendered = render_git_describe(pieces) 630 | elif style == "git-describe-long": 631 | rendered = render_git_describe_long(pieces) 632 | else: 633 | raise ValueError("unknown style '%s'" % style) 634 | 635 | return {"version": rendered, "full-revisionid": pieces["long"], 636 | "dirty": pieces["dirty"], "error": None, 637 | "date": pieces.get("date")} 638 | 639 | 640 | def get_versions() -> Dict[str, Any]: 641 | """Get version information or return default if unable to do so.""" 642 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 643 | # __file__, we can work backwards from there to the root. Some 644 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 645 | # case we can only use expanded keywords. 646 | 647 | cfg = get_config() 648 | verbose = cfg.verbose 649 | 650 | try: 651 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 652 | verbose) 653 | except NotThisMethod: 654 | pass 655 | 656 | try: 657 | root = os.path.realpath(__file__) 658 | # versionfile_source is the relative path from the top of the source 659 | # tree (where the .git directory might live) to this file. Invert 660 | # this to find the root from __file__. 661 | for _ in cfg.versionfile_source.split('/'): 662 | root = os.path.dirname(root) 663 | except NameError: 664 | return {"version": "0+unknown", "full-revisionid": None, 665 | "dirty": None, 666 | "error": "unable to find root of source tree", 667 | "date": None} 668 | 669 | try: 670 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 671 | return render(pieces, cfg.style) 672 | except NotThisMethod: 673 | pass 674 | 675 | try: 676 | if cfg.parentdir_prefix: 677 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 678 | except NotThisMethod: 679 | pass 680 | 681 | return {"version": "0+unknown", "full-revisionid": None, 682 | "dirty": None, 683 | "error": "unable to compute version", "date": None} 684 | -------------------------------------------------------------------------------- /src/spake2/ed25519_basic.py: -------------------------------------------------------------------------------- 1 | import binascii, hashlib, itertools 2 | from .groups import expand_arbitrary_element_seed 3 | 4 | Q = 2**255 - 19 5 | L = 2**252 + 27742317777372353535851937790883648493 6 | 7 | def inv(x): 8 | return pow(x, Q-2, Q) 9 | 10 | d = -121665 * inv(121666) 11 | I = pow(2,(Q-1)//4,Q) 12 | 13 | def xrecover(y): 14 | xx = (y*y-1) * inv(d*y*y+1) 15 | x = pow(xx,(Q+3)//8,Q) 16 | if (x*x - xx) % Q != 0: x = (x*I) % Q 17 | if x % 2 != 0: x = Q-x 18 | return x 19 | 20 | By = 4 * inv(5) 21 | Bx = xrecover(By) 22 | B = [Bx % Q,By % Q] 23 | 24 | # Extended Coordinates: x=X/Z, y=Y/Z, x*y=T/Z 25 | # http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html 26 | 27 | def xform_affine_to_extended(pt): 28 | (x, y) = pt 29 | return (x%Q, y%Q, 1, (x*y)%Q) # (X,Y,Z,T) 30 | 31 | def xform_extended_to_affine(pt): 32 | (x, y, z, _) = pt 33 | return ((x*inv(z))%Q, (y*inv(z))%Q) 34 | 35 | def double_element(pt): # extended->extended 36 | # dbl-2008-hwcd 37 | (X1, Y1, Z1, _) = pt 38 | A = (X1*X1) 39 | B = (Y1*Y1) 40 | C = (2*Z1*Z1) 41 | D = (-A) % Q 42 | J = (X1+Y1) % Q 43 | E = (J*J-A-B) % Q 44 | G = (D+B) % Q 45 | F = (G-C) % Q 46 | H = (D-B) % Q 47 | X3 = (E*F) % Q 48 | Y3 = (G*H) % Q 49 | Z3 = (F*G) % Q 50 | T3 = (E*H) % Q 51 | return (X3, Y3, Z3, T3) 52 | 53 | def add_elements(pt1, pt2): # extended->extended 54 | # add-2008-hwcd-3 . Slightly slower than add-2008-hwcd-4, but -3 is 55 | # unified, so it's safe for general-purpose addition 56 | (X1, Y1, Z1, T1) = pt1 57 | (X2, Y2, Z2, T2) = pt2 58 | A = ((Y1-X1)*(Y2-X2)) % Q 59 | B = ((Y1+X1)*(Y2+X2)) % Q 60 | C = T1*(2*d)*T2 % Q 61 | D = Z1*2*Z2 % Q 62 | E = (B-A) % Q 63 | F = (D-C) % Q 64 | G = (D+C) % Q 65 | H = (B+A) % Q 66 | X3 = (E*F) % Q 67 | Y3 = (G*H) % Q 68 | T3 = (E*H) % Q 69 | Z3 = (F*G) % Q 70 | return (X3, Y3, Z3, T3) 71 | 72 | def scalarmult_element_safe_slow(pt, n): 73 | # this form is slightly slower, but tolerates arbitrary points, including 74 | # those which are not in the main 1*L subgroup. This includes points of 75 | # order 1 (the neutral element Zero), 2, 4, and 8. 76 | assert n >= 0 77 | if n==0: 78 | return xform_affine_to_extended((0,1)) 79 | _ = double_element(scalarmult_element_safe_slow(pt, n>>1)) 80 | return add_elements(_, pt) if n&1 else _ 81 | 82 | def _add_elements_nonunfied(pt1, pt2): # extended->extended 83 | # add-2008-hwcd-4 : NOT unified, only for pt1!=pt2. About 10% faster than 84 | # the (unified) add-2008-hwcd-3, and safe to use inside scalarmult if you 85 | # aren't using points of order 1/2/4/8 86 | (X1, Y1, Z1, T1) = pt1 87 | (X2, Y2, Z2, T2) = pt2 88 | A = ((Y1-X1)*(Y2+X2)) % Q 89 | B = ((Y1+X1)*(Y2-X2)) % Q 90 | C = (Z1*2*T2) % Q 91 | D = (T1*2*Z2) % Q 92 | E = (D+C) % Q 93 | F = (B-A) % Q 94 | G = (B+A) % Q 95 | H = (D-C) % Q 96 | X3 = (E*F) % Q 97 | Y3 = (G*H) % Q 98 | Z3 = (F*G) % Q 99 | T3 = (E*H) % Q 100 | return (X3, Y3, Z3, T3) 101 | 102 | def scalarmult_element(pt, n): # extended->extended 103 | # This form only works properly when given points that are a member of 104 | # the main 1*L subgroup. It will give incorrect answers when called with 105 | # the points of order 1/2/4/8, including point Zero. (it will also work 106 | # properly when given points of order 2*L/4*L/8*L) 107 | assert n >= 0 108 | if n==0: 109 | return xform_affine_to_extended((0,1)) 110 | _ = double_element(scalarmult_element(pt, n>>1)) 111 | return _add_elements_nonunfied(_, pt) if n&1 else _ 112 | 113 | # points are encoded as 32-bytes little-endian, b255 is sign, b2b1b0 are 0 114 | 115 | def encodepoint(P): 116 | x = P[0] 117 | y = P[1] 118 | # MSB of output equals x.b0 (=x&1) 119 | # rest of output is little-endian y 120 | assert 0 <= y < (1<<255) # always < 0x7fff..ff 121 | if x & 1: 122 | y += 1<<255 123 | return binascii.unhexlify(("%064x" % y).encode("ascii"))[::-1] 124 | 125 | def isoncurve(P): 126 | x = P[0] 127 | y = P[1] 128 | return (-x*x + y*y - 1 - d*x*x*y*y) % Q == 0 129 | 130 | class NotOnCurve(Exception): 131 | pass 132 | 133 | def decodepoint(s): 134 | unclamped = int(binascii.hexlify(s[:32][::-1]), 16) 135 | clamp = (1 << 255) - 1 136 | y = unclamped & clamp # clear MSB 137 | x = xrecover(y) 138 | if bool(x & 1) != bool(unclamped & (1<<255)): x = Q-x 139 | P = [x,y] 140 | if not isoncurve(P): raise NotOnCurve("decoding point that is not on curve") 141 | return P 142 | 143 | # scalars are encoded as 32-bytes little-endian 144 | 145 | def bytes_to_scalar(s): 146 | assert len(s) == 32, len(s) 147 | return int(binascii.hexlify(s[::-1]), 16) 148 | 149 | def bytes_to_clamped_scalar(s): 150 | # Ed25519 private keys clamp the scalar to ensure two things: 151 | # 1: integer value is in L/2 .. L, to avoid small-logarithm 152 | # non-wraparaound 153 | # 2: low-order 3 bits are zero, so a small-subgroup attack won't learn 154 | # any information 155 | # set the top two bits to 01, and the bottom three to 000 156 | a_unclamped = bytes_to_scalar(s) 157 | AND_CLAMP = (1<<254) - 1 - 7 158 | OR_CLAMP = (1<<254) 159 | a_clamped = (a_unclamped & AND_CLAMP) | OR_CLAMP 160 | return a_clamped 161 | 162 | def random_scalar(entropy_f): # 0..L-1 inclusive 163 | # reduce the bias to a safe level by generating 256 extra bits 164 | oversized = int(binascii.hexlify(entropy_f(32+32)), 16) 165 | return oversized % L 166 | 167 | # unused, in favor of common HKDF approach in groups.py 168 | #def password_to_scalar(pw): 169 | # oversized = hashlib.sha512(pw).digest() 170 | # return int(binascii.hexlify(oversized), 16) % L 171 | 172 | def scalar_to_bytes(y): 173 | y = y % L 174 | assert 0 <= y < 2**256 175 | return binascii.unhexlify(("%064x" % y).encode("ascii"))[::-1] 176 | 177 | # Elements, of various orders 178 | 179 | def is_extended_zero(XYTZ): 180 | # catch Zero 181 | (X, Y, Z, T) = XYTZ 182 | Y = Y % Q 183 | Z = Z % Q 184 | if X==0 and Y==Z and Y!=0: 185 | return True 186 | return False 187 | 188 | class ElementOfUnknownGroup: 189 | # This is used for points of order 2,4,8,2*L,4*L,8*L 190 | def __init__(self, XYTZ): 191 | assert isinstance(XYTZ, tuple) 192 | assert len(XYTZ) == 4 193 | self.XYTZ = XYTZ 194 | 195 | def add(self, other): 196 | if not isinstance(other, ElementOfUnknownGroup): 197 | raise TypeError("elements can only be added to other elements") 198 | sum_XYTZ = add_elements(self.XYTZ, other.XYTZ) 199 | if is_extended_zero(sum_XYTZ): 200 | return Zero 201 | return ElementOfUnknownGroup(sum_XYTZ) 202 | 203 | def scalarmult(self, s): 204 | if isinstance(s, ElementOfUnknownGroup): 205 | raise TypeError("elements cannot be multiplied together") 206 | assert s >= 0 207 | product = scalarmult_element_safe_slow(self.XYTZ, s) 208 | return ElementOfUnknownGroup(product) 209 | 210 | def to_bytes(self): 211 | return encodepoint(xform_extended_to_affine(self.XYTZ)) 212 | def __eq__(self, other): 213 | return self.to_bytes() == other.to_bytes() 214 | def __ne__(self, other): 215 | return not self == other 216 | 217 | class Element(ElementOfUnknownGroup): 218 | # this only holds elements in the main 1*L subgroup. It never holds Zero, 219 | # or elements of order 1/2/4/8, or 2*L/4*L/8*L. 220 | 221 | def add(self, other): 222 | if not isinstance(other, ElementOfUnknownGroup): 223 | raise TypeError("elements can only be added to other elements") 224 | sum_element = ElementOfUnknownGroup.add(self, other) 225 | if sum_element is Zero: 226 | return sum_element 227 | if isinstance(other, Element): 228 | # adding two subgroup elements results in another subgroup 229 | # element, or Zero, and we've already excluded Zero 230 | return Element(sum_element.XYTZ) 231 | # not necessarily a subgroup member, so assume not 232 | return sum_element 233 | 234 | def scalarmult(self, s): 235 | if isinstance(s, ElementOfUnknownGroup): 236 | raise TypeError("elements cannot be multiplied together") 237 | # scalarmult of subgroup members can be done modulo the subgroup 238 | # order, and using the faster non-unified function. 239 | s = s % L 240 | # scalarmult(s=0) gets you Zero 241 | if s == 0: 242 | return Zero 243 | # scalarmult(s=1) gets you self, which is a subgroup member 244 | # scalarmult(s= scalar_size_bytes 78 | i = bytes_to_number(oversized) 79 | return i % q 80 | 81 | def expand_arbitrary_element_seed(data, num_bytes): 82 | return hkdf.HKDF( 83 | algorithm=hashes.SHA256(), 84 | length=num_bytes, 85 | salt=b"", 86 | info=b"SPAKE2 arbitrary element" 87 | ).derive(data) 88 | 89 | class _Element: 90 | def __init__(self, group, e): 91 | self._group = group 92 | self._e = e 93 | 94 | def add(self, other): 95 | return self._group._add(self, other) 96 | def scalarmult(self, s): 97 | return self._group._scalarmult(self, s) 98 | 99 | def to_bytes(self): 100 | return self._group._element_to_bytes(self) 101 | 102 | class IntegerGroup: 103 | def __init__(self, p, q, g): 104 | self.q = q # the subgroup order, used for scalars 105 | self.scalar_size_bytes = size_bytes(self.q) 106 | _s = self.scalar_to_bytes(self.password_to_scalar(b"")) 107 | assert isinstance(_s, bytes) 108 | assert len(_s) >= self.scalar_size_bytes 109 | self.Zero = _Element(self, 1) 110 | self.Base = _Element(self, g) # generator of the subgroup 111 | 112 | # these are the public system parameters 113 | self.p = p # the field size 114 | self.element_size_bits = size_bits(self.p) 115 | self.element_size_bytes = size_bytes(self.p) 116 | 117 | # double-check that the generator has the right order 118 | assert pow(g, self.q, self.p) == 1 119 | 120 | def order(self): 121 | return self.q 122 | 123 | def random_scalar(self, entropy_f): 124 | return unbiased_randrange(0, self.q, entropy_f) 125 | 126 | def scalar_to_bytes(self, i): 127 | # both for hashing into transcript, and save/restore of 128 | # intermediate state 129 | assert isinstance(i, int) 130 | assert 0 <= 0 < self.q 131 | return number_to_bytes(i, self.q) 132 | 133 | def bytes_to_scalar(self, b): 134 | # for restore of intermediate state 135 | assert isinstance(b, bytes) 136 | assert len(b) == self.scalar_size_bytes 137 | i = bytes_to_number(b) 138 | assert 0 <= i < self.q, (0, i, self.q) 139 | return i 140 | 141 | def password_to_scalar(self, pw): 142 | return password_to_scalar(pw, self.scalar_size_bytes, self.q) 143 | 144 | 145 | def arbitrary_element(self, seed): 146 | # we do *not* know the discrete log of this one. Nobody should. 147 | assert isinstance(seed, bytes) 148 | processed_seed = expand_arbitrary_element_seed(seed, 149 | self.element_size_bytes) 150 | assert isinstance(processed_seed, bytes) 151 | assert len(processed_seed) == self.element_size_bytes 152 | # The larger (non-prime-order) group (Zp*) we're using has order 153 | # p-1. The smaller (prime-order) subgroup has order q. Subgroup 154 | # orders always divide the larger group order, so r*q=p-1 for 155 | # some integer r. If h is an arbitrary element of the larger 156 | # group Zp*, then e=h^r will be an element of the subgroup. If h 157 | # is selected uniformly at random, so will e, and nobody will 158 | # know its discrete log. We can enforce this for pre-selected 159 | # parameters by choosing h as the output of a hash function. 160 | r = (self.p - 1) // self.q 161 | assert r * self.q == self.p - 1 162 | h = bytes_to_number(processed_seed) % self.p 163 | element = _Element(self, pow(h, r, self.p)) 164 | assert self._is_member(element) 165 | return element 166 | 167 | def _is_member(self, e): 168 | if not e._group is self: 169 | return False 170 | if pow(e._e, self.q, self.p) == 1: 171 | return True 172 | return False 173 | 174 | def _element_to_bytes(self, e): 175 | # for sending to other side, and hashing into transcript 176 | assert isinstance(e, _Element) 177 | assert e._group is self 178 | return number_to_bytes(e._e, self.p) 179 | 180 | def bytes_to_element(self, b): 181 | # for receiving from other side: test group membership here 182 | assert isinstance(b, bytes) 183 | assert len(b) == self.element_size_bytes 184 | i = bytes_to_number(b) 185 | if i <= 0 or i >= self.p: # Zp* excludes 0 186 | raise ValueError("alleged element not in the field") 187 | e = _Element(self, i) 188 | if not self._is_member(e): 189 | raise ValueError("element is not in the right group") 190 | return e 191 | 192 | def _scalarmult(self, e1, i): 193 | if not isinstance(e1, _Element): 194 | raise TypeError("E*N requires E be an element") 195 | assert e1._group is self 196 | if not isinstance(i, int): 197 | raise TypeError("E*N requires N be a scalar") 198 | return _Element(self, pow(e1._e, i % self.q, self.p)) 199 | 200 | def _add(self, e1, e2): 201 | if not isinstance(e1, _Element): 202 | raise TypeError("E*N requires E be an element") 203 | assert e1._group is self 204 | if not isinstance(e2, _Element): 205 | raise TypeError("E*N requires E be an element") 206 | assert e2._group is self 207 | return _Element(self, (e1._e * e2._e) % self.p) 208 | 209 | 210 | # This 1024-bit group originally came from the J-PAKE demo code, 211 | # http://haofeng66.googlepages.com/JPAKEDemo.java . That java code 212 | # recommended these 2048 and 3072 bit groups from this NIST document: 213 | # http://csrc.nist.gov/groups/ST/toolkit/documents/Examples/DSA2_All.pdf 214 | 215 | # L=1024, N=160 216 | I1024 = IntegerGroup( 217 | p=0xE0A67598CD1B763BC98C8ABB333E5DDA0CD3AA0E5E1FB5BA8A7B4EABC10BA338FAE06DD4B90FDA70D7CF0CB0C638BE3341BEC0AF8A7330A3307DED2299A0EE606DF035177A239C34A912C202AA5F83B9C4A7CF0235B5316BFC6EFB9A248411258B30B839AF172440F32563056CB67A861158DDD90E6A894C72A5BBEF9E286C6B, 218 | q=0xE950511EAB424B9A19A2AEB4E159B7844C589C4F, 219 | g=0xD29D5121B0423C2769AB21843E5A3240FF19CACC792264E3BB6BE4F78EDD1B15C4DFF7F1D905431F0AB16790E1F773B5CE01C804E509066A9919F5195F4ABC58189FD9FF987389CB5BEDF21B4DAB4F8B76A055FFE2770988FE2EC2DE11AD92219F0B351869AC24DA3D7BA87011A701CE8EE7BFE49486ED4527B7186CA4610A75, 220 | ) 221 | 222 | # L=2048, N=224 223 | I2048 = IntegerGroup( 224 | p=0xC196BA05AC29E1F9C3C72D56DFFC6154A033F1477AC88EC37F09BE6C5BB95F51C296DD20D1A28A067CCC4D4316A4BD1DCA55ED1066D438C35AEBAABF57E7DAE428782A95ECA1C143DB701FD48533A3C18F0FE23557EA7AE619ECACC7E0B51652A8776D02A425567DED36EABD90CA33A1E8D988F0BBB92D02D1D20290113BB562CE1FC856EEB7CDD92D33EEA6F410859B179E7E789A8F75F645FAE2E136D252BFFAFF89528945C1ABE705A38DBC2D364AADE99BE0D0AAD82E5320121496DC65B3930E38047294FF877831A16D5228418DE8AB275D7D75651CEFED65F78AFC3EA7FE4D79B35F62A0402A1117599ADAC7B269A59F353CF450E6982D3B1702D9CA83, 225 | q=0x90EAF4D1AF0708B1B612FF35E0A2997EB9E9D263C9CE659528945C0D, 226 | g=0xA59A749A11242C58C894E9E5A91804E8FA0AC64B56288F8D47D51B1EDC4D65444FECA0111D78F35FC9FDD4CB1F1B79A3BA9CBEE83A3F811012503C8117F98E5048B089E387AF6949BF8784EBD9EF45876F2E6A5A495BE64B6E770409494B7FEE1DBB1E4B2BC2A53D4F893D418B7159592E4FFFDF6969E91D770DAEBD0B5CB14C00AD68EC7DC1E5745EA55C706C4A1C5C88964E34D09DEB753AD418C1AD0F4FDFD049A955E5D78491C0B7A2F1575A008CCD727AB376DB6E695515B05BD412F5B8C2F4C77EE10DA48ABD53F5DD498927EE7B692BBBCDA2FB23A516C5B4533D73980B2A3B60E384ED200AE21B40D273651AD6060C13D97FD69AA13C5611A51B9085, 227 | ) 228 | 229 | # L=3072, N=256 230 | I3072 = IntegerGroup( 231 | p=0x90066455B5CFC38F9CAA4A48B4281F292C260FEEF01FD61037E56258A7795A1C7AD46076982CE6BB956936C6AB4DCFE05E6784586940CA544B9B2140E1EB523F009D20A7E7880E4E5BFA690F1B9004A27811CD9904AF70420EEFD6EA11EF7DA129F58835FF56B89FAA637BC9AC2EFAAB903402229F491D8D3485261CD068699B6BA58A1DDBBEF6DB51E8FE34E8A78E542D7BA351C21EA8D8F1D29F5D5D15939487E27F4416B0CA632C59EFD1B1EB66511A5A0FBF615B766C5862D0BD8A3FE7A0E0DA0FB2FE1FCB19E8F9996A8EA0FCCDE538175238FC8B0EE6F29AF7F642773EBE8CD5402415A01451A840476B2FCEB0E388D30D4B376C37FE401C2A2C2F941DAD179C540C1C8CE030D460C4D983BE9AB0B20F69144C1AE13F9383EA1C08504FB0BF321503EFE43488310DD8DC77EC5B8349B8BFE97C2C560EA878DE87C11E3D597F1FEA742D73EEC7F37BE43949EF1A0D15C3F3E3FC0A8335617055AC91328EC22B50FC15B941D3D1624CD88BC25F3E941FDDC6200689581BFEC416B4B2CB73, 232 | q=0xCFA0478A54717B08CE64805B76E5B14249A77A4838469DF7F7DC987EFCCFB11D, 233 | g=0x5E5CBA992E0A680D885EB903AEA78E4A45A469103D448EDE3B7ACCC54D521E37F84A4BDD5B06B0970CC2D2BBB715F7B82846F9A0C393914C792E6A923E2117AB805276A975AADB5261D91673EA9AAFFEECBFA6183DFCB5D3B7332AA19275AFA1F8EC0B60FB6F66CC23AE4870791D5982AAD1AA9485FD8F4A60126FEB2CF05DB8A7F0F09B3397F3937F2E90B9E5B9C9B6EFEF642BC48351C46FB171B9BFA9EF17A961CE96C7E7A7CC3D3D03DFAD1078BA21DA425198F07D2481622BCE45969D9C4D6063D72AB7A0F08B2F49A7CC6AF335E08C4720E31476B67299E231F8BD90B39AC3AE3BE0C6B6CACEF8289A2E2873D58E51E029CAFBD55E6841489AB66B5B4B9BA6E2F784660896AFF387D92844CCB8B69475496DE19DA2E58259B090489AC8E62363CDF82CFD8EF2A427ABCD65750B506F56DDE3B988567A88126B914D7828E2B63A6D7ED0747EC59E0E0A23CE7D8A74C1D2C2A7AFB6A29799620F00E11C33787F7DED3B30E1A22D09F1FBDA1ABBBFBF25CAE05A13F812E34563F99410E73B, 234 | ) 235 | -------------------------------------------------------------------------------- /src/spake2/parameters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/warner/python-spake2/a13d32d2b6cc5945eca182c4f89b6b4bafb2d78a/src/spake2/parameters/__init__.py -------------------------------------------------------------------------------- /src/spake2/parameters/all.py: -------------------------------------------------------------------------------- 1 | from .ed25519 import ParamsEd25519 2 | from .i1024 import Params1024 3 | from .i2048 import Params2048 4 | from .i3072 import Params3072 5 | -------------------------------------------------------------------------------- /src/spake2/parameters/ed25519.py: -------------------------------------------------------------------------------- 1 | from ..params import _Params 2 | from ..ed25519_group import Ed25519Group 3 | 4 | ParamsEd25519 = _Params(Ed25519Group) 5 | -------------------------------------------------------------------------------- /src/spake2/parameters/i1024.py: -------------------------------------------------------------------------------- 1 | from ..params import _Params 2 | from ..groups import I1024 3 | # Params1024 is roughly as secure as an 80-bit symmetric key, and uses a 4 | # 1024-bit modulus. 5 | Params1024 = _Params(I1024) 6 | -------------------------------------------------------------------------------- /src/spake2/parameters/i2048.py: -------------------------------------------------------------------------------- 1 | from ..params import _Params 2 | from ..groups import I2048 3 | # Params2048 has 112-bit security and comes from NIST. 4 | Params2048 = _Params(I2048) 5 | -------------------------------------------------------------------------------- /src/spake2/parameters/i3072.py: -------------------------------------------------------------------------------- 1 | from ..params import _Params 2 | from ..groups import I3072 3 | # Params3072 has 128-bit security. 4 | Params3072 = _Params(I3072) 5 | -------------------------------------------------------------------------------- /src/spake2/params.py: -------------------------------------------------------------------------------- 1 | 2 | # M and N are defined as "randomly chosen elements of the group". It is 3 | # important that nobody knows their discrete log (if your 4 | # parameter-provider picked a secret 'haha' and told you to use 5 | # M=pow(g,haha,p), you couldn't tell that M wasn't randomly chosen, but 6 | # they could then mount an active attack against your PAKE session). S 7 | # is the same, but used for both sides of a symmetric session. 8 | # 9 | # The safe way to choose these is to hash a public string. 10 | 11 | class _Params: 12 | def __init__(self, group, M=b"M", N=b"N", S=b"symmetric"): 13 | self.group = group 14 | self.M = group.arbitrary_element(seed=M) 15 | self.N = group.arbitrary_element(seed=N) 16 | self.S = group.arbitrary_element(seed=S) 17 | self.M_str = M 18 | self.N_str = N 19 | self.S_str = S 20 | -------------------------------------------------------------------------------- /src/spake2/spake2.py: -------------------------------------------------------------------------------- 1 | import os, json 2 | from binascii import hexlify, unhexlify 3 | from hashlib import sha256 4 | from .params import _Params 5 | from .parameters.ed25519 import ParamsEd25519 6 | 7 | DefaultParams = ParamsEd25519 8 | 9 | class SPAKEError(Exception): 10 | pass 11 | class OnlyCallStartOnce(SPAKEError): 12 | """start() may only be called once. Re-using a SPAKE2 instance is likely 13 | to reveal the password or the derived key.""" 14 | class OnlyCallFinishOnce(SPAKEError): 15 | """finish() may only be called once. Re-using a SPAKE2 instance is likely 16 | to reveal the password or the derived key.""" 17 | class OffSides(SPAKEError): 18 | """I received a message from someone on the same side that I'm on: I was 19 | expecting the opposite side.""" 20 | class SerializedTooEarly(SPAKEError): 21 | pass 22 | class WrongSideSerialized(SPAKEError): 23 | """You tried to unserialize data stored for the other side.""" 24 | class WrongGroupError(SPAKEError): 25 | pass 26 | class ReflectionThwarted(SPAKEError): 27 | """Someone tried to reflect our message back to us.""" 28 | 29 | SideA = b"A" 30 | SideB = b"B" 31 | SideSymmetric = b"S" 32 | 33 | # x = random(Zp) 34 | # X = scalarmult(g, x) 35 | # X* = X + scalarmult(M, int(pw)) 36 | # y = random(Zp) 37 | # Y = scalarmult(g, y) 38 | # Y* = Y + scalarmult(N, int(pw)) 39 | # KA = scalarmult(Y* + scalarmult(N, -int(pw)), x) 40 | # key = H(H(pw) + H(idA) + H(idB) + X* + Y* + KA) 41 | # KB = scalarmult(X* + scalarmult(M, -int(pw)), y) 42 | # key = H(H(pw) + H(idA) + H(idB) + X* + Y* + KB) 43 | 44 | # to serialize intermediate state, just remember x and A-vs-B. And M/N. 45 | 46 | def finalize_SPAKE2(idA, idB, X_msg, Y_msg, K_bytes, pw): 47 | transcript = b"".join([sha256(pw).digest(), 48 | sha256(idA).digest(), sha256(idB).digest(), 49 | X_msg, Y_msg, K_bytes]) 50 | key = sha256(transcript).digest() 51 | return key 52 | 53 | def finalize_SPAKE2_symmetric(idSymmetric, msg1, msg2, K_bytes, pw): 54 | # since we don't know which side is which, we must sort the messages 55 | first_msg, second_msg = sorted([msg1, msg2]) 56 | transcript = b"".join([sha256(pw).digest(), 57 | sha256(idSymmetric).digest(), 58 | first_msg, second_msg, K_bytes]) 59 | key = sha256(transcript).digest() 60 | return key 61 | 62 | class _SPAKE2_Base: 63 | "This class manages one side of a SPAKE2 key negotiation." 64 | 65 | side = None # set by the subclass 66 | 67 | def __init__(self, password, 68 | params=DefaultParams, entropy_f=os.urandom): 69 | assert isinstance(password, bytes) 70 | self.pw = password 71 | self.pw_scalar = params.group.password_to_scalar(password) 72 | 73 | assert isinstance(params, _Params), repr(params) 74 | self.params = params 75 | self.entropy_f = entropy_f 76 | 77 | self._started = False 78 | self._finished = False 79 | 80 | def start(self): 81 | if self._started: 82 | raise OnlyCallStartOnce("start() can only be called once") 83 | self._started = True 84 | 85 | g = self.params.group 86 | self.xy_scalar = g.random_scalar(self.entropy_f) 87 | self.xy_elem = g.Base.scalarmult(self.xy_scalar) 88 | self.compute_outbound_message() 89 | # Guard against both sides using the same side= by adding a side byte 90 | # to the message. This is not included in the transcript hash at the 91 | # end. 92 | outbound_side_and_message = self.side + self.outbound_message 93 | return outbound_side_and_message 94 | 95 | def compute_outbound_message(self): 96 | #message_elem = self.xy_elem + (self.my_blinding() * self.pw_scalar) 97 | pw_blinding = self.my_blinding().scalarmult(self.pw_scalar) 98 | message_elem = self.xy_elem.add(pw_blinding) 99 | self.outbound_message = message_elem.to_bytes() 100 | 101 | def finish(self, inbound_side_and_message): 102 | if self._finished: 103 | raise OnlyCallFinishOnce("finish() can only be called once") 104 | self._finished = True 105 | 106 | self.inbound_message = self._extract_message(inbound_side_and_message) 107 | 108 | g = self.params.group 109 | inbound_elem = g.bytes_to_element(self.inbound_message) 110 | if inbound_elem.to_bytes() == self.outbound_message: 111 | raise ReflectionThwarted 112 | #K_elem = (inbound_elem + (self.my_unblinding() * -self.pw_scalar) 113 | # ) * self.xy_scalar 114 | pw_unblinding = self.my_unblinding().scalarmult(-self.pw_scalar) 115 | K_elem = inbound_elem.add(pw_unblinding).scalarmult(self.xy_scalar) 116 | K_bytes = K_elem.to_bytes() 117 | key = self._finalize(K_bytes) 118 | return key 119 | 120 | 121 | def hash_params(self): 122 | # We can't really reconstruct the group from static data, but we'll 123 | # record enough of the params to confirm that we're using the same 124 | # ones upon restore. Otherwise the failure mode is silent key 125 | # disagreement. Any changes to the group or the M/N seeds should 126 | # cause this to change. 127 | g = self.params.group 128 | pieces = [g.arbitrary_element(b"").to_bytes(), 129 | g.scalar_to_bytes(g.password_to_scalar(b"")), 130 | self.params.M.to_bytes(), 131 | self.params.N.to_bytes(), 132 | ] 133 | return sha256(b"".join(pieces)).hexdigest() 134 | 135 | def serialize(self): 136 | if not self._started: 137 | raise SerializedTooEarly("call .start() before .serialize()") 138 | return json.dumps(self._serialize_to_dict()).encode("ascii") 139 | 140 | @classmethod 141 | def from_serialized(klass, data, params=DefaultParams): 142 | d = json.loads(data.decode("ascii")) 143 | return klass._deserialize_from_dict(d, params) 144 | 145 | class _SPAKE2_Asymmetric(_SPAKE2_Base): 146 | def __init__(self, password, idA=b"", idB=b"", 147 | params=DefaultParams, entropy_f=os.urandom): 148 | _SPAKE2_Base.__init__(self, password, 149 | params=params, entropy_f=entropy_f) 150 | 151 | assert isinstance(idA, bytes), repr(idA) 152 | assert isinstance(idB, bytes), repr(idB) 153 | self.idA = idA 154 | self.idB = idB 155 | 156 | def _extract_message(self, inbound_side_and_message): 157 | other_side = inbound_side_and_message[0:1] 158 | inbound_message = inbound_side_and_message[1:] 159 | 160 | if other_side not in (SideA, SideB): 161 | raise OffSides("I don't know what side they're on") 162 | if self.side == other_side: 163 | if self.side == SideA: 164 | raise OffSides("I'm A, but I got a message from A (not B).") 165 | else: 166 | raise OffSides("I'm B, but I got a message from B (not A).") 167 | return inbound_message 168 | 169 | def _finalize(self, K_bytes): 170 | return finalize_SPAKE2(self.idA, self.idB, 171 | self.X_msg(), self.Y_msg(), 172 | K_bytes, self.pw) 173 | 174 | def _serialize_to_dict(self): 175 | g = self.params.group 176 | d = {"hashed_params": self.hash_params(), 177 | "side": self.side.decode("ascii"), 178 | "idA": hexlify(self.idA).decode("ascii"), 179 | "idB": hexlify(self.idB).decode("ascii"), 180 | "password": hexlify(self.pw).decode("ascii"), 181 | "xy_scalar": hexlify(g.scalar_to_bytes(self.xy_scalar)).decode("ascii"), 182 | } 183 | return d 184 | 185 | @classmethod 186 | def _deserialize_from_dict(klass, d, params): 187 | def _should_be_unused(count): raise NotImplementedError 188 | self = klass(password=unhexlify(d["password"].encode("ascii")), 189 | idA=unhexlify(d["idA"].encode("ascii")), 190 | idB=unhexlify(d["idB"].encode("ascii")), 191 | params=params, entropy_f=_should_be_unused) 192 | if d["side"].encode("ascii") != self.side: 193 | raise WrongSideSerialized 194 | if d["hashed_params"] != self.hash_params(): 195 | err = ("SPAKE2.from_serialized() must be called with the same" 196 | "params= that were used to create the serialized data." 197 | "These are different somehow.") 198 | raise WrongGroupError(err) 199 | g = self.params.group 200 | self._started = True 201 | xy_scalar_bytes = unhexlify(d["xy_scalar"].encode("ascii")) 202 | self.xy_scalar = g.bytes_to_scalar(xy_scalar_bytes) 203 | self.xy_elem = g.Base.scalarmult(self.xy_scalar) 204 | self.compute_outbound_message() 205 | return self 206 | 207 | 208 | # applications should use SPAKE2_A and SPAKE2_B, not raw _SPAKE2_Base() 209 | 210 | class SPAKE2_A(_SPAKE2_Asymmetric): 211 | side = SideA 212 | def my_blinding(self): return self.params.M 213 | def my_unblinding(self): return self.params.N 214 | def X_msg(self): return self.outbound_message 215 | def Y_msg(self): return self.inbound_message 216 | 217 | class SPAKE2_B(_SPAKE2_Asymmetric): 218 | side = SideB 219 | def my_blinding(self): return self.params.N 220 | def my_unblinding(self): return self.params.M 221 | def X_msg(self): return self.inbound_message 222 | def Y_msg(self): return self.outbound_message 223 | 224 | class SPAKE2_Symmetric(_SPAKE2_Base): 225 | side = SideSymmetric 226 | def __init__(self, password, idSymmetric=b"", 227 | params=DefaultParams, entropy_f=os.urandom): 228 | _SPAKE2_Base.__init__(self, password, 229 | params=params, entropy_f=entropy_f) 230 | self.idSymmetric = idSymmetric 231 | 232 | def my_blinding(self): return self.params.S 233 | def my_unblinding(self): return self.params.S 234 | 235 | def _extract_message(self, inbound_side_and_message): 236 | other_side = inbound_side_and_message[0:1] 237 | inbound_message = inbound_side_and_message[1:] 238 | if other_side == SideA: 239 | raise OffSides("I'm Symmetric, but I got a message from A") 240 | if other_side == SideB: 241 | raise OffSides("I'm Symmetric, but I got a message from B") 242 | assert other_side == SideSymmetric 243 | return inbound_message 244 | 245 | def _finalize(self, K_bytes): 246 | return finalize_SPAKE2_symmetric(self.idSymmetric, 247 | self.inbound_message, 248 | self.outbound_message, 249 | K_bytes, self.pw) 250 | 251 | def hash_params(self): 252 | g = self.params.group 253 | pieces = [g.arbitrary_element(b"").to_bytes(), 254 | g.scalar_to_bytes(g.password_to_scalar(b"")), 255 | self.params.S.to_bytes(), 256 | ] 257 | return sha256(b"".join(pieces)).hexdigest() 258 | 259 | def _serialize_to_dict(self): 260 | g = self.params.group 261 | d = {"hashed_params": self.hash_params(), 262 | "side": self.side.decode("ascii"), 263 | "idS": hexlify(self.idSymmetric).decode("ascii"), 264 | "password": hexlify(self.pw).decode("ascii"), 265 | "xy_scalar": hexlify(g.scalar_to_bytes(self.xy_scalar)).decode("ascii"), 266 | } 267 | return d 268 | 269 | @classmethod 270 | def _deserialize_from_dict(klass, d, params): 271 | if d["side"].encode("ascii") != SideSymmetric: 272 | raise WrongSideSerialized 273 | def _should_be_unused(count): raise NotImplementedError 274 | self = klass(password=unhexlify(d["password"].encode("ascii")), 275 | idSymmetric=unhexlify(d["idS"].encode("ascii")), 276 | params=params, entropy_f=_should_be_unused) 277 | if d["hashed_params"] != self.hash_params(): 278 | err = ("SPAKE2.from_serialized() must be called with the same" 279 | "params= that were used to create the serialized data." 280 | "These are different somehow.") 281 | raise WrongGroupError(err) 282 | g = self.params.group 283 | self._started = True 284 | xy_scalar_bytes = unhexlify(d["xy_scalar"].encode("ascii")) 285 | self.xy_scalar = g.bytes_to_scalar(xy_scalar_bytes) 286 | self.xy_elem = g.Base.scalarmult(self.xy_scalar) 287 | self.compute_outbound_message() 288 | return self 289 | 290 | # add ECC version for smaller messages/storage 291 | # consider timing attacks 292 | # try for compatibility with Boneh's JS version 293 | -------------------------------------------------------------------------------- /src/spake2/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/warner/python-spake2/a13d32d2b6cc5945eca182c4f89b6b4bafb2d78a/src/spake2/test/__init__.py -------------------------------------------------------------------------------- /src/spake2/test/common.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha256 2 | from itertools import count 3 | 4 | class PRG: 5 | # this returns a callable which, when invoked with an integer N, will 6 | # return N pseudorandom bytes derived from the seed 7 | def __init__(self, seed): 8 | self.generator = self.block_generator(seed) 9 | 10 | def __call__(self, numbytes): 11 | return b"".join([next(self.generator) for i in range(numbytes)]) 12 | 13 | def block_generator(self, seed): 14 | assert isinstance(seed, type(b"")) 15 | for counter in count(): 16 | cseed = b"".join([b"prng-", 17 | str(counter).encode("ascii"), 18 | b"-", 19 | seed]) 20 | block = sha256(cseed).digest() 21 | for i in range(len(block)): 22 | yield block[i:i+1] 23 | -------------------------------------------------------------------------------- /src/spake2/test/myhkdf.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha256, sha1 2 | from binascii import unhexlify 3 | import hmac 4 | 5 | def HKDF(IKM, dkLen, salt=None, info=b"", digest=sha256, 6 | _test_expected_PRK=None): 7 | assert isinstance(IKM, bytes) 8 | assert isinstance(salt, (bytes,type(None))) 9 | assert isinstance(info, bytes) 10 | hlen = len(digest(b"").digest()) 11 | assert dkLen <= hlen*255 12 | if salt is None: 13 | salt = b"\x00"*hlen 14 | # extract 15 | PRK = hmac.new(salt, IKM, digest).digest() 16 | if _test_expected_PRK and _test_expected_PRK != PRK: 17 | raise ValueError("test failed") 18 | # expand 19 | blocks = [] 20 | counter = 1 21 | t = b"" 22 | while hlen*len(blocks) < dkLen: 23 | t = hmac.new(PRK, t+info+unhexlify("%02x"%counter), digest).digest() 24 | blocks.append(t) 25 | counter += 1 26 | return b"".join(blocks)[:dkLen] 27 | 28 | def power_on_self_test(): 29 | from binascii import hexlify, unhexlify 30 | 31 | def _test(IKM, salt, info, L, PRK, OKM, digest=sha256): 32 | def remove_prefix(prefix, s): 33 | assert s.startswith(prefix) 34 | return s[len(prefix):] 35 | ikm = unhexlify(remove_prefix("0x", IKM)) 36 | salt = unhexlify(remove_prefix("0x", salt)) 37 | info = unhexlify(remove_prefix("0x", info)) 38 | prk = unhexlify(remove_prefix("0x", PRK)) 39 | okm = unhexlify(remove_prefix("0x", OKM)) 40 | assert isinstance(ikm, bytes) 41 | assert isinstance(salt, bytes) 42 | assert isinstance(info, bytes) 43 | assert isinstance(prk, bytes) 44 | assert isinstance(okm, bytes) 45 | if digest is None: 46 | out = HKDF(ikm, L, salt, info, _test_expected_PRK=prk) 47 | else: 48 | out = HKDF(ikm, L, salt, info, digest=digest, 49 | _test_expected_PRK=prk) 50 | if okm != out: 51 | raise ValueError("got %s, expected %s" % (hexlify(out), hexlify(okm))) 52 | 53 | # test vectors from RFC5869 54 | _test(IKM="0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", 55 | salt="0x000102030405060708090a0b0c", 56 | info="0xf0f1f2f3f4f5f6f7f8f9", 57 | L=42, 58 | PRK=("0x077709362c2e32df0ddc3f0dc47bba63" 59 | "90b6c73bb50f9c3122ec844ad7c2b3e5"), 60 | OKM=("0x3cb25f25faacd57a90434f64d0362f2a" 61 | "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" 62 | "34007208d5b887185865")) 63 | 64 | _test(IKM=("0x000102030405060708090a0b0c0d0e0f" 65 | "101112131415161718191a1b1c1d1e1f" 66 | "202122232425262728292a2b2c2d2e2f" 67 | "303132333435363738393a3b3c3d3e3f" 68 | "404142434445464748494a4b4c4d4e4f"), 69 | salt=("0x606162636465666768696a6b6c6d6e6f" 70 | "707172737475767778797a7b7c7d7e7f" 71 | "808182838485868788898a8b8c8d8e8f" 72 | "909192939495969798999a9b9c9d9e9f" 73 | "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"), 74 | info=("0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebf" 75 | "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" 76 | "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" 77 | "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" 78 | "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"), 79 | L=82, 80 | PRK=("0x06a6b88c5853361a06104c9ceb35b45c" 81 | "ef760014904671014a193f40c15fc244"), 82 | OKM=("0xb11e398dc80327a1c8e7f78c596a4934" 83 | "4f012eda2d4efad8a050cc4c19afa97c" 84 | "59045a99cac7827271cb41c65e590e09" 85 | "da3275600c2f09b8367793a9aca3db71" 86 | "cc30c58179ec3e87c14c01d5c1f3434f" 87 | "1d87")) 88 | 89 | _test(IKM="0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", 90 | salt="0x", 91 | info="0x", 92 | L=42, 93 | PRK=("0x19ef24a32c717b167f33a91d6f648bdf" 94 | "96596776afdb6377ac434c1c293ccb04"), 95 | OKM=("0x8da4e775a563c18f715f802a063c5a31" 96 | "b8a11f5c5ee1879ec3454e5f3c738d2d" 97 | "9d201395faa4b61a96c8")) 98 | 99 | _test(digest=sha1, 100 | IKM="0x0b0b0b0b0b0b0b0b0b0b0b", 101 | salt="0x000102030405060708090a0b0c", 102 | info="0xf0f1f2f3f4f5f6f7f8f9", 103 | L=42, 104 | PRK="0x9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243", 105 | OKM=("0x085a01ea1b10f36933068b56efa5ad81" 106 | "a4f14b822f5b091568a9cdd4f155fda2" 107 | "c22e422478d305f3f896")) 108 | _test(digest=sha1, 109 | IKM=("0x000102030405060708090a0b0c0d0e0f" 110 | "101112131415161718191a1b1c1d1e1f" 111 | "202122232425262728292a2b2c2d2e2f" 112 | "303132333435363738393a3b3c3d3e3f" 113 | "404142434445464748494a4b4c4d4e4f"), 114 | salt=("0x606162636465666768696a6b6c6d6e6f" 115 | "707172737475767778797a7b7c7d7e7f" 116 | "808182838485868788898a8b8c8d8e8f" 117 | "909192939495969798999a9b9c9d9e9f" 118 | "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"), 119 | info=("0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebf" 120 | "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" 121 | "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" 122 | "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" 123 | "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"), 124 | L=82, 125 | PRK="0x8adae09a2a307059478d309b26c4115a224cfaf6", 126 | OKM=("0x0bd770a74d1160f7c9f12cd5912a06eb" 127 | "ff6adcae899d92191fe4305673ba2ffe" 128 | "8fa3f1a4e5ad79f3f334b3b202b2173c" 129 | "486ea37ce3d397ed034c7f9dfeb15c5e" 130 | "927336d0441f4c4300e2cff0d0900b52" 131 | "d3b4")) 132 | _test(digest=sha1, 133 | IKM="0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", 134 | salt="0x", 135 | info="0x", 136 | L=42, 137 | PRK="0xda8c8a73c7fa77288ec6f5e7c297786aa0d32d01", 138 | OKM=("0x0ac1af7002b3d761d1e55298da9d0506" 139 | "b9ae52057220a306e07b6b87e8df21d0" 140 | "ea00033de03984d34918")) 141 | 142 | _test(digest=sha1, 143 | IKM="0x0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c", 144 | salt="0x", 145 | info="0x", 146 | L=42, 147 | PRK="0x2adccada18779e7c2077ad2eb19d3f3e731385dd", 148 | OKM=("0x2c91117204d745f3500d636a62f64f0a" 149 | "b3bae548aa53d423b0d1f27ebba6f5e5" 150 | "673a081d70cce7acfc48")) 151 | 152 | # finally test that HKDF() without a digest= uses SHA256 153 | 154 | _test(digest=None, 155 | IKM="0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", 156 | salt="0x", 157 | info="0x", 158 | L=42, 159 | PRK=("0x19ef24a32c717b167f33a91d6f648bdf" 160 | "96596776afdb6377ac434c1c293ccb04"), 161 | OKM=("0x8da4e775a563c18f715f802a063c5a31" 162 | "b8a11f5c5ee1879ec3454e5f3c738d2d" 163 | "9d201395faa4b61a96c8")) 164 | #print "all test passed" 165 | 166 | power_on_self_test() 167 | -------------------------------------------------------------------------------- /src/spake2/test/test_compat.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from binascii import hexlify, unhexlify 3 | from hashlib import sha256 4 | from cryptography.hazmat.primitives.kdf import hkdf 5 | from cryptography.hazmat.primitives import hashes 6 | from .myhkdf import HKDF as myHKDF 7 | from spake2 import groups, ed25519_group 8 | from spake2.spake2 import (SPAKE2_A, SPAKE2_B, SPAKE2_Symmetric, 9 | finalize_SPAKE2, finalize_SPAKE2_symmetric) 10 | from .common import PRG 11 | 12 | class TestPRG(unittest.TestCase): 13 | def test_basic(self): 14 | PRGA = PRG(b"A") 15 | dataA = PRGA(16) 16 | self.assertEqual(hexlify(dataA), b"c1d59d78903e9d7874d9064e12d36c58") 17 | PRGB = PRG(b"B") 18 | dataB = PRGB(16) 19 | self.assertEqual(hexlify(dataB), b"2af6d4b843a9e6cd1d185eb5de870f77") 20 | 21 | class SPAKE2(unittest.TestCase): 22 | """Make sure we know when an incompatible change has landed""" 23 | def test_asymmetric(self): 24 | PRGA = PRG(b"A") 25 | PRGB = PRG(b"B") 26 | pw = b"password" 27 | sA,sB = SPAKE2_A(pw, entropy_f=PRGA), SPAKE2_B(pw, entropy_f=PRGB) 28 | m1A,m1B = sA.start(), sB.start() 29 | self.assertEqual(hexlify(m1A), b"416fc960df73c9cf8ed7198b0c9534e2e96a5984bfc5edc023fd24dacf371f2af9") 30 | self.assertEqual(hexlify(m1B), b"42354e97b88406922b1df4bea1d7870f17aed3dba7c720b313edae315b00959309") 31 | # peek at the scalars, since it ought to be stable, and other 32 | # implementations that want to use this as a test vector might start 33 | # with the scalar, rather than duplicating our deterministic RNG 34 | self.assertEqual(sA.pw_scalar, 35 | 3515301705789368674385125653994241092664323519848410154015274772661223168839) 36 | self.assertEqual(sB.pw_scalar, 37 | 3515301705789368674385125653994241092664323519848410154015274772661223168839) 38 | self.assertEqual(sA.xy_scalar, 39 | 2611694063369306139794446498317402240796898290761098242657700742213257926693) 40 | self.assertEqual(sB.xy_scalar, 41 | 7002393159576182977806091886122272758628412261510164356026361256515836884383) 42 | 43 | kA,kB = sA.finish(m1B), sB.finish(m1A) 44 | self.assertEqual(hexlify(kA), 45 | b"a480bca13fa04464bb644f10e340125e96c9494f7399fef7c2bda67eb0fdf06d") 46 | self.assertEqual(hexlify(kA), hexlify(kB)) 47 | self.assertEqual(len(kA), len(sha256().digest())) 48 | 49 | def test_symmetric(self): 50 | PRG1 = PRG(b"1") 51 | PRG2 = PRG(b"2") 52 | pw = b"password" 53 | s1 = SPAKE2_Symmetric(pw, entropy_f=PRG1) 54 | s2 = SPAKE2_Symmetric(pw, entropy_f=PRG2) 55 | m11,m12 = s1.start(), s2.start() 56 | self.assertEqual(hexlify(m11), b"5308f692d38c4034ad6e2e1054c469ca1dbe990bcaec4bbd3ad78c7d968eadd0b3") 57 | self.assertEqual(hexlify(m12), b"5329e2d5f9b7a53e609204115c6458921b0bb27419ce82a27679fc5961002897df") 58 | 59 | k1,k2 = s1.finish(m12), s2.finish(m11) 60 | self.assertEqual(hexlify(k1), 61 | b"9c4fccaa3f0740615cee6fd10ed5d3a311b91b5bdc65f53e4ea7cb2fe8aa96eb") 62 | self.assertEqual(hexlify(k1), hexlify(k2)) 63 | self.assertEqual(len(k1), len(sha256().digest())) 64 | 65 | GROUPS = { 66 | "I1024": groups.I1024, 67 | "I2048": groups.I2048, 68 | "I3072": groups.I3072, 69 | "Ed25519": ed25519_group.Ed25519Group, 70 | } 71 | 72 | # These vectors exercise the password-to-scalar conversion step. The vectors 73 | # should be all JSON, so in the future we can cut-and-paste them into other 74 | # implementations for compatibility testing. 75 | # hexlify(b"pw") == "7077" 76 | # "0001feff" is meant to test non-ASCII passwords 77 | P2S_TEST_VECTORS = [ 78 | {"group": "I1024", "pw_hex": "7077", 79 | "bytes_hex": "28f73d0d793a38cb21694b751cd0affb181474be"}, 80 | {"group": "I1024", "pw_hex": "0001feff", 81 | "bytes_hex": "37044fd99e0499af9b263a21e13dd737b7b022bf"}, 82 | {"group": "I2048", "pw_hex": "7077", 83 | "bytes_hex": "56db566c2740f46557d8c3695a5eb6fb736797b63f98c58931267ae6"}, 84 | {"group": "I2048", "pw_hex": "0001feff", 85 | "bytes_hex": "058062c322379afd9eba83c084b8cf5b23aa9f69aeb659bac912222a"}, 86 | {"group": "I3072", "pw_hex": "7077", 87 | "bytes_hex": "49454ea9faa9e70213573c8f271163d6d430b994fdba8af482478c3a3ae43f04"}, 88 | {"group": "I3072", "pw_hex": "0001feff", 89 | "bytes_hex": "a1b0ffda72070f4d1bc565933904fb92307b40bc2d32ad1394eea3598128ba9a"}, 90 | {"group": "Ed25519", "pw_hex": "7077", 91 | "bytes_hex": "cf090b60384cb818b12c8d972dfbaf910c0c7295c5cfe560e508f5f062f3960f"}, 92 | {"group": "Ed25519", "pw_hex": "0001feff", 93 | "bytes_hex": "e86622bb57ea0f6f9f963354f2973a43a9e981901a478e6478682374441b0c04"}, 94 | ] 95 | 96 | class PasswordToScalar(unittest.TestCase): 97 | def test_vectors(self): 98 | for vector in P2S_TEST_VECTORS: 99 | group = GROUPS[vector["group"]] 100 | pw = unhexlify(vector["pw_hex"].encode("ascii")) 101 | scalar = group.password_to_scalar(pw) 102 | scalar_bytes = group.scalar_to_bytes(scalar) 103 | self.assertEqual(len(scalar_bytes), group.scalar_size_bytes) 104 | #print(hexlify(scalar_bytes)) 105 | expected = vector["bytes_hex"].encode("ascii") 106 | self.assertEqual(hexlify(scalar_bytes), expected, vector) 107 | 108 | # check for endian issues, number-of-leading-zeros 109 | S2B_TEST_VECTORS = [ 110 | {"group": "I1024", "scalar": 1, 111 | "bytes_hex": "0000000000000000000000000000000000000001"}, 112 | {"group": "I1024", "scalar": 2, 113 | "bytes_hex": "0000000000000000000000000000000000000002"}, 114 | {"group": "I2048", "scalar": 1, 115 | "bytes_hex": "00000000000000000000000000000000000000000000000000000001"}, 116 | {"group": "I2048", "scalar": 2, 117 | "bytes_hex": "00000000000000000000000000000000000000000000000000000002"}, 118 | {"group": "I3072", "scalar": 1, 119 | "bytes_hex": "0000000000000000000000000000000000000000000000000000000000000001"}, 120 | {"group": "I3072", "scalar": 2, 121 | "bytes_hex": "0000000000000000000000000000000000000000000000000000000000000002"}, 122 | {"group": "Ed25519", "scalar": 1, 123 | "bytes_hex": "0100000000000000000000000000000000000000000000000000000000000000"}, 124 | {"group": "Ed25519", "scalar": 2, 125 | "bytes_hex": "0200000000000000000000000000000000000000000000000000000000000000"}, 126 | ] 127 | 128 | class ScalarToBytes(unittest.TestCase): 129 | def test_vectors(self): 130 | for vector in S2B_TEST_VECTORS: 131 | group = GROUPS[vector["group"]] 132 | scalar = vector["scalar"] 133 | scalar_bytes = group.scalar_to_bytes(scalar) 134 | #print(hexlify(scalar_bytes)) 135 | expected = vector["bytes_hex"].encode("ascii") 136 | self.assertEqual(hexlify(scalar_bytes), expected, vector) 137 | 138 | AE_TEST_VECTORS = [ 139 | {"group": "I1024", "seed_hex": "41", 140 | "element_hex": "933084f15747174af82ece8ba242f83e38db4a64b8887f9ef275c318ae0b0f4338e9fafc6ff601d1b0f8b3dfe63bbaf774117c820abb16f5d054833e897647813083d2bed14c88d54717e2b5e9d161bc87fd0265c2d10002a6ac14fadf8da81fd3710c1d179c7247ffecc148f764d0a19c9319c698aa553dd825ae4112e6128d"}, 141 | {"group": "I1024", "seed_hex": "42", 142 | "element_hex": "4ac273e831a27542a1a9017d896dc32128e8e19aa726d261ae0214d7860a69958d82ad1525a8fa16c78a7b66cf52a977aefd6f4d99fb5aa26b99b0d1d9e8a8079ebd272ac78ea574df52dccb454fa253a9fad9621f8edf824b2235e02b129d357b8d3c10026357734dd4c98f018fc9ff15978679347e9b6e0a3bbd1f5402a679"}, 143 | {"group": "I2048", "seed_hex": "41", 144 | "element_hex": "bb192daef4e0ab05f5e908a3f3ddefedfc8388b4a4daae894a23125322fd24c95606b85fb7d4a041c9f312d890a057c8d3587bedfe7843d997e78140fdd530d0cb2bb2738f9ec0befbb09a863f48a5ea3b503300db65a8666e55ba875640f6db8c3cfd2d55ef3b4c67bb51d28d4dd4ba3fd443e655870fa54dfa1ce7b4b493d48c692c1c46977eeac82ea5e87afe72db3d778112a689ad0275135cd0fc8378139d700e03cfd2f7bfc0f142b6f4a5f5ecd12e09ea960e91fe3c10db637770f188fb9cb3004fda24ef7957bb7f8890fd12b77b8ade48a95620dc3442aa717b9d04dcec7ffec8a33cbf51735ab4acb1520f82e03d0b465c9c9a23f75e32f6b58208"}, 145 | {"group": "I2048", "seed_hex": "42", 146 | "element_hex": "a4964933a4dd3e8b5e172e5a48e01dc346d046b4d960004e0802f2da0636d081f21a9975f470480874c36dcd2e83f41cdeeb192659b8f03c4c7339eca7861672cab1f0ed765de48d8ac68cdeb2cd873b1415a73bb03eef497b221ff1a5635e6bcb96de1a444d09f986e964e2e842fb010712c261678d2ac8adb35f30986f90bda797528e32cefcfbb0733a15d59758e1021a3f13ff117e2519ba06ceeff3956212ddf07c1516ae68499c89d7c373c586119948e2b05c518d0a736baee46ffabd6756d354d38cb642b53ca7778eb3786b035163e76a868828dc71b28a63b6a24c11e9e280e5bf147f4dc20cb2cbd7fb7a961a2b0983119285d6dd6988554cb45b"}, 147 | {"group": "I3072", "seed_hex": "41", 148 | "element_hex": "5459e7c980132e3002b49d6756b06139ae80b4ec7183607d9534f9f0fcb7a80cdd8db3707feddca4132ef0ea2733cd09b715092dfd330da941454cc3230188b24453878223ab6498c99a64384cf6d14f9f57213aac2fb95d7a77ccb3304e9345eb09d59dea807ff9644ffb83c5b33eff26876b5261261295e1b732b77ea26b745934499d4120d7d345edee9d1d004eda574d71884436ffe953ab7d857cebad2b5b062814c649256a20fcf1b6957c0513fd7fc3ac21edd335a368812a31daa654eec5c320d235357e642eba0c3964e1d3e40cdd913da23a88ceb8272ab920e939026348580999aceef6dbbd0babec1846be0b35b5134f8420fabbddec2ad0188dbff920e9f839208b0140f240dd24890f50b89a09899f50f26422b28fa99cefb16b9b2d6d08342be802f2d84861475ee6a47d940e681cd735c42fd124403c3ce78c5c90be8203f8497c15f07f16553f31a155ae7b5eb4b5cc93f52ff8d095f56d5930c5ff9944589d89f0e153d61b6ecb00649e716f0cb95fe8280ebb282c9f3a"}, 149 | {"group": "I3072", "seed_hex": "42", 150 | "element_hex": "69e247a5284cb5683fa48c3f60ce7b83857b0f67db156d9b120a7338a52514f223d319c3a39dc81169c4a0efdcc03742a0dd08d7c1e177f8c83c19d4a1fb7955b6572051d73b0ca241a48477194fb84020e917081dc2e04ab474f6b018f68ef797cd2d2403049c6af7f6583faf19651f6e263f6a8ddf3a23165cf1703ead9dfaf3d59f6520906fd13479aa72f12777a9ea469518d4159cc832b1f39ce2b84153e3a2bc3940cdd07ee6353e7f7867bf0cb8634b54a33967958f709e5429e992eb76dfb49341530916d237cb1d2f42412443f62beeb6042c270af3633ea9561dd0cde544e21cb8b11f550f44020d3c744faa3cfc153657512000d2c13cdf53fd950c79e39cd74a0efd39c5a6f7f835e9287fbaf031c120e657ae1eeb29800e049d97517772c8dd504e0b1a124abb7b3592d1434caa0e786f01b673d19548b9e6498782b83962a5fffc389d6c4c6afe618ce5c1e9fe479e7dfef9777c62de43a657bff65218a050826bdc3ae3ba978a3fc018cc5413008b9fd7903a62a0758d51f1"}, 151 | {"group": "Ed25519", "seed_hex": "41", 152 | "element_hex": "4637592ae2914247de5804be805867266ccac99c635df8077dcdc1d72becf354"}, 153 | {"group": "Ed25519", "seed_hex": "42", 154 | "element_hex": "88228ee4046ba5d5fa2f23a0480a99efb1a9554ce50153d69330928215d50775"}, 155 | ] 156 | 157 | class ArbitraryElement(unittest.TestCase): 158 | def test_vectors(self): 159 | for vector in AE_TEST_VECTORS: 160 | group = GROUPS[vector["group"]] 161 | seed = unhexlify(vector["seed_hex"].encode("ascii")) 162 | elem = group.arbitrary_element(seed) 163 | elem_bytes = elem.to_bytes() 164 | self.assertEqual(len(elem_bytes), group.element_size_bytes) 165 | #print(hexlify(elem_bytes)) 166 | expected = vector["element_hex"].encode("ascii") 167 | self.assertEqual(hexlify(elem_bytes), expected, vector) 168 | 169 | # test vectors from RFC5869 170 | 171 | HKDF_TEST_VECTORS = [ 172 | { 173 | "IKM": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", 174 | "salt": "000102030405060708090a0b0c", 175 | "info": "f0f1f2f3f4f5f6f7f8f9", 176 | "L": 42, 177 | "PRK": "077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5", 178 | "OKM": "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865" 179 | }, 180 | { 181 | "IKM": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f", 182 | "salt": "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf", 183 | "info": "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", 184 | "L": 82, 185 | "PRK": "06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244", 186 | "OKM": "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87" 187 | }, 188 | { 189 | "IKM": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", 190 | "salt": "", 191 | "info": "", 192 | "L": 42, 193 | "PRK": "19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04", 194 | "OKM": "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8" 195 | }, 196 | ] 197 | 198 | # some additional short vectors. Note that "salt" is zero-padded to length of 199 | # the hash (and hashed down if longer), so e.g. salt="", salt="00", and 200 | # salt="0000" all give the same results. 201 | HKDF_TEST_VECTORS += [ 202 | {"salt": "", "IKM": "01", "info": "02", "L": 4, "OKM": "f4a855e4"}, 203 | {"salt": "00", "IKM": "01", "info": "02", "L": 4, "OKM": "f4a855e4"}, 204 | {"salt": "", "IKM": "01", "info": "", "L": 4, "OKM": "be7e83fb"}, 205 | {"salt": "00", "IKM": "01", "info": "", "L": 4, "OKM": "be7e83fb"}, 206 | {"salt": "01", "IKM": "01", "info": "", "L": 4, "OKM": "f0f7dcf9"}, 207 | {"salt": "01", "IKM": "01", "info": "", "L": 8, "OKM": "f0f7dcf9fe847ae5"}, 208 | {"salt": "01", "IKM": "01", "info": "", "L": 16, "OKM": "f0f7dcf9fe847ae58a24e82b13737c52"}, 209 | {"salt": "01", "IKM": "01", "info": "", "L": 31, "OKM": "f0f7dcf9fe847ae58a24e82b13737c52bf6a4a45810f5d819ec3932eaa6012"}, 210 | {"salt": "01", "IKM": "01", "info": "", "L": 32, "OKM": "f0f7dcf9fe847ae58a24e82b13737c52bf6a4a45810f5d819ec3932eaa601290"}, 211 | {"salt": "01", "IKM": "01", "info": "", "L": 33, "OKM": "f0f7dcf9fe847ae58a24e82b13737c52bf6a4a45810f5d819ec3932eaa60129072"}, 212 | {"salt": "01", "IKM": "01", "info": "", "L": 64, "OKM": "f0f7dcf9fe847ae58a24e82b13737c52bf6a4a45810f5d819ec3932eaa60129072a91afe92cffe2f2327b65ba4e2b2b6b51ed34363c9c4cca58ae7409209b97d"}, 213 | {"salt": "01", "IKM": "01", "info": "", "L": 65, "OKM": "f0f7dcf9fe847ae58a24e82b13737c52bf6a4a45810f5d819ec3932eaa60129072a91afe92cffe2f2327b65ba4e2b2b6b51ed34363c9c4cca58ae7409209b97d76"}, 214 | {"salt": "00", "IKM": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "info": "", "L": 4, "OKM": "37ad2910"}, 215 | ] 216 | 217 | class TestHKDF(unittest.TestCase): 218 | def test_vectors(self): 219 | for vector in HKDF_TEST_VECTORS: 220 | salt = unhexlify(vector["salt"].encode("ascii")) 221 | IKM = unhexlify(vector["IKM"].encode("ascii")) 222 | info = unhexlify(vector["info"].encode("ascii")) 223 | h = hkdf.HKDF(algorithm=hashes.SHA256(), length=vector["L"], salt=salt, info=info) 224 | digest = h.derive(IKM) 225 | self.assertEqual(digest, myHKDF(IKM, vector["L"], salt, info)) 226 | #print(hexlify(digest)) 227 | expected = vector["OKM"].encode("ascii") 228 | self.assertEqual(hexlify(digest), expected, vector) 229 | 230 | class Finalize(unittest.TestCase): 231 | def test_asymmetric(self): 232 | key = finalize_SPAKE2(b"idA", b"idB", b"X_msg", b"Y_msg", 233 | b"K_bytes", b"pw") 234 | self.assertEqual(hexlify(key), b"aa02a627537543399bb1b4b430646480b6d36ab5c44842e738c8f78694d8afac") 235 | 236 | def test_symmetric(self): 237 | key1 = finalize_SPAKE2_symmetric(b"idSymmetric", 238 | b"X_msg", b"Y_msg", 239 | b"K_bytes", b"pw") 240 | self.assertEqual(hexlify(key1), b"330a7ce7bb010fea7dae7e15b2261315403ab5dc269e461f6eb1cc6566620790") 241 | key2 = finalize_SPAKE2_symmetric(b"idSymmetric", 242 | b"Y_msg", b"X_msg", 243 | b"K_bytes", b"pw") 244 | self.assertEqual(hexlify(key1), hexlify(key2)) 245 | -------------------------------------------------------------------------------- /src/spake2/test/test_group.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from binascii import hexlify 3 | from hashlib import sha256 4 | from spake2 import groups, ed25519_group 5 | from spake2.parameters.i1024 import Params1024 6 | from spake2.parameters.i2048 import Params2048 7 | from spake2.parameters.i3072 import Params3072 8 | from spake2.parameters.ed25519 import ParamsEd25519 9 | from spake2.spake2 import SPAKE2_A, SPAKE2_B 10 | from .common import PRG 11 | 12 | ALL_INTEGER_GROUPS = [groups.I1024, groups.I2048, groups.I3072] 13 | ALL_GROUPS = ALL_INTEGER_GROUPS + [ed25519_group.Ed25519Group] 14 | ALL_INTEGER_PARAMS = [Params1024, Params2048, Params3072] 15 | ALL_PARAMS = ALL_INTEGER_PARAMS + [ParamsEd25519] 16 | 17 | def random_element(g, entropy_f): 18 | s = g.random_scalar(entropy_f) 19 | return s, g.Base.scalarmult(s) 20 | 21 | class Group(unittest.TestCase): 22 | def assertElementsEqual(self, e1, e2, msg=None): 23 | self.assertEqual(hexlify(e1.to_bytes()), hexlify(e2.to_bytes()), msg) 24 | def assertElementsNotEqual(self, e1, e2, msg=None): 25 | self.assertNotEqual(hexlify(e1.to_bytes()), hexlify(e2.to_bytes()), msg) 26 | 27 | def test_basic(self): 28 | for g in ALL_GROUPS: 29 | fr = PRG(b"0") 30 | i = g.random_scalar(entropy_f=fr) 31 | self.assertTrue(0 <= i < g.order()) 32 | b = g.scalar_to_bytes(i) 33 | self.assertEqual(len(b), g.scalar_size_bytes) 34 | self.assertEqual(i, g.bytes_to_scalar(b)) 35 | e = g.Base.scalarmult(i) 36 | self.assertEqual(len(e.to_bytes()), g.element_size_bytes) 37 | e = g.arbitrary_element(b"") 38 | self.assertEqual(len(e.to_bytes()), g.element_size_bytes) 39 | self.assertElementsEqual(e, g.bytes_to_element(e.to_bytes())) 40 | 41 | def test_math(self): 42 | for g in ALL_GROUPS: 43 | sb = g.Base.scalarmult 44 | e0 = sb(0) 45 | self.assertElementsEqual(e0, g.Zero) 46 | e1 = sb(1) 47 | e2 = sb(2) 48 | self.assertElementsEqual(e1.add(e0), e1) 49 | self.assertElementsEqual(e1.add(e1), e1.scalarmult(2)) 50 | self.assertElementsEqual(e1.scalarmult(2), e2) 51 | self.assertElementsEqual(e1.add(e2), e2.add(e1)) 52 | e_m1 = sb(g.order()-1) 53 | self.assertElementsEqual(e_m1, sb(-1)) 54 | self.assertElementsEqual(e_m1.add(e1), e0) 55 | e3 = sb(3) 56 | e4 = sb(4) 57 | e5 = sb(5) 58 | self.assertElementsEqual(e2.add(e3), e1.add(e4)) 59 | #self.assertElementsEqual(e5 - e3, e2) 60 | self.assertElementsEqual(e1.scalarmult(g.order()), e0) 61 | self.assertElementsEqual(e2.scalarmult(g.order()), e0) 62 | self.assertElementsEqual(e3.scalarmult(g.order()), e0) 63 | self.assertElementsEqual(e4.scalarmult(g.order()), e0) 64 | self.assertElementsEqual(e5.scalarmult(g.order()), e0) 65 | 66 | def test_bad_math(self): 67 | for g in ALL_GROUPS: 68 | base = g.Base 69 | # you cannot multiply two group elements together, only add them 70 | self.assertRaises(TypeError, lambda: base.scalarmult(base)) 71 | # you cannot add group elements to scalars, you can only multiply 72 | # group elements *by* scalars 73 | self.assertRaises(TypeError, lambda: base.add(1)) 74 | self.assertRaises(TypeError, lambda: base.add(-1)) 75 | 76 | def test_from_bytes(self): 77 | for g in ALL_GROUPS: 78 | fr = PRG(b"0") 79 | e = g.Base 80 | self.assertElementsEqual(g.bytes_to_element(e.to_bytes()), e) 81 | e = g.Base.scalarmult(2) 82 | self.assertElementsEqual(g.bytes_to_element(e.to_bytes()), e) 83 | e = g.Base.scalarmult(g.random_scalar(fr)) 84 | self.assertElementsEqual(g.bytes_to_element(e.to_bytes()), e) 85 | 86 | self.assertFalse(groups.I1024._is_member(groups.I2048.Zero)) 87 | for g in ALL_INTEGER_GROUPS: 88 | # we must bypass the normal API to create an element that's 89 | # marked as being of the right group, but the actual number is 90 | # not in the subgroup 91 | s = groups.number_to_bytes(0, g.p) 92 | self.assertRaises(ValueError, g.bytes_to_element, s) 93 | s = groups.number_to_bytes(2, g.p) 94 | self.assertRaises(ValueError, g.bytes_to_element, s) 95 | 96 | def test_arbitrary_element(self): 97 | for g in ALL_GROUPS: 98 | gx = g.arbitrary_element(b"") 99 | self.assertElementsEqual(gx.scalarmult(-2), 100 | gx.scalarmult(2).scalarmult(-1)) 101 | gy = g.arbitrary_element(b"2") 102 | self.assertElementsNotEqual(gx, gy) 103 | 104 | def test_blinding(self): 105 | for g in ALL_GROUPS: 106 | fr = PRG(b"0") 107 | _, pubkey = random_element(g, fr) 108 | _, U = random_element(g, fr) 109 | pw = g.random_scalar(fr) 110 | # X+U*pw -U*pw == X 111 | blinding_factor = U.scalarmult(pw) 112 | blinded_pubkey = pubkey.add(blinding_factor) 113 | inverse_pw = (-pw) % g.order() 114 | inverse_blinding_factor = U.scalarmult(inverse_pw) 115 | self.assertElementsEqual(inverse_blinding_factor, U.scalarmult(-pw)) 116 | self.assertElementsEqual(U.scalarmult(-pw), 117 | U.scalarmult(pw).scalarmult(-1)) 118 | self.assertElementsEqual(inverse_blinding_factor, 119 | blinding_factor.scalarmult(-1)) 120 | unblinded_pubkey = blinded_pubkey.add(inverse_blinding_factor) 121 | self.assertElementsEqual(pubkey, unblinded_pubkey) 122 | 123 | def test_password(self): 124 | for g in ALL_GROUPS: 125 | i = g.password_to_scalar(b"") 126 | self.assertTrue(0 <= i < g.order()) 127 | 128 | def test_math_trivial(self): 129 | g = I23 130 | e1 = g.Base.scalarmult(1) 131 | e2 = g.Base.scalarmult(2) 132 | e3 = g.Base.scalarmult(3) 133 | e4 = g.Base.scalarmult(4) 134 | e5 = g.Base.scalarmult(5) 135 | e6 = g.Base.scalarmult(6) 136 | self.assertEqual([e1._e, e2._e, e3._e, e4._e, e5._e, e6._e], 137 | [2, 4, 8, 16, 9, 18]) 138 | self.assertElementsEqual(e1.add(e1), e1.scalarmult(2)) 139 | self.assertElementsEqual(e1.scalarmult(2), e2) 140 | self.assertElementsEqual(e1.add(e2), e2.add(e1)) 141 | self.assertElementsEqual(e2.add(e3), e1.add(e4)) 142 | 143 | I23 = groups.IntegerGroup(p=23, q=11, g=2) 144 | 145 | class Parameters(unittest.TestCase): 146 | def test_params(self): 147 | for p in ALL_PARAMS: 148 | pw = b"password" 149 | sA,sB = SPAKE2_A(pw, params=p), SPAKE2_B(pw, params=p) 150 | m1A,m1B = sA.start(), sB.start() 151 | #print len(json.dumps(m1A)) 152 | kA,kB = sA.finish(m1B), sB.finish(m1A) 153 | self.assertEqual(hexlify(kA), hexlify(kB)) 154 | self.assertEqual(len(kA), len(sha256().digest())) 155 | 156 | sA,sB = SPAKE2_A(pw, params=p), SPAKE2_B(b"passwerd", params=p) 157 | m1A,m1B = sA.start(), sB.start() 158 | kA,kB = sA.finish(m1B), sB.finish(m1A) 159 | self.assertNotEqual(hexlify(kA), hexlify(kB)) 160 | self.assertEqual(len(kA), len(sha256().digest())) 161 | self.assertEqual(len(kB), len(sha256().digest())) 162 | 163 | def test_default_is_ed25519(self): 164 | pw = b"password" 165 | sA,sB = SPAKE2_A(pw, params=ParamsEd25519), SPAKE2_B(pw) 166 | m1A,m1B = sA.start(), sB.start() 167 | kA,kB = sA.finish(m1B), sB.finish(m1A) 168 | self.assertEqual(hexlify(kA), hexlify(kB)) 169 | self.assertEqual(len(kA), len(sha256().digest())) 170 | -------------------------------------------------------------------------------- /src/spake2/test/test_spake2.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | from spake2 import spake2 4 | from spake2.parameters.i1024 import Params1024 5 | from spake2.parameters.i3072 import Params3072 6 | from spake2.spake2 import SPAKE2_A, SPAKE2_B, SPAKE2_Symmetric 7 | from binascii import hexlify 8 | from hashlib import sha256 9 | from .common import PRG 10 | 11 | class Basic(unittest.TestCase): 12 | def test_success(self): 13 | pw = b"password" 14 | sA,sB = SPAKE2_A(pw), SPAKE2_B(pw) 15 | m1A,m1B = sA.start(), sB.start() 16 | kA,kB = sA.finish(m1B), sB.finish(m1A) 17 | self.assertEqual(hexlify(kA), hexlify(kB)) 18 | self.assertEqual(len(kA), len(sha256().digest())) 19 | 20 | def test_success_id(self): 21 | pw = b"password" 22 | sA = SPAKE2_A(pw, idA=b"alice", idB=b"bob") 23 | sB = SPAKE2_B(pw, idA=b"alice", idB=b"bob") 24 | m1A,m1B = sA.start(), sB.start() 25 | kA,kB = sA.finish(m1B), sB.finish(m1A) 26 | self.assertEqual(hexlify(kA), hexlify(kB)) 27 | self.assertEqual(len(kA), len(sha256().digest())) 28 | 29 | def test_failure_wrong_password(self): 30 | pw = b"password" 31 | sA,sB = SPAKE2_A(pw), SPAKE2_B(b"passwerd") 32 | m1A,m1B = sA.start(), sB.start() 33 | kA,kB = sA.finish(m1B), sB.finish(m1A) 34 | self.assertNotEqual(hexlify(kA), hexlify(kB)) 35 | self.assertEqual(len(kA), len(sha256().digest())) 36 | self.assertEqual(len(kB), len(sha256().digest())) 37 | 38 | def test_failure_wrong_id(self): 39 | pw = b"password" 40 | sA = SPAKE2_A(pw, idA=b"alice", idB=b"bob") 41 | sB = SPAKE2_B(pw, idA=b"not-alice", idB=b"bob") 42 | m1A,m1B = sA.start(), sB.start() 43 | kA,kB = sA.finish(m1B), sB.finish(m1A) 44 | self.assertNotEqual(hexlify(kA), hexlify(kB)) 45 | 46 | def test_failure_swapped_id(self): 47 | pw = b"password" 48 | sA = SPAKE2_A(pw, idA=b"alice", idB=b"bob") 49 | sB = SPAKE2_B(pw, idA=b"bob", idB=b"alice") 50 | m1A,m1B = sA.start(), sB.start() 51 | kA,kB = sA.finish(m1B), sB.finish(m1A) 52 | self.assertNotEqual(hexlify(kA), hexlify(kB)) 53 | 54 | def test_reflect(self): 55 | pw = b"password" 56 | s1 = SPAKE2_A(pw) 57 | m1 = s1.start() 58 | reflected = b"B" + m1[1:] 59 | self.assertRaises(spake2.ReflectionThwarted, s1.finish, reflected) 60 | 61 | 62 | class OtherEntropy(unittest.TestCase): 63 | def test_entropy(self): 64 | fr = PRG(b"seed") 65 | pw = b"password" 66 | sA,sB = SPAKE2_A(pw, entropy_f=fr), SPAKE2_B(pw, entropy_f=fr) 67 | m1A1,m1B1 = sA.start(), sB.start() 68 | kA1,kB1 = sA.finish(m1B1), sB.finish(m1A1) 69 | self.assertEqual(hexlify(kA1), hexlify(kB1)) 70 | 71 | # run it again with the same entropy stream: all messages should be 72 | # identical 73 | fr = PRG(b"seed") 74 | sA,sB = SPAKE2_A(pw, entropy_f=fr), SPAKE2_B(pw, entropy_f=fr) 75 | m1A2,m1B2 = sA.start(), sB.start() 76 | kA2,kB2 = sA.finish(m1B2), sB.finish(m1A2) 77 | self.assertEqual(hexlify(kA2), hexlify(kB2)) 78 | 79 | self.assertEqual(m1A1, m1A2) 80 | self.assertEqual(m1B1, m1B2) 81 | self.assertEqual(kA1, kA2) 82 | self.assertEqual(kB1, kB2) 83 | 84 | class Serialize(unittest.TestCase): 85 | def test_serialize(self): 86 | pw = b"password" 87 | sA,sB = SPAKE2_A(pw), SPAKE2_B(pw) 88 | self.assertRaises(spake2.SerializedTooEarly, sA.serialize) 89 | m1A,m1B = sA.start(), sB.start() 90 | sA = SPAKE2_A.from_serialized(sA.serialize()) 91 | kA,kB = sA.finish(m1B), sB.finish(m1A) 92 | self.assertEqual(hexlify(kA), hexlify(kB)) 93 | self.assertEqual(len(kA), len(sha256().digest())) 94 | 95 | class Symmetric(unittest.TestCase): 96 | def test_success(self): 97 | pw = b"password" 98 | s1,s2 = SPAKE2_Symmetric(pw), SPAKE2_Symmetric(pw) 99 | m1,m2 = s1.start(), s2.start() 100 | k1,k2 = s1.finish(m2), s2.finish(m1) 101 | self.assertEqual(hexlify(k1), hexlify(k2)) 102 | 103 | def test_success_id(self): 104 | pw = b"password" 105 | s1 = SPAKE2_Symmetric(pw, idSymmetric=b"sym") 106 | s2 = SPAKE2_Symmetric(pw, idSymmetric=b"sym") 107 | m1,m2 = s1.start(), s2.start() 108 | k1,k2 = s1.finish(m2), s2.finish(m1) 109 | self.assertEqual(hexlify(k1), hexlify(k2)) 110 | 111 | def test_failure_wrong_password(self): 112 | s1,s2 = SPAKE2_Symmetric(b"password"), SPAKE2_Symmetric(b"wrong") 113 | m1,m2 = s1.start(), s2.start() 114 | k1,k2 = s1.finish(m2), s2.finish(m1) 115 | self.assertNotEqual(hexlify(k1), hexlify(k2)) 116 | 117 | def test_failure_wrong_id(self): 118 | pw = b"password" 119 | s1 = SPAKE2_Symmetric(pw, idSymmetric=b"sym") 120 | s2 = SPAKE2_Symmetric(pw, idSymmetric=b"not-sym") 121 | m1,m2 = s1.start(), s2.start() 122 | k1,k2 = s1.finish(m2), s2.finish(m1) 123 | self.assertNotEqual(hexlify(k1), hexlify(k2)) 124 | 125 | def test_serialize(self): 126 | pw = b"password" 127 | s1,s2 = SPAKE2_Symmetric(pw), SPAKE2_Symmetric(pw) 128 | m1,m2 = s1.start(), s2.start() 129 | s1 = SPAKE2_Symmetric.from_serialized(s1.serialize()) 130 | k1,k2 = s1.finish(m2), s2.finish(m1) 131 | self.assertEqual(hexlify(k1), hexlify(k2)) 132 | 133 | def test_reflect(self): 134 | pw = b"password" 135 | s1 = SPAKE2_Symmetric(pw) 136 | m1 = s1.start() 137 | # reflect Alice's message back to her 138 | self.assertRaises(spake2.ReflectionThwarted, s1.finish, m1) 139 | 140 | class Errors(unittest.TestCase): 141 | def test_start_twice(self): 142 | s = SPAKE2_A(b"password") 143 | s.start() 144 | self.assertRaises(spake2.OnlyCallStartOnce, s.start) 145 | 146 | def test_finish_twice(self): 147 | pw = b"password" 148 | sA,sB = SPAKE2_A(pw), SPAKE2_B(pw) 149 | sA.start() 150 | msg = sB.start() 151 | sA.finish(msg) 152 | self.assertRaises(spake2.OnlyCallFinishOnce, sA.finish, msg) 153 | 154 | def test_wrong_side(self): 155 | pw = b"password" 156 | sA1,sA2 = SPAKE2_A(pw), SPAKE2_A(pw) 157 | sA1.start() 158 | msgA = sA2.start() 159 | self.assertRaises(spake2.OffSides, sA1.finish, msgA) 160 | 161 | sB1,sB2 = SPAKE2_B(pw), SPAKE2_B(pw) 162 | sB1.start() 163 | msgB = sB2.start() 164 | self.assertRaises(spake2.OffSides, sB1.finish, msgB) 165 | 166 | self.assertRaises(spake2.OffSides, sA2.finish, b"C"+msgB) 167 | 168 | sS = SPAKE2_Symmetric(pw) 169 | sS.start() 170 | self.assertRaises(spake2.OffSides, sS.finish, msgA) 171 | sS = SPAKE2_Symmetric(pw) 172 | sS.start() 173 | self.assertRaises(spake2.OffSides, sS.finish, msgB) 174 | 175 | 176 | def test_unserialize_wrong(self): 177 | s = SPAKE2_A(b"password", params=Params1024) 178 | s.start() 179 | data = s.serialize() 180 | SPAKE2_A.from_serialized(data, params=Params1024) # this is ok 181 | self.assertRaises(spake2.WrongGroupError, 182 | SPAKE2_A.from_serialized, data) # default is P2048 183 | self.assertRaises(spake2.WrongGroupError, 184 | SPAKE2_A.from_serialized, data, 185 | params=Params3072) 186 | self.assertRaises(spake2.WrongSideSerialized, 187 | SPAKE2_B.from_serialized, data, 188 | params=Params1024) 189 | 190 | ss = SPAKE2_Symmetric(b"password", params=Params1024) 191 | ss.start() 192 | sdata = ss.serialize() 193 | 194 | SPAKE2_Symmetric.from_serialized(sdata, params=Params1024) # ok 195 | self.assertRaises(spake2.WrongGroupError, # default is P2048 196 | SPAKE2_Symmetric.from_serialized, sdata) 197 | self.assertRaises(spake2.WrongGroupError, 198 | SPAKE2_Symmetric.from_serialized, sdata, 199 | params=Params3072) 200 | self.assertRaises(spake2.WrongSideSerialized, 201 | SPAKE2_Symmetric.from_serialized, data, # from A 202 | params=Params1024) 203 | 204 | if __name__ == '__main__': 205 | unittest.main() 206 | 207 | -------------------------------------------------------------------------------- /src/spake2/test/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from spake2 import util 3 | from .common import PRG 4 | 5 | class Utils(unittest.TestCase): 6 | def test_binsize(self): 7 | def sizebb(maxval): 8 | num_bits = util.size_bits(maxval) 9 | num_bytes = util.size_bytes(maxval) 10 | return (num_bytes, num_bits) 11 | self.assertEqual(sizebb(0x0f), (1, 4)) 12 | self.assertEqual(sizebb(0x1f), (1, 5)) 13 | self.assertEqual(sizebb(0x10), (1, 5)) 14 | self.assertEqual(sizebb(0xff), (1, 8)) 15 | self.assertEqual(sizebb(0x100), (2, 9)) 16 | self.assertEqual(sizebb(0x101), (2, 9)) 17 | self.assertEqual(sizebb(0x1fe), (2, 9)) 18 | self.assertEqual(sizebb(0x1ff), (2, 9)) 19 | self.assertEqual(sizebb(2**255-19), (32, 255)) 20 | 21 | def test_number_to_bytes(self): 22 | n2b = util.number_to_bytes 23 | self.assertEqual(n2b(0x00, 0xff), b"\x00") 24 | self.assertEqual(n2b(0x01, 0xff), b"\x01") 25 | self.assertEqual(n2b(0xff, 0xff), b"\xff") 26 | self.assertEqual(n2b(0x100, 0xffff), b"\x01\x00") 27 | self.assertEqual(n2b(0x101, 0xffff), b"\x01\x01") 28 | self.assertEqual(n2b(0x102, 0xffff), b"\x01\x02") 29 | self.assertEqual(n2b(0x1fe, 0xffff), b"\x01\xfe") 30 | self.assertEqual(n2b(0x1ff, 0xffff), b"\x01\xff") 31 | self.assertEqual(n2b(0x200, 0xffff), b"\x02\x00") 32 | self.assertEqual(n2b(0xffff, 0xffff), b"\xff\xff") 33 | self.assertEqual(n2b(0x10000, 0xffffff), b"\x01\x00\x00") 34 | self.assertEqual(n2b(0x1, 0xffffffff), b"\x00\x00\x00\x01") 35 | self.assertRaises(ValueError, n2b, 0x10000, 0xff) 36 | 37 | def test_bytes_to_number(self): 38 | b2n = util.bytes_to_number 39 | self.assertEqual(b2n(b"\x00"), 0x00) 40 | self.assertEqual(b2n(b"\x01"), 0x01) 41 | self.assertEqual(b2n(b"\xff"), 0xff) 42 | self.assertEqual(b2n(b"\x01\x00"), 0x0100) 43 | self.assertEqual(b2n(b"\x01\x01"), 0x0101) 44 | self.assertEqual(b2n(b"\x01\x02"), 0x0102) 45 | self.assertEqual(b2n(b"\x01\xfe"), 0x01fe) 46 | self.assertEqual(b2n(b"\x01\xff"), 0x01ff) 47 | self.assertEqual(b2n(b"\x02\x00"), 0x0200) 48 | self.assertEqual(b2n(b"\xff\xff"), 0xffff) 49 | self.assertEqual(b2n(b"\x01\x00\x00"), 0x010000) 50 | self.assertEqual(b2n(b"\x00\x00\x00\x01"), 0x01) 51 | self.assertRaises(TypeError, b2n, 42) 52 | if type("") != type(b""): 53 | self.assertRaises(TypeError, b2n, "not bytes") 54 | 55 | def test_mask(self): 56 | gen = util.generate_mask 57 | self.assertEqual(gen(0x01), (0x01, 1)) 58 | self.assertEqual(gen(0x02), (0x03, 1)) 59 | self.assertEqual(gen(0x03), (0x03, 1)) 60 | self.assertEqual(gen(0x04), (0x07, 1)) 61 | self.assertEqual(gen(0x07), (0x07, 1)) 62 | self.assertEqual(gen(0x08), (0x0f, 1)) 63 | self.assertEqual(gen(0x09), (0x0f, 1)) 64 | self.assertEqual(gen(0x0f), (0x0f, 1)) 65 | self.assertEqual(gen(0x10), (0x1f, 1)) 66 | self.assertEqual(gen(0x7f), (0x7f, 1)) 67 | self.assertEqual(gen(0x80), (0xff, 1)) 68 | self.assertEqual(gen(0xff), (0xff, 1)) 69 | self.assertEqual(gen(0x0100), (0x01, 2)) 70 | self.assertEqual(gen(2**255-19), (0x7f, 32)) 71 | mask = util.mask_list_of_ints 72 | self.assertEqual(mask(0x03, [0xff, 0x55, 0xaa]), [0x03, 0x55, 0xaa]) 73 | self.assertEqual(mask(0xff, [0xff]), [0xff]) 74 | def test_l2n(self): 75 | l2n = util.list_of_ints_to_number 76 | self.assertEqual(l2n([0x00]), 0x00) 77 | self.assertEqual(l2n([0x01]), 0x01) 78 | self.assertEqual(l2n([0x7f]), 0x7f) 79 | self.assertEqual(l2n([0x80]), 0x80) 80 | self.assertEqual(l2n([0xff]), 0xff) 81 | self.assertEqual(l2n([0x01, 0x00]), 0x0100) 82 | 83 | def test_unbiased_randrange(self): 84 | for seed in range(1000): 85 | self.do_test_unbiased_randrange(0, 254, seed) 86 | self.do_test_unbiased_randrange(0, 255, seed) 87 | self.do_test_unbiased_randrange(0, 256, seed) 88 | self.do_test_unbiased_randrange(0, 257, seed) 89 | self.do_test_unbiased_randrange(1, 257, seed) 90 | 91 | def do_test_unbiased_randrange(self, start, stop, seed): 92 | seed_b = str(seed).encode("ascii") 93 | num = util.unbiased_randrange(start, stop, entropy_f=PRG(seed_b)) 94 | self.assertTrue(start <= num < stop, (num, seed)) 95 | -------------------------------------------------------------------------------- /src/spake2/util.py: -------------------------------------------------------------------------------- 1 | import os, binascii, math 2 | 3 | def size_bits(maxval): 4 | if hasattr(maxval, "bit_length"): # python-2.7 or 3.x 5 | return maxval.bit_length() or 1 6 | # 2.6 7 | return len(bin(maxval)) - 2 8 | 9 | def size_bytes(maxval): 10 | return int(math.ceil(size_bits(maxval) / 8)) 11 | 12 | def number_to_bytes(num, maxval): 13 | if num > maxval: 14 | raise ValueError 15 | num_bytes = size_bytes(maxval) 16 | fmt_str = "%0" + str(2*num_bytes) + "x" 17 | s_hex = fmt_str % num 18 | s = binascii.unhexlify(s_hex.encode("ascii")) 19 | assert len(s) == num_bytes 20 | assert isinstance(s, type(b"")) 21 | return s 22 | 23 | def bytes_to_number(s): 24 | if not isinstance(s, type(b"")): 25 | raise TypeError 26 | return int(binascii.hexlify(s), 16) 27 | 28 | def generate_mask(maxval): 29 | num_bytes = size_bytes(maxval) 30 | num_bits = size_bits(maxval) 31 | leftover_bits = num_bits % 8 32 | if leftover_bits: 33 | top_byte_mask_int = (0x1 << leftover_bits) - 1 34 | else: 35 | top_byte_mask_int = 0xff 36 | assert 0 <= top_byte_mask_int <= 0xff 37 | return (top_byte_mask_int, num_bytes) 38 | 39 | def random_list_of_ints(count, entropy_f=os.urandom): 40 | # return a list of ints, each 0<=x<=255, for masking 41 | return list(iter(entropy_f(count))) 42 | def mask_list_of_ints(top_byte_mask_int, list_of_ints): 43 | return [top_byte_mask_int & list_of_ints[0]] + list_of_ints[1:] 44 | def list_of_ints_to_number(l): 45 | s = "".join(["%02x" % b for b in l]) 46 | return int(s, 16) 47 | 48 | def unbiased_randrange(start, stop, entropy_f): 49 | """Return a random integer k such that start <= k < stop, uniformly 50 | distributed across that range, like random.randrange but 51 | cryptographically bound and unbiased. 52 | 53 | r(0,p) provides a random group element of the integer group Zp. 54 | r(1,p) provides a random group element of the integer group Zp*. 55 | """ 56 | 57 | # we generate a random binary string up to 7 bits larger than we really 58 | # need, mask that down to be the right number of bits, then compare 59 | # against the range and try again if it's wrong. This will take a random 60 | # number of tries, but on average less than two 61 | 62 | # first we get 0<=number<(stop-start) 63 | maxval = stop - start 64 | 65 | top_byte_mask_int, num_bytes = generate_mask(maxval) 66 | while True: 67 | enough_bytes = random_list_of_ints(num_bytes, entropy_f) 68 | assert len(enough_bytes) == num_bytes 69 | candidate_bytes = mask_list_of_ints(top_byte_mask_int, enough_bytes) 70 | candidate_int = list_of_ints_to_number(candidate_bytes) 71 | #print ["0x%02x" % b for b in candidate_bytes], candidate_int 72 | if candidate_int < maxval: 73 | return start + candidate_int 74 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py39, py310, py311, py312 8 | 9 | [testenv] 10 | usedevelop = True 11 | deps = 12 | pytest 13 | commands = py.test {posargs:src/spake2} 14 | 15 | [testenv:coverage] 16 | deps = 17 | coverage 18 | pytest 19 | commands = coverage run -m pytest {posargs:src/spake2} 20 | 21 | [testenv:speed] 22 | commands = {envpython} setup.py speed 23 | --------------------------------------------------------------------------------