Merge pull request #43 from dstufft/cffi1.0

Migrate to using CFFI 1.0
This commit is contained in:
Paul Kehrer 2015-06-11 22:53:44 -05:00
commit fe0835805f
26 changed files with 157 additions and 200 deletions

View File

@ -20,9 +20,9 @@ env:
- TOXENV=pep8
- TOXENV=py3pep8
install: pip install tox
install: .travis/install.sh
script: tox
script: .travis/run.sh
branches:
only:

15
.travis/install.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/bash
set -e
set -x
if [[ "${TOXENV}" == "pypy" ]]; then
git clone https://github.com/yyuu/pyenv.git ~/.pyenv
PYENV_ROOT="$HOME/.pyenv"
PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
pyenv install pypy-2.6.0
pyenv global pypy-2.6.0
fi
pip install tox

13
.travis/run.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
set -e
set -x
if [[ "${TOXENV}" == "pypy" ]]; then
PYENV_ROOT="$HOME/.pyenv"
PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
pyenv global pypy-2.6.0
fi
tox

View File

@ -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 *

View File

@ -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)

View File

@ -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
View 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
View 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],
)

View File

@ -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."