Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop support for Python 2.7 and 3.5. #6

Closed
wants to merge 11 commits into from
12 changes: 3 additions & 9 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
# custom
language: python
sudo: false
python:
- 2.7
icemac marked this conversation as resolved.
Show resolved Hide resolved
- 3.5
- 3.6
- pypy
- 3.7
- 3.8
- 3.9-dev
- pypy3
matrix:
include:
- python: "3.7"
dist: xenial
sudo: true
install:
- pip install coverage coveralls
- pip install -e .[test,bcrypt]
Expand Down
6 changes: 4 additions & 2 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
Changelog
=========

4.2 (unreleased)
5.0 (unreleased)
----------------

- Nothing changed yet.
- Add support for Python 3.8 and 3.9.

- Drop support for Python 2 and 3.5.


4.1 (2018-10-30)
Expand Down
6 changes: 4 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

setup(
name='AuthEncoding',
version='4.2.dev0',
version='5.0.dev0',
url='https://github.com/zopefoundation/AuthEncoding',
license='ZPL 2.1',
description="Framework for handling LDAP style password hashes.",
Expand All @@ -36,13 +36,15 @@
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
icemac marked this conversation as resolved.
Show resolved Hide resolved
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP", # noqa: E501
],
python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*',
icemac marked this conversation as resolved.
Show resolved Hide resolved
install_requires=[
'six',
],
icemac marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
90 changes: 50 additions & 40 deletions src/AuthEncoding/AuthEncoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@
##############################################################################

import binascii
import six
from binascii import b2a_base64, a2b_base64
from hashlib import sha1 as sha
from hashlib import sha256
from os import getpid
import time
from .compat import long, b, u


# Use the system PRNG if possible
Expand All @@ -39,7 +37,7 @@ def _reseed():
# properties of the chosen random sequence slightly, but this
# is better than absolute predictability.
random.seed(sha256(
"%s%s%s" % (random.getstate(), time.time(), getpid())
"{}{}{}".format(random.getstate(), time.time(), getpid())
icemac marked this conversation as resolved.
Show resolved Hide resolved
).digest())


Expand All @@ -53,6 +51,20 @@ def _randrange(r):
return random.randrange(r)


def binary(arg):
"""Convert `arg` to latin-1 encoded bytes."""
if not isinstance(arg, bytes):
arg = arg.encode("latin-1")
return arg


def text(arg):
"""Convert `arg` to text assuming it to be latin-1 encoded."""
if isinstance(arg, bytes):
arg = arg.decode('ascii', 'replace')
return arg


