#! /usr/bin/python # hashpw -- prompts user for a password and prints the hash # # Version: 2.1 # Copyright: (C) 2013 Alastair Irvine # Keywords: security passwd crypt # Licence: This file is released under the GNU General Public License # usage = """Usage: hashpw [ -c | -C | -m | -a | -A | -b | -2 [ -l ] | -5 [ -l ] | -S | -p | -M ] [ | -v [ -q ] ] -l Force a salt of length 16 to be used with SHA-256 or SHA-512 -e Also prefix the hash with the scheme prefix used by "doveadm pw" -v Verify instead of printing a hash -q Don't print verification result (exit codes only; 0 = suceeded, 2 = failed) Algorithm options: -m MD5 (default) -c crypt (DES), with a two character salt -x Extended DES, with a nine character salt (FreeBSD 4.x and NetBSD only) -b blowfish (OpenBSD only) -a Apache MD5* -A Apache SHA-1 (RFC 2307; can be used by OpenLDAP) (does not use a salt; INSECURE!!) -2 SHA-256 -5 SHA-512 (Linux standard password hashing method) -S SSHA (used by OpenLDAP) -o MySQL OLD_PASSWORD() custom algorithm*** (does not use a salt; INSECURE!!) -p MySQL v4.1+ PASSWORD() double SHA-1 (does not use a salt; INSECURE!!) -M MySQL MD5() -- just hex encoding (does not use a salt; INSECURE!!) -P phpass** (Portable PHP password hashing framework), as used by WordPress -B phpBB3: Same as -P except the hash starts with "$H$" instead of "$P$" -C CRAM-MD5 (does not use a salt; INSECURE!!) -D DIGEST-MD5 (requires username) -s SCRAM-SHA-1 (RFC 5802; see https://en.wikipedia.org/wiki/Salted_Challenge_Response_Authentication_Mechanism) * requires 'pyapache' from https://github.com/mcrute/pyapache ** requires 'phpass' from https://github.com/exavolt/python-phpass *** requires the script from http://djangosnippets.org/snippets/1508""" # # Requires Python 2.4 for random.SystemRandom # Unless using Python 2.5 or later, requires http://pypi.python.org/pypi/hashlib (built into Python 2.5) # # See http://forum.insidepro.com/viewtopic.php?t=8225 for more algorithms # # # Licence details: # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or (at # your option) any later version. # # See http://www.gnu.org/licenses/gpl-2.0.html for more information. # # You can find the complete text of the GPLv2 in the file # /usr/share/common-licenses/GPL-2 on Debian systems. # Or see the file COPYING in the same directory as this program. # # # TO-DO: # + if the prefix matches but the salt is too short, report an error # + other options for bcrypt (Phpass) using one of the following: # - http://www.mindrot.org/projects/py-bcrypt/ # - http://packages.python.org/passlib/ # + support Drupal 7's SHA-512-based secure hash (hash type identifier = "$S$") # + support Password-Based Key Derivation Function 2 # - http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf # - $pdkdf2$ (SHA-1) # - $pdkdf2-sha256$ (SHA-256) # - $pdkdf2-sha512$ (SHA-512) # + support phpBB3: copy -P (hash type identifier = "$H$") # + handle http://en.wikipedia.org/wiki/Crypt_(Unix)#Blowfish-based_scheme # + convert to a module # + for Blowfish, recognise "$2y$" and provide an option to use it. # - see CRYPT_BLOWFISH comments at http://www.php.net/manual/en/function.crypt.php # + support scrypt # + option to generate/recognise simple hashes (e.g. BasicMD5, OldPassword) with prefixes # + accept password on standard input (without confirmation) # + activate settings['long_salt'] if a long salt (or hash with long salt) is provided on the command line # + implement -C # + implement -D # + implement -e # + support "doveadm pw" encoding scheme suffixes (.b64, .base64 and .hex); see # http://wiki2.dovecot.org/Authentication/PasswordSchemes # + support Argon2i password hashing algorithm: https://wiki.php.net/rfc/argon2_password_hash import sys import base64 import binascii import random import getpass import struct import crypt import getopt import hashlib program_name = "hashpw" DEFAULT_MODE = "md5" # == general-purpose functions == def barf(msg, exitstatus): "Shows an error message to stderr and exits with a given value" print >> sys.stderr, program_name + ":", msg sys.exit(exitstatus) def help(): print usage # *** Processing code *** class ShortSaltException(Exception): def __init__(self, msg="salt is too short"): Exception.__init__(self, msg) class SaltPrefixException(Exception): pass class BadAlgException(Exception): pass class Algorithm(object): supports_salt = False @staticmethod def init(c): pass @staticmethod def final_prep(c): """Called by the constructor, i.e. only if the algorithm class is actually going to be used. Initialises things in the class that are used by various static helper methods. Designed to be overridden. Subclasses should call this method on their superclass, but beware that if that superclass inherits final_prep(), its class object is still where attributes will be set.""" ## print "Algorithm.final_prep()..." pass def __init__(self): self.final_prep(self.__class__) @staticmethod def recognise_full(c, s): """Returns whether or not @p s matches the encoding format of algorithm @p c""" return len(s) >= c.min_length and s[:len(c.prefix)] == c.prefix def hash(self, plaintext): """Returns an encoded hash""" class SaltedAlgorithm(Algorithm): """Stores a salt, which includes the prefix.""" supports_salt = True r = random.SystemRandom() @staticmethod def init(c): c.comp_len = len(c.prefix) + c.salt_length + len(c.suffix) def __init__(self, salt): # Note that unlike SaltedAlgorithm, Algorithm's constructor doesn't take # an argument super(SaltedAlgorithm,self).__init__() if salt: self.salt = self.extract_salt(self, salt) else: self.salt = self.generate_salt(self) @staticmethod def recognise_full(c, s): """Returns whether or not @p s matches this algorithm's encoding format""" return len(s) >= c.min_length and c.recognise_salt_internal(c, s) @staticmethod def recognise_salt_internal(c, s): """Returns whether or not @p s matches the leading part of this algorithm's encoding format""" return s[:len(c.prefix)] == c.prefix @staticmethod def recognise_salt(c, s): """Returns whether or not @p s matches the leading part of this algorithm's encoding format and is long enough to contain a salt.""" return s[:len(c.prefix)] == c.prefix and len(s) >= c.comp_len @staticmethod def generate_salt(c): """Calculates an encoded salt string, including prefix, for algorithm @p c . Note that blowfish supports up to a 22-character salt, but only 16 is provided by this method.""" # make a salt consisting of 96 bits of random data, packed into a # string, encoded using a variant of base-64 encoding and surrounded # by the correct markers rand_bits = struct.pack('> sys.stderr, program_name + ":", e sys.exit(1) if ("--help",'') in opts or ("-h",'') in opts: help() sys.exit(0) for optpair in opts: if len(optpair[0]) == 2 and optpair[0][0] == "-": # short option if short_to_long.has_key(optpair[0][1]): mode = short_to_long[optpair[0][1]] elif optpair[0] == "-l": settings['long_salt'] = True elif optpair[0] == "-v": settings['verify'] = True elif optpair[0] == "-q": settings['quiet'] = True else: # long option if optpair[0][2:] in short_to_long.values(): if mode: barf("Multiple mode options are not allowed", 13) mode = optpair[0][2:] # -- pre-preparation -- if settings['verify']: recognise_algorithm = recognise_algorithm_by_hash else: recognise_algorithm = recognise_algorithm_by_salt # have to do this after option handling but before algorithm recognition for a in algorithms: a.init(a) # -- argument handling -- # handle a salt if one was supplied if len(args) > 0: salt = args[0] # try to guess the algorithm if not mode: for a in algorithms: if recognise_algorithm(a, salt): mode = a.name break else: if settings['verify']: barf("Verify mode cannot be used if no salt is supplied", 4) salt = None # == preparation == if not mode: mode = DEFAULT_MODE # determine algorithm alg_class = None for a in algorithms: if a.name == mode: alg_class = a if not a: barf("mode " + mode + " not found", 14) # == sanity checking == if settings['verify'] and not recognise_algorithm_by_hash(alg_class, salt): barf("Verify mode requires a full hash to check against", 3) # == processing == # get two password(s) pw1 = getpass.getpass() if not settings['verify']: pw2 = getpass.getpass("Re-enter password: ") # compare them and if they don't match, report an error if pw1 != pw2: barf("Passwords do not match", 5) else: if pw1 == "": print >> sys.stderr, program_name + ":", "warning: password is blank!!" # hash password try: if alg_class.supports_salt: hasher = alg_class(salt) else: if salt and not settings['verify']: print >> sys.stderr, "ignoring salt" hasher = alg_class() if not settings['verify']: print hasher.hash(pw1) else: # verify mode (would have barfed by now if there was no salt) if hasher.hash(pw1) == salt: if not settings['quiet']: print("Verify suceeded.") else: if not settings['quiet']: print("Verify failed!") exit(2) # don't re-use mismatch code except ShortSaltException, e: barf(e, 7) except SaltPrefixException, e: barf(e, 8) except BadAlgException, e: barf(e, 10) except ImportError, e: barf("Cannot find required algorithm handler: %s" % (e,), 11)