// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package com.google.crypto.tink.tinkkey;

import static com.google.crypto.tink.internal.KeyTemplateProtoConverter.getOutputPrefixType;

import com.google.crypto.tink.KeyManager;
import com.google.crypto.tink.KeyTemplate;
import com.google.crypto.tink.KeyTemplate.OutputPrefixType;
import com.google.crypto.tink.TinkProtoParametersFormat;
import com.google.crypto.tink.internal.KeyManagerRegistry;
import com.google.crypto.tink.internal.Util;
import com.google.crypto.tink.proto.KeyData;
import com.google.crypto.tink.tinkkey.internal.ProtoKey;
import com.google.errorprone.annotations.Immutable;
import com.google.protobuf.ExtensionRegistryLite;
import com.google.protobuf.InvalidProtocolBufferException;
import java.security.GeneralSecurityException;

/**
 * Wraps a {@link TinkKey} and enforces access to the underlying {@link TinkKey} with {@link
 * KeyAccess}. Specifically, if the underlying {@link TinkKey} has a secret, then one can only get
 * it with a {@link SecretKeyAccess} instance.
 *
 * <p>Do not use this in new code. Instead, use {@link com.google.crypto.tink.Key} and
 * these facilities.
 */
@Immutable
public class KeyHandle {
  private static KeyData newKeyData(com.google.crypto.tink.KeyTemplate keyTemplate)
      throws GeneralSecurityException {
    try {
      byte[] serializedKeyTemplate =
          TinkProtoParametersFormat.serialize(keyTemplate.toParameters());
      com.google.crypto.tink.proto.KeyTemplate protoTemplate =
          com.google.crypto.tink.proto.KeyTemplate.parseFrom(
              serializedKeyTemplate, ExtensionRegistryLite.getEmptyRegistry());
      KeyManager<?> manager =
          KeyManagerRegistry.globalInstance().getUntypedKeyManager(protoTemplate.getTypeUrl());
      if (KeyManagerRegistry.globalInstance().isNewKeyAllowed(protoTemplate.getTypeUrl())) {
        return manager.newKeyData(protoTemplate.getValue());
      } else {
        throw new GeneralSecurityException(
            "newKey-operation not permitted for key type " + protoTemplate.getTypeUrl());
      }
    } catch (InvalidProtocolBufferException e) {
      throw new GeneralSecurityException("Failed to parse serialized parameters", e);
    }
  }

  /**
   * KeyStatusType is metadata associated to a key which is only meaningful when the key is part of
   * a {@link Keyset}. A key's status in the Keyset is either ENABLED (able to perform cryptographic
   * operations), DISABLED (unable to perform operations, but could be re-enabled), or DESTROYED
   * (the key's data is no longer present in the keyset).
   */
  public enum KeyStatusType {
    ENABLED,
    DISABLED,
    DESTROYED;
  }

  /**
   * Returns a {@link KeyHandle} instance with {@code key} as the underlying {@link TinkKey} if the
   * caller provides the correct {@link KeyAccess} instance.
   *
   * @throws GeneralSecurityException if {@code access} does not grant access to {@code key}
   */
  public static KeyHandle createFromKey(TinkKey key, KeyAccess access)
      throws GeneralSecurityException {
    KeyHandle result = new KeyHandle(key);
    result.checkAccess(access);
    return result;
  }

  /**
   * Returns a {@link KeyHandle} instance where the underlying {@link TinkKey} wraps the input
   * {@code keyData}. The returned KeyHandle has a secret if keyData has key material of type
   * UNKNOWN_KEYMATERIAL, SYMMETRIC, or ASYMMETRIC_PRIVATE.
   *
   * <p>Do not use this in new code. Instead, use {@link com.google.crypto.tink.Key} and these
   * facilities.
   */
  public static KeyHandle createFromKey(KeyData keyData, OutputPrefixType opt) {
    return new KeyHandle(new ProtoKey(keyData, opt));
  }

  private final TinkKey key;
  private final KeyStatusType status;
  private final int id;

  /**
   * Constructs a KeyHandle wrapping the input TinkKey. The KeyStatusType is set to ENABLED and an
   * arbitrary key ID is assigned.
   */
  private KeyHandle(TinkKey key) {
    this.key = key;
    this.status = KeyStatusType.ENABLED;
    this.id = Util.randKeyId();
  }

  /**
   * Constructor intended for Tink internal purposes; allows one to set all the member variables of
   * a {@link KeyHandle}.
   */
  protected KeyHandle(TinkKey key, KeyStatusType status, int keyId) {
    this.key = key;
    this.status = status;
    this.id = keyId;
  }

  /**
   * Generates a new {@link KeyHandle} that contains a fresh key generated according to {@code
   * keyTemplate}.
   *
   * @throws GeneralSecurityException if the key template's type URL has not been registered with
   *     the {@link Registry}.
   */
  public static KeyHandle generateNew(KeyTemplate keyTemplate) throws GeneralSecurityException {
    ProtoKey protoKey = new ProtoKey(newKeyData(keyTemplate), getOutputPrefixType(keyTemplate));
    return new KeyHandle(protoKey);
  }

  /** Returns {@code true} if the underlying {@link TinkKey} has a secret. */
  public boolean hasSecret() {
    return key.hasSecret();
  }

  /** Returns the status of the key. See {@link KeyStatusType}. */
  public KeyStatusType getStatus() {
    return this.status;
  }

  /**
   * Returns the key ID of this key. The key ID is not guaranteed to be unique among all KeyHandles.
   */
  public int getId() {
    return id;
  }

  /**
   * Returns the underlying {@link TinkKey} key if {@code access} is a {@link SecretKeyAccess} and
   * the key has a secret, or if the key does not have a secret, otherwise throws a {@link
   * GeneralSecurityException}.
   */
  public TinkKey getKey(KeyAccess access) throws GeneralSecurityException {
    checkAccess(access);
    return key;
  }

  /**
   * Returns the {@link KeyTemplate} of the underlying {@link TinkKey}.
   *
   * @throws UnsupportedOperationException if the underlying {@link TinkKey} has not implemented
   *     getKeyTemplate().
   */
  public KeyTemplate getKeyTemplate() {
    return key.getKeyTemplate();
  }

  private void checkAccess(KeyAccess access) throws GeneralSecurityException {
    if (hasSecret() && !access.canAccessSecret()) {
      throw new GeneralSecurityException("No access");
    }
  }
}