def constant_time_compare(val1, val2):
"""
Returns True if the two strings are equal, False otherwise.
Expand All @@ -62,7 +74,7 @@ def constant_time_compare(val1, val2):
if len(val1) != len(val2):
return False
result = 0
for x, y in zip(six.iterbytes(val1), six.iterbytes(val2)):
for x, y in zip(iter(val1), iter(val2)):
icemac marked this conversation as resolved.
Show resolved Hide resolved
result |= x ^ y
return result == 0

Expand All @@ -89,7 +101,7 @@ def registerScheme(id, s):
'''
Registers an LDAP password encoding scheme.
'''
_schemes.append((id, u'{%s}' % id, s))
_schemes.append((id, '{%s}' % id, s))


def listSchemes():
Expand All @@ -110,7 +122,7 @@ def generate_salt(self):
# All 256 characters are available.
salt = b''
for n in range(7):
salt += six.int2byte(_randrange(256))
salt += bytes((_randrange(256),))
return salt
icemac marked this conversation as resolved.
Show resolved Hide resolved

def encrypt(self, pw):
Expand All @@ -127,11 +139,11 @@ def validate(self, reference, attempt):
return constant_time_compare(compare, reference)

def _encrypt_with_salt(self, pw, salt):
pw = b(pw)
pw = binary(pw)
return b2a_base64(sha(pw + salt).digest() + salt)[:-1]


registerScheme(u'SSHA', SSHADigestScheme())
registerScheme('SSHA', SSHADigestScheme())


class SHADigestScheme:
Expand All @@ -144,24 +156,24 @@ def validate(self, reference, attempt):
return constant_time_compare(compare, reference)

def _encrypt(self, pw):
pw = b(pw)
pw = binary(pw)
return b2a_base64(sha(pw).digest())[:-1]


registerScheme(u'SHA', SHADigestScheme())
registerScheme('SHA', SHADigestScheme())


class SHA256DigestScheme:

def encrypt(self, pw):
return b(sha256(b(pw)).hexdigest())
return binary(sha256(binary(pw)).hexdigest())

def validate(self, reference, attempt):
a = self.encrypt(attempt)
return constant_time_compare(a, reference)


registerScheme(u'SHA256', SHA256DigestScheme())
registerScheme('SHA256', SHA256DigestScheme())


# Bcrypt support may not have been requested at installation time
Expand All @@ -178,7 +190,7 @@ class BCRYPTHashingScheme:
@staticmethod
def _ensure_bytes(pw, encoding='utf-8'):
"""Ensures the given password `pw` is returned as bytes."""
if isinstance(pw, six.text_type):
if isinstance(pw, str):
pw = pw.encode(encoding)
return pw

Expand All @@ -194,7 +206,7 @@ def validate(self, reference, attempt):


if bcrypt is not None:
registerScheme(u'BCRYPT', BCRYPTHashingScheme())
registerScheme('BCRYPT', BCRYPTHashingScheme())


# Bogosity on various platforms due to ITAR restrictions
Expand All @@ -208,83 +220,81 @@ def validate(self, reference, attempt):
class CryptDigestScheme:

def generate_salt(self):
choices = (u"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
u"abcdefghijklmnopqrstuvwxyz"
u"0123456789./")
choices = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789./")
return _choice(choices) + _choice(choices)

def encrypt(self, pw):
return b(crypt(self._recode_password(pw), self.generate_salt()))
return binary(
crypt(self._recode_password(pw), self.generate_salt()))

def validate(self, reference, attempt):
attempt = self._recode_password(attempt)
a = b(crypt(attempt, reference[:2].decode('ascii')))
a = binary(crypt(attempt, reference[:2].decode('ascii')))
return constant_time_compare(a, reference)

def _recode_password(self, pw):
# crypt always requires `str` which has a different meaning among
# the Python versions:
if six.PY3:
return u(pw)
return b(pw)
# crypt requires `str`:
return text(pw)

registerScheme(u'CRYPT', CryptDigestScheme())
registerScheme('CRYPT', CryptDigestScheme())


class MySQLDigestScheme:

def encrypt(self, pw):
pw = u(pw)
nr = long(1345345333)
pw = text(pw)
nr = int(1345345333)
add = 7
nr2 = long(0x12345671)
nr2 = int(0x12345671)
for i in pw:
if i == ' ' or i == '\t':
continue
nr ^= (((nr & 63) + add) * ord(i)) + (nr << 8)
nr2 += (nr2 << 8) ^ nr
add += ord(i)
r0 = nr & ((long(1) << 31) - long(1))
r1 = nr2 & ((long(1) << 31) - long(1))
return (u"%08lx%08lx" % (r0, r1)).encode('ascii')
r0 = nr & ((int(1) << 31) - int(1))
r1 = nr2 & ((int(1) << 31) - int(1))
return ("{:08x}{:08x}".format(r0, r1)).encode('ascii')

def validate(self, reference, attempt):
a = self.encrypt(attempt)
return constant_time_compare(a, reference)


registerScheme(u'MYSQL', MySQLDigestScheme())
registerScheme('MYSQL', MySQLDigestScheme())


def pw_validate(reference, attempt):
"""Validate the provided password string, which uses LDAP-style encoding
notation. Reference is the correct password, attempt is clear text
password attempt."""
reference = b(reference)
reference = binary(reference)
for id, prefix, scheme in _schemes:
lp = len(prefix)
if reference[:lp] == b(prefix):
if reference[:lp] == binary(prefix):
return scheme.validate(reference[lp:], attempt)
# Assume cleartext.
return constant_time_compare(reference, b(attempt))
return constant_time_compare(reference, binary(attempt))


def is_encrypted(pw):
pw = b(pw)
pw = binary(pw)
for id, prefix, scheme in _schemes:
lp = len(prefix)
if pw[:lp] == b(prefix):
if pw[:lp] == binary(prefix):
return 1
return 0


def pw_encrypt(pw, encoding=u'SSHA'):
def pw_encrypt(pw, encoding='SSHA'):
"""Encrypt the provided plain text password using the encoding if provided
and return it in an LDAP-style representation."""
encoding = u(encoding)
encoding = text(encoding)
for id, prefix, scheme in _schemes:
if encoding == id:
return b(prefix) + scheme.encrypt(pw)
return binary(prefix) + scheme.encrypt(pw)
raise ValueError('Not supported: %s' % encoding)


Expand Down
20 changes: 0 additions & 20 deletions src/AuthEncoding/compat.py

This file was deleted.