Migrate to using CFFI 1.0
* Move everything under src/ to ensure we test against the installed library. * Create a build_bcrypt.py script which will build _bcrypt.so. * Refactor to utilize the new _bcrypt.so instead of implicit compile.
This commit is contained in:
parent
f3280bda0a
commit
51ebadb34d
@ -1,2 +1,3 @@
|
||||
include LICENSE README.rst
|
||||
recursive-include bcrypt/crypt_blowfish-1.3 *
|
||||
include src/build_bcrypt.py
|
||||
recursive-include src/crypt_blowfish-1.3 *
|
||||
|
||||
@ -1,150 +0,0 @@
|
||||
# Author:: Donald Stufft (<donald@stufft.io>)
|
||||
# Copyright:: Copyright (c) 2013 Donald Stufft
|
||||
# License:: Apache License, Version 2.0
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
|
||||
import binascii
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from cffi import FFI
|
||||
from cffi.verifier import Verifier
|
||||
|
||||
import six
|
||||
|
||||
from .__about__ import (
|
||||
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
||||
__uri__, __version__,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"__title__", "__summary__", "__uri__", "__version__", "__author__",
|
||||
"__email__", "__license__", "__copyright__",
|
||||
"gensalt", "hashpw",
|
||||
]
|
||||
|
||||
|
||||
def _create_modulename(cdef_sources, source, sys_version):
|
||||
"""
|
||||
This is the same as CFFI's create modulename except we don't include the
|
||||
CFFI version.
|
||||
"""
|
||||
key = '\x00'.join([sys_version[:3], source, cdef_sources])
|
||||
key = key.encode('utf-8')
|
||||
k1 = hex(binascii.crc32(key[0::2]) & 0xffffffff)
|
||||
k1 = k1.lstrip('0x').rstrip('L')
|
||||
k2 = hex(binascii.crc32(key[1::2]) & 0xffffffff)
|
||||
k2 = k2.lstrip('0').rstrip('L')
|
||||
return '_bcrypt_cffi_{0}{1}'.format(k1, k2)
|
||||
|
||||
|
||||
def _compile_module(*args, **kwargs):
|
||||
raise RuntimeError(
|
||||
"Attempted implicit compile of a cffi module. All cffi modules should "
|
||||
"be pre-compiled at installation time."
|
||||
)
|
||||
|
||||
|
||||
class LazyLibrary(object):
|
||||
def __init__(self, ffi):
|
||||
self._ffi = ffi
|
||||
self._lib = None
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def __getattr__(self, name):
|
||||
if self._lib is None:
|
||||
with self._lock:
|
||||
# We no cover this because this guard is here just to protect
|
||||
# against concurrent loads of this library. This is pretty
|
||||
# hard to test and the logic is simple and should hold fine
|
||||
# without testing (famous last words).
|
||||
if self._lib is None: # pragma: no cover
|
||||
self._lib = self._ffi.verifier.load_library()
|
||||
|
||||
return getattr(self._lib, name)
|
||||
|
||||
|
||||
_crypt_blowfish_dir = "crypt_blowfish-1.3"
|
||||
_bundled_dir = os.path.join(os.path.dirname(__file__), _crypt_blowfish_dir)
|
||||
|
||||
|
||||
CDEF = """
|
||||
char *crypt_gensalt_rn(const char *prefix, unsigned long count,
|
||||
const char *input, int size, char *output, int output_size);
|
||||
|
||||
char *crypt_rn(const char *key, const char *setting, void *data, int size);
|
||||
"""
|
||||
|
||||
SOURCE = """
|
||||
#include "ow-crypt.h"
|
||||
"""
|
||||
|
||||
_ffi = FFI()
|
||||
_ffi.cdef(CDEF)
|
||||
_ffi.verifier = Verifier(
|
||||
_ffi,
|
||||
SOURCE,
|
||||
sources=[
|
||||
str(os.path.join(_bundled_dir, "crypt_blowfish.c")),
|
||||
str(os.path.join(_bundled_dir, "crypt_gensalt.c")),
|
||||
str(os.path.join(_bundled_dir, "wrapper.c")),
|
||||
# How can we get distutils to work with a .S file?
|
||||
# Set bcrypt/crypt_blowfish-1.3/crypt_blowfish.c#57 back to 1 if we
|
||||
# get ASM loaded.
|
||||
# str(os.path.join(_bundled_dir, "x86.S")),
|
||||
],
|
||||
include_dirs=[str(_bundled_dir)],
|
||||
modulename=_create_modulename(CDEF, SOURCE, sys.version),
|
||||
)
|
||||
|
||||
# Patch the Verifier() instance to prevent CFFI from compiling the module
|
||||
_ffi.verifier.compile_module = _compile_module
|
||||
_ffi.verifier._compile_module = _compile_module
|
||||
|
||||
|
||||
_bcrypt_lib = LazyLibrary(_ffi)
|
||||
|
||||
|
||||
def gensalt(rounds=12):
|
||||
salt = os.urandom(16)
|
||||
output = _ffi.new("unsigned char[]", 30)
|
||||
|
||||
retval = _bcrypt_lib.crypt_gensalt_rn(
|
||||
b"$2a$", rounds, salt, len(salt), output, len(output),
|
||||
)
|
||||
|
||||
if not retval:
|
||||
raise ValueError("Invalid rounds")
|
||||
|
||||
return _ffi.string(output)
|
||||
|
||||
|
||||
def hashpw(password, salt):
|
||||
if isinstance(password, six.text_type) or isinstance(salt, six.text_type):
|
||||
raise TypeError("Unicode-objects must be encoded before hashing")
|
||||
|
||||
if b"\x00" in password:
|
||||
raise ValueError("password may not contain NUL bytes")
|
||||
|
||||
hashed = _ffi.new("unsigned char[]", 128)
|
||||
retval = _bcrypt_lib.crypt_rn(password, salt, hashed, len(hashed))
|
||||
|
||||
if not retval:
|
||||
raise ValueError("Invalid salt")
|
||||
|
||||
return _ffi.string(hashed)
|
||||
55
setup.py
55
setup.py
@ -7,48 +7,21 @@ from setuptools.command.install import install
|
||||
from setuptools.command.test import test
|
||||
|
||||
|
||||
CFFI_DEPENDENCY = "cffi>=0.8"
|
||||
CFFI_DEPENDENCY = "cffi>=1.1"
|
||||
SIX_DEPENDENCY = "six>=1.4.1"
|
||||
|
||||
|
||||
CFFI_MODULES = [
|
||||
"src/build_bcrypt.py:ffi",
|
||||
]
|
||||
|
||||
|
||||
# Manually extract the __about__
|
||||
__about__ = {}
|
||||
with open("bcrypt/__about__.py") as fp:
|
||||
with open("src/bcrypt/__about__.py") as fp:
|
||||
exec(fp.read(), __about__)
|
||||
|
||||
|
||||
def get_ext_modules():
|
||||
from bcrypt import _ffi
|
||||
return [_ffi.verifier.get_extension()]
|
||||
|
||||
|
||||
class CFFIBuild(build):
|
||||
"""
|
||||
This class exists, instead of just providing ``ext_modules=[...]`` directly
|
||||
in ``setup()`` because importing cryptography requires we have several
|
||||
packages installed first.
|
||||
|
||||
By doing the imports here we ensure that packages listed in
|
||||
``setup_requires`` are already installed.
|
||||
"""
|
||||
|
||||
def finalize_options(self):
|
||||
self.distribution.ext_modules = get_ext_modules()
|
||||
build.finalize_options(self)
|
||||
|
||||
|
||||
class CFFIInstall(install):
|
||||
"""
|
||||
As a consequence of CFFIBuild and it's late addition of ext_modules, we
|
||||
need the equivalent for the ``install`` command to install into platlib
|
||||
install-dir rather than purelib.
|
||||
"""
|
||||
|
||||
def finalize_options(self):
|
||||
self.distribution.ext_modules = get_ext_modules()
|
||||
install.finalize_options(self)
|
||||
|
||||
|
||||
class PyTest(test):
|
||||
def finalize_options(self):
|
||||
test.finalize_options(self)
|
||||
@ -159,12 +132,11 @@ def keywords_with_side_effects(argv):
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"setup_requires": [CFFI_DEPENDENCY, SIX_DEPENDENCY],
|
||||
"setup_requires": [CFFI_DEPENDENCY],
|
||||
"cmdclass": {
|
||||
"build": CFFIBuild,
|
||||
"install": CFFIInstall,
|
||||
"test": PyTest,
|
||||
}
|
||||
},
|
||||
"cffi_modules": CFFI_MODULES,
|
||||
}
|
||||
|
||||
|
||||
@ -231,14 +203,11 @@ setup(
|
||||
"pytest",
|
||||
],
|
||||
|
||||
package_dir={"": "src"},
|
||||
packages=[
|
||||
"bcrypt",
|
||||
],
|
||||
|
||||
package_data={
|
||||
"bcrypt": ["crypt_blowfish-1.3/*"],
|
||||
},
|
||||
|
||||
zip_safe=False,
|
||||
|
||||
classifiers=[
|
||||
@ -252,5 +221,7 @@ setup(
|
||||
"Programming Language :: Python :: 3.3",
|
||||
],
|
||||
|
||||
ext_package="bcrypt",
|
||||
|
||||
**keywords_with_side_effects(sys.argv)
|
||||
)
|
||||
|
||||
65
src/bcrypt/__init__.py
Normal file
65
src/bcrypt/__init__.py
Normal file
@ -0,0 +1,65 @@
|
||||
# Author:: Donald Stufft (<donald@stufft.io>)
|
||||
# Copyright:: Copyright (c) 2013 Donald Stufft
|
||||
# License:: Apache License, Version 2.0
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
|
||||
import os
|
||||
|
||||
import six
|
||||
|
||||
from bcrypt import _bcrypt
|
||||
|
||||
from .__about__ import (
|
||||
__author__, __copyright__, __email__, __license__, __summary__, __title__,
|
||||
__uri__, __version__,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"__title__", "__summary__", "__uri__", "__version__", "__author__",
|
||||
"__email__", "__license__", "__copyright__",
|
||||
"gensalt", "hashpw",
|
||||
]
|
||||
|
||||
|
||||
def gensalt(rounds=12):
|
||||
salt = os.urandom(16)
|
||||
output = _bcrypt.ffi.new("unsigned char[]", 30)
|
||||
|
||||
retval = _bcrypt.lib.crypt_gensalt_rn(
|
||||
b"$2a$", rounds, salt, len(salt), output, len(output),
|
||||
)
|
||||
|
||||
if not retval:
|
||||
raise ValueError("Invalid rounds")
|
||||
|
||||
return _bcrypt.ffi.string(output)
|
||||
|
||||
|
||||
def hashpw(password, salt):
|
||||
if isinstance(password, six.text_type) or isinstance(salt, six.text_type):
|
||||
raise TypeError("Unicode-objects must be encoded before hashing")
|
||||
|
||||
if b"\x00" in password:
|
||||
raise ValueError("password may not contain NUL bytes")
|
||||
|
||||
hashed = _bcrypt.ffi.new("unsigned char[]", 128)
|
||||
retval = _bcrypt.lib.crypt_rn(password, salt, hashed, len(hashed))
|
||||
|
||||
if not retval:
|
||||
raise ValueError("Invalid salt")
|
||||
|
||||
return _bcrypt.ffi.string(hashed)
|
||||
47
src/build_bcrypt.py
Normal file
47
src/build_bcrypt.py
Normal file
@ -0,0 +1,47 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os.path
|
||||
|
||||
from cffi import FFI
|
||||
|
||||
|
||||
BLOWFISH_DIR = os.path.join(os.path.dirname(__file__), "crypt_blowfish-1.3")
|
||||
|
||||
|
||||
ffi = FFI()
|
||||
|
||||
ffi.cdef(
|
||||
"""
|
||||
char *crypt_gensalt_rn(const char *prefix, unsigned long count,
|
||||
const char *input, int size, char *output, int output_size);
|
||||
|
||||
char *crypt_rn(const char *key, const char *setting, void *data, int size);
|
||||
"""
|
||||
)
|
||||
|
||||
ffi.set_source(
|
||||
"_bcrypt",
|
||||
"""
|
||||
#include "ow-crypt.h"
|
||||
""",
|
||||
sources=[
|
||||
os.path.join(BLOWFISH_DIR, "crypt_blowfish.c"),
|
||||
os.path.join(BLOWFISH_DIR, "crypt_gensalt.c"),
|
||||
os.path.join(BLOWFISH_DIR, "wrapper.c"),
|
||||
# How can we get distutils to work with a .S file?
|
||||
# Set bcrypt/crypt_blowfish-1.3/crypt_blowfish.c#57 back to 1 if we
|
||||
# get ASM loaded.
|
||||
# os.path.join(BLOWFISH_DIR, "x86.S"),
|
||||
],
|
||||
include_dirs=[BLOWFISH_DIR],
|
||||
)
|
||||
@ -7,11 +7,6 @@ import six
|
||||
import bcrypt
|
||||
|
||||
|
||||
def test_raise_implicit_compile():
|
||||
with pytest.raises(RuntimeError):
|
||||
bcrypt._compile_module()
|
||||
|
||||
|
||||
def test_gensalt_basic(monkeypatch):
|
||||
monkeypatch.setattr(os, "urandom", lambda n: b"0000000000000000")
|
||||
assert bcrypt.gensalt() == b"$2a$12$KB.uKB.uKB.uKB.uKB.uK."
|
||||
|
||||
Loading…
Reference in New Issue
Block a user