├── .gitattributes ├── .gitignore ├── .gitmodules ├── LICENSE ├── MANIFEST.in ├── README.md ├── REQUIREMENTS ├── VERSION ├── makefile ├── requirements.txt ├── setup.py └── src └── pyescrypt ├── __init__.py ├── py.typed └── pyescrypt.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # Intellij 141 | .idea/ 142 | *.pickle 143 | *.exe 144 | *.bin 145 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "yescrypt"] 2 | path = yescrypt 3 | url = https://github.com/openwall/yescrypt 4 | [submodule "src/yescrypt"] 5 | path = src/yescrypt 6 | url = https://github.com/openwall/yescrypt 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | - 2 | 3 | pyescrypt 4 | 5 | 6 | Copyright 2021 Colt Blackmore 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, 12 | this list of conditions and the following disclaimer. 13 | 14 | 2. Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. 29 | 30 | 31 | - 32 | 33 | yescrypt 34 | 35 | 36 | Copyright 2013-2018 Alexander Peslyak 37 | All rights reserved. 38 | 39 | Redistribution and use in source and binary forms, with or without 40 | modification, are permitted. 41 | 42 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 43 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 44 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 45 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 46 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 47 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 48 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 49 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 50 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 51 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 52 | SUCH DAMAGE. 53 | 54 | 55 | - 56 | 57 | scrypt 58 | 59 | 60 | The included code and documentation ("scrypt") is distributed under the 61 | following terms: 62 | 63 | Copyright 2005-2020 Colin Percival. All rights reserved. 64 | Copyright 2011-2020 Tarsnap Backup Inc. All rights reserved. 65 | Copyright 2014 Sean Kelly. All rights reserved. 66 | 67 | Redistribution and use in source and binary forms, with or without 68 | modification, are permitted provided that the following conditions 69 | are met: 70 | 1. Redistributions of source code must retain the above copyright 71 | notice, this list of conditions and the following disclaimer. 72 | 2. Redistributions in binary form must reproduce the above copyright 73 | notice, this list of conditions and the following disclaimer in the 74 | documentation and/or other materials provided with the distribution. 75 | 76 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 77 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 78 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 79 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 80 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 81 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 82 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 83 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 84 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 85 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 86 | SUCH DAMAGE. 87 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include makefile 2 | include src/yescrypt/* 3 | exclude src/yescrypt/.git 4 | include REQUIREMENTS 5 | include VERSION 6 | include LICENSE 7 | include README.md 8 | include requirements.txt 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyescrypt 2 | Python bindings for [yescrypt](https://github.com/openwall/yescrypt), a memory-hard password hashing scheme that meets the requirements of NIST SP 800-63B. Yescrypt is the only scheme from the [Password Hashing Competition](https://www.password-hashing.net/) to receive recognition *and* meet these requirements (by being built on SHA-256, HMAC, and PBKDF2; see NIST SP 800-63B §5.1.1.2). Unfortunately Argon2, Catena, Lyra2, and Makwa use unapproved primitives and aren't suitable for NIST-compliant work. 3 | 4 | 5 | ## Usage 6 | ```python 7 | import secrets 8 | import time 9 | 10 | # All default settings. 11 | hasher = Yescrypt(n=2 ** 16, r=8, p=1, mode=Mode.JSON) 12 | password = secrets.token_bytes(32) 13 | 14 | start = time.time() 15 | hashed = hasher.digest( 16 | password=password, 17 | salt=secrets.token_bytes(32)) 18 | stop = time.time() - start 19 | 20 | try: 21 | hasher.compare(password, hashed) 22 | except WrongPasswordConfiguration: 23 | print("Passwords have different configurations.") 24 | except WrongPassword: 25 | print("Passwords don't match.") 26 | 27 | print( 28 | f"Yescrypt took {stop:.2f} seconds to generate password hash {h.decode()} and " 29 | f"used {128 * 2**16 * 8 / 1024**2:.2f} MiB memory." 30 | ) 31 | ``` 32 | TODO: Explain. 33 | 34 | 35 | ## Installation 36 | ```shell 37 | $ pip -m install pyescrypt 38 | Collecting pyescrypt 39 | Downloading pyescrypt-0.1.0.tar.gz (73 kB) 40 | |████████████████████████████████| 73 kB 1.9 MB/s 41 | Requirement already satisfied: cffi>=1.0.0 in ./.local/lib/python3.8/site-packages (from pyescrypt) (1.14.6) 42 | Requirement already satisfied: pycparser in ./.local/lib/python3.8/site-packages (from cffi>=1.0.0->pyescrypt) (2.20) 43 | Building wheels for collected packages: pyescrypt 44 | Building wheel for pyescrypt (setup.py) ... done 45 | Created wheel for pyescrypt: filename=pyescrypt-0.1.0-py3-none-linux_x86_64.whl size=39771 sha256=db53f817c32b69f9c856eeb450cd1fb9a208e118d5ff467b0f740bc440def001 46 | Stored in directory: /home/0xcb/.cache/pip/wheels/ee/e3/9e/6f47431888cf3f05b020d4b6e2d50d0eafb834b290fc84558a 47 | Successfully built pyescrypt 48 | Installing collected packages: pyescrypt 49 | Successfully installed pyescrypt-0.1.0 50 | ``` 51 | ### Wheels 52 | Wheels are available for Windows and macOS. Other platforms build from source with Make and GCC. 53 | 54 | Note: The macOS x86-64 wheel is compiled without AVX support, since Big Sur's Python3 can't execute it. Given yescrypt is explicitly designed not to benefit from registers wider than 128 bits, AVX is no loss. 55 | 56 | (Presumably Big Sur's Python3 troubles with AVX are related to Rosetta. See the ["What Can't Be Translated"](https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment) section on the Rosetta page. The same binaries run without issue outside of Python.) 57 | 58 | ### Building from Source 59 | Building pyescrypt from source requires GCC or a compatible compiler and (GNU) Make, regardless of platform. On Windows, the [Winlibs](https://github.com/brechtsanders/winlibs_mingw) distribution of MinGW is an excellent option. 60 | 61 | A GCC-like compiler is necessary because yescrypt makes liberal use of GCC preprocessor and C extensions that Microsoft's compiler doesn't support (#warning, restrict, etc.). Clang works, but not everywhere. The version that ships with macOS Big Sur for example is missing OpenMP support. 62 | 63 | By default, pyescrypt statically links GOMP (GNU OpenMP) and its dependencies on Windows and macOS x86-64, since GOMP isn't automatically available on non-Linux platforms. Sometimes (e.g. the AWS Lambda Python 3.8 runtime) GOMP even gets left out of Linux, but finding a copy of libgomp.so is easy (whereas an `-fPIC`-compiled libgomp.a has to be built, along with *GCC in its entirety*), so GOMP isn't statically linked on Linux. 64 | 65 | #### macOS x86-64 66 | To build on macOS x86-64 there are a few options, but the easiest is to `brew install gcc` and change the compiler to `gcc-11`, since `gcc` is otherwise just an alias for Clang. GCC gives you the option of static or dynamic builds. 67 | 68 | You can also stick with Clang, `brew install libomp`, and change the makefile to use `libomp` instead of `libgomp`. Or you can `brew install llvm` for a more featureful Clang build, change the compiler, and also move to `libomp` (which comes packaged with LLVM). 69 | 70 | #### macOS ARM 71 | On ARM macOS, neither GCC builds nor GOMP builds work, nor do builds using the included copy of Clang, which has removed support for `libomp`. Instead, `brew install llvm`, then `make dynamic`, and the result will be a dynamically linked library using OpenMP. (Static builds haven't been figured out yet, so `brew install llvm` will be needed on users' machines as well.) No makefile editing is necessary. 72 | 73 | 74 | ## License 75 | Scrypt, yescrypt, and pyescrypt are all released under the 2-clause BSD license. 76 | 77 | A few parts of the yescrypt repository have an even more permissive license with no attribution requirement, but these are separate from the actual library (e.g. the Makefile, PHC interface, and ROM demo code). 78 | 79 | Note that because pyescrypt links GOMP, GPL-licensed code is also included. Unless you're doing something unusual with compilation, though, there's nothing to worry about: GOMP falls under the [GCC Runtime Library Exception](https://www.gnu.org/licenses/gcc-exception-3.1-faq.en.html), and can be shared under other licenses or no license at all regardless of how it's linked. 80 | 81 | 82 | ## Useful Setuptools Commands 83 | - `build`: Build binaries and link them statically. 84 | - `build_dynamic`: Build binaries and link them dynamically. 85 | - `bdist_wheel`: Build binaries, link them statically, and package them in a wheel. 86 | - `bdist_wheel_dynamic`: Build binaries, link them dynamically, and package them in a wheel. This is what builds the ARM Mac wheels. 87 | 88 | 89 | ## Useful Make Targets 90 | - `make static`: Build binaries and link them statically. 91 | - `make dynamic`: Build binaries and link them dynamically. 92 | - `make clean`: Clear build products and intermediates. 93 | 94 | 95 | ## Version History 96 | - 2024-03-8 97 | - Added support for macOS ARM builds (dynamic only). A macOS ARM wheel is now available for v0.1. (Thanks to @erikrose!) 98 | 99 | - 0.1 100 | - Initial release. 101 | -------------------------------------------------------------------------------- /REQUIREMENTS: -------------------------------------------------------------------------------- 1 | cffi>=1.0.0 2 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | ifneq ($(MAKECMDGOALS),clean) 2 | # Yescrypt makes liberal use of GCC preprocessor and C extensions that 3 | # Microsoft's compiler doesn't support (#warning, restrict, etc.). Clang 4 | # supports them, but is generally brittle for the options we need across 5 | # platforms, so we prefer GCC everywhere. 6 | 7 | # LLVM's OMP has a simpler license (MIT) than GNU's GOMP (GPL), but as long 8 | # as we're using GCC in the normal way linking GOMP falls under the GCC 9 | # Runtime Library Exception. See 10 | # https://www.gnu.org/licenses/gcc-exception-3.1-faq.en.html. Static and 11 | # dynamic linking are treated equally here. 12 | ifndef OMP_PATH 13 | $(warning WARNING: OMP_PATH not set, linker may not be able to find OpenMP) 14 | else 15 | OMP_PATH = -L"$(OMP_PATH)" 16 | endif 17 | endif 18 | 19 | SRC_DIR = src/yescrypt 20 | BUILD_DIR = build 21 | TARGET_DIR = src/pyescrypt 22 | OBJS = $(BUILD_DIR)/yescrypt-opt.o $(BUILD_DIR)/yescrypt-common.o \ 23 | $(BUILD_DIR)/sha256.o $(BUILD_DIR)/insecure_memzero.o 24 | 25 | PLATFORM = 26 | ifeq ($(OS),Windows_NT) 27 | PLATFORM = Windows 28 | else 29 | UNAME := $(shell uname) 30 | ifeq ($(UNAME),Darwin) 31 | PLATFORM = macOS 32 | else 33 | PLATFORM = Linux 34 | endif 35 | endif 36 | 37 | ifeq ($(PLATFORM),Windows) 38 | ARCH = x86_64 39 | else 40 | ARCH := $(shell uname -m) 41 | endif 42 | 43 | # Note: On macOS for ARM, this builds using a brew-installed version of clang. 44 | # The system clang lacks support for OpenMP. `brew install llvm`, then run, for 45 | # example, `make static CC=/opt/homebrew/opt/llvm/bin/clang`. v17.0.6 is known 46 | # to work. 47 | ifndef COMPILER 48 | $(warning WARNING: COMPILER not set, Make may not be able to find the compiler) 49 | COMPILER = gcc 50 | ifeq ($(PLATFORM),macOS) 51 | ifeq ($(ARCH),arm64) 52 | COMPILER = /opt/homebrew/opt/llvm/bin/clang 53 | endif 54 | endif 55 | endif 56 | 57 | ifeq ($(PLATFORM),Windows) 58 | CLEANUP = del /f /Q "$(BUILD_DIR)\*" 59 | else 60 | CLEANUP = rm -f $(OBJS) 61 | endif 62 | 63 | SIMD = 64 | ifeq ($(PLATFORM),macOS) 65 | ifeq ($(ARCH),x86_64) 66 | SIMD = -msse2 67 | endif 68 | else 69 | SIMD = -mavx 70 | endif 71 | 72 | OMP = 73 | ifeq ($(PLATFORM),Windows) 74 | OMP = -static -lgomp 75 | else ifeq ($(PLATFORM),macOS) 76 | OMP = -static -lgomp 77 | else 78 | # Ubuntu ships with non-fPIC GOMP, so passing `-l:libgomp.a` fails. This is 79 | # generally fine, since the only missing GOMP we've seen on Linux is Amazon's 80 | # Python 3.8 Lambda runtime. 81 | OMP = -lgomp 82 | endif 83 | 84 | # Link GOMP statically when we can since it's not distributed with most systems. 85 | .PHONY: static 86 | static: $(OBJS) 87 | $(COMPILER) -shared -fPIC $(OBJS) $(OMP_PATH) -fopenmp $(OMP) -o $(TARGET_DIR)/yescrypt.bin 88 | 89 | .PHONY: dynamic 90 | dynamic: $(OBJS) 91 | $(COMPILER) -shared -fPIC $(OBJS) $(OMP_PATH) -fopenmp -o $(TARGET_DIR)/yescrypt.bin 92 | 93 | # Note: DSKIP_MEMZERO isn't actually used (the code only has a SKIP_MEMZERO 94 | # guard), but we retain it in case it's used later. 95 | $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR) 96 | $(COMPILER) -Wall -O2 -fPIC -funroll-loops -fomit-frame-pointer -fopenmp -DSKIP_MEMZERO $(SIMD) -c $< -o $@ 97 | 98 | $(BUILD_DIR): 99 | mkdir $@ 100 | 101 | yescrypt-opt.o: $(SRC_DIR)/yescrypt-platform.c 102 | 103 | .PHONY: clean 104 | clean: 105 | - $(CLEANUP) 106 | rm -f $(TARGET_DIR)/yescrypt.bin 107 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cffi>=1.0.0 2 | setuptools 3 | wheel 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import subprocess 3 | import sys 4 | from distutils.command.build import build 5 | from platform import machine, system 6 | 7 | from setuptools import find_packages, setup # type: ignore 8 | from setuptools.command.develop import develop # type: ignore 9 | from setuptools.command.install import install # type: ignore 10 | from wheel.bdist_wheel import bdist_wheel as _bdist_wheel # type: ignore 11 | 12 | _MAKE_TYPE = "" 13 | 14 | 15 | class BdistWheel(_bdist_wheel): 16 | """ 17 | Yoink: https://github.com/Yelp/dumb-init/blob/ 18 | 48db0c0d0ecb4598d1a6400710445b85d67616bf/setup.py#L11-L27 19 | 20 | Even so setuptools is confused about yescrypt.bin being pure, presumably 21 | because it doesn't have a standard platform executable extension. But this 22 | at least gets it to name the wheels correctly, which is important since 23 | the names are stateful and will prevent pip installing when incorrect. 24 | """ 25 | 26 | def finalize_options(self) -> None: 27 | _bdist_wheel.finalize_options(self) 28 | self.root_is_pure = False # noqa 29 | 30 | def get_tag(self) -> tuple[str, str, str]: 31 | python, abi, plat = _bdist_wheel.get_tag(self) 32 | python, abi = "py3", "none" 33 | return python, abi, plat 34 | 35 | 36 | def _build_source(static_or_dynamic: str) -> None: 37 | if subprocess.call(["make", "clean"]) != 0: 38 | sys.exit(-1) 39 | if subprocess.call(["make", static_or_dynamic]) != 0: 40 | sys.exit(-1) 41 | 42 | 43 | class Build(build): 44 | """Clear any built binaries and rebuild with make.""" 45 | 46 | def run(self) -> None: 47 | _build_source(_MAKE_TYPE) 48 | super().run() 49 | 50 | 51 | class Develop(develop): 52 | """Remember to build the DLL even when people use ``pip install -e``.""" 53 | 54 | def run(self) -> None: 55 | # macOS ARM static builds haven't been figured out yet, so, in order 56 | # that develop builds may work at all on ARM Macs, implicitly do a 57 | # dynamic build. 58 | static_or_dynamic = ( 59 | "dynamic" if (system() == "Darwin" and machine() == "arm64") else _MAKE_TYPE 60 | ) 61 | _build_source(static_or_dynamic) 62 | super().run() 63 | 64 | 65 | if __name__ == "__main__": 66 | with open("REQUIREMENTS") as f: 67 | required = f.read().splitlines() 68 | 69 | with open("VERSION") as f: 70 | # Black automatically adds '\n'. 71 | version = f.readline().strip() 72 | 73 | if sys.argv[1] in ("build_dynamic", "bdist_wheel_dynamic"): 74 | _MAKE_TYPE = "dynamic" 75 | else: 76 | _MAKE_TYPE = "static" 77 | 78 | setup( 79 | name="pyescrypt", 80 | version=version, 81 | description=( 82 | "Python bindings for yescrypt: memory-hard, NIST-compliant password " 83 | "hashing." 84 | ), 85 | author="Colt Blackmore", 86 | author_email="coltblackmore+pyescrypt@gmail.com", 87 | install_requires=required, 88 | license="BSD", 89 | url="https://github.com/0xcb/pyescrypt", 90 | packages=find_packages("src"), 91 | package_dir={"": "src"}, 92 | package_data={"": ["yescrypt.bin", "py.typed"]}, 93 | cmdclass={ 94 | # Build yescrypt when installing from source. 95 | "install": install, 96 | "develop": Develop, 97 | "build": Build, 98 | "build_dynamic": Build, 99 | "bdist_wheel": BdistWheel, 100 | "bdist_wheel_dynamic": BdistWheel, 101 | }, 102 | include_package_data=True, 103 | zip_safe=False, 104 | ) 105 | -------------------------------------------------------------------------------- /src/pyescrypt/__init__.py: -------------------------------------------------------------------------------- 1 | from .pyescrypt import Yescrypt, Mode, WrongPassword, WrongPasswordConfiguration 2 | 3 | __all__ = ["Yescrypt", "Mode", "WrongPassword", "WrongPasswordConfiguration"] 4 | -------------------------------------------------------------------------------- /src/pyescrypt/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xcb/pyescrypt/dd590679f5eeb834451c43172b020c56e600dd7b/src/pyescrypt/py.typed -------------------------------------------------------------------------------- /src/pyescrypt/pyescrypt.py: -------------------------------------------------------------------------------- 1 | # Yescrypt makes liberal use of GCC preprocessor and C extensions that break 2 | # CL (#warning, restrict, etc.) and Clang. Realistically, it's not worth the 3 | # headache of compiling with anything other than GCC. Unfortunately, distutils 4 | # (and CFFI, which uses distutils for compilation under the hood) is unutterably 5 | # bad at using GCC on Windows, forces specific file prefixes and extensions on 6 | # compilation and linking, and just generally Does Not Work. This makes 7 | # supporting Yescrypt on multiple compilers and multiple platforms for CFFI's 8 | # API mode a non-starter. Instead we use a simple makefile and load the 9 | # generated binary in ABI mode. 10 | import json 11 | import secrets 12 | from base64 import b64decode, b64encode 13 | from enum import Enum 14 | from json import JSONDecodeError 15 | from pathlib import Path 16 | from typing import Any, Optional, cast 17 | 18 | from cffi import FFI # type: ignore 19 | 20 | ffi = FFI() 21 | 22 | # Refer to yescrypt.h for details and private defines. 23 | # TODO: PARAMETERS are a compile-time decision. Using values other than those in 24 | # YESCRYPT_DEFAULTS below will error out in yescrypt_kdf(), unless the C source 25 | # values for Swidth, PWXsimple, and PWXgather are modified -- but the latter two 26 | # aren't currently set up to be editable, and thus even the S-box size (Sbytes) 27 | # is pinned. 28 | # Explicit values: 29 | # https://github.com/openwall/yescrypt/blob/03d4b65753e2b5568c93eec4fbf6f52b4ceefc40/yescrypt-opt.c#L403 30 | # Derived S-box value: 31 | # https://github.com/openwall/yescrypt/blob/03d4b65753e2b5568c93eec4fbf6f52b4ceefc40/yescrypt-opt.c#L412 32 | YESCRYPT_WORM = 1 33 | YESCRYPT_RW = 0x002 34 | YESCRYPT_ROUNDS_3 = 0x000 35 | YESCRYPT_ROUNDS_6 = 0x004 36 | YESCRYPT_GATHER_1 = 0x000 37 | YESCRYPT_GATHER_2 = 0x008 38 | YESCRYPT_GATHER_4 = 0x010 39 | YESCRYPT_GATHER_8 = 0x018 40 | YESCRYPT_SIMPLE_1 = 0x000 41 | YESCRYPT_SIMPLE_2 = 0x020 42 | YESCRYPT_SIMPLE_4 = 0x040 43 | YESCRYPT_SIMPLE_8 = 0x060 44 | YESCRYPT_SBOX_6K = 0x000 45 | YESCRYPT_SBOX_12K = 0x080 46 | YESCRYPT_SBOX_24K = 0x100 47 | YESCRYPT_SBOX_48K = 0x180 48 | YESCRYPT_SBOX_96K = 0x200 49 | YESCRYPT_SBOX_192K = 0x280 50 | YESCRYPT_SBOX_384K = 0x300 51 | YESCRYPT_SBOX_768K = 0x380 52 | # Only valid for yescrypt_init_shared() 53 | YESCRYPT_SHARED_PREALLOCATED = 0x10000 54 | 55 | YESCRYPT_RW_DEFAULTS = ( 56 | YESCRYPT_RW 57 | | YESCRYPT_ROUNDS_6 58 | | YESCRYPT_GATHER_4 59 | | YESCRYPT_SIMPLE_2 60 | | YESCRYPT_SBOX_12K 61 | ) 62 | 63 | YESCRYPT_DEFAULTS = YESCRYPT_RW_DEFAULTS 64 | 65 | ffi.cdef( 66 | """ 67 | typedef uint32_t yescrypt_flags_t; 68 | 69 | typedef struct { 70 | void *base, *aligned; 71 | size_t base_size, aligned_size; 72 | } yescrypt_region_t; 73 | 74 | typedef yescrypt_region_t yescrypt_shared_t; 75 | typedef yescrypt_region_t yescrypt_local_t; 76 | 77 | /** 78 | * yescrypt parameters combined into one struct. N, r, p are the same as in 79 | * classic scrypt, except that the meaning of p changes when YESCRYPT_RW is 80 | * set. flags, t, g, NROM are special to yescrypt. 81 | */ 82 | typedef struct { 83 | yescrypt_flags_t flags; 84 | uint64_t N; 85 | uint32_t r, p, t, g; 86 | uint64_t NROM; 87 | } yescrypt_params_t; 88 | 89 | typedef union { 90 | unsigned char uc[32]; 91 | uint64_t u64[4]; 92 | } yescrypt_binary_t; 93 | 94 | int yescrypt_init_local(yescrypt_local_t *local); 95 | int yescrypt_free_local(yescrypt_local_t *local); 96 | int yescrypt_init_shared(yescrypt_shared_t *shared, 97 | const uint8_t *seed, size_t seedlen, const yescrypt_params_t *params); 98 | int yescrypt_free_shared(yescrypt_shared_t *shared); 99 | 100 | uint8_t *yescrypt_encode_params(const yescrypt_params_t *params, 101 | const uint8_t *src, size_t srclen); 102 | 103 | int yescrypt_kdf(const yescrypt_shared_t *shared, 104 | yescrypt_local_t *local, 105 | const uint8_t *passwd, size_t passwdlen, 106 | const uint8_t *salt, size_t saltlen, 107 | const yescrypt_params_t *params, 108 | uint8_t *buf, size_t buflen); 109 | 110 | uint8_t *yescrypt_r(const yescrypt_shared_t *shared, yescrypt_local_t *local, 111 | const uint8_t *passwd, size_t passwdlen, 112 | const uint8_t *setting, 113 | const yescrypt_binary_t *key, 114 | uint8_t *buf, size_t buflen); 115 | """ 116 | ) 117 | 118 | try: 119 | _LIB = ffi.dlopen(f"{Path(__file__).parent.resolve()}/yescrypt.bin") 120 | except OSError as exc: 121 | if "/opt/homebrew/opt/llvm/lib/libomp.dylib" in exc.args[0]: 122 | raise FileNotFoundError( 123 | exc.errno, 'OpenMP library not found. Please "brew install llvm".' 124 | ) 125 | 126 | 127 | class Mode(Enum): 128 | MCF = 1 129 | JSON = 2 130 | RAW = 3 131 | 132 | 133 | class WrongPassword(Exception): 134 | pass 135 | 136 | 137 | class WrongPasswordConfiguration(Exception): 138 | pass 139 | 140 | 141 | class Yescrypt: 142 | _mode: Mode 143 | 144 | # Reuse across calls. 145 | _params: Any 146 | _local_region: Any 147 | # Not implemented yet. 148 | _shared_region: Any 149 | 150 | def __init__( 151 | self, 152 | n: int = 2**16, 153 | r: int = 8, 154 | t: int = 0, 155 | p: int = 1, 156 | mode: Mode = Mode.JSON, 157 | ): 158 | """ 159 | Creates a Yescrypt hasher with settings preconfigured and memory 160 | preallocated. 161 | 162 | The hasher should be considered immutable. This allows its parameters and 163 | the local memory used for hashing to be allocated once and reused, which is 164 | important as the amount of memory used grows (allocation is slow and 165 | dominates hashing time when using GiBs of memory). 166 | 167 | Note that instances of Yescrypt aren't thread-safe externally. They're of 168 | course thread-safe internally for their own `p` value, but you can't hash 169 | with the same Yescrypt instance across multiple threads simultaneously. 170 | 171 | :param n: Block count (capital 'N' in yescrypt proper). 172 | :param r: Block size, in 128-byte units. 173 | :param t: An additional time factor. Useful for making hashing more 174 | expensive when more memory is not available. 175 | :param p: Parallelism. Unlike scrypt, threads in yescrypt don't increase 176 | memory usage. 177 | :param mode: The encoding to expect for inputs and to generate for outputs. 178 | `Mode.JSON` encodes all relevant data and is the default. `Mode.MCF` is 179 | similar but uses the Modular Crypt Format, forces hashes to be 32 bytes 180 | (length is implicit, not encoded), and limits salts to 64 bytes. `Mode.RAW` 181 | applies no special encoding and leaves everything in the user's hands. 182 | """ 183 | self._mode = mode 184 | 185 | # Yescrypt doesn't use g (hash upgrade) currently. 186 | g = 0 187 | nrom = 0 188 | self._params = ffi.new( 189 | "yescrypt_params_t*", (YESCRYPT_RW_DEFAULTS, n, r, p, t, g, nrom) 190 | ) 191 | self._local_region = ffi.new("yescrypt_local_t*") 192 | if _LIB.yescrypt_init_local(self._local_region): 193 | raise Exception("Initialization Error: yescrypt_init_local failed.") 194 | # Force OS to allocate the memory for these parameters. New parameters 195 | # should get a new Yescrypt instance. 196 | # NB: We use YESCRYPT_RW exclusively, so unlike in scrypt p doesn't 197 | # contribute to the size. 198 | # TODO: The first execution of digest() isn't any faster when we pre-init 199 | # memory like this. Need to investigate. For now we'll simply call 200 | # digest() on load (cold start, not warm-up). 201 | # from ctypes import memset 202 | # ptr = int(ffi.cast('uint64_t', self._local_region.aligned)) 203 | # memset(ptr, 0, self._local_region.aligned_size) 204 | 205 | def digest( 206 | self, 207 | password: bytes, 208 | salt: Optional[bytes] = None, 209 | settings: Optional[bytes] = None, 210 | hash_length: int = 32, 211 | ) -> bytes: 212 | """ 213 | Generates a yescrypt hash for `password` in the mode this Yescrypt instance 214 | is configured to use. 215 | 216 | Note that in `Mode.MCF` the Modular Crypt Format string contains a salt, 217 | found in `settings`, and `hash_length` is fixed at 32. 218 | 219 | :param password: A password to hash. 220 | :param salt: A salt for the hash. Required unless using `settings` with 221 | `Mode.MCF`. 222 | :param settings: An MCF-encoded paraneter string. Only used with `Mode.MCF`. 223 | :param hash_length: The desired hash length. Must be 32 when using `Mode.MCF`. 224 | :return: JSON-, MCF-, or raw-encoded hash bytes. 225 | """ 226 | if self._mode is Mode.MCF: 227 | if hash_length != 32: 228 | raise ValueError( 229 | "Argument Error: Yescrypt assumes 256-bit hashes for MCF and " 230 | "does not store length in the crypt string. The hash_length " 231 | "argument must be 32 in MCF mode." 232 | ) 233 | if not settings: 234 | if not salt: 235 | raise ValueError( 236 | "Argument Error: A salt is required if not using MCF-encoded " 237 | "settings." 238 | ) 239 | settings = _LIB.yescrypt_encode_params(self._params, salt, len(salt)) 240 | if not settings: 241 | raise Exception("Hashing Error: yescrypt_encode_params failed.") 242 | # Buffer for encoded 32-byte password and max 64-byte salt (128 bytes), 243 | # with a 'y' for yescrypt, 4 $ delimeters, up to 8 6-byte parameters, 244 | # and a null terminator. 245 | buf_length = 181 246 | with ffi.new(f"uint8_t[{buf_length}]") as hash_buffer: 247 | if not _LIB.yescrypt_r( 248 | ffi.NULL, 249 | self._local_region, 250 | password, 251 | len(password), 252 | settings, 253 | ffi.NULL, 254 | hash_buffer, 255 | buf_length, 256 | ): 257 | raise Exception("Hashing Error: yescrypt_r failed.") 258 | digest = ffi.string(hash_buffer, 10000) 259 | else: 260 | with ffi.new(f"uint8_t[{hash_length}]") as hash_buffer: 261 | if _LIB.yescrypt_kdf( 262 | ffi.NULL, 263 | self._local_region, 264 | password, 265 | len(password), 266 | salt, 267 | len(cast(bytes, salt)), 268 | self._params, 269 | hash_buffer, 270 | hash_length, 271 | ): 272 | raise Exception("Hashing Error: yescrypt_kdf failed.") 273 | digest = bytes(hash_buffer) 274 | if self._mode is Mode.JSON: 275 | digest = json.dumps( 276 | { 277 | "alg": "yescrypt", 278 | "ver": "1.1", 279 | "cfg": {k: getattr(self._params, k) for k in dir(self._params)}, 280 | "key": b64encode(digest).decode(), 281 | "slt": b64encode(cast(bytes, salt)).decode(), 282 | }, 283 | separators=(",", ":"), 284 | ).encode() 285 | 286 | return digest 287 | 288 | def compare( 289 | self, password: bytes, hashed_password: bytes, salt: Optional[bytes] = None 290 | ) -> None: 291 | """ 292 | Generates a yescrypt hash for `password`, securely compares it to an 293 | existing `hashed_password`, and raises an exception if they don't match. 294 | Mismatches between the Yescrypt instance Mode and the `hashed_password` 295 | format raise a ValueError, except in Mode.RAW, where all bets are off. 296 | 297 | In Mode.JSON the encoded arguments are checked against this Yescrypt 298 | instance and a special exception is raised if they don't match. This 299 | shortcircuits hashing of `password` of entirely. 300 | 301 | In Mode.MCF, the internal yescrypt library decodes the arguments from the 302 | MCF string and the comparison simply fails if they don't match, since a 303 | different hash will be produced. In the future this may be enhanced with 304 | a more specific exception like Mode.JSON. 305 | 306 | In Mode.RAW, all responsibility is left in the hands of the caller, 307 | including supplying the salt (the parameters are already present in the 308 | Yescrypt instance, but may not match those used for hashed_password). 309 | 310 | :param password: A password to check. 311 | :param hashed_password: A password hash to check against. 312 | :param salt: The salt used for `hashed_password`. Only required when using 313 | Mode.RAW. 314 | :raises: WrongPassword, WrongPasswordConfiguration, ValueError 315 | """ 316 | if self._mode == Mode.JSON: 317 | try: 318 | data = json.loads(hashed_password) 319 | except JSONDecodeError: 320 | if hashed_password.startswith(b"$y$"): 321 | raise ValueError( 322 | "Argument Error: MCF string passed to a JSON instance of " 323 | "Yescrypt." 324 | ) 325 | else: 326 | raise ValueError( 327 | "Argument Error: Raw (probably) data passed to a JSON " 328 | "instance of Yescrypt." 329 | ) 330 | # Make sure the parameters of this instance of Yescrypt are compatible 331 | # with those of the hashed password. 332 | cfg = data["cfg"] 333 | for k in cfg.keys(): 334 | if cfg[k] != getattr(self._params, k): 335 | raise WrongPasswordConfiguration( 336 | "Error: Password configurations are incompatible." 337 | ) 338 | salt = b64decode(data["slt"]) 339 | password_hash = self.digest( 340 | password, salt=salt, hash_length=len(b64decode(data["key"])) 341 | ) 342 | elif self._mode == Mode.MCF: 343 | if not hashed_password.startswith(b"$y$"): 344 | try: 345 | _ = json.loads(hashed_password) 346 | raise ValueError( 347 | "Argument Error: JSON passed to an MCF instance of Yescrypt." 348 | ) 349 | except ValueError: 350 | raise 351 | except Exception: 352 | raise ValueError( 353 | "Argument Error: Raw (probably) data passed to an MCF " 354 | "instance of Yescrypt." 355 | ) 356 | settings = hashed_password[: hashed_password.rfind(b"$")] 357 | 358 | # Length is always 32 in MCF mode. 359 | password_hash = self.digest(password, settings=settings, hash_length=32) 360 | else: 361 | if not salt: 362 | raise ValueError("Argument Error: A salt is required in RAW mode.") 363 | password_hash = self.digest( 364 | password, salt=salt, hash_length=len(hashed_password) 365 | ) 366 | if not secrets.compare_digest(password_hash, hashed_password): 367 | raise WrongPassword("Error: Password does not match stored hash.") 368 | 369 | def __del__(self) -> None: 370 | if hasattr(self, "_local_region"): 371 | _LIB.yescrypt_free_local(self._local_region) 372 | 373 | 374 | def main() -> None: 375 | # Example usage. 376 | import time 377 | 378 | # All default settings. 379 | hasher = Yescrypt(n=2**16, r=8, p=1, mode=Mode.JSON) 380 | for _ in range(5): 381 | password = secrets.token_bytes(32) 382 | salt = secrets.token_bytes(32) 383 | start = time.time() 384 | h = hasher.digest(password, salt) 385 | stop = time.time() - start 386 | try: 387 | hasher.compare(password, h) 388 | except WrongPasswordConfiguration: 389 | print("Passwords have different configurations.") 390 | except WrongPassword: 391 | print("Passwords don't match.") 392 | print( 393 | f"Yescrypt took {stop:.2f} seconds to generate main hash {h.decode()} and " 394 | f"used {128 * 2**16 * 8 / 1024**2:.2f} MiB memory." 395 | ) 396 | 397 | 398 | if __name__ == "__main__": 399 | main() 400 | --------------------------------------------------------------------------------