"""
<Program Name>
  keydb.py

<Author>
  Vladimir Diaz <vladimir.v.diaz@gmail.com>

<Started>
  March 21, 2012.  Based on a previous version of this module by Geremy Condra.

<Copyright>
  See LICENSE for licensing information.

<Purpose>
  Represent a collection of keys and their organization.  This module ensures
  the layout of the collection remain consistent and easily verifiable.
  Provided are functions to add and delete keys from the database, retrieve a
  single key, and assemble a collection from keys stored in TUF 'Root' Metadata
  files. The Update Framework process maintains a single keydb. 
  
  RSA keys are currently supported and a collection of keys is organized as a 
  dictionary indexed by key ID.  Key IDs are used as identifiers for keys (e.g.,
  RSA key).  They are the hexadecimal representations of the hash of key objects
  (specifically, the key object containing only the public key).  See 'rsa_key.py'
  and the '_get_keyid()' function to learn precisely how keyids are generated.
  One may get the keyid of a key object by simply accessing the dictionary's
  'keyid' key (i.e., rsakey['keyid']).
"""

# Help with Python 3 compatibility, where the print statement is a function, an
# implicit relative import is invalid, and the '/' operator performs true
# division.  Example:  print 'hello world' raises a 'SyntaxError' exception.
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

import logging
import copy

import tuf
import tuf.formats
import tuf.keys
import six

# List of strings representing the key types supported by TUF.
_SUPPORTED_KEY_TYPES = ['rsa', 'ed25519']

# See 'log.py' to learn how logging is handled in TUF.
logger = logging.getLogger('tuf.keydb')

# The key database.
_keydb_dict = {}


def create_keydb_from_root_metadata(root_metadata):
  """
  <Purpose>
    Populate the key database with the unique keys found in 'root_metadata'.
    The database dictionary will conform to 'tuf.formats.KEYDB_SCHEMA' and
    have the form: {keyid: key, ...}.  
    The 'keyid' conforms to 'tuf.formats.KEYID_SCHEMA' and 'key' to its
    respective type.  In the case of RSA keys, this object would match
    'RSAKEY_SCHEMA'.

  <Arguments>
    root_metadata:
      A dictionary conformant to 'tuf.formats.ROOT_SCHEMA'.  The keys found
      in the 'keys' field of 'root_metadata' are needed by this function.

  <Exceptions>
    tuf.FormatError, if 'root_metadata' does not have the correct format.

  <Side Effects>
    A function to add the key to the database is called.  In the case of RSA
    keys, this function is add_key().
    
    The old keydb key database is replaced.

  <Returns>
    None.
  """

  # Does 'root_metadata' have the correct format?
  # This check will ensure 'root_metadata' has the appropriate number of objects
  # and object types, and that all dict keys are properly named.
  # Raise 'tuf.FormatError' if the check fails.
  tuf.formats.ROOT_SCHEMA.check_match(root_metadata)

  # Clear the key database.
  _keydb_dict.clear()

  # Iterate the keys found in 'root_metadata' by converting them to
  # 'RSAKEY_SCHEMA' if their type is 'rsa', and then adding them to the
  # database.
  for keyid, key_metadata in six.iteritems(root_metadata['keys']):
    if key_metadata['keytype'] in _SUPPORTED_KEY_TYPES:
      # 'key_metadata' is stored in 'KEY_SCHEMA' format.  Call
      # create_from_metadata_format() to get the key in 'RSAKEY_SCHEMA'
      # format, which is the format expected by 'add_key()'.
      key_dict = tuf.keys.format_metadata_to_key(key_metadata)
      try:
        add_key(key_dict, keyid)
      
      # Although keyid duplicates should *not* occur (unique dict keys), log a
      # warning and continue.
      except tuf.KeyAlreadyExistsError as e: # pragma: no cover
        logger.warning(e)
        continue
      
      # 'tuf.Error' raised if keyid does not match the keyid for 'rsakey_dict'.
      except tuf.Error as e:
        logger.error(e)
        continue
    
    else:
      logger.warning('Root Metadata file contains a key with an invalid keytype.')





