# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
import i_need_py25

"""Des fonctions pour gérer les formes cryptées des mots de passe

Les scheme supportés sont {CRYPT}, {SSHA}, {SMD5}, {SHA}, {MD5}
"""

__all__ = (
    'is_crypt_scheme', 'is_smd5_scheme', 'is_ssha_scheme', 'is_md5_scheme', 'is_sha_scheme', 'get_scheme',
    'get_crypt_salt', 'get_smd5_salt', 'get_ssha_salt', 'get_salt',
    'gen_crypt_salt', 'gen_binary_salt', 'gen_salt',
    'gen_crypt_hash', 'gen_binary_hash', 'gen_hash',
)

import base64, random, hashlib

try: from crypt import crypt
except: crypt = None

from uio import Utf8IO
_utf8 = Utf8IO()

def is_crypt_scheme(pw):
    if pw is None: return False
    return pw[:7].lower() == '{crypt}' or pw.lower() == 'crypt'

def is_smd5_scheme(pw):
    if pw is None: return False
    return pw[:6].lower() == '{smd5}' or pw.lower() == 'smd5'

def is_ssha_scheme(pw):
    if pw is None: return False
    return pw[:6].lower() == '{ssha}' or pw.lower() == 'ssha'

def is_md5_scheme(pw):
    if pw is None: return False
    return pw[:5].lower() == '{md5}' or pw.lower() == 'md5'

def is_sha_scheme(pw):
    if pw is None: return False
    return pw[:5].lower() == '{sha}' or pw.lower() == 'sha'

def get_scheme(pw):
    if pw is None: return None
    if is_crypt_scheme(pw): return '{CRYPT}'
    elif is_smd5_scheme(pw): return '{SMD5}'
    elif is_ssha_scheme(pw): return '{SSHA}'
    elif is_md5_scheme(pw): return '{MD5}'
    elif is_sha_scheme(pw): return '{SHA}'
    else: return '{CRYPT}'

def get_crypt_salt(pw):
    if pw is None: return None
    if pw[:7].lower() == '{crypt}': pw = pw[7:]
    return pw[:2]

def get_smd5_salt(pw):
    if pw is None: return None
    if pw[:6].lower() == '{smd5}': pw = pw[6:]
    return base64.decodestring(pw)[16:]

def get_ssha_salt(pw):
    if pw is None: return None
    if pw[:6].lower() == '{ssha}': pw = pw[6:]
    return base64.decodestring(pw)[20:]

def get_salt(pw):
    if pw is None: return None
    if pw[:7].lower() == '{crypt}': return get_crypt_salt(pw)
    if pw[:6].lower() == '{smd5}': return get_smd5_salt(pw)
    if pw[:6].lower() == '{ssha}': return get_ssha_salt(pw)
    # pas de salt
    if pw[:5].lower() == '{md5}': return ''
    if pw[:5].lower() == '{sha}': return ''
    # par défaut, considérer le salt comme pour crypt
    return get_crypt_salt(pw)

CRYPT_SALT_CHARS = "./abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
def gen_crypt_salt():
    return random.choice(CRYPT_SALT_CHARS) + random.choice(CRYPT_SALT_CHARS)

def gen_binary_salt(length=20):
    salt = []
    for i in range(length):
        salt.append(chr(random.randint(0, 255)))
    return ''.join(salt)

def gen_salt(pw=None, length=20):
    if pw is not None:
        if pw[:7].lower() == '{crypt}' or pw.lower() == 'crypt': return gen_crypt_salt()
        if pw[:6].lower() == '{smd5}' or pw.lower() == 'smd5': return gen_binary_salt(length)
        if pw[:6].lower() == '{ssha}' or pw.lower() == 'ssha': return gen_binary_salt(length)
        # pas de salt pour MD5 et SHA
        if pw[:5].lower() == '{md5}' or pw.lower() == 'md5': return ''
        if pw[:5].lower() == '{sha}' or pw.lower() == 'sha': return ''
    # par défaut, générer un salt pour crypt
    return gen_crypt_salt()

def gen_crypt_hash(pw, salt=None):
    if salt is None: salt = gen_crypt_salt()
    if crypt is None: raise ValueError("crypt is unvailable")
    return crypt(pw, salt)

def gen_binary_hash(pw, scheme, salt=None):
    if is_md5_scheme(scheme):
        m = hashlib.md5()
    elif is_sha_scheme(scheme):
        m = hashlib.sha1()
    elif is_smd5_scheme(scheme):
        m = hashlib.md5()
        if salt is None: salt = gen_binary_salt()
    elif is_ssha_scheme(scheme):
        m = hashlib.sha1()
        if salt is None: salt = gen_binary_salt()
    else: raise ValueError("unsupported scheme: %s" % scheme)

    pw = _utf8.s(pw)
    m.update(pw)
    if salt is not None: m.update(salt)

    hash = m.digest()
    if salt is not None: hash += salt

    return base64.b64encode(hash)

def gen_hash(pw, scheme=None, ref=None, salt=None, normalized=True):
    if ref is not None:
        scheme = get_scheme(ref)
        if salt is None: salt = get_salt(ref)
        if is_crypt_scheme(scheme):
            hash = gen_crypt_hash(pw, salt)
        else:
            hash = gen_binary_hash(pw, scheme, salt)
    elif scheme is not None:
        scheme = get_scheme(scheme)
        if is_crypt_scheme(scheme):
            hash = gen_crypt_hash(pw, salt)
        else:
            hash = gen_binary_hash(pw, scheme, salt)
    else:
        # Par défaut, hasher en crypt
        scheme = '{CRYPT}'
        hash = gen_crypt_hash(pw, salt)
    if normalized: hash = scheme + hash
    return hash
