# -*- coding: utf-8 -*-
"""Password hashing functions and constants for the mudpy engine."""
-# Copyright (c) 2004-2012 Jeremy Stanley <fungi@yuggoth.org>. Permission
+# Copyright (c) 2004-2014 Jeremy Stanley <fungi@yuggoth.org>. Permission
# to use, copy, modify, and distribute this software is granted under
# terms provided in the LICENSE file distributed with this software.
+import base64
+import hashlib
+import math
+import random
+import re
+import struct
+
# convenience constants for indexing the supported hashing algorithms,
# guaranteed a stable part of the interface
MD5 = 0 # hashlib.md5
def _pack_bytes(numbers):
- """
+ """Make a packed byte sequence:
+
This is a wrapper around struct.pack, used to turn a list of integers
between 0 and 255 into a packed sequence akin to a C-style string.
"""
- import struct
- # this will need to be declared as b"" during 2to3 migration
- packed = ""
+ packed = b""
for number in numbers:
number = int(number)
assert 0 <= number <= 255
- # need to use b"B" during 2to3 migration
packed += struct.pack("B", number)
return packed
def _bytes_to_text(byte_sequence):
- """
+ """Generate printable representation of 8-bit data:
+
This is a wrapper around base64.b64encode with preferences
appropriate for encoding Unix-style passwd hash strings.
"""
- import base64
return base64.b64encode(
byte_sequence,
- "./".encode("ascii")
- ).rstrip("=")
+ b"./"
+ ).decode("ascii").rstrip("=")
def _generate_salt(salt_len=2):
- """
+ """Generate salt for a password hash:
+
This simply generates a sequence of pseudo-random characters (with
6-bits of effective entropy per character). Since it relies on base64
encoding (which operates on 6-bit chunks of data), we only generate
need and discard any excess characters over the specified length.
This ensures full distribution over each character of the salt.
"""
- import math
- import random
salt = []
- for i in xrange(int(math.ceil(salt_len * 0.75))):
+ for i in range(int(math.ceil(salt_len * 0.75))):
salt.append(random.randint(0, 255))
return _bytes_to_text(_pack_bytes(salt))[:salt_len]
def upgrade_legacy_hash(legacy_hash, salt, sep="$"):
- """
+ """Upgrade an older password hash:
+
This utility function is meant to provide a migration path for users
of mudpy's legacy account-name-salted MD5 hexdigest password hashes.
By passing the old passhash (as legacy_hash) and name (as salt)
facets to this function, a conforming new-style password hash will be
returned.
"""
- import re
assert re.match("^[0-9a-f]{32}$",
legacy_hash), "Not a valid MD5 hexdigest"
- # this needs to be declared as b"" in 2to3
- collapsed = ""
- for i in xrange(16):
+ collapsed = b""
+ for i in range(16):
# this needs to become a byte() call in 2to3
- collapsed += chr(int(legacy_hash[2 * i:2 * i + 2], 16))
+ collapsed += bytes(legacy_hash[2 * i:2 * i + 2].decode("ascii"))
return "%s%s%s%s%s%s%s%s" % (
sep,
MD5,
salt_len=2,
sep="$"
):
- """
+ """Generate a password hash:
+
The meat of the module, this function takes a provided password and
generates a Unix-like passwd hash suitable for storage in portable,
text-based data files. The password is prepended with a salt (which
create(password, algorithm=SHA256, rounds=12, salt_len=16)
"""
- import hashlib
# if a specific salt wasn't specified, we need to generate one
if not salt:
# iterate the hashing algorithm over its own digest the specified
# number of times
- for i in xrange(2 ** rounds):
- hashed = algorithms[algorithm](hashed).digest()
+ for i in range(2 ** rounds):
+ hashed = algorithms[algorithm](hashed.encode("utf-8")).digest()
+ hashed = "".join(format(x, "02x") for x in bytes(hashed))
# concatenate the output fields, coercing into text form as needed
return "%s%s%s%s%s%s%s%s" % (
- sep, algorithm, sep, rounds, sep, salt, sep, _bytes_to_text(hashed)
+ sep, algorithm, sep, rounds, sep, salt, sep,
+ _bytes_to_text(hashed.encode("ascii"))
)
def verify(password, encoded_hash):
- """
+ """Verify a password:
+
This simple function requires a text password and a mudpy-format
password hash (as generated by the create function). It returns True
if the password, hashed with the parameters from the encoded_hash,
comes out the same as the encoded_hash.
"""
sep = encoded_hash[0]
- import mudpy.misc
algorithm, rounds, salt, hashed = encoded_hash.split(sep)[1:]
if encoded_hash == create(
password=password,