# -*- coding: utf-8 -*-
"""The NOOP password module

Copyright © 2015-2016, Anders Andersen, The Arctic University of Norway.
See http://www.cs.uit.no/~aa/dist/tools/noop/COPYING (../COPYING) for
details.

"""


# Default service name in keyring
SERVICENAME = "noop.crypto.password"

# Default key size
SYMKEYSIZE = 256


# Import system modules
import sys, os	#, base64


# Python 3 only!
assert (sys.version_info[0] > 2 and sys.version_info[1] > 3), \
       "This NOOP module \"%s\" is Python 3.4 (or greater) only!" % (__name__,)


# Import modules
import keyring
from string import ascii_letters, digits, printable
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC


# Use SystemRandom to replace random.choice with random.SystemRandom().choice
from random import SystemRandom


# Load noop libraries (if available)
try:
    from noop.core.signature import signature, one, opt
    from noop.core.misc import idstr
except ImportError:
    def signature(f): return f
    class one:
        def __init__(*args): pass
    opt = one
    def idstr(self=None): return __name__


# Default characters in passwords/names
PASSWDCHARS = ascii_letters + digits
PASSWDLENGTH = 16
SALTCHARS = printable.encode()
SALTLENGTH = PASSWDLENGTH
SALTEXT = ".salt"

@signature
def mkpasswd(
        length:opt(int) = PASSWDLENGTH,
        letters:opt(str) = PASSWDCHARS) -> str:
    return "".join(SystemRandom().choice(letters) for i in range(length))

@signature
def mksalt(
        length:opt(int) = SALTLENGTH,
        letters:opt(bytes) = SALTCHARS) -> bytes:
    return bytes([SystemRandom().choice(letters) for i in range(length)])


class Password:
    R"""Create and manage password in the keychain

    The `Password` class is used to store and access passwords in the
    keychain.  It is implemented using the `keyring` module.

    Warning: You should not first create a temporary password and then
    a persistent password with the same name.  This might give
    unexpected results since the password will be deleted from the
    keychan when the first password object is deleted.

    """

    @signature
    def __init__(
            self,
            name:opt(str) = None,
            passwd:opt(str) = None,
            service:opt(str) = SERVICENAME,
            persistent:opt(bool) = False,
            hastobefresh:opt(bool) = False):
        R"""Password constructor

        Initialize the password with name, password and service.  The
        password in not stored as an attribute in the object.  It is
        only stored in the keychain on the computer.  The password has
        a name and is part of a service.  A non-persistent password is
        delted from the keychain when the object is deleted (this is
        the default behavior).  If the password is persistent and a
        password with the same name allready exists, the default
        behaviour is to use that password. This behaviour can be
        overridden by the `hastobefresh` argument.

        """

        # Save service name and if it is a temporary password
        self.service = service
        self.persistent = persistent
        self.salt = None

        # If the name is provided we use it, otherwise we generate a random name
        if name:
            self.name = name

            # Another password with the same name in keychain?
            if keyring.get_password(self.service, self.name) != None:

                # This will not work for a temporary password
                if not self.persistent:
                    raise ValueError(
                        idstr(self) + \
                        ": Persistent password with this name exists.")

                # If password has to be fresh
                elif hastobefresh:
                    raise ValueError(
                        idstr(self) + \
                        ": Non-fresh password with this name exists.")

                # Update with new password
                if passwd != None:
                    keyring.set_password(self.service, self.name, passwd)

                # Check to see if the password has a salt
                saltx = keyring.get_password(self.service, self.name + SALTEXT)
                if saltx != None:
                    self.salt = saltx.encode()

            # Store either provided or generated password in keychain
            elif passwd != None:
                keyring.set_password(self.service, self.name, passwd)
            else:
                keyring.set_password(self.service, self.name, mkpasswd())

        # No name provided.  Can we use a random name?
        else:

            # Only non-persistent password can have a random name
            if not self.persistent:
                self.name = mkpasswd()

                # Find a random name that is not used (just in case...)
                while keyring.get_password(self.service, self.name) != None:
                    self.name = mkpasswd()

                # Store either provided or generated password in keychain
                if passwd != None:
                    keyring.set_password(self.service, self.name, passwd)
                else:
                    keyring.set_password(self.service, self.name, mkpasswd())

            # A persistent password can not have a random name
            else:
                raise ValueError(
                    idstr(self) + \
                    ": Only non-persistent password can have a random name.")

    @signature
    def __str__(self) -> str:
        R"""Get password string

        Fetches and returns the password from the keychain.

        """
        if keyring.get_password(self.service, self.name):
            return keyring.get_password(self.service, self.name)
        else:
            return ""

    @signature
    def __bytes__(self) -> bytes:
        R"""Get password as a byte array

        Fetches and returns the password as a byte array.

        """
        if keyring.get_password(self.service, self.name):
            return keyring.get_password(self.service, self.name).encode()
        else:
            return b""

    @signature
    def __del__(self):
        R"""Delete object

        If it is a temporary password the password is also deleted
        from the keychain.

        """
        if not self.persistent:
            self.delete()

    @signature
    def delete(self):
        R"""Delete object

        Delete the password from the keychain.

        """
        try:
            keyring.delete_password(self.service, self.name)
        except:
            pass
        if self.salt and self.persistent:
            try:
                keyring.delete_password(self.service, self.name + SALTEXT)
            except:
                pass
        self.salt = None

    @signature
    def update(self, passwd:opt(str) = None):
        R"""Update password

        Change the password.

        """
        if passwd != None:
            keyring.set_password(self.service, self.name, passwd)
        else:
            keyring.set_password(self.service, self.name, mkpasswd())

    @signature
    def addsalt(
            self,
            salt:opt(bytes) = None,
            override:opt(bool) = False):
        R"""Add salt

        Add salt to the password (used when generating key from password).

        """
        if keyring.get_password(self.service, self.name) == None:
            raise ValueError(
                idstr(self) + ": Can not add salt: password not set.")
        if self.salt and not override:
            raise ValueError(
                idstr(self) + ": Can not add salt: salt already set.")
        if salt:
            self.salt = salt
        else:
            self.salt = mksalt()
        if self.persistent:
            keyring.set_password(
                self.service, self.name + SALTEXT, self.salt.decode())

    @signature
    def mkkey(self, keysize:opt(int) = SYMKEYSIZE) -> bytes:
        R"""Create a key

        Create a key from the password (and salt)

        """
        if str(self) and self.salt:
            kdf = PBKDF2HMAC(
                algorithm=hashes.SHA256(),
                length=keysize//8,
                salt=self.salt,
                iterations=100000,
                backend=default_backend())
            return kdf.derive(bytes(self))
        else:
            raise ValueError(
                idstr(self) + ": Can not create key: no password or salt.")

    @signature
    def mkiv(self, blocksize:opt(int) = algorithms.AES.block_size) -> bytes:
        R"""Create IV

        Create IV from the the salt.

        """
        if str(self) and self.salt:
            if len(self.salt) == blocksize//8:
                return self.salt
            else:
                kdf = PBKDF2HMAC(
                    algorithm=hashes.SHA256(),
                    length=blocksize//8,
                    salt=self.salt,
                    iterations=100000,
                    backend=default_backend())
                return kdf.derive(self.salt)
        else:
            raise ValueError(
                idstr(self) + ": Can not create iv: no password or salt.")