#!/usr/bin/python3

import PAM
import subprocess
import sys

config = {
    'db': 'pam',
    'db_user': 'pam',
    'db_password': 'pampasswd',
    'db_table': 'users',
    'user_column': 'username',
    'password_column': 'password',
    'pam_service': 'pamtest',
    'config_file': '/etc/pam-mysql.conf'
}

# The hashes here aren't derived on-the-fly from the password, because
# they simulate database field values, which don't change with any
# environment (even OS level) change.
passwords = {
    'plain'    : { 'crypt':  0, 'hash': 'foopwd' },
    'Y'        : { 'crypt':  1, 'md5': 'false', 'hash': 'xycbw66FMoYGI' }, # mkpasswd -m descrypt foopwd xy
    'Y_MD5'    : { 'crypt':  1, 'md5': 'true' , 'hash': '$1$abcdefgh$cKmmXi05KpTgjDSOXaL4X/' }, # mkpasswd -m md5crypt foopwd abcdefgh
    'mysql'    : { 'crypt':  2, '323': 'false', 'hash': '*1A8A8D8A90F03E8A15D4FFB3FC91A4693F077A84' }, # select PASSWORD('foopwd')
    'mysql_old': { 'crypt':  2, '323': 'true' , 'hash': '2a7c7b955b2d807b' }, # select OLD_PASSWORD('foopwd') (original, pre-4.1 hash)
    'md5'      : { 'crypt':  3, 'hash': '1631f7e7ed3c261d02b309016087f8a9' }, # select MD5('foopwd')
    'sha1'     : { 'crypt':  4, 'hash': '794ed3d18464baff93f8ded1cfd00d9a2d9fe316' }, # select SHA1('foopwd')
    'drupal7'  : { 'crypt':  5, 'hash': '$S$5925NCiI4OFCbPvIIbhLVZagCu/GkASIhxqJHhuMfEP6WRqJLNwe' }, # https://www.useotools.com/drupal-password-hash-generator
    'joomla15' : { 'crypt':  6, 'hash': 'e6a39083bd9bbc7a4919942146230321:dksVPHnEP2MPyD4CgkSKxSAvTIIuAERa' }, # https://www.useotools.com/joomla-password-hash-generator
    'ssha'     : { 'crypt':  7, 'hash': '3TwCepGYd95hd7m7B8verbpYpksxMjM0' }, # salt=b'1234'; base64.b64encode(hashlib.sha1(b'foopwd'+salt).digest()+salt).decode('ascii')
    'sha512'   : { 'crypt':  8, 'hash': 'd9ea269f6b13bea79e6af9a34b2e9312f5897b689664b9ac7a57f8e50c0034188a69631613901303048e7ade717ebb7d58cdb43147e5d1c9c37b33950cf64d82' }, # printf foopwd | sha512sum
    'sha256'   : { 'crypt':  9, 'hash': 'df6555b9bdcb7fbe4fe0f8986a446e6c6f5dbacdca4a36ff288041cd59b4fb5f' }, # printf foopwd | sha256sum
}

mysql = subprocess.Popen('mysql', stdin=subprocess.PIPE, text=True)
mysql.communicate('''\
DROP DATABASE IF EXISTS {db};
CREATE DATABASE {db};
USE {db};
CREATE TABLE {db_table} (
  {user_column} VARCHAR (10) NOT NULL,
  {password_column} VARCHAR ({maxlen}) NOT NULL
);
INSERT INTO {db_table} ({user_column}, {password_column})
  VALUES {users};
DROP USER IF EXISTS {db_user}@localhost;
CREATE USER {db_user}@localhost IDENTIFIED BY '{db_password}';
GRANT SELECT ON {db}.{db_table} TO {db_user}@localhost;
'''.format(users = ','.join(f"('{k}','{v['hash']}')" for k,v in passwords.items()),
           maxlen = max(len(rec['hash']) for rec in passwords.values()),
           **config))

pam_conf = open('/etc/pam.d/{pam_service}'.format(**config), 'w')
pam_conf.write('''\
auth required pam_mysql.so config_file={config_file}
'''.format(**config))
pam_conf.close()

def write_config_file(conf):
    pam_mysql_conf = open(config['config_file'], 'w')
    pam_mysql_conf.write('''\
users.host 		= localhost
users.database 		= {db}
users.db_user 		= {db_user}
users.db_passwd		= {db_password}
users.table 		= {db_table}
users.user_column 	= {user_column}
users.password_column 	= {password_column}
users.password_crypt 	= {crypt}
'''.format(**config, crypt = conf['crypt']))
    if 'md5' in conf:
        pam_mysql_conf.write(f'users.use_md5 = {conf["md5"]}\n')
    elif '323' in conf:
        pam_mysql_conf.write(f'users.use_323_password = {conf["323"]}\n')
    pam_mysql_conf.close()

def pam_conv(auth, query_list, userData):
    if query_list != [('Password:', PAM.PAM_PROMPT_ECHO_OFF)]:
        print('Unexpected query_list', query_list)
        return None
    return [(userData,0)]

def good_auth(user, password):
    p = PAM.pam()
    p.start(config['pam_service'], user, pam_conv)
    p.setUserData(password)
    try:
        p.authenticate()
    except PAM.error as err:
        if err.args == ('Authentication failure', 7):
            return False
        else:
            raise
    else:
        return True

successes = 0
failures = 0
for user, conf in passwords.items():
    write_config_file(conf)
    if good_auth(user, 'foopwd'):
        print(f'OK: {user}: correct password accepted')
        successes += 1
    else:
        print(f'FAIL: {user}: correct password refused')
        failures += 1
    if good_auth(user, 'wrong_password'):
        print(f'FAIL: {user}: incorrect password accepted')
        failures += 1
    else:
        print(f'OK: {user}: incorrect password refused')
        successes += 1

print(f'Summary: {successes} successes and {failures} failures')
sys.exit(0 if failures == 0 else 1)
