├── .coveragerc ├── .gitignore ├── .travis.yml ├── CHANGES ├── Dockerfile ├── LICENSE ├── Makefile ├── README ├── README.md ├── example.py ├── pylibscrypt ├── __init__.py ├── bench.py ├── common.py ├── hashlibscrypt.py ├── inline.py ├── libsodium_load.py ├── mcf.py ├── pylibscrypt.py ├── pylibsodium.py ├── pypyscrypt.py ├── pypyscrypt_inline.py ├── pyscrypt.py ├── test_properties.py └── tests.py ├── run_coverage.sh ├── run_docker.sh ├── setup.py └── test_fallback.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [paths] 2 | source = 3 | pylibscrypt 4 | /app/pylibscrypt 5 | 6 | 7 | [report] 8 | exclude_lines = 9 | # Not called from coverage tests 10 | if __name__ == .__main__.: 11 | 12 | # Untestable from a working environment 13 | except AttributeError: 14 | except TypeError 15 | 16 | include = pylibscrypt/* 17 | omit = pylibscrypt/libsodium_load.py,pylibscrypt/test_properties.py,pylibscrypt/tests.py 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *~ 3 | i-*/ 4 | .coverage 5 | *,cover 6 | htmlcov/ 7 | build/ 8 | dist/ 9 | MANIFEST 10 | .hypothesis/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | services: 8 | - docker 9 | 10 | install: 11 | - docker pull jvarho/pylibscrypt 12 | - pip install coveralls 13 | 14 | script: 15 | - docker run -v ${PWD}:/app jvarho/pylibscrypt 16 | 17 | after_success: 18 | - coverage combine 19 | - coveralls 20 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Release History 2 | == 3 | 4 | 2.0.0 5 | -- 6 | 2021-02-07 7 | 8 | Major release: 9 | - Drops support for python 2.x and 3.3 10 | - Drops legacy backends 11 | 12 | 13 | 1.8.0 14 | -- 15 | 2019-03-09 16 | 17 | Feature release: 18 | - Support unicode passwords with scrypt_mcf 19 | - Last release to support python 3.3 and <2.7.8 20 | 21 | 22 | 1.7.1 23 | -- 24 | 2017-12-16 25 | 26 | Bugfix release: 27 | - Prevent DepracationWarning from pylibsodium.py 28 | 29 | 30 | 1.7.0 31 | -- 32 | 2017-11-16 33 | 34 | Feature release: 35 | - Improved error messages 36 | - Added coveralls testing 37 | - Improved test coverage using docker 38 | - Deprecated fallbacks for old python and libsodium versions 39 | 40 | 41 | 1.6.1 42 | -- 43 | 2017-05-08 44 | 45 | Bugfix release: 46 | - Fix hashlib.scrypt calls with large values 47 | 48 | 49 | 1.6.0 50 | -- 51 | 2017-01-04 52 | 53 | Feature release: 54 | - Use hashlib.scrypt on Python 3.6 + OpenSSL 1.1 55 | - Travis CI integration 56 | - Improved test coverage 57 | - Code and build cleanups 58 | 59 | 60 | 1.5.3 61 | -- 62 | 2015-06-19 63 | 64 | Bugfix release: 65 | - Fix libsodium incompatibility fallback 66 | 67 | 68 | 1.5.2 69 | -- 70 | 2015-12-22 71 | 72 | Bugfix release: 73 | - Fix library loading on OS/X with packaged apps 74 | 75 | 76 | 1.5.1 77 | -- 78 | 2015-9-15 79 | 80 | Bugfix release: 81 | - Compare MCF hashes in constant time 82 | 83 | 84 | 1.5.0 85 | -- 86 | 2015-9-7 87 | 88 | Feature release: 89 | - Handle dependency failures better 90 | - Improve test coverage 91 | - Misc. cleanups 92 | 93 | 94 | 1.4.1 95 | -- 96 | 2015-3-27 97 | 98 | Bugfix release: 99 | - Fix missing import so Windows/Darwin may actually work 100 | 101 | 102 | 1.4.0 103 | -- 104 | 2015-3-23 105 | 106 | Feature release: 107 | - Improve library loading on Windows and Darwin. 108 | - Obsolete libsodium_salsa implementation. 109 | 110 | 111 | 1.3.0 112 | -- 113 | 2014-7-8 114 | 115 | Feature release: 116 | - Support for libsodium 0.6+ changes. 117 | - Use the low-level scrypt interface with libsodium so all calls are fast. 118 | - Support python3 when running tests. 119 | - Switch to the less verbose ISC license where possible 120 | 121 | 122 | 1.2.1 123 | -- 124 | 2014-5-17 125 | 126 | Bugfix release: 127 | - Fix python3 support. 128 | - Remember to import the libsodium backend added in 1.2.0. 129 | 130 | 131 | 1.2.0 132 | -- 133 | 2014-5-16 134 | 135 | Feature release: 136 | - Support for the $7$ Modular Crypt Format that's been semi-standardized and 137 | used by libsodium. (The $s1$ format remains the default.) 138 | - Use libsodium 0.5+ pwhash as another fallback when parameters support it. 139 | - More liberal in reading MCF hashes, so those from other implementations work. 140 | - Fuzz testing of scrypt inputs. 141 | 142 | 143 | 1.1.3 144 | -- 145 | 2014-5-16 146 | 147 | Bugfix release: 148 | - Disallow null in MCF passwords, because that could result in a password that 149 | libscrypt can't verify. 150 | - Verify MCF passwords with null even when using libscrypt to stay compatible 151 | with earlier releases. 152 | 153 | 154 | 1.1.2 155 | -- 156 | 2014-5-12 157 | 158 | Bugfix release: 159 | - Fix passwords longer than 256 bytes failing on python2 160 | 161 | 162 | 1.1.1 163 | -- 164 | 2014-5-12 165 | 166 | Bugfix release: 167 | - Fix some r and p values over/underflowing with libscrypt backend. 168 | - Always check that r*p < 2**30. 169 | 170 | 171 | 1.1.0 172 | -- 173 | 2014-5-8 174 | 175 | Feature release: 176 | - On CPython libsodium is used for salsa20/8, for a 4x speedup when C scrypt 177 | is not available but libsodium is (optional). 178 | - Tests have been converted to unittest. More readable and better coverage. 179 | 180 | 181 | 1.0.3 182 | -- 183 | 2014-5-5 184 | 185 | Bugfix release: 186 | - Guard against some libscrypt bugs found in older versions. 187 | - Fix broken package import when libscrypt isn't available 188 | 189 | 190 | 1.0.2 191 | -- 192 | 2014-5-3 193 | 194 | Packaging fix 195 | 196 | 197 | 1.0.1 198 | -- 199 | 2014-5-3 200 | 201 | Bugfix release: 202 | - Enforced parameter requirements that libscrypt has for MCF form hashes. 203 | Initial release could accept parameters for scrypt_mcf that scrypt_mcf_check 204 | didn't support when using libscrypt backend. 205 | - Fixed documentation and tests. 206 | 207 | 208 | 1.0.0 209 | -- 210 | 2014-5-2 211 | 212 | Initial release 213 | 214 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | RUN apt-get update && apt-get install -y python3 python3-pip libpython3-dev libscrypt0 libsodium23 python-openssl libssl-dev 3 | RUN python3 -m pip install coverage hypothesis scrypt 4 | WORKDIR /app 5 | CMD ["./run_docker.sh"] 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2021, Jan Varho 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: inline 3 | 4 | 5 | inline: pylibscrypt/pypyscrypt_inline.py 6 | 7 | pylibscrypt/pypyscrypt_inline.py: pylibscrypt/inline.py pylibscrypt/pypyscrypt.py 8 | env python3 -m pylibscrypt.inline 9 | 10 | 11 | clean: 12 | rm -f *~ .*~ *.pyc *,cover .coverage pylibscrypt/*~ pylibscrypt/*.pyc pylibscrypt/*,cover 13 | rm -rf __pycache__/ pylibscrypt/__pycache__/ htmlcov/ 14 | 15 | 16 | distclean: clean 17 | rm -f MANIFEST 18 | rm -rf build/ dist/ 19 | 20 | 21 | test: inline 22 | env python3 -m pylibscrypt.tests 23 | 24 | 25 | fuzz: inline 26 | env python3 -m pylibscrypt.fuzz 27 | 28 | 29 | coverage: inline 30 | ./run_coverage.sh 31 | 32 | 33 | bench: inline 34 | env python3 -m pylibscrypt.bench 35 | 36 | 37 | pypi-upload: 38 | env python3 setup.py sdist upload -r https://upload.pypi.org/legacy/ 39 | 40 | 41 | docker-build: 42 | docker build -t pylibscrypt . 43 | 44 | 45 | docker-run: docker-build 46 | docker run -v ${PWD}:/app pylibscrypt 47 | 48 | 49 | docker-push: docker-build 50 | docker tag pylibscrypt jvarho/pylibscrypt 51 | docker push jvarho/pylibscrypt 52 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Scrypt for Python 2 | == 3 | 4 | [![Build Status](https://travis-ci.org/jvarho/pylibscrypt.svg)](https://travis-ci.org/jvarho/pylibscrypt) 5 | [![Coverage Status](https://coveralls.io/repos/github/jvarho/pylibscrypt/badge.svg?branch=master)](https://coveralls.io/github/jvarho/pylibscrypt?branch=master) 6 | [![PyPI version](https://img.shields.io/pypi/v/pylibscrypt.svg)](https://pypi.python.org/pypi/pylibscrypt) 7 | 8 | There are a lot of different scrypt modules for Python, but none of them have 9 | everything that I'd like, so here's One More[1]. 10 | 11 | 12 | Features 13 | -- 14 | * Uses hashlib.scrypt on Python 3.6+ and OpenSSL 1.1+. 15 | * Uses system libscrypt[2] as the next choice. 16 | * If neither is available, tries the scrypt Python module[3] or libsodium[4]. 17 | * Offers a pure Python scrypt implementation for when there is no C scrypt. 18 | * Not unusably slow, even in pure Python... at least with pypy[5]. 19 | 20 | With PyPy as the interpreter the Python implementation is around one fifth the 21 | speed of C scrypt. With CPython it is about 250x slower. 22 | 23 | 24 | Requirements 25 | -- 26 | * Python 3.4+. Equivalent versions of PyPy should also work. 27 | * For Python 2.7.8+ support install the latest version 1.x instead. 28 | * If you want speed, you should use one of: 29 | - Python 3.6+ with OpenSSL 1.1+ 30 | - libscrypt 1.8+ (older may work) 31 | - py-scrypt 0.6+ (pip install scrypt) 32 | - libsodium 1.0+ 33 | 34 | 35 | Usage 36 | -- 37 | 38 | You can install the most recent release from PyPi using: 39 | 40 | pip install pylibscrypt 41 | 42 | You most likely want to create MCF hashes and store them somewhere, then check 43 | user-entered passwords against those hashes. For that you only need to use two 44 | functions from the API: 45 | 46 | from pylibscrypt import scrypt_mcf, scrypt_mcf_check 47 | # Generate an MCF hash with random salt 48 | mcf = scrypt_mcf('Hello World') 49 | # Test it 50 | print(scrypt_mcf_check(mcf, 'Hello World')) # prints True 51 | print(scrypt_mcf_check(mcf, 'HelloPyWorld')) # prints False 52 | 53 | For full API, you can try help(pylibscrypt) from python after importing. 54 | 55 | It is highly recommended that you use a random salt, i.e. don't pass one. 56 | 57 | 58 | Versioning 59 | -- 60 | The package has a version number that can be read from python like so: 61 | 62 | print(pylibscrypt.__version__) 63 | 64 | The version number is of the form X.Y.Z, following Semantic Versioning[6]. 65 | Unreleased versions include a -git version specifier, e.g. 2.0.0-git < 2.0.0. 66 | Releases are tagged vX.Y.Z and release branches bX.Y.x when they differ from 67 | master. 68 | 69 | 70 | Development 71 | -- 72 | Development happens on GitHub[7]. If you find a bug, please open an issue there. 73 | 74 | Running pylibscrypt.tests will test all implementations with some quick tests. 75 | Running any implementation directly (e.g. pylibscrypt.pylibsodium) will also 76 | compare to scrypt test vectors from the paper but this is slow for the pure 77 | Python version (pypyscrypt) unless running with pypy. 78 | 79 | You can test more comprehensively using the docker test environment. Either 80 | build and run using `make docker-run` or pull the jvarho/pylibscrypt image and 81 | run using `docker run -v ${PWD}:/app jvarho/pylibscrypt`. 82 | 83 | Pull requests should be automatically tested and will not be merged if broken. 84 | 85 | [1]:https://xkcd.com/927/ 86 | [2]:https://github.com/technion/libscrypt 87 | [3]:https://bitbucket.org/mhallin/py-scrypt/src 88 | [4]:https://github.com/jedisct1/libsodium 89 | [5]:http://pypy.org/ 90 | [6]:http://semver.org/spec/v2.0.0.html 91 | [7]:https://github.com/jvarho/pylibscrypt 92 | 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2021, Jan Varho 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | from base64 import b16encode 18 | 19 | from pylibscrypt import scrypt, scrypt_mcf, scrypt_mcf_check 20 | 21 | # Print a raw scrypt hash in hex 22 | print(b16encode(scrypt(b'Hello World', b'salt'))) 23 | 24 | # Generate an MCF hash with random salt 25 | mcf = scrypt_mcf('Hello World') 26 | 27 | # Test it 28 | print(scrypt_mcf_check(mcf, 'Hello World')) 29 | print(scrypt_mcf_check(mcf, 'HelloPyWorld')) 30 | 31 | -------------------------------------------------------------------------------- /pylibscrypt/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2021, Jan Varho 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | """Scrypt for Python""" 16 | 17 | __version__ = '2.0.0' 18 | 19 | # First, try hashlib 20 | _done = False 21 | try: 22 | from .hashlibscrypt import * 23 | except ImportError: 24 | pass 25 | else: 26 | _done = True 27 | 28 | # If that didn't work, try loading libscrypt 29 | if not _done: 30 | try: 31 | from .pylibscrypt import * 32 | except ImportError: 33 | pass 34 | else: 35 | _done = True 36 | 37 | # Next: try the scrypt module 38 | if not _done: 39 | try: 40 | from .pyscrypt import * 41 | except ImportError: 42 | pass 43 | else: 44 | _done = True 45 | 46 | # Next: libsodium 47 | if not _done: 48 | try: 49 | from .pylibsodium import * 50 | except ImportError: 51 | pass 52 | else: 53 | _done = True 54 | 55 | # If that didn't work either, the inlined Python version 56 | if not _done: 57 | from .pypyscrypt_inline import * 58 | 59 | __all__ = ['scrypt', 'scrypt_mcf', 'scrypt_mcf_check'] 60 | 61 | 62 | -------------------------------------------------------------------------------- /pylibscrypt/bench.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2021, Jan Varho 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | """Simple benchmark of python vs c scrypt""" 16 | 17 | import time 18 | 19 | from .pylibscrypt import scrypt 20 | from .pypyscrypt_inline import scrypt as pyscrypt 21 | 22 | 23 | # Benchmark time in seconds 24 | tmin = 5 25 | Nmax = 20 26 | 27 | t1 = time.time() 28 | for i in range(1, Nmax+1): 29 | pyscrypt(b'password', b'NaCl', N=2**i) 30 | if time.time() - t1 > tmin: 31 | Nmax = i 32 | break 33 | t1 = time.time() - t1 34 | print('Using N = 2,4,..., 2**%d' % Nmax) 35 | print('Python scrypt took %.2fs' % t1) 36 | 37 | t3 = time.time() 38 | for i in range(1, Nmax+1): 39 | scrypt(b'password', b'NaCl', N=2**i) 40 | t3 = time.time() - t3 41 | print('C scrypt took %.2fs' % t3) 42 | 43 | print('Python scrypt took %.2f times as long as C' % (t1 / t3)) 44 | 45 | -------------------------------------------------------------------------------- /pylibscrypt/common.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2021, Jan Varho 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | """Common constants and functions used by scrypt implementations""" 16 | 17 | import numbers 18 | 19 | 20 | SCRYPT_MCF_PREFIX_7 = b'$7$' 21 | SCRYPT_MCF_PREFIX_s1 = b'$s1$' 22 | SCRYPT_MCF_PREFIX_DEFAULT = b'$s1$' 23 | SCRYPT_MCF_PREFIX_ANY = None 24 | 25 | SCRYPT_N = 1<<14 26 | SCRYPT_r = 8 27 | SCRYPT_p = 1 28 | 29 | # The last one differs from libscrypt defaults, but matches the 'interactive' 30 | # work factor from the original paper. For long term storage where runtime of 31 | # key derivation is not a problem, you could use 16 as in libscrypt or better 32 | # yet increase N if memory is plentiful. 33 | 34 | 35 | def check_args(password, salt, N, r, p, olen=64): 36 | if not isinstance(password, bytes): 37 | raise TypeError('password must be a byte string') 38 | if not isinstance(salt, bytes): 39 | raise TypeError('salt must be a byte string') 40 | if not isinstance(N, numbers.Integral): 41 | raise TypeError('N must be an integer') 42 | if not isinstance(r, numbers.Integral): 43 | raise TypeError('r must be an integer') 44 | if not isinstance(p, numbers.Integral): 45 | raise TypeError('p must be an integer') 46 | if not isinstance(olen, numbers.Integral): 47 | raise TypeError('length must be an integer') 48 | if N > 2**63: 49 | raise ValueError('N cannot be larger than 2**63') 50 | if (N & (N - 1)) or N < 2: 51 | raise ValueError('N must be a power of two larger than 1') 52 | if r <= 0: 53 | raise ValueError('r must be positive') 54 | if p <= 0: 55 | raise ValueError('p must be positive') 56 | if r * p >= 2**30: 57 | raise ValueError('r * p must be less than 2 ** 30') 58 | if olen <= 0: 59 | raise ValueError('length must be positive') 60 | 61 | -------------------------------------------------------------------------------- /pylibscrypt/hashlibscrypt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2017, Jan Varho 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | """Scrypt implementation that calls scrypt from hashlib""" 16 | 17 | 18 | try: 19 | from hashlib import scrypt as _scrypt 20 | except ImportError: 21 | raise 22 | except: 23 | raise ImportError('hashlib.scrypt failed to import') 24 | 25 | from . import mcf as mcf_mod 26 | from .common import ( 27 | SCRYPT_N, SCRYPT_r, SCRYPT_p, SCRYPT_MCF_PREFIX_DEFAULT, check_args) 28 | 29 | 30 | def scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64): 31 | """Returns a key derived using the scrypt key-derivarion function 32 | 33 | N must be a power of two larger than 1 but no larger than 2 ** 63 (insane) 34 | r and p must be positive numbers such that r * p < 2 ** 30 35 | 36 | The default values are: 37 | N -- 2**14 (~16k) 38 | r -- 8 39 | p -- 1 40 | 41 | Memory usage is proportional to N*r. Defaults require about 16 MiB. 42 | Time taken is proportional to N*p. Defaults take <100ms of a recent x86. 43 | 44 | The last one differs from libscrypt defaults, but matches the 'interactive' 45 | work factor from the original paper. For long term storage where runtime of 46 | key derivation is not a problem, you could use 16 as in libscrypt or better 47 | yet increase N if memory is plentiful. 48 | """ 49 | check_args(password, salt, N, r, p, olen) 50 | 51 | # Set the memory required based on parameter values 52 | m = 128 * r * (N + p + 2) 53 | 54 | try: 55 | return _scrypt( 56 | password=password, salt=salt, n=N, r=r, p=p, maxmem=m, dklen=olen) 57 | except: 58 | raise ValueError 59 | 60 | 61 | def scrypt_mcf(password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, 62 | prefix=SCRYPT_MCF_PREFIX_DEFAULT): 63 | """Derives a Modular Crypt Format hash using the scrypt KDF 64 | 65 | Parameter space is smaller than for scrypt(): 66 | N must be a power of two larger than 1 but no larger than 2 ** 31 67 | r and p must be positive numbers between 1 and 255 68 | Salt must be a byte string 1-16 bytes long. 69 | 70 | If no salt is given, a random salt of 128+ bits is used. (Recommended.) 71 | """ 72 | return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) 73 | 74 | 75 | def scrypt_mcf_check(mcf, password): 76 | """Returns True if the password matches the given MCF hash""" 77 | return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) 78 | 79 | 80 | if __name__ == "__main__": 81 | import sys 82 | from . import tests 83 | tests.run_scrypt_suite(sys.modules[__name__]) 84 | -------------------------------------------------------------------------------- /pylibscrypt/inline.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2015, Jan Varho 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | """Inlines the salsa20 core lines into salsa20_8""" 16 | 17 | of = open('pylibscrypt/pypyscrypt_inline.py', 'w') 18 | assert of 19 | 20 | def indent(line): 21 | i = 0 22 | while i < len(line) and line[i] == ' ': 23 | i += 1 24 | return i 25 | 26 | with open('pylibscrypt/pypyscrypt.py', 'r') as f: 27 | in_loop = False 28 | loop_indent = 0 29 | lc = 0 30 | rl = [] 31 | skipping = False 32 | for line in f: 33 | lc += 1 34 | if lc == 1: 35 | of.write('# Automatically generated file, see inline.py\n\n') 36 | i = indent(line) 37 | if line[i:].startswith('def R('): 38 | skipping = True 39 | elif line[i:].startswith('def array_overwrite('): 40 | skipping = True 41 | elif skipping: 42 | if line[i:].startswith('def'): 43 | of.write(line) 44 | skipping = False 45 | 46 | elif line[i:].startswith('R('): 47 | parts = line.split(';') 48 | rl += parts 49 | if len(rl) == 32: 50 | # Interleave to reduce dependencies for pypy 51 | rl1 = rl[:16] 52 | rl2 = rl[16:] 53 | rl1 = rl1[0::4] + rl1[1::4] + rl1[2::4] + rl1[3::4] 54 | rl2 = rl2[0::4] + rl2[1::4] + rl2[2::4] + rl2[3::4] 55 | rl = rl1 + rl2 56 | for p, q in zip(rl[::2], rl[1::2]): 57 | pvals = p.split(',')[1:] 58 | pvals = [int(v.strip(' )\n')) for v in pvals] 59 | qvals = q.split(',')[1:] 60 | qvals = [int(v.strip(' )\n')) for v in qvals] 61 | of.write(' '*i) 62 | of.write('a = (x[%d]+x[%d]) & 0xffffffff\n' % 63 | (pvals[1], pvals[2])) 64 | of.write(' '*i) 65 | of.write('b = (x[%d]+x[%d]) & 0xffffffff\n' % 66 | (qvals[1], qvals[2])) 67 | of.write(' '*i) 68 | of.write('x[%d] ^= (a << %d) | (a >> %d)\n' % 69 | (pvals[0], pvals[3], 32 - pvals[3])) 70 | of.write(' '*i) 71 | of.write('x[%d] ^= (b << %d) | (b >> %d)\n' % 72 | (qvals[0], qvals[3], 32 - qvals[3])) 73 | 74 | elif line[i:].startswith('array_overwrite('): 75 | vals = line.split(',') 76 | vals[0] = vals[0].split('(')[1] 77 | vals[-1] = vals[-1].split(')')[0] 78 | vals = [v.strip() for v in vals] 79 | assert len(vals) == 5 80 | of.write(' '*i) 81 | of.write(vals[2] + '[' + vals[3] + ':(' + vals[3] + ')+(' + 82 | vals[4] + ')] = ' + vals[0] + '[' + vals[1] + ':(' + 83 | vals[1] + ')+(' + vals[4] + ')]\n') 84 | 85 | else: 86 | of.write(line) 87 | 88 | -------------------------------------------------------------------------------- /pylibscrypt/libsodium_load.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015, Jan Varho 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | import ctypes.util 16 | import sys 17 | 18 | 19 | def get_libsodium(): 20 | '''Locate the libsodium C library''' 21 | 22 | __SONAMES = (13, 10, 5, 4) 23 | # Import libsodium from system 24 | sys_sodium = ctypes.util.find_library('sodium') 25 | if sys_sodium is None: 26 | sys_sodium = ctypes.util.find_library('libsodium') 27 | 28 | if sys_sodium: 29 | try: 30 | return ctypes.CDLL(sys_sodium) 31 | except OSError: 32 | pass 33 | 34 | # Import from local path 35 | if sys.platform.startswith('win'): 36 | 37 | try: 38 | return ctypes.cdll.LoadLibrary('libsodium') 39 | except OSError: 40 | pass 41 | for soname_ver in __SONAMES: 42 | try: 43 | return ctypes.cdll.LoadLibrary( 44 | 'libsodium-{0}'.format(soname_ver) 45 | ) 46 | except OSError: 47 | pass 48 | elif sys.platform.startswith('darwin'): 49 | try: 50 | return ctypes.cdll.LoadLibrary('libsodium.dylib') 51 | except OSError: 52 | try: 53 | libidx = __file__.find('lib') 54 | if libidx > 0: 55 | libpath = __file__[0:libidx+3] + '/libsodium.dylib' 56 | return ctypes.cdll.LoadLibrary(libpath) 57 | except OSError: 58 | pass 59 | else: 60 | try: 61 | return ctypes.cdll.LoadLibrary('libsodium.so') 62 | except OSError: 63 | pass 64 | 65 | for soname_ver in __SONAMES: 66 | try: 67 | return ctypes.cdll.LoadLibrary( 68 | 'libsodium.so.{0}'.format(soname_ver) 69 | ) 70 | except OSError: 71 | pass 72 | 73 | -------------------------------------------------------------------------------- /pylibscrypt/mcf.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2021, Jan Varho 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | """Modular Crypt Format support for scrypt 16 | 17 | Compatible with libscrypt scrypt_mcf_check also supports the $7$ format. 18 | 19 | libscrypt format: 20 | $s1$NNrrpp$salt$hash 21 | NN - hex encoded N log2 (two hex digits) 22 | rr - hex encoded r in 1-255 23 | pp - hex encoded p in 1-255 24 | salt - base64 encoded salt 1-16 bytes decoded 25 | hash - base64 encoded 64-byte scrypt hash 26 | 27 | $7$ format: 28 | $7$Nrrrrrpppppsalt$hash 29 | N - crypt base64 N log2 30 | rrrrr - crypt base64 r (little-endian 30 bits) 31 | ppppp - crypt base64 p (little-endian 30 bits) 32 | salt - raw salt (0-43 bytes that should be limited to crypt base64) 33 | hash - crypt base64 encoded 32-byte scrypt hash (43 bytes) 34 | 35 | (crypt base64 is base64 with the alphabet: ./0-9A-Za-z) 36 | 37 | When reading, we are more lax, allowing salts and hashes to be longer and 38 | incorrectly encoded, since the worst that can happen is that the password does 39 | not verify. 40 | """ 41 | 42 | 43 | import base64 44 | import binascii 45 | import os 46 | import struct 47 | 48 | from .common import ( 49 | SCRYPT_N, SCRYPT_r, SCRYPT_p, SCRYPT_MCF_PREFIX_7, SCRYPT_MCF_PREFIX_s1, 50 | SCRYPT_MCF_PREFIX_DEFAULT, SCRYPT_MCF_PREFIX_ANY) 51 | 52 | 53 | def _scrypt_mcf_encode_s1(N, r, p, salt, hash): 54 | h64 = base64.b64encode(hash) 55 | s64 = base64.b64encode(salt) 56 | 57 | t = 1 58 | while 2**t < N: 59 | t += 1 60 | params = p + (r << 8) + (t << 16) 61 | 62 | return ( 63 | b'$s1' + 64 | ('$%06x' % params).encode() + 65 | b'$' + s64 + 66 | b'$' + h64 67 | ) 68 | 69 | 70 | def _b64decode(b64): 71 | for b in (b64, b64 + b'=', b64 + b'=='): 72 | try: 73 | return base64.b64decode(b) 74 | except (TypeError, binascii.Error): 75 | pass 76 | raise ValueError('Incorrect base64 in MCF') 77 | 78 | 79 | def _scrypt_mcf_decode_s1(mcf): 80 | s = mcf.split(b'$') 81 | if not (mcf.startswith(b'$s1$') and len(s) == 5): 82 | return None 83 | 84 | params, s64, h64 = s[2:] 85 | params = base64.b16decode(params, True) 86 | salt = _b64decode(s64) 87 | hash = _b64decode(h64) 88 | 89 | if len(params) != 3: 90 | raise ValueError('Unrecognized MCF parameters') 91 | t, r, p = struct.unpack('3B', params) 92 | N = 2 ** t 93 | return N, r, p, salt, hash, len(hash) 94 | 95 | 96 | # Crypt base 64 97 | _cb64 = b'./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 98 | _cb64a = bytearray(_cb64) 99 | _icb64 = ( 100 | [None] * 46 + 101 | [ 102 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, None, None, None, None, None, 103 | None, None, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 104 | 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, None, None, None, 105 | None, None, None, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 106 | 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63 107 | ] + 108 | [None] * 133 109 | ) 110 | 111 | 112 | def _cb64enc(arr): 113 | arr = bytearray(arr) 114 | out = bytearray() 115 | val = bits = 0 116 | for b in arr: 117 | val += b << bits 118 | bits += 8 119 | while bits >= 0: 120 | out.append(_cb64a[val & 0x3f]) 121 | bits -= 6 122 | val = val >> 6 123 | return bytes(out) 124 | 125 | 126 | def _scrypt_mcf_encode_7(N, r, p, salt, hash): 127 | t = 1 128 | while 2**t < N: 129 | t += 1 130 | return ( 131 | b'$7$' + 132 | # N 133 | _cb64[t::64] + 134 | # r 135 | _cb64[r & 0x3f::64] + _cb64[(r >> 6) & 0x3f::64] + 136 | _cb64[(r >> 12) & 0x3f::64] + _cb64[(r >> 18) & 0x3f::64] + 137 | _cb64[(r >> 24) & 0x3f::64] + 138 | # p 139 | _cb64[p & 0x3f::64] + _cb64[(p >> 6) & 0x3f::64] + 140 | _cb64[(p >> 12) & 0x3f::64] + _cb64[(p >> 18) & 0x3f::64] + 141 | _cb64[(p >> 24) & 0x3f::64] + 142 | # rest 143 | salt + 144 | b'$' + _cb64enc(hash) 145 | ) 146 | 147 | 148 | def _cb64dec(arr): 149 | out = bytearray() 150 | val = bits = 0 151 | for b in arr: 152 | val += _icb64[b] << bits 153 | bits += 6 154 | if bits >= 8: 155 | out.append(val & 0xff) 156 | bits -= 8 157 | val >>= 8 158 | return out 159 | 160 | 161 | def _scrypt_mcf_decode_7(mcf): 162 | s = mcf.split(b'$') 163 | if not (mcf.startswith(b'$7$') and len(s) == 4): 164 | return None 165 | 166 | s64 = bytearray(s[2]) 167 | h64 = bytearray(s[3]) 168 | try: 169 | N = 2 ** _icb64[s64[0]] 170 | r = (_icb64[s64[1]] + (_icb64[s64[2]] << 6) + (_icb64[s64[3]] << 12) + 171 | (_icb64[s64[4]] << 18) + (_icb64[s64[5]] << 24)) 172 | p = (_icb64[s64[6]] + (_icb64[s64[7]] << 6) + (_icb64[s64[8]] << 12) + 173 | (_icb64[s64[9]] << 18) + (_icb64[s64[10]] << 24)) 174 | salt = bytes(s64[11:]) 175 | hash = bytes(_cb64dec(h64)) 176 | except (IndexError, TypeError): 177 | raise ValueError('Unrecognized MCF format') 178 | 179 | return N, r, p, salt, hash, len(hash) 180 | 181 | 182 | def _scrypt_mcf_7_is_standard(mcf): 183 | params = _scrypt_mcf_decode_7(mcf) 184 | if params is None: 185 | return False 186 | _N, _r, _p, salt, _hash, hlen = params 187 | return len(salt) == 43 and hlen == 32 188 | 189 | 190 | def _scrypt_mcf_decode(mcf): 191 | params = _scrypt_mcf_decode_s1(mcf) 192 | if params is None: 193 | params = _scrypt_mcf_decode_7(mcf) 194 | if params is None: 195 | raise ValueError('Unrecognized MCF hash') 196 | return params 197 | 198 | 199 | def scrypt_mcf(scrypt, password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, 200 | prefix=SCRYPT_MCF_PREFIX_DEFAULT): 201 | """Derives a Modular Crypt Format hash using the scrypt KDF given 202 | 203 | Expects the signature: 204 | scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64) 205 | 206 | If no salt is given, a random salt of 128+ bits is used. (Recommended.) 207 | """ 208 | if isinstance(password, str): 209 | password = password.encode('utf8') 210 | elif not isinstance(password, bytes): 211 | raise TypeError('password must be a unicode or byte string') 212 | if salt is not None and not isinstance(salt, bytes): 213 | raise TypeError('salt must be a byte string') 214 | if salt is not None and not (1 <= len(salt) <= 16): 215 | raise ValueError('salt must be 1-16 bytes') 216 | if r > 255: 217 | raise ValueError('scrypt_mcf r out of range [1,255]') 218 | if p > 255: 219 | raise ValueError('scrypt_mcf p out of range [1,255]') 220 | if N > 2**31: 221 | raise ValueError('scrypt_mcf N out of range [2,2**31]') 222 | if b'\0' in password: 223 | raise ValueError('scrypt_mcf password must not contain zero bytes') 224 | 225 | if prefix == SCRYPT_MCF_PREFIX_s1: 226 | if salt is None: 227 | salt = os.urandom(16) 228 | hash = scrypt(password, salt, N, r, p) 229 | return _scrypt_mcf_encode_s1(N, r, p, salt, hash) 230 | elif prefix == SCRYPT_MCF_PREFIX_7 or prefix == SCRYPT_MCF_PREFIX_ANY: 231 | if salt is None: 232 | salt = os.urandom(32) 233 | salt = _cb64enc(salt) 234 | hash = scrypt(password, salt, N, r, p, 32) 235 | return _scrypt_mcf_encode_7(N, r, p, salt, hash) 236 | else: 237 | raise ValueError("Unrecognized MCF format") 238 | 239 | 240 | def scrypt_mcf_check(scrypt, mcf, password): 241 | """Returns True if the password matches the given MCF hash 242 | 243 | Supports both the libscrypt $s1$ format and the $7$ format. 244 | """ 245 | if not isinstance(mcf, bytes): 246 | raise TypeError('MCF must be a byte string') 247 | if isinstance(password, str): 248 | password = password.encode('utf8') 249 | elif not isinstance(password, bytes): 250 | raise TypeError('password must be a unicode or byte string') 251 | 252 | N, r, p, salt, hash, hlen = _scrypt_mcf_decode(mcf) 253 | h = scrypt(password, salt, N=N, r=r, p=p, olen=hlen) 254 | cmp = 0 255 | for i, j in zip(bytearray(h), bytearray(hash)): 256 | cmp |= i ^ j 257 | return cmp == 0 258 | 259 | -------------------------------------------------------------------------------- /pylibscrypt/pylibscrypt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2021, Jan Varho 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | """Scrypt implementation that calls into system libscrypt""" 16 | 17 | 18 | import base64 19 | import ctypes 20 | from ctypes import c_char_p, c_size_t, c_uint64, c_uint32 21 | from ctypes.util import find_library 22 | import os 23 | 24 | from .common import ( 25 | SCRYPT_N, SCRYPT_r, SCRYPT_p, SCRYPT_MCF_PREFIX_s1, 26 | SCRYPT_MCF_PREFIX_DEFAULT, SCRYPT_MCF_PREFIX_ANY, check_args) 27 | from . import mcf as mcf_mod 28 | 29 | 30 | _libscrypt_soname = find_library('scrypt') 31 | if _libscrypt_soname is None: 32 | raise ImportError('Unable to find libscrypt') 33 | 34 | try: 35 | _libscrypt = ctypes.CDLL(_libscrypt_soname) 36 | _libscrypt_scrypt = _libscrypt.libscrypt_scrypt 37 | _libscrypt_mcf = _libscrypt.libscrypt_mcf 38 | _libscrypt_check = _libscrypt.libscrypt_check 39 | except OSError: 40 | raise ImportError('Unable to load libscrypt: ' + _libscrypt_soname) 41 | except AttributeError: 42 | raise ImportError('Incompatible libscrypt: ' + _libscrypt_soname) 43 | 44 | _libscrypt_scrypt.argtypes = [ 45 | c_char_p, # password 46 | c_size_t, # password length 47 | c_char_p, # salt 48 | c_size_t, # salt length 49 | c_uint64, # N 50 | c_uint32, # r 51 | c_uint32, # p 52 | c_char_p, # out 53 | c_size_t, # out length 54 | ] 55 | 56 | _libscrypt_mcf.argtypes = [ 57 | c_uint64, # N 58 | c_uint32, # r 59 | c_uint32, # p 60 | c_char_p, # salt 61 | c_char_p, # hash 62 | c_char_p, # out (125+ bytes) 63 | ] 64 | 65 | _libscrypt_check.argtypes = [ 66 | c_char_p, # mcf (modified) 67 | c_char_p, # hash 68 | ] 69 | 70 | 71 | def scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64): 72 | """Returns a key derived using the scrypt key-derivarion function 73 | 74 | N must be a power of two larger than 1 but no larger than 2 ** 63 (insane) 75 | r and p must be positive numbers such that r * p < 2 ** 30 76 | 77 | The default values are: 78 | N -- 2**14 (~16k) 79 | r -- 8 80 | p -- 1 81 | 82 | Memory usage is proportional to N*r. Defaults require about 16 MiB. 83 | Time taken is proportional to N*p. Defaults take <100ms of a recent x86. 84 | 85 | The last one differs from libscrypt defaults, but matches the 'interactive' 86 | work factor from the original paper. For long term storage where runtime of 87 | key derivation is not a problem, you could use 16 as in libscrypt or better 88 | yet increase N if memory is plentiful. 89 | """ 90 | check_args(password, salt, N, r, p, olen) 91 | 92 | out = ctypes.create_string_buffer(olen) 93 | ret = _libscrypt_scrypt(password, len(password), salt, len(salt), 94 | N, r, p, out, len(out)) 95 | if ret: 96 | raise ValueError 97 | 98 | return out.raw 99 | 100 | 101 | def scrypt_mcf(password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, 102 | prefix=SCRYPT_MCF_PREFIX_DEFAULT): 103 | """Derives a Modular Crypt Format hash using the scrypt KDF 104 | 105 | Parameter space is smaller than for scrypt(): 106 | N must be a power of two larger than 1 but no larger than 2 ** 31 107 | r and p must be positive numbers between 1 and 255 108 | Salt must be a byte string 1-16 bytes long. 109 | 110 | If no salt is given, a random salt of 128+ bits is used. (Recommended.) 111 | """ 112 | if (prefix != SCRYPT_MCF_PREFIX_s1 and prefix != SCRYPT_MCF_PREFIX_ANY): 113 | return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) 114 | if isinstance(password, str): 115 | password = password.encode('utf8') 116 | elif not isinstance(password, bytes): 117 | raise TypeError('password must be a unicode or byte string') 118 | if salt is None: 119 | salt = os.urandom(16) 120 | elif not (1 <= len(salt) <= 16): 121 | raise ValueError('salt must be 1-16 bytes') 122 | if N > 2**31: 123 | raise ValueError('N > 2**31 not supported') 124 | if b'\0' in password: 125 | raise ValueError('scrypt_mcf password must not contain zero bytes') 126 | 127 | hash = scrypt(password, salt, N, r, p) 128 | 129 | h64 = base64.b64encode(hash) 130 | s64 = base64.b64encode(salt) 131 | 132 | out = ctypes.create_string_buffer(125) 133 | ret = _libscrypt_mcf(N, r, p, s64, h64, out) 134 | if not ret: 135 | raise ValueError 136 | 137 | out = out.raw.strip(b'\0') 138 | # XXX: Hack to support old libscrypt (like in Ubuntu 14.04) 139 | if len(out) == 123: 140 | out = out + b'=' 141 | 142 | return out 143 | 144 | 145 | def scrypt_mcf_check(mcf, password): 146 | """Returns True if the password matches the given MCF hash""" 147 | if not isinstance(mcf, bytes): 148 | raise TypeError('MCF must be a byte string') 149 | if isinstance(password, str): 150 | password = password.encode('utf8') 151 | elif not isinstance(password, bytes): 152 | raise TypeError('password must be a unicode or byte string') 153 | if len(mcf) != 124 or b'\0' in password: 154 | return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) 155 | 156 | mcfbuf = ctypes.create_string_buffer(mcf) 157 | ret = _libscrypt_check(mcfbuf, password) 158 | if ret < 0: 159 | return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) 160 | 161 | return bool(ret) 162 | 163 | 164 | if __name__ == "__main__": 165 | import sys 166 | from . import tests 167 | tests.run_scrypt_suite(sys.modules[__name__]) 168 | 169 | -------------------------------------------------------------------------------- /pylibscrypt/pylibsodium.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2021, Jan Varho 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | """Scrypt implementation that calls into system libsodium""" 16 | 17 | 18 | import ctypes 19 | from ctypes import c_char_p, c_size_t, c_uint64, c_uint32, c_void_p 20 | 21 | from . import mcf as mcf_mod 22 | from . import libsodium_load 23 | from .common import ( 24 | SCRYPT_N, SCRYPT_r, SCRYPT_p, SCRYPT_MCF_PREFIX_7, SCRYPT_MCF_PREFIX_s1, 25 | SCRYPT_MCF_PREFIX_DEFAULT, SCRYPT_MCF_PREFIX_ANY, check_args) 26 | from . import pypyscrypt_inline as scr_mod 27 | 28 | 29 | _lib = libsodium_load.get_libsodium() 30 | if _lib is None: 31 | raise ImportError('Unable to load libsodium') 32 | 33 | try: 34 | _scrypt_ll = _lib.crypto_pwhash_scryptsalsa208sha256_ll 35 | _scrypt_ll.argtypes = [ 36 | c_void_p, # passwd 37 | c_size_t, # passwdlen 38 | c_void_p, # salt 39 | c_size_t, # saltlen 40 | c_uint64, # N 41 | c_uint32, # r 42 | c_uint32, # p 43 | c_void_p, # buf 44 | c_size_t, # buflen 45 | ] 46 | except AttributeError: 47 | _scrypt_ll = None 48 | 49 | try: 50 | _scrypt = _lib.crypto_pwhash_scryptsalsa208sha256 51 | _scrypt_str = _lib.crypto_pwhash_scryptsalsa208sha256_str 52 | _scrypt_str_chk = _lib.crypto_pwhash_scryptsalsa208sha256_str_verify 53 | _scrypt_str_bytes = _lib.crypto_pwhash_scryptsalsa208sha256_strbytes() 54 | _scrypt_salt = _lib.crypto_pwhash_scryptsalsa208sha256_saltbytes() 55 | if _scrypt_str_bytes != 102 and not _scrypt_ll: 56 | raise ImportError('Incompatible libsodium') 57 | except AttributeError: 58 | try: 59 | _scrypt = _lib.crypto_pwhash_scryptxsalsa208sha256 60 | _scrypt_str = _lib.crypto_pwhash_scryptxsalsa208sha256_str 61 | _scrypt_str_chk = _lib.crypto_pwhash_scryptxsalsa208sha256_str_verify 62 | _scrypt_str_bytes = _lib.crypto_pwhash_scryptxsalsa208sha256_strbytes() 63 | _scrypt_salt = _lib.crypto_pwhash_scryptxsalsa208sha256_saltbytes 64 | _scrypt_salt = _scrypt_salt() 65 | if _scrypt_str_bytes != 102 and not _scrypt_ll: 66 | raise ImportError('Incompatible libsodium') 67 | except AttributeError: 68 | if not _scrypt_ll: 69 | raise ImportError('Incompatible libsodium') 70 | 71 | _scrypt.argtypes = [ 72 | c_void_p, # out 73 | c_uint64, # outlen 74 | c_void_p, # passwd 75 | c_uint64, # passwdlen 76 | c_void_p, # salt 77 | c_uint64, # opslimit 78 | c_size_t, # memlimit 79 | ] 80 | 81 | _scrypt_str.argtypes = [ 82 | c_void_p, # out (102 bytes) 83 | c_void_p, # passwd 84 | c_uint64, # passwdlen 85 | c_uint64, # opslimit 86 | c_size_t, # memlimit 87 | ] 88 | 89 | _scrypt_str_chk.argtypes = [ 90 | c_char_p, # str (102 bytes) 91 | c_void_p, # passwd 92 | c_uint64, # passwdlen 93 | ] 94 | 95 | 96 | def scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64): 97 | """Returns a key derived using the scrypt key-derivarion function 98 | 99 | N must be a power of two larger than 1 but no larger than 2 ** 63 (insane) 100 | r and p must be positive numbers such that r * p < 2 ** 30 101 | 102 | The default values are: 103 | N -- 2**14 (~16k) 104 | r -- 8 105 | p -- 1 106 | 107 | Memory usage is proportional to N*r. Defaults require about 16 MiB. 108 | Time taken is proportional to N*p. Defaults take <100ms of a recent x86. 109 | 110 | The last one differs from libscrypt defaults, but matches the 'interactive' 111 | work factor from the original paper. For long term storage where runtime of 112 | key derivation is not a problem, you could use 16 as in libscrypt or better 113 | yet increase N if memory is plentiful. 114 | """ 115 | check_args(password, salt, N, r, p, olen) 116 | 117 | if _scrypt_ll: 118 | out = ctypes.create_string_buffer(olen) 119 | if _scrypt_ll(password, len(password), salt, len(salt), 120 | N, r, p, out, olen): 121 | raise ValueError 122 | return out.raw 123 | 124 | if len(salt) != _scrypt_salt or r != 8 or (p & (p - 1)) or (N*p <= 512): 125 | return scr_mod.scrypt(password, salt, N, r, p, olen) 126 | 127 | s = next(i for i in range(1, 64) if 2**i == N) 128 | t = next(i for i in range(0, 30) if 2**i == p) 129 | m = 2**(10 + s) 130 | o = 2**(5 + t + s) 131 | if s > 53 or t + s > 58: 132 | raise ValueError 133 | out = ctypes.create_string_buffer(olen) 134 | if _scrypt(out, olen, password, len(password), salt, o, m) != 0: 135 | raise ValueError 136 | return out.raw 137 | 138 | 139 | def scrypt_mcf(password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, 140 | prefix=SCRYPT_MCF_PREFIX_DEFAULT): 141 | """Derives a Modular Crypt Format hash using the scrypt KDF 142 | 143 | Parameter space is smaller than for scrypt(): 144 | N must be a power of two larger than 1 but no larger than 2 ** 31 145 | r and p must be positive numbers between 1 and 255 146 | Salt must be a byte string 1-16 bytes long. 147 | 148 | If no salt is given, a random salt of 128+ bits is used. (Recommended.) 149 | """ 150 | if isinstance(password, str): 151 | password = password.encode('utf8') 152 | elif not isinstance(password, bytes): 153 | raise TypeError('password must be a unicode or byte string') 154 | if N < 2 or (N & (N - 1)): 155 | raise ValueError('scrypt N must be a power of 2 greater than 1') 156 | if p > 255 or p < 1: 157 | raise ValueError('scrypt_mcf p out of range [1,255]') 158 | if N > 2**31: 159 | raise ValueError('scrypt_mcf N out of range [2,2**31]') 160 | 161 | if (salt is not None or r != 8 or (p & (p - 1)) or (N*p <= 512) or 162 | prefix not in (SCRYPT_MCF_PREFIX_7, SCRYPT_MCF_PREFIX_s1, 163 | SCRYPT_MCF_PREFIX_ANY) or 164 | _scrypt_ll): 165 | return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) 166 | 167 | s = next(i for i in range(1, 32) if 2**i == N) 168 | t = next(i for i in range(0, 8) if 2**i == p) 169 | m = 2**(10 + s) 170 | o = 2**(5 + t + s) 171 | mcf = ctypes.create_string_buffer(102) 172 | if _scrypt_str(mcf, password, len(password), o, m) != 0: 173 | return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) 174 | 175 | if prefix in (SCRYPT_MCF_PREFIX_7, SCRYPT_MCF_PREFIX_ANY): 176 | return mcf.raw.strip(b'\0') 177 | 178 | _N, _r, _p, salt, hash, _olen = mcf_mod._scrypt_mcf_decode_7(mcf.raw[:-1]) 179 | assert _N == N and _r == r and _p == p, (_N, _r, _p, N, r, p, o, m) 180 | return mcf_mod._scrypt_mcf_encode_s1(N, r, p, salt, hash) 181 | 182 | 183 | def scrypt_mcf_check(mcf, password): 184 | """Returns True if the password matches the given MCF hash""" 185 | if isinstance(password, str): 186 | password = password.encode('utf8') 187 | elif not isinstance(password, bytes): 188 | raise TypeError('password must be a unicode or byte string') 189 | if not isinstance(mcf, bytes): 190 | raise TypeError('MCF must be a byte string') 191 | if mcf_mod._scrypt_mcf_7_is_standard(mcf) and not _scrypt_ll: 192 | return _scrypt_str_chk(mcf, password, len(password)) == 0 193 | return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) 194 | 195 | 196 | if __name__ == "__main__": 197 | import sys 198 | from . import tests 199 | try: 200 | from . import pylibscrypt 201 | scr_mod = pylibscrypt 202 | except ImportError: 203 | pass 204 | tests.run_scrypt_suite(sys.modules[__name__]) 205 | 206 | -------------------------------------------------------------------------------- /pylibscrypt/pypyscrypt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Richard Moore 2 | # Copyright (c) 2014-2021 Jan Varho 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | """Python implementation of Scrypt password-based key derivation function""" 23 | 24 | # Scrypt definition: 25 | # http://www.tarsnap.com/scrypt/scrypt.pdf 26 | 27 | # It was originally written for a pure-Python Litecoin CPU miner: 28 | # https://github.com/ricmoo/nightminer 29 | # Imported to this project from: 30 | # https://github.com/ricmoo/pyscrypt 31 | # And owes thanks to: 32 | # https://github.com/wg/scrypt 33 | 34 | 35 | from hashlib import pbkdf2_hmac as _pbkdf2 36 | import struct 37 | 38 | from . import mcf as mcf_mod 39 | from .common import ( 40 | SCRYPT_N, SCRYPT_r, SCRYPT_p, SCRYPT_MCF_PREFIX_DEFAULT, check_args) 41 | 42 | 43 | def array_overwrite(source, s_start, dest, d_start, length): 44 | dest[d_start:d_start + length] = source[s_start:s_start + length] 45 | 46 | 47 | def blockxor(source, s_start, dest, d_start, length): 48 | for i in range(length): 49 | dest[d_start + i] ^= source[s_start + i] 50 | 51 | 52 | def integerify(B, r): 53 | """A bijection from ({0, 1} ** k) to {0, ..., (2 ** k) - 1""" 54 | 55 | Bi = (2 * r - 1) * 16 56 | return B[Bi] 57 | 58 | 59 | def R(X, destination, a1, a2, b): 60 | """A single Salsa20 row operation""" 61 | 62 | a = (X[a1] + X[a2]) & 0xffffffff 63 | X[destination] ^= ((a << b) | (a >> (32 - b))) 64 | 65 | 66 | def salsa20_8(B, x, src, s_start, dest, d_start): 67 | """Salsa20/8 http://en.wikipedia.org/wiki/Salsa20""" 68 | 69 | # Merged blockxor for speed 70 | for i in range(16): 71 | x[i] = B[i] = B[i] ^ src[s_start + i] 72 | 73 | # This is the actual Salsa 20/8: four identical double rounds 74 | for i in range(4): 75 | R(x, 4, 0,12, 7);R(x, 8, 4, 0, 9);R(x,12, 8, 4,13);R(x, 0,12, 8,18) 76 | R(x, 9, 5, 1, 7);R(x,13, 9, 5, 9);R(x, 1,13, 9,13);R(x, 5, 1,13,18) 77 | R(x,14,10, 6, 7);R(x, 2,14,10, 9);R(x, 6, 2,14,13);R(x,10, 6, 2,18) 78 | R(x, 3,15,11, 7);R(x, 7, 3,15, 9);R(x,11, 7, 3,13);R(x,15,11, 7,18) 79 | R(x, 1, 0, 3, 7);R(x, 2, 1, 0, 9);R(x, 3, 2, 1,13);R(x, 0, 3, 2,18) 80 | R(x, 6, 5, 4, 7);R(x, 7, 6, 5, 9);R(x, 4, 7, 6,13);R(x, 5, 4, 7,18) 81 | R(x,11,10, 9, 7);R(x, 8,11,10, 9);R(x, 9, 8,11,13);R(x,10, 9, 8,18) 82 | R(x,12,15,14, 7);R(x,13,12,15, 9);R(x,14,13,12,13);R(x,15,14,13,18) 83 | 84 | # While we are handling the data, write it to the correct dest. 85 | # The latter half is still part of salsa20 86 | for i in range(16): 87 | dest[d_start + i] = B[i] = (x[i] + B[i]) & 0xffffffff 88 | 89 | 90 | def blockmix_salsa8(BY, Yi, r): 91 | """Blockmix; Used by SMix""" 92 | 93 | start = (2 * r - 1) * 16 94 | X = BY[start:start+16] # BlockMix - 1 95 | tmp = [0]*16 96 | 97 | for i in range(2 * r): # BlockMix - 2 98 | #blockxor(BY, i * 16, X, 0, 16) # BlockMix - 3(inner) 99 | salsa20_8(X, tmp, BY, i * 16, BY, Yi + i*16) # BlockMix - 3(outer) 100 | #array_overwrite(X, 0, BY, Yi + (i * 16), 16) # BlockMix - 4 101 | 102 | for i in range(r): # BlockMix - 6 103 | array_overwrite(BY, Yi + (i * 2) * 16, BY, i * 16, 16) 104 | array_overwrite(BY, Yi + (i*2 + 1) * 16, BY, (i + r) * 16, 16) 105 | 106 | 107 | def smix(B, Bi, r, N, V, X): 108 | """SMix; a specific case of ROMix based on Salsa20/8""" 109 | 110 | array_overwrite(B, Bi, X, 0, 32 * r) # ROMix - 1 111 | 112 | for i in range(N): # ROMix - 2 113 | array_overwrite(X, 0, V, i * (32 * r), 32 * r) # ROMix - 3 114 | blockmix_salsa8(X, 32 * r, r) # ROMix - 4 115 | 116 | for i in range(N): # ROMix - 6 117 | j = integerify(X, r) & (N - 1) # ROMix - 7 118 | blockxor(V, j * (32 * r), X, 0, 32 * r) # ROMix - 8(inner) 119 | blockmix_salsa8(X, 32 * r, r) # ROMix - 9(outer) 120 | 121 | array_overwrite(X, 0, B, Bi, 32 * r) # ROMix - 10 122 | 123 | 124 | def scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64): 125 | """Returns a key derived using the scrypt key-derivarion function 126 | 127 | N must be a power of two larger than 1 but no larger than 2 ** 63 (insane) 128 | r and p must be positive numbers such that r * p < 2 ** 30 129 | 130 | The default values are: 131 | N -- 2**14 (~16k) 132 | r -- 8 133 | p -- 1 134 | 135 | Memory usage is proportional to N*r. Defaults require about 16 MiB. 136 | Time taken is proportional to N*p. Defaults take <100ms of a recent x86. 137 | 138 | The last one differs from libscrypt defaults, but matches the 'interactive' 139 | work factor from the original paper. For long term storage where runtime of 140 | key derivation is not a problem, you could use 16 as in libscrypt or better 141 | yet increase N if memory is plentiful. 142 | """ 143 | 144 | check_args(password, salt, N, r, p, olen) 145 | 146 | # Everything is lists of 32-bit uints for all but pbkdf2 147 | try: 148 | B = _pbkdf2('sha256', password, salt, 1, p * 128 * r) 149 | B = list(struct.unpack('<%dI' % (len(B) // 4), B)) 150 | XY = [0] * (64 * r) 151 | V = [0] * (32 * r * N) 152 | except (MemoryError, OverflowError): 153 | raise ValueError("scrypt parameters don't fit in memory") 154 | 155 | for i in range(p): 156 | smix(B, i * 32 * r, r, N, V, XY) 157 | 158 | B = struct.pack('<%dI' % len(B), *B) 159 | return _pbkdf2('sha256', password, B, 1, olen) 160 | 161 | 162 | def scrypt_mcf(password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, 163 | prefix=SCRYPT_MCF_PREFIX_DEFAULT): 164 | """Derives a Modular Crypt Format hash using the scrypt KDF 165 | 166 | Parameter space is smaller than for scrypt(): 167 | N must be a power of two larger than 1 but no larger than 2 ** 31 168 | r and p must be positive numbers between 1 and 255 169 | Salt must be a byte string 1-16 bytes long. 170 | 171 | If no salt is given, a random salt of 128+ bits is used. (Recommended.) 172 | """ 173 | return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) 174 | 175 | 176 | def scrypt_mcf_check(mcf, password): 177 | """Returns True if the password matches the given MCF hash""" 178 | return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) 179 | 180 | 181 | __all__ = ['scrypt', 'scrypt_mcf', 'scrypt_mcf_check'] 182 | 183 | 184 | if __name__ == "__main__": 185 | import sys 186 | from . import tests 187 | tests.run_scrypt_suite(sys.modules[__name__]) 188 | 189 | -------------------------------------------------------------------------------- /pylibscrypt/pypyscrypt_inline.py: -------------------------------------------------------------------------------- 1 | # Automatically generated file, see inline.py 2 | 3 | # Copyright (c) 2014 Richard Moore 4 | # Copyright (c) 2014-2021 Jan Varho 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | """Python implementation of Scrypt password-based key derivation function""" 25 | 26 | # Scrypt definition: 27 | # http://www.tarsnap.com/scrypt/scrypt.pdf 28 | 29 | # It was originally written for a pure-Python Litecoin CPU miner: 30 | # https://github.com/ricmoo/nightminer 31 | # Imported to this project from: 32 | # https://github.com/ricmoo/pyscrypt 33 | # And owes thanks to: 34 | # https://github.com/wg/scrypt 35 | 36 | 37 | from hashlib import pbkdf2_hmac as _pbkdf2 38 | import struct 39 | 40 | from . import mcf as mcf_mod 41 | from .common import ( 42 | SCRYPT_N, SCRYPT_r, SCRYPT_p, SCRYPT_MCF_PREFIX_DEFAULT, check_args) 43 | 44 | 45 | def blockxor(source, s_start, dest, d_start, length): 46 | for i in range(length): 47 | dest[d_start + i] ^= source[s_start + i] 48 | 49 | 50 | def integerify(B, r): 51 | """A bijection from ({0, 1} ** k) to {0, ..., (2 ** k) - 1""" 52 | 53 | Bi = (2 * r - 1) * 16 54 | return B[Bi] 55 | 56 | 57 | def salsa20_8(B, x, src, s_start, dest, d_start): 58 | """Salsa20/8 http://en.wikipedia.org/wiki/Salsa20""" 59 | 60 | # Merged blockxor for speed 61 | for i in range(16): 62 | x[i] = B[i] = B[i] ^ src[s_start + i] 63 | 64 | # This is the actual Salsa 20/8: four identical double rounds 65 | for i in range(4): 66 | a = (x[0]+x[12]) & 0xffffffff 67 | b = (x[5]+x[1]) & 0xffffffff 68 | x[4] ^= (a << 7) | (a >> 25) 69 | x[9] ^= (b << 7) | (b >> 25) 70 | a = (x[10]+x[6]) & 0xffffffff 71 | b = (x[15]+x[11]) & 0xffffffff 72 | x[14] ^= (a << 7) | (a >> 25) 73 | x[3] ^= (b << 7) | (b >> 25) 74 | a = (x[4]+x[0]) & 0xffffffff 75 | b = (x[9]+x[5]) & 0xffffffff 76 | x[8] ^= (a << 9) | (a >> 23) 77 | x[13] ^= (b << 9) | (b >> 23) 78 | a = (x[14]+x[10]) & 0xffffffff 79 | b = (x[3]+x[15]) & 0xffffffff 80 | x[2] ^= (a << 9) | (a >> 23) 81 | x[7] ^= (b << 9) | (b >> 23) 82 | a = (x[8]+x[4]) & 0xffffffff 83 | b = (x[13]+x[9]) & 0xffffffff 84 | x[12] ^= (a << 13) | (a >> 19) 85 | x[1] ^= (b << 13) | (b >> 19) 86 | a = (x[2]+x[14]) & 0xffffffff 87 | b = (x[7]+x[3]) & 0xffffffff 88 | x[6] ^= (a << 13) | (a >> 19) 89 | x[11] ^= (b << 13) | (b >> 19) 90 | a = (x[12]+x[8]) & 0xffffffff 91 | b = (x[1]+x[13]) & 0xffffffff 92 | x[0] ^= (a << 18) | (a >> 14) 93 | x[5] ^= (b << 18) | (b >> 14) 94 | a = (x[6]+x[2]) & 0xffffffff 95 | b = (x[11]+x[7]) & 0xffffffff 96 | x[10] ^= (a << 18) | (a >> 14) 97 | x[15] ^= (b << 18) | (b >> 14) 98 | a = (x[0]+x[3]) & 0xffffffff 99 | b = (x[5]+x[4]) & 0xffffffff 100 | x[1] ^= (a << 7) | (a >> 25) 101 | x[6] ^= (b << 7) | (b >> 25) 102 | a = (x[10]+x[9]) & 0xffffffff 103 | b = (x[15]+x[14]) & 0xffffffff 104 | x[11] ^= (a << 7) | (a >> 25) 105 | x[12] ^= (b << 7) | (b >> 25) 106 | a = (x[1]+x[0]) & 0xffffffff 107 | b = (x[6]+x[5]) & 0xffffffff 108 | x[2] ^= (a << 9) | (a >> 23) 109 | x[7] ^= (b << 9) | (b >> 23) 110 | a = (x[11]+x[10]) & 0xffffffff 111 | b = (x[12]+x[15]) & 0xffffffff 112 | x[8] ^= (a << 9) | (a >> 23) 113 | x[13] ^= (b << 9) | (b >> 23) 114 | a = (x[2]+x[1]) & 0xffffffff 115 | b = (x[7]+x[6]) & 0xffffffff 116 | x[3] ^= (a << 13) | (a >> 19) 117 | x[4] ^= (b << 13) | (b >> 19) 118 | a = (x[8]+x[11]) & 0xffffffff 119 | b = (x[13]+x[12]) & 0xffffffff 120 | x[9] ^= (a << 13) | (a >> 19) 121 | x[14] ^= (b << 13) | (b >> 19) 122 | a = (x[3]+x[2]) & 0xffffffff 123 | b = (x[4]+x[7]) & 0xffffffff 124 | x[0] ^= (a << 18) | (a >> 14) 125 | x[5] ^= (b << 18) | (b >> 14) 126 | a = (x[9]+x[8]) & 0xffffffff 127 | b = (x[14]+x[13]) & 0xffffffff 128 | x[10] ^= (a << 18) | (a >> 14) 129 | x[15] ^= (b << 18) | (b >> 14) 130 | 131 | # While we are handling the data, write it to the correct dest. 132 | # The latter half is still part of salsa20 133 | for i in range(16): 134 | dest[d_start + i] = B[i] = (x[i] + B[i]) & 0xffffffff 135 | 136 | 137 | def blockmix_salsa8(BY, Yi, r): 138 | """Blockmix; Used by SMix""" 139 | 140 | start = (2 * r - 1) * 16 141 | X = BY[start:start+16] # BlockMix - 1 142 | tmp = [0]*16 143 | 144 | for i in range(2 * r): # BlockMix - 2 145 | #blockxor(BY, i * 16, X, 0, 16) # BlockMix - 3(inner) 146 | salsa20_8(X, tmp, BY, i * 16, BY, Yi + i*16) # BlockMix - 3(outer) 147 | #array_overwrite(X, 0, BY, Yi + (i * 16), 16) # BlockMix - 4 148 | 149 | for i in range(r): # BlockMix - 6 150 | BY[i * 16:(i * 16)+(16)] = BY[Yi + (i * 2) * 16:(Yi + (i * 2) * 16)+(16)] 151 | BY[(i + r) * 16:((i + r) * 16)+(16)] = BY[Yi + (i*2 + 1) * 16:(Yi + (i*2 + 1) * 16)+(16)] 152 | 153 | 154 | def smix(B, Bi, r, N, V, X): 155 | """SMix; a specific case of ROMix based on Salsa20/8""" 156 | 157 | X[0:(0)+(32 * r)] = B[Bi:(Bi)+(32 * r)] 158 | 159 | for i in range(N): # ROMix - 2 160 | V[i * (32 * r):(i * (32 * r))+(32 * r)] = X[0:(0)+(32 * r)] 161 | blockmix_salsa8(X, 32 * r, r) # ROMix - 4 162 | 163 | for i in range(N): # ROMix - 6 164 | j = integerify(X, r) & (N - 1) # ROMix - 7 165 | blockxor(V, j * (32 * r), X, 0, 32 * r) # ROMix - 8(inner) 166 | blockmix_salsa8(X, 32 * r, r) # ROMix - 9(outer) 167 | 168 | B[Bi:(Bi)+(32 * r)] = X[0:(0)+(32 * r)] 169 | 170 | 171 | def scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64): 172 | """Returns a key derived using the scrypt key-derivarion function 173 | 174 | N must be a power of two larger than 1 but no larger than 2 ** 63 (insane) 175 | r and p must be positive numbers such that r * p < 2 ** 30 176 | 177 | The default values are: 178 | N -- 2**14 (~16k) 179 | r -- 8 180 | p -- 1 181 | 182 | Memory usage is proportional to N*r. Defaults require about 16 MiB. 183 | Time taken is proportional to N*p. Defaults take <100ms of a recent x86. 184 | 185 | The last one differs from libscrypt defaults, but matches the 'interactive' 186 | work factor from the original paper. For long term storage where runtime of 187 | key derivation is not a problem, you could use 16 as in libscrypt or better 188 | yet increase N if memory is plentiful. 189 | """ 190 | 191 | check_args(password, salt, N, r, p, olen) 192 | 193 | # Everything is lists of 32-bit uints for all but pbkdf2 194 | try: 195 | B = _pbkdf2('sha256', password, salt, 1, p * 128 * r) 196 | B = list(struct.unpack('<%dI' % (len(B) // 4), B)) 197 | XY = [0] * (64 * r) 198 | V = [0] * (32 * r * N) 199 | except (MemoryError, OverflowError): 200 | raise ValueError("scrypt parameters don't fit in memory") 201 | 202 | for i in range(p): 203 | smix(B, i * 32 * r, r, N, V, XY) 204 | 205 | B = struct.pack('<%dI' % len(B), *B) 206 | return _pbkdf2('sha256', password, B, 1, olen) 207 | 208 | 209 | def scrypt_mcf(password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, 210 | prefix=SCRYPT_MCF_PREFIX_DEFAULT): 211 | """Derives a Modular Crypt Format hash using the scrypt KDF 212 | 213 | Parameter space is smaller than for scrypt(): 214 | N must be a power of two larger than 1 but no larger than 2 ** 31 215 | r and p must be positive numbers between 1 and 255 216 | Salt must be a byte string 1-16 bytes long. 217 | 218 | If no salt is given, a random salt of 128+ bits is used. (Recommended.) 219 | """ 220 | return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) 221 | 222 | 223 | def scrypt_mcf_check(mcf, password): 224 | """Returns True if the password matches the given MCF hash""" 225 | return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) 226 | 227 | 228 | __all__ = ['scrypt', 'scrypt_mcf', 'scrypt_mcf_check'] 229 | 230 | 231 | if __name__ == "__main__": 232 | import sys 233 | from . import tests 234 | tests.run_scrypt_suite(sys.modules[__name__]) 235 | 236 | -------------------------------------------------------------------------------- /pylibscrypt/pyscrypt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2016, Jan Varho 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | """Scrypt implementation that calls into the 'scrypt' python module""" 16 | 17 | 18 | try: 19 | from scrypt import hash as _scrypt 20 | except ImportError: 21 | raise 22 | except: 23 | raise ImportError('scrypt module failed to import') 24 | 25 | from . import mcf as mcf_mod 26 | from .common import ( 27 | SCRYPT_N, SCRYPT_r, SCRYPT_p, SCRYPT_MCF_PREFIX_DEFAULT, check_args) 28 | 29 | 30 | # scrypt < 0.6 doesn't support hash length 31 | try: 32 | _scrypt(b'password', b'NaCl', N=2, r=1, p=1, buflen=42) 33 | except TypeError: 34 | raise ImportError('scrypt module version unsupported, 0.6+ required') 35 | 36 | 37 | def scrypt(password, salt, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, olen=64): 38 | """Returns a key derived using the scrypt key-derivarion function 39 | 40 | N must be a power of two larger than 1 but no larger than 2 ** 63 (insane) 41 | r and p must be positive numbers such that r * p < 2 ** 30 42 | 43 | The default values are: 44 | N -- 2**14 (~16k) 45 | r -- 8 46 | p -- 1 47 | 48 | Memory usage is proportional to N*r. Defaults require about 16 MiB. 49 | Time taken is proportional to N*p. Defaults take <100ms of a recent x86. 50 | 51 | The last one differs from libscrypt defaults, but matches the 'interactive' 52 | work factor from the original paper. For long term storage where runtime of 53 | key derivation is not a problem, you could use 16 as in libscrypt or better 54 | yet increase N if memory is plentiful. 55 | """ 56 | check_args(password, salt, N, r, p, olen) 57 | 58 | try: 59 | return _scrypt(password=password, salt=salt, N=N, r=r, p=p, buflen=olen) 60 | except: 61 | raise ValueError 62 | 63 | 64 | def scrypt_mcf(password, salt=None, N=SCRYPT_N, r=SCRYPT_r, p=SCRYPT_p, 65 | prefix=SCRYPT_MCF_PREFIX_DEFAULT): 66 | """Derives a Modular Crypt Format hash using the scrypt KDF 67 | 68 | Parameter space is smaller than for scrypt(): 69 | N must be a power of two larger than 1 but no larger than 2 ** 31 70 | r and p must be positive numbers between 1 and 255 71 | Salt must be a byte string 1-16 bytes long. 72 | 73 | If no salt is given, a random salt of 128+ bits is used. (Recommended.) 74 | """ 75 | return mcf_mod.scrypt_mcf(scrypt, password, salt, N, r, p, prefix) 76 | 77 | 78 | def scrypt_mcf_check(mcf, password): 79 | """Returns True if the password matches the given MCF hash""" 80 | return mcf_mod.scrypt_mcf_check(scrypt, mcf, password) 81 | 82 | 83 | if __name__ == "__main__": 84 | import sys 85 | from . import tests 86 | tests.run_scrypt_suite(sys.modules[__name__]) 87 | 88 | -------------------------------------------------------------------------------- /pylibscrypt/test_properties.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2021, Jan Varho 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | """Tests scrypt implementations using hypothesis""" 16 | 17 | 18 | import sys 19 | import unittest 20 | 21 | from hypothesis import given, settings 22 | from hypothesis.strategies import ( 23 | binary, integers, none, one_of, sampled_from, text) 24 | 25 | from .common import ( 26 | SCRYPT_MCF_PREFIX_7, SCRYPT_MCF_PREFIX_s1, 27 | SCRYPT_MCF_PREFIX_DEFAULT, SCRYPT_MCF_PREFIX_ANY) 28 | 29 | 30 | # Strategies for producing parameters 31 | 32 | def valid_pass(): 33 | return binary() 34 | 35 | def valid_mcf_pass(): 36 | return one_of(binary().filter(lambda b: b'\0' not in b), 37 | text().filter(lambda b: u'\0' not in b)) 38 | 39 | def valid_salt(): 40 | return binary() 41 | 42 | def valid_mcf_salt(): 43 | return one_of(binary(min_size=1, max_size=16), none()) 44 | 45 | def valid_olen(): 46 | return integers(min_value=1, max_value=2**20) 47 | 48 | def mcf_prefix(): 49 | return sampled_from([ 50 | SCRYPT_MCF_PREFIX_7, 51 | SCRYPT_MCF_PREFIX_s1, 52 | SCRYPT_MCF_PREFIX_DEFAULT, 53 | SCRYPT_MCF_PREFIX_ANY, 54 | ]) 55 | 56 | 57 | class ScryptTests(unittest.TestCase): 58 | """Tests an scrypt implementation from module""" 59 | set_up_lambda = lambda self:None 60 | tear_down_lambda = lambda self:None 61 | module = None 62 | ref = None 63 | 64 | def setUp(self): 65 | if not self.module: 66 | self.skipTest('module not tested') 67 | self.set_up_lambda() 68 | 69 | def tearDown(self): 70 | self.tear_down_lambda() 71 | 72 | def invalidPass(self, pw): 73 | try: 74 | return pw + b'_' 75 | except TypeError: 76 | return pw + u'_' 77 | 78 | @given(valid_pass(), valid_salt(), valid_olen()) 79 | @settings(deadline=500) 80 | def test_scrypt(self, pw, salt, olen): 81 | h1 = self.module.scrypt(pw, salt, 2, 2, 2, olen) 82 | self.assertEqual(olen, len(h1)) 83 | if (self.ref): 84 | h2 = self.ref.scrypt(pw, salt, 2, 2, 2, olen) 85 | self.assertEqual(h1, h2) 86 | if olen >= 16: # short hashes can collide 87 | h2 = self.module.scrypt(self.invalidPass(pw), salt, 2, 2, 2, olen) 88 | h3 = self.module.scrypt(pw, salt + b'_', 2, 2, 2, olen) 89 | self.assertNotEqual(h1, h2) 90 | self.assertNotEqual(h1, h3) 91 | 92 | @given(valid_mcf_pass(), valid_mcf_salt(), mcf_prefix()) 93 | @settings(deadline=500) 94 | def test_mcf_scrypt(self, pw, salt, prefix): 95 | m = self.module.scrypt_mcf(pw, salt, 2, 2, 2, prefix) 96 | self.assertTrue(self.module.scrypt_mcf_check(m, pw)) 97 | self.assertFalse(self.module.scrypt_mcf_check(m, self.invalidPass(pw))) 98 | if (self.ref): 99 | self.assertTrue(self.ref.scrypt_mcf_check(m, pw)) 100 | self.assertFalse(self.ref.scrypt_mcf_check(m, self.invalidPass(pw))) 101 | if salt and prefix != SCRYPT_MCF_PREFIX_ANY: 102 | m2 = self.ref.scrypt_mcf(pw, salt, 2, 2, 2, prefix) 103 | self.assertEqual(m, m2) 104 | 105 | 106 | def load_scrypt_suite(name, module, ref=None): 107 | tests = type(name, (ScryptTests,), {'module': module, 'ref': ref}) 108 | return unittest.defaultTestLoader.loadTestsFromTestCase(tests) 109 | 110 | 111 | if __name__ == "__main__": 112 | suite = unittest.TestSuite() 113 | ref = None 114 | try: 115 | from . import hashlibscrypt 116 | suite.addTest(load_scrypt_suite('hashlibscryptTests', hashlibscrypt, ref)) 117 | ref = hashlibscrypt 118 | except ImportError: 119 | suite.addTest(load_scrypt_suite('hashlibscryptTests', None, ref)) 120 | 121 | try: 122 | from . import pylibscrypt 123 | suite.addTest(load_scrypt_suite('pylibscryptTests', pylibscrypt, ref)) 124 | ref = ref or pylibscrypt 125 | except ImportError: 126 | suite.addTest(load_scrypt_suite('pylibscryptTests', None, ref)) 127 | 128 | try: 129 | from . import pyscrypt 130 | suite.addTest(load_scrypt_suite('pyscryptTests', pyscrypt, ref)) 131 | ref = ref or pyscrypt 132 | except ImportError: 133 | suite.addTest(load_scrypt_suite('pyscryptTests', None, ref)) 134 | 135 | try: 136 | from . import pylibsodium 137 | suite.addTest(load_scrypt_suite('pylibsodiumTests', 138 | pylibsodium, ref)) 139 | from . import pylibscrypt 140 | loader = unittest.defaultTestLoader 141 | def set_up_ll(self): 142 | if not self.module._scrypt_ll: 143 | self.skipTest('no ll') 144 | self.tmp_ll = self.module._scrypt_ll 145 | self.tmp_scr = self.module.scr_mod 146 | self.module._scrypt_ll = None 147 | self.module.scr_mod = pylibscrypt 148 | def tear_down_ll(self): 149 | self.module._scrypt_ll = self.tmp_ll 150 | self.module.scr_mod = self.tmp_scr 151 | tmp = type( 152 | 'pylibsodiumFallbackTests', (ScryptTests,), 153 | { 154 | 'module': pylibsodium, 155 | 'fast': False, # supports only large parameters 156 | 'set_up_lambda': set_up_ll, 157 | 'tear_down_lambda': tear_down_ll, 158 | } 159 | ) 160 | suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(tmp)) 161 | except ImportError: 162 | suite.addTest(load_scrypt_suite('pylibsodiumTests', None, ref)) 163 | 164 | try: 165 | from . import pypyscrypt_inline as pypyscrypt 166 | suite.addTest(load_scrypt_suite('pypyscryptTests', pypyscrypt, ref)) 167 | except ImportError: 168 | suite.addTest(load_scrypt_suite('pypyscryptTests', None, ref)) 169 | 170 | result = unittest.TextTestRunner().run(suite) 171 | sys.exit(not result.wasSuccessful()) 172 | -------------------------------------------------------------------------------- /pylibscrypt/tests.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014-2021, Jan Varho 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | """Tests scrypt and PBKDF2 implementations""" 16 | 17 | 18 | import base64 19 | import sys 20 | import unittest 21 | 22 | 23 | class ScryptTests(unittest.TestCase): 24 | """Tests an scrypt implementation from module""" 25 | set_up_lambda = lambda self:None 26 | tear_down_lambda = lambda self:None 27 | replace_scrypt_mcf = None 28 | module = None 29 | fast = False 30 | 31 | def setUp(self): 32 | if not self.module: 33 | self.skipTest('module not tested') 34 | self.set_up_lambda() 35 | 36 | def tearDown(self): 37 | self.tear_down_lambda() 38 | if self.replace_scrypt_mcf: 39 | self.module._libscrypt_mcf = self.replace_scrypt_mcf 40 | self.replace_scrypt_mcf = None 41 | 42 | def _test_vector(self, vector): 43 | pw, s, N, r, p, h, m = vector 44 | self.assertEqual( 45 | self.module.scrypt(pw, s, N, r, p), 46 | base64.b16decode(h, True) 47 | ) 48 | if m is not None: 49 | self.assertEqual( 50 | self.module.scrypt_mcf(pw, s, N, r, p), 51 | m 52 | ) 53 | self.assertTrue(self.module.scrypt_mcf_check(m, pw)) 54 | self.assertFalse(self.module.scrypt_mcf_check(m, b'x' + pw)) 55 | 56 | def test_vector0(self): 57 | # From http://www.tarsnap.com/scrypt/scrypt.pdf Appendix B 58 | self._test_vector(( 59 | b'', b'', 16, 1, 1, 60 | b'77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442' 61 | b'fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906', 62 | None 63 | )) 64 | 65 | def test_vector1(self): 66 | # From http://www.tarsnap.com/scrypt/scrypt.pdf Appendix B 67 | # with MCF added 68 | if self.fast: 69 | self.skipTest('slow testcase') 70 | self._test_vector(( 71 | b'password', b'NaCl', 1024, 8, 16, 72 | b'fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b373162' 73 | b'2eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640', 74 | b'$s1$0a0810$TmFDbA==$/bq+HJ00cgB4VucZDQHp/nxq18vII3gw53N2Y0s3MWIu' 75 | b'rzDZLiKjiG/xCSedmDDaxyevuUqD7m2DYMvfoswGQA==' 76 | )) 77 | 78 | def test_vector2(self): 79 | # From http://www.tarsnap.com/scrypt/scrypt.pdf Appendix B 80 | # with MCF added 81 | if self.fast: 82 | self.skipTest('slow testcase') 83 | self._test_vector(( 84 | b'pleaseletmein', b'SodiumChloride', 16384, 8, 1, 85 | b'7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2' 86 | b'd5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887', 87 | b'$s1$0e0801$U29kaXVtQ2hsb3JpZGU=$cCO9yzr9c0hGHAbNgf046/2o+7qQT44+' 88 | b'qbVD9lRdofLVQylVYT8Pz2LUlwUkKpr55h6F3A1lHkDfzwF7RVdYhw==' 89 | )) 90 | 91 | def test_vector3(self): 92 | # From http://www.tarsnap.com/scrypt/scrypt.pdf Appendix B 93 | self.skipTest('very slow testcase') 94 | self._test_vector(( 95 | b'pleaseletmein', b'SodiumChloride', 1048576, 8, 1, 96 | b'2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa47' 97 | b'8e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4', 98 | None 99 | )) 100 | 101 | def test_vector4(self): 102 | self._test_vector(( 103 | b'password', b'NaCl', 2, 8, 1, 104 | b'e5ed8edc019edfef2d3ced0896faf9eec6921dcc68125ce81c10d53474ce' 105 | b'1be545979159700d324e77c68d34c553636a8429c4f3c99b9566466877f9' 106 | b'dca2b92b', 107 | b'$s1$010801$TmFDbA==$5e2O3AGe3+8tPO0Ilvr57saSHcxoElzoHBDVNHTO' 108 | b'G+VFl5FZcA0yTnfGjTTFU2NqhCnE88mblWZGaHf53KK5Kw==' 109 | )) 110 | 111 | def test_vector5(self): 112 | self._test_vector(( 113 | b'pleaseletmein', b'SodiumChloride', 4, 1, 1, 114 | b'BB1D77016C543A99FE632C9C43C60180FD05E0CAC8B29374DBD1854569CB' 115 | b'534F487240CFC069D6A59A35F2FA5C7428B21D9BE9F84315446D5371119E' 116 | b'016FEDF7', 117 | b'$s1$020101$U29kaXVtQ2hsb3JpZGU=$ux13AWxUOpn+YyycQ8YBgP0F4MrI' 118 | b'spN029GFRWnLU09IckDPwGnWpZo18vpcdCiyHZvp+EMVRG1TcRGeAW/t9w==' 119 | )) 120 | 121 | def test_vector6(self): 122 | if self.fast: 123 | self.skipTest('slow testcase') 124 | self._test_vector(( 125 | b'pleaseletmein', b'X'*32, 2**10, 8, 1, 126 | b'cd81f46bd79125651e017a1bf5a28295f68d4b68d397815514bfdc2f3684' 127 | b'f034ae2a5df332a48e915f7567306df2d401387b70d8f02f83bd6f4c69ff' 128 | b'89d2663c', 129 | None 130 | )) 131 | 132 | def test_vector7(self): 133 | self._test_vector(( 134 | b'pa\0ss', b'salt'*4, 32, 2, 2, 135 | b'76c5260f1dc6339512ae87143d799089f5b508c823c870a3d55f641efa84' 136 | b'63a813221050c93a44255ac8027804c49a87c1ecc9911356b9fc17e06eda' 137 | b'85f23ff5', 138 | None 139 | )) 140 | 141 | def test_vector8(self): 142 | if self.fast: 143 | self.skipTest('slow testcase') 144 | self._test_vector(( 145 | b'pleaseletmein', b'X'*32, 2**10, 8, 2, 146 | b'1693cc02b680b18b3d0a874d459d23a5f63ff4f9de0fb5917ef899226af6' 147 | b'bd33d3a3dfe569d3b6f4f762f0cb64f5f406d485aca501a54645d7389fe6' 148 | b'e28e261e', 149 | None 150 | )) 151 | 152 | def test_maxmem(self): 153 | """Tests hashlib maxmem, quite slow""" 154 | if self.fast: 155 | self.skipTest('slow testcase') 156 | self._test_vector(( 157 | b'pleaseletmein', b'X'*32, 2**15, 8, 2, 158 | b'12e88b08ea7a534fb4bc97be54ebdcb93516efe73b3cff901a7ad565b1ae' 159 | b'511cd881b569bd9b65ed006f8ace0fbd75534035395e5967a340f0a4586e' 160 | b'f79e8ab1', 161 | None 162 | )) 163 | 164 | def test_bytes_enforced(self): 165 | self.assertRaisesRegex(TypeError, 'password', 166 | self.module.scrypt, u'pass', b'salt') 167 | self.assertRaisesRegex(TypeError, 'password', 168 | self.module.scrypt, 42, b'salt') 169 | self.assertRaisesRegex(TypeError, 'salt', 170 | self.module.scrypt, b'pass', None) 171 | 172 | def test_mcf_types_enforced(self): 173 | self.assertRaisesRegex(TypeError, 'password', 174 | self.module.scrypt_mcf, object) 175 | self.assertRaisesRegex(TypeError, 'salt', 176 | self.module.scrypt_mcf, b'pass', u'salt') 177 | umcf = ( 178 | u'$s1$020101$U29kaXVtQ2hsb3JpZGU=$ux13AWxUOpn+YyycQ8YBgP0F4MrI' 179 | u'spN029GFRWnLU09IckDPwGnWpZo18vpcdCiyHZvp+EMVRG1TcRGeAW/t9w==' 180 | ) 181 | self.assertRaisesRegex(TypeError, 'MCF', 182 | self.module.scrypt_mcf_check, umcf, b'blah') 183 | 184 | def test_salt_length_mcf(self): 185 | pw = b'pass' 186 | self.assertRaises(ValueError, self.module.scrypt_mcf, pw, b'') 187 | self.assertRaises(ValueError, self.module.scrypt_mcf, pw, b'a'*17) 188 | 189 | def test_salt_generation(self): 190 | pw, N = b'pass', 2 191 | m1 = self.module.scrypt_mcf(pw, N=N) 192 | m2 = self.module.scrypt_mcf(pw, N=N) 193 | self.assertNotEqual(m1, m2) 194 | self.assertTrue(self.module.scrypt_mcf_check(m1, pw)) 195 | self.assertTrue(self.module.scrypt_mcf_check(m2, pw)) 196 | 197 | def test_invalid_N(self): 198 | pw, s = b'password', b'salt'*8 199 | self.assertRaises(TypeError, self.module.scrypt, pw, s, 7.5) 200 | self.assertRaises(ValueError, self.module.scrypt, pw, s, -1) 201 | self.assertRaises(ValueError, self.module.scrypt, pw, s, 1) 202 | self.assertRaises(ValueError, self.module.scrypt, pw, s, 42) 203 | self.assertRaises(ValueError, self.module.scrypt, pw, s, 2**66) 204 | self.assertRaises(ValueError, self.module.scrypt, pw, s, 2**66+2) 205 | self.assertRaises(ValueError, self.module.scrypt_mcf, pw, None, 1) 206 | self.assertRaises(ValueError, self.module.scrypt_mcf, pw, None, 2**32) 207 | 208 | def test_huge_N(self): 209 | pw, s = b'password', b'salt'*8 210 | self.assertRaises(ValueError, self.module.scrypt, pw, s, 2**50) 211 | self.assertRaises(ValueError, self.module.scrypt, pw, s, 2**60) 212 | self.assertRaises(ValueError, self.module.scrypt_mcf, pw, 213 | N=2**31, prefix=b'$7$') 214 | 215 | def test_invalid_r(self): 216 | pw, s, N = b'password', b'salt', 2 217 | self.assertRaises(ValueError, self.module.scrypt, pw, s, N, 0) 218 | self.assertRaises(ValueError, self.module.scrypt, pw, s, N, -1) 219 | self.assertRaises(TypeError, self.module.scrypt, pw, s, N, 7.5) 220 | self.assertRaises(ValueError, self.module.scrypt, pw, s, N, 2**31) 221 | self.assertRaises(ValueError, self.module.scrypt_mcf, pw, s, N, 256) 222 | 223 | def test_invalid_p(self): 224 | pw, s, N = b'password', b'salt', 2 225 | self.assertRaises(ValueError, self.module.scrypt, pw, s, N, 1, 0) 226 | self.assertRaises(ValueError, self.module.scrypt, pw, s, N, 1, -2**31) 227 | self.assertRaises(TypeError, self.module.scrypt, pw, s, N, 1, 7.5) 228 | self.assertRaises(ValueError, self.module.scrypt, pw, s, N, 2**35) 229 | self.assertRaises(ValueError, self.module.scrypt_mcf, pw, s, N, 1, 256) 230 | 231 | def test_olen(self): 232 | pw, s, N = b'password', b'salt', 2 233 | self.assertEqual(len(self.module.scrypt(pw, s, N, olen=42)), 42) 234 | self.assertEqual(len(self.module.scrypt(pw, s, N, olen=100)), 100) 235 | self.assertRaises(TypeError, self.module.scrypt, pw, s, N, olen=b'7') 236 | self.assertRaises(ValueError, self.module.scrypt, pw, s, N, olen=-1) 237 | 238 | def test_invalid_olen(self): 239 | pw, s, N = b'password', b'salt', 2**10 240 | self.assertRaises(TypeError, self.module.scrypt, pw, s, N, olen=b'7') 241 | self.assertRaises(ValueError, self.module.scrypt, pw, s, N, olen=-1) 242 | 243 | def test_mcf(self): 244 | pw = b'password' 245 | self.assertRaises(ValueError, self.module.scrypt_mcf_check, b'', pw) 246 | self.assertRaises(ValueError, self.module.scrypt_mcf_check, 247 | b'$s1$ffffffff$aaaa$bbbb', pw) 248 | self.assertRaises(TypeError, self.module.scrypt_mcf_check, u'mcf', pw) 249 | self.assertRaises(TypeError, self.module.scrypt_mcf_check, b'mcf', 42) 250 | 251 | def test_mcf_padding(self): 252 | if self.fast: 253 | self.skipTest('slow testcase') 254 | pw = b'pleaseletmein' 255 | m1 = ( 256 | b'$s1$020101$U29kaXVtQ2hsb3JpZGU$ux13AWxUOpn+YyycQ8YBgP0F4MrI' 257 | b'spN029GFRWnLU09IckDPwGnWpZo18vpcdCiyHZvp+EMVRG1TcRGeAW/t9w==' 258 | ) 259 | m2 = ( 260 | b'$s1$020101$U29kaXVtQ2hsb3JpZGU=$ux13AWxUOpn+YyycQ8YBgP0F4MrI' 261 | b'spN029GFRWnLU09IckDPwGnWpZo18vpcdCiyHZvp+EMVRG1TcRGeAW/t9w=' 262 | ) 263 | m3 = ( 264 | b'$s1$020101$U29kaXVtQ2hsb3JpZGU=$ux13AWxUOpn+YyycQ8YBgP0F4MrI' 265 | b'spN029GFRWnLU09IckDPwGnWpZo18vpcdCiyHZvp+EMVRG1TcRGeAW/t9' 266 | ) 267 | self.assertTrue(self.module.scrypt_mcf_check(m1, pw)) 268 | self.assertTrue(self.module.scrypt_mcf_check(m2, pw)) 269 | self.assertRaises(ValueError, self.module.scrypt_mcf_check, m3, pw) 270 | 271 | def test_mcf_nonstandard(self): 272 | pw = b'pass' 273 | m1 = ( # empty salt 274 | b'$s1$010801$$WA1vBj+HFlIk7pG/OPS5bY4NKHBGeGIxEY99farnu2C9uOHxKe' 275 | b'LWP3sCXRvP98F7lVi2JNT/Bmte38iodf81VEYB0Nu3pBw9JqTwiCAqMwL+2kqB' 276 | ) 277 | m2 = ( # 31 byte hash 278 | b'$7$16..../....l/htqjrI38qNowkQZL8RxFVxS8JV9PPJr1+A/WTQWiU' 279 | b'$wOcPY0vsHHshxa0u87FDhmTo42WZr0JbSHY2w2Zkyr1' 280 | ) 281 | m3 = ( # 44 byte salt, 31 byte hash 282 | b'$7$12..../....aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 283 | b'$14hkhieutTQcbq.iU1FDZzYz1vW8NPYowy4WERDM70' 284 | ) 285 | self.assertTrue(self.module.scrypt_mcf_check(m1, pw)) 286 | self.assertTrue(self.module.scrypt_mcf_check(m2, pw)) 287 | self.assertTrue(self.module.scrypt_mcf_check(m3, pw)) 288 | 289 | def test_mcf_7(self): 290 | if self.fast: 291 | self.skipTest('slow testcase') 292 | p, m = b'pleaseletmein', ( 293 | b'$7$C6..../....SodiumChloride' 294 | b'$kBGj9fHznVYFQMEn/qDCfrDevf9YDtcDdKvEqHJLV8D' 295 | ) 296 | self.assertTrue(self.module.scrypt_mcf_check(m, p)) 297 | self.assertFalse(self.module.scrypt_mcf_check(m, b'X'+p)) 298 | self.assertRaises(ValueError, self.module.scrypt_mcf_check, 299 | b'$7$$', p 300 | ) 301 | self.assertRaises(ValueError, self.module.scrypt_mcf_check, 302 | b'$7$$$', p 303 | ) 304 | 305 | def test_mcf_7_2(self): 306 | if self.fast: 307 | self.skipTest('slow testcase') 308 | p = b'pleaseletmein' 309 | m1 = self.module.scrypt_mcf(p, None, 2**10, 8, 2, b'$7$') 310 | self.assertTrue(m1.startswith(b'$7$')) 311 | self.assertTrue(self.module.scrypt_mcf_check(m1, p)) 312 | m2 = self.module.scrypt_mcf(p, None, 2**10, 8, 2, b'$s1$') 313 | self.assertTrue(m2.startswith(b'$s1$')) 314 | self.assertTrue(self.module.scrypt_mcf_check(m1, p)) 315 | 316 | def test_mcf_7_fast(self): 317 | p, m1 = b'pleaseletmein', ( 318 | b'$7$06..../....SodiumChloride' 319 | b'$ENlyo6fGw4PCcDBOFepfSZjFUnVatHzCcW55.ZGz3B0' 320 | ) 321 | self.assertTrue(self.module.scrypt_mcf_check(m1, p)) 322 | m2 = self.module.scrypt_mcf(p, b'NaCl', 4, 8, 1, b'$7$') 323 | self.assertTrue(self.module.scrypt_mcf_check(m2, p)) 324 | 325 | def test_mcf_unknown(self): 326 | p = b'pleaseletmein' 327 | self.assertRaises(ValueError, self.module.scrypt_mcf, p, prefix=b'$$') 328 | 329 | def test_mcf_null(self): 330 | p1, p2, p3 = b'please', b'please\0letmein', b'pleaseletmein' 331 | self.assertRaises(ValueError, self.module.scrypt_mcf, p2, N=4) 332 | m = ( 333 | b'$s1$020801$m8/OZVv4hi8rHFVTvOH3tQ==$jwi4vgiCjyqrZKOaksMFks5A' 334 | b'M9ZRcrVPhAwqT1iRMTqXYrwkTngwjR2rwbAet9cSGdFfSverOEVLiLuUzG4k' 335 | b'Hg==' 336 | ) 337 | self.assertTrue(self.module.scrypt_mcf_check(m, p2)) 338 | self.assertFalse(self.module.scrypt_mcf_check(m, p1)) 339 | self.assertFalse(self.module.scrypt_mcf_check(m, p3)) 340 | 341 | def test_mcf_salt_dollar(self): 342 | p, s = b'pass', b'sa$lt' 343 | m1 = self.module.scrypt_mcf(p, salt=s, N=4, prefix=b'$s1$') 344 | m2 = self.module.scrypt_mcf(p, salt=s, N=4, prefix=b'$7$') 345 | self.assertTrue(self.module.scrypt_mcf_check(m1, p)) 346 | self.assertTrue(self.module.scrypt_mcf_check(m2, p)) 347 | 348 | def test_mcf_utf(self): 349 | p, u = b'pass', u'pass' 350 | u2 = u'\xe5\xe4\xf6' 351 | m1 = self.module.scrypt_mcf(p, N=4) 352 | m2 = self.module.scrypt_mcf(u2, N=4) 353 | self.assertTrue(self.module.scrypt_mcf_check(m1, u)) 354 | self.assertTrue(self.module.scrypt_mcf_check(m2, u2)) 355 | 356 | def test_old_libscrypt_support(self): 357 | try: 358 | self.replace_scrypt_mcf = self.module._libscrypt_mcf 359 | except AttributeError: 360 | self.skipTest('not testing pylibscrypt') 361 | def scrypt_mcf(*a): 362 | r = self.replace_scrypt_mcf(*a) 363 | a[5][-2] = b'\0' 364 | self.assertTrue(len(a[5].raw.strip(b'\0')) == 123) 365 | return r 366 | self.module._libscrypt_mcf = scrypt_mcf 367 | pw, N = b'pass', 2 368 | m1 = self.module.scrypt_mcf(pw, N=N) 369 | m2 = self.module.scrypt_mcf(pw, N=N) 370 | self.assertNotEqual(m1, m2) 371 | self.assertTrue(self.module.scrypt_mcf_check(m1, pw)) 372 | self.assertTrue(self.module.scrypt_mcf_check(m2, pw)) 373 | 374 | 375 | def load_scrypt_suite(name, module, fast=True): 376 | tests = type(name, (ScryptTests,), {'module': module, 'fast': fast}) 377 | return unittest.defaultTestLoader.loadTestsFromTestCase(tests) 378 | 379 | 380 | def run_scrypt_suite(module, fast=False): 381 | suite = unittest.TestSuite() 382 | suite.addTest(load_scrypt_suite('scryptTests', module, fast)) 383 | unittest.TextTestRunner().run(suite) 384 | 385 | 386 | if __name__ == "__main__": 387 | suite = unittest.TestSuite() 388 | try: 389 | from . import hashlibscrypt 390 | suite.addTest(load_scrypt_suite('hashlibscryptTests', hashlibscrypt, True)) 391 | except ImportError: 392 | suite.addTest(load_scrypt_suite('hashlibscryptTests', None, True)) 393 | 394 | try: 395 | from . import pylibscrypt 396 | suite.addTest(load_scrypt_suite('pylibscryptTests', pylibscrypt, True)) 397 | except ImportError: 398 | suite.addTest(load_scrypt_suite('pylibscryptTests', None, True)) 399 | 400 | try: 401 | from . import pyscrypt 402 | suite.addTest(load_scrypt_suite('pyscryptTests', pyscrypt, True)) 403 | except ImportError: 404 | suite.addTest(load_scrypt_suite('pyscryptTests', None, True)) 405 | 406 | try: 407 | from . import pylibsodium 408 | suite.addTest(load_scrypt_suite('pylibsodiumTests', 409 | pylibsodium, True)) 410 | from . import pylibscrypt 411 | loader = unittest.defaultTestLoader 412 | def set_up_ll(self): 413 | if not self.module._scrypt_ll: 414 | self.skipTest('no ll') 415 | self.tmp_ll = self.module._scrypt_ll 416 | self.tmp_scr = self.module.scr_mod 417 | self.module._scrypt_ll = None 418 | self.module.scr_mod = pylibscrypt 419 | def tear_down_ll(self): 420 | self.module._scrypt_ll = self.tmp_ll 421 | self.module.scr_mod = self.tmp_scr 422 | tmp = type( 423 | 'pylibsodiumFallbackTests', (ScryptTests,), 424 | { 425 | 'module': pylibsodium, 426 | 'fast': False, # supports only large parameters 427 | 'set_up_lambda': set_up_ll, 428 | 'tear_down_lambda': tear_down_ll, 429 | } 430 | ) 431 | suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(tmp)) 432 | except ImportError: 433 | suite.addTest(load_scrypt_suite('pylibsodiumTests', None, True)) 434 | 435 | try: 436 | from . import pypyscrypt_inline as pypyscrypt 437 | suite.addTest(load_scrypt_suite('pypyscryptTests', pypyscrypt, True)) 438 | except ImportError: 439 | suite.addTest(load_scrypt_suite('pypyscryptTests', None, True)) 440 | 441 | result = unittest.TextTestRunner().run(suite) 442 | sys.exit(not result.wasSuccessful()) 443 | 444 | -------------------------------------------------------------------------------- /run_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | PYTHON=python3 4 | $PYTHON -m coverage run --branch -m pylibscrypt.tests 5 | $PYTHON -m coverage run --branch -a -m pylibscrypt.pylibscrypt 6 | $PYTHON -m coverage run --branch -a -m pylibscrypt.pylibsodium 7 | $PYTHON -m coverage run --branch -a test_fallback.py 8 | $PYTHON -m coverage run --branch -a test_fallback.py -p 9 | $PYTHON -m coverage run --branch -a test_fallback.py -e 10 | $PYTHON -m coverage html 11 | $PYTHON -m coverage report 12 | -------------------------------------------------------------------------------- /run_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | PYTHON=python3 4 | $PYTHON -m coverage run --branch -p -m pylibscrypt.tests 5 | $PYTHON -m coverage run --branch -p -m pylibscrypt.test_properties 6 | $PYTHON -m coverage run --branch -p -m pylibscrypt.pylibscrypt 7 | $PYTHON -m coverage run --branch -p -m pylibscrypt.pylibsodium 8 | $PYTHON -m coverage run --branch -p test_fallback.py 9 | $PYTHON -m coverage run --branch -p test_fallback.py -e 10 | $PYTHON -m coverage run --branch -p test_fallback.py -p 11 | $PYTHON -m coverage run --branch -p test_fallback.py -e -p 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from distutils.core import setup 4 | 5 | # Read README for long description 6 | readme = open('README').read() 7 | try: 8 | import pypandoc 9 | pypandoc.get_pandoc_formats() 10 | readme = pypandoc.convert_text(readme, to='rst', format='markdown') 11 | except: 12 | pass 13 | 14 | with open('pylibscrypt/__init__.py') as f: 15 | for l in f.readlines(): 16 | if l.startswith('__version__'): 17 | version = l.split('=')[1].strip().strip("'") 18 | 19 | setup(name='pylibscrypt', 20 | version=version, 21 | description='Scrypt for Python', 22 | long_description=readme, 23 | author='Jan Varho', 24 | author_email='jan@varho.org', 25 | url='https://github.com/jvarho/pylibscrypt', 26 | license='ISC License', 27 | packages=['pylibscrypt'], 28 | classifiers=[ 29 | 'Development Status :: 5 - Production/Stable', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: ISC License (ISCL)', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: Implementation :: CPython', 35 | 'Programming Language :: Python :: Implementation :: PyPy', 36 | 'Topic :: Security :: Cryptography', 37 | 'Topic :: Software Development :: Libraries', 38 | ], 39 | ) 40 | 41 | -------------------------------------------------------------------------------- /test_fallback.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2014-2021, Jan Varho 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | import ctypes.util 18 | import hashlib 19 | import platform 20 | import sys 21 | 22 | if '-p' in sys.argv: 23 | platform.python_implementation = lambda:'PyPy' 24 | 25 | def raises(e): 26 | def raising(*arg, **kwarg): 27 | raise e 28 | return raising 29 | 30 | def unimport(mod=None): 31 | del sys.modules['pylibscrypt'] 32 | sys.modules.pop('pylibscrypt.common', None) 33 | sys.modules.pop('pylibscrypt.mcf', None) 34 | sys.modules.pop('pylibscrypt.libsodium_load', None) 35 | if mod is not None: 36 | sys.modules.pop(mod, None) 37 | 38 | import pylibscrypt 39 | sys.modules['pylibscrypt.hashlibscrypt'] = None 40 | 41 | if '-e' in sys.argv: 42 | unimport() 43 | tmp1 = ctypes.util.find_library 44 | tmp2 = ctypes.cdll.LoadLibrary 45 | tmp3 = ctypes.CDLL 46 | ctypes.util.find_library = lambda *args, **kw: None 47 | ctypes.cdll.LoadLibrary = lambda *args, **kw: None 48 | import pylibscrypt 49 | ctypes.util.find_library = tmp1 50 | ctypes.cdll.LoadLibrary = tmp2 51 | unimport('pylibscrypt.pylibscrypt') 52 | ctypes.CDLL = lambda *args, **kw: None 53 | import pylibscrypt 54 | unimport('pylibscrypt.pylibscrypt') 55 | ctypes.CDLL = raises(OSError) 56 | import pylibscrypt 57 | ctypes.CDLL = tmp3 58 | 59 | unimport('pylibscrypt.pylibscrypt') 60 | ctypes.CDLL = lambda *args, **kw: None 61 | import pylibscrypt 62 | 63 | unimport() 64 | import pylibscrypt 65 | 66 | unimport('pylibscrypt.pylibscrypt') 67 | sys.modules['pylibscrypt.pylibscrypt'] = None 68 | import pylibscrypt 69 | 70 | unimport('pylibscrypt.pyscrypt') 71 | sys.modules['scrypt'] = None 72 | import pylibscrypt 73 | 74 | unimport() 75 | sys.modules['pylibscrypt.pyscrypt'] = None 76 | import pylibscrypt 77 | 78 | unimport() 79 | sys.modules['pylibscrypt.pylibsodium'] = None 80 | import pylibscrypt 81 | 82 | --------------------------------------------------------------------------------