def add_key(key_dict, keyid=None):
  """
  <Purpose>
    Add 'rsakey_dict' to the key database while avoiding duplicates.
    If keyid is provided, verify it is the correct keyid for 'rsakey_dict'
    and raise an exception if it is not.
  
  <Arguments>
    key_dict:
      A dictionary conformant to 'tuf.formats.ANYKEY_SCHEMA'.
      It has the form:
      {'keytype': 'rsa',
       'keyid': keyid,
       'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
                  'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
    
    keyid:
      An object conformant to 'KEYID_SCHEMA'.  It is used as an identifier
      for RSA keys.

  <Exceptions>
    tuf.FormatError, if 'rsakey_dict' or 'keyid' does not have the 
    correct format.

    tuf.Error, if 'keyid' does not match the keyid for 'rsakey_dict'.

    tuf.KeyAlreadyExistsError, if 'rsakey_dict' is found in the key database.

  <Side Effects>
    The keydb key database is modified.

  <Returns>
    None.
  """

  # Does 'rsakey_dict' have the correct format?
  # This check will ensure 'rsakey_dict' has the appropriate number of objects
  # and object types, and that all dict keys are properly named.
  # Raise 'tuf.FormatError if the check fails.
  tuf.formats.ANYKEY_SCHEMA.check_match(key_dict)

  # Does 'keyid' have the correct format?
  if keyid is not None:
    # Raise 'tuf.FormatError' if the check fails. 
    tuf.formats.KEYID_SCHEMA.check_match(keyid)

    # Check if the keyid found in 'key_dict' matches 'keyid'.
    if keyid != key_dict['keyid']:
      raise tuf.Error('Incorrect keyid ' + key_dict['keyid'] + ' expected ' + keyid)
 
  # Check if the keyid belonging to 'rsakey_dict' is not already
  # available in the key database before returning.
  keyid = key_dict['keyid']
  if keyid in _keydb_dict:
    raise tuf.KeyAlreadyExistsError('Key: '+keyid)
 
  _keydb_dict[keyid] = copy.deepcopy(key_dict)





def get_key(keyid):
  """ 
  <Purpose>
    Return the key belonging to 'keyid'.

  <Arguments>
    keyid:
      An object conformant to 'tuf.formats.KEYID_SCHEMA'.  It is used as an
      identifier for keys.

  <Exceptions>
    tuf.FormatError, if 'keyid' does not have the correct format.

    tuf.UnknownKeyError, if 'keyid' is not found in the keydb database.

  <Side Effects>
    None.

  <Returns>
    The key matching 'keyid'.  In the case of RSA keys, a dictionary conformant
    to 'tuf.formats.RSAKEY_SCHEMA' is returned.
  """

  # Does 'keyid' have the correct format?
  # This check will ensure 'keyid' has the appropriate number of objects
  # and object types, and that all dict keys are properly named.
  # Raise 'tuf.FormatError' is the match fails.
  tuf.formats.KEYID_SCHEMA.check_match(keyid)

  # Return the key belonging to 'keyid', if found in the key database.
  try:
    return copy.deepcopy(_keydb_dict[keyid])
  except KeyError:
    raise tuf.UnknownKeyError('Key: '+keyid)





def remove_key(keyid):
  """ 
  <Purpose>
    Remove the key belonging to 'keyid'.

  <Arguments>
    keyid:
      An object conformant to 'tuf.formats.KEYID_SCHEMA'.  It is used as an
      identifier for keys.

  <Exceptions>
    tuf.FormatError, if 'keyid' does not have the correct format.

    tuf.UnknownKeyError, if 'keyid' is not found in key database.

  <Side Effects>
    The key, identified by 'keyid', is deleted from the key database.

  <Returns>
    None.
  """

  # Does 'keyid' have the correct format?
  # This check will ensure 'keyid' has the appropriate number of objects
  # and object types, and that all dict keys are properly named.
  # Raise 'tuf.FormatError' is the match fails.
  tuf.formats.KEYID_SCHEMA.check_match(keyid)

  # Remove the key belonging to 'keyid' if found in the key database.
  if keyid in _keydb_dict: 
    del _keydb_dict[keyid]
  else:
    raise tuf.UnknownKeyError('Key: '+keyid)





def clear_keydb():
  """
  <Purpose>
    Clear the keydb key database.

  <Arguments>
    None.

  <Exceptions>
    None.

  <Side Effects>
    The keydb key database is reset.

  <Returns>
    None.
  """
  
  _keydb_dict.clear()
