Criptografia do lado do cliente com Tink e Cloud KMS

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

Neste tópico, descrevemos como criptografar dados localmente e fazer upload deles para o Cloud Storage com o Tink e o Cloud Key Management Service (Cloud KMS). A Tink é uma biblioteca de criptografia de código aberto criada por criptografistas e engenheiros de segurança do Google.

Informações gerais

A criptografia no lado do cliente é qualquer criptografia realizada antes do envio dos dados para a nuvem. Ao usar a criptografia do lado do cliente, você é responsável por criar e gerenciar suas chaves e criptografar seus dados antes de enviá-los para a nuvem.

Neste tópico, você vai implementar a criptografia de envelope do lado do cliente com o Tink usando uma chave de criptografia no Cloud KMS.

Antes de começar

  1. Crie uma chave de criptografia simétrica do Cloud KMS para criptografia. Anote o URI da chave. Você precisará disso mais tarde.
  2. Instale o Tink para usar com o Cloud KMS.
  3. Crie um bucket no Cloud Storage para fazer upload dos dados criptografados.

Papéis obrigatórios

Para garantir que sua conta de serviço tenha as permissões necessárias para usar as chaves do Cloud KMS com o Tink, peça ao administrador para conceder à sua conta de serviço o papel de criptografador/descriptografador de CryptoKey do Cloud KMS (roles/cloudkms.cryptoKeyEncrypterDecrypter) na chave. Para obter mais informações sobre como conceder papéis, consulte Gerenciar o acesso.

Também é possível que o administrador conceda à sua conta de serviço as permissões necessárias por meio de papéis personalizados ou outros papéis predefinidos.

Criptografia de envelope com o Tink

Na criptografia de envelope, a chave do Cloud KMS atua como uma chave de criptografia de chaves (KEK, na sigla em inglês). Ou seja, ela é usada para criptografar chaves de criptografia de dados (DEK, na sigla em inglês) que, por sua vez, são usadas para criptografar dados reais.

Depois de criar uma KEK no Cloud KMS, faça o seguinte para criptografar cada mensagem:

  1. Gere uma chave de criptografia de dados (DEK, na sigla em inglês) localmente.
  2. Use essa DEK localmente para criptografar a mensagem.
  3. Use o Cloud KMS para criptografar (unir) a DEK com a KEK.
  4. Armazene os dados criptografados e a DEK unida.

Não é preciso implementar esse processo de criptografia de envelope do zero quando usa o Tink.

Para usar a Tink na criptografia do envelope, forneça a ela um URI e credenciais importantes. O URI da chave aponta para a KEK no Cloud KMS, e as credenciais permitem que a Tink use a KEK. A Tink gera a DEK, criptografa os dados, os une e retorna um único texto criptografado com os dados criptografados e a DEK encapsulada.

O Tink suporta criptografia de envelope em Python, Java, C ++ e Go usando a criptografia autenticada com dados associados (AEAD, na sigla em inglês).

Conectar o Tink e o Cloud KMS

Para criptografar as DEKs geradas pelo Tink com sua KEK no Cloud KMS, você precisa conseguir o URI da sua KEK. No Cloud KMS, o URI da KEK tem o formato:

gcp-kms://projects/<PROJECT>/locations/<LOCATION>/keyRings/ <KEY RING>/cryptoKeys/<KEY NAME>/cryptoKeyVersions/<VERSION>

Consulte Como conseguir um ID de recurso do Cloud KMS para ver detalhes sobre como acessar o caminho para a chave.

Inicializar o Tink e criptografar dados

O Tink usa primitivos (elementos básicos de criptografia que gerenciam os detalhes dos algoritmos subjacentes) para que você possa executar tarefas com segurança. Cada primário oferece uma API que processa uma tarefa específica. Aqui estamos usando AEAD, então usamos o primitivo TAD AEAD.

Python

Python

Para saber como instalar e usar a biblioteca de cliente do Cloud KMS, consulte Bibliotecas de cliente do Cloud KMS.

"""A command-line utility for performing file encryption using GCS.

It is inteded for use with small files, utilizes envelope encryption and
facilitates ciphertexts stored in GCS.
"""

from absl import app
from absl import flags
from absl import logging
from google.cloud import storage

import tink
from tink import aead
from tink.integration import gcpkms

FLAGS = flags.FLAGS

flags.DEFINE_enum('mode', None, ['encrypt', 'decrypt'],
                  'The operation to perform.')
flags.DEFINE_string('kek_uri', None,
                    'The Cloud KMS URI of the key encryption key.')
flags.DEFINE_string('gcp_credential_path', None,
                    'Path to the GCP credentials JSON file.')
flags.DEFINE_string('gcp_project_id', None,
                    'The ID of the GCP project hosting the GCS blobs.')
flags.DEFINE_string('local_path', None, 'Path to the local file.')
flags.DEFINE_string('gcs_blob_path', None, 'Path to the GCS blob.')

_GCS_PATH_PREFIX = 'gs://'

def main(argv):
  del argv  # Unused.

  # Initialise Tink
  try:
    aead.register()
  except tink.TinkError as e:
    logging.exception('Error initialising Tink: %s', e)
    return 1

  # Read the GCP credentials and setup client
  try:
    gcpkms.GcpKmsClient.register_client(
        FLAGS.kek_uri, FLAGS.gcp_credential_path)
  except tink.TinkError as e:
    logging.exception('Error initializing GCP client: %s', e)
    return 1

  # Create envelope AEAD primitive using AES256 GCM for encrypting the data
  try:
    template = aead.aead_key_templates.create_kms_envelope_aead_key_template(
        kek_uri=FLAGS.kek_uri,
        dek_template=aead.aead_key_templates.AES256_GCM)
    handle = tink.new_keyset_handle(template)
    env_aead = handle.primitive(aead.Aead)
  except tink.TinkError as e:
    logging.exception('Error creating primitive: %s', e)
    return 1

  storage_client = storage.Client.from_service_account_json(
      FLAGS.gcp_credential_path)

  try:
    bucket_name, object_name = _get_bucket_and_object(FLAGS.gcs_blob_path)
  except ValueError as e:
    logging.exception('Error parsing GCS blob path: %s', e)
    return 1
  bucket = storage_client.bucket(bucket_name)
  blob = bucket.blob(object_name)
  associated_data = FLAGS.gcs_blob_path.encode('utf-8')

  if FLAGS.mode == 'encrypt':
    with open(FLAGS.local_path, 'rb') as input_file:
      output_data = env_aead.encrypt(input_file.read(), associated_data)
    blob.upload_from_string(output_data)

  elif FLAGS.mode == 'decrypt':
    ciphertext = blob.download_as_bytes()
    with open(FLAGS.local_path, 'wb') as output_file:
      output_file.write(env_aead.decrypt(ciphertext, associated_data))

  else:
    logging.error(
        'Error mode not supported. Please choose "encrypt" or "decrypt".')
    return 1

def _get_bucket_and_object(gcs_blob_path):
  """Extract bucket and object name from a GCS blob path.

  Args:
    gcs_blob_path: path to a GCS blob

  Returns:
    The bucket and object name of the GCS blob

  Raises:
    ValueError: If gcs_blob_path parsing fails.
  """
  if not gcs_blob_path.startswith(_GCS_PATH_PREFIX):
    raise ValueError(
        f'GCS blob paths must start with gs://, got {gcs_blob_path}')
  path = gcs_blob_path[len(_GCS_PATH_PREFIX):]
  parts = path.split('/', 1)
  if len(parts) < 2:
    raise ValueError(
        'GCS blob paths must be in format gs://bucket-name/object-name, '
        f'got {gcs_blob_path}')
  return parts[0], parts[1]

if __name__ == '__main__':
  flags.mark_flags_as_required([
      'mode', 'kek_uri', 'gcp_credential_path', 'gcp_project_id', 'local_path',
      'gcs_blob_path'])
  app.run(main)

Java

Java

Para saber como instalar e usar a biblioteca de cliente do Cloud KMS, consulte Bibliotecas de cliente do Cloud KMS.

package gcs;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import com.google.crypto.tink.Aead;
import com.google.crypto.tink.KeyTemplates;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.aead.AeadConfig;
import com.google.crypto.tink.aead.KmsEnvelopeAeadKeyManager;
import com.google.crypto.tink.integration.gcpkms.GcpKmsClient;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Optional;

/**
 * A command-line utility for encrypting small files with envelope encryption and uploading the
 * results to GCS.
 *
 * <p>The CLI takes the following required arguments:
 *
 * <ul>
 *   <li>mode: "encrypt" or "decrypt" to indicate if you want to encrypt or decrypt.
 *   <li>kek-uri: The URI for the Cloud KMS key to be used for envelope encryption.
 *   <li>gcp-credential-file: Name of the file with the GCP credentials (in JSON format) that can
 *       access the Cloud KMS key and the GCS input/output blobs.
 *   <li>gcp-project-id: The ID of the GCP project hosting the GCS blobs that you want to encrypt or
 *       decrypt.
 * </ul>
 *
 * <p>When mode is "encrypt", it takes the following additional arguments:
 *
 * <ul>
 *   <li>local-input-file: Read the plaintext from this local file.
 *   <li>gcs-output-blob: Write the encryption result to this blob in GCS. The encryption result is
 *       bound to the location of this blob. That is, if you rename or move it to a different
 *       bucket, decryption will fail.
 * </ul>
 *
 * <p>When mode is "decrypt", it takes the following additional arguments:
 *
 * <ul>
 *   <li>gcs-input-blob: Read the ciphertext from this blob in GCS.
 *   <li>local-output-file: Write the decryption result to this local file.
 */
public final class GcsEnvelopeAeadExample {
  private static final String MODE_ENCRYPT = "encrypt";
  private static final String MODE_DECRYPT = "decrypt";
  private static final String GCS_PATH_PREFIX = "gs://";

  public static void main(String[] args) throws Exception {
    if (args.length != 6) {
      System.err.printf("Expected 6 parameters, got %d\n", args.length);
      System.err.println(
          "Usage: java GcsEnvelopeAeadExample encrypt/decrypt kek-uri gcp-credential-file"
              + " gcp-project-id input-file output-file");
      System.exit(1);
    }
    String mode = args[0];
    String kekUri = args[1];
    String gcpCredentialFilename = args[2];
    String gcpProjectId = args[3];

    // Initialise Tink: register all AEAD key types with the Tink runtime
    AeadConfig.register();

    // Read the GCP credentials and set up client
    try {
      GcpKmsClient.register(Optional.of(kekUri), Optional.of(gcpCredentialFilename));
    } catch (GeneralSecurityException ex) {
      System.err.println("Error initializing GCP client: " + ex);
      System.exit(1);
    }

    // Create envelope AEAD primitive using AES256 GCM for encrypting the data
    Aead aead = null;
    try {
      KeysetHandle handle =
          KeysetHandle.generateNew(
              KmsEnvelopeAeadKeyManager.createKeyTemplate(kekUri, KeyTemplates.get("AES256_GCM")));
      aead = handle.getPrimitive(Aead.class);
    } catch (GeneralSecurityException ex) {
      System.err.println("Error creating primitive: %s " + ex);
      System.exit(1);
    }

    GoogleCredentials credentials =
        GoogleCredentials.fromStream(new FileInputStream(gcpCredentialFilename))
            .createScoped(Arrays.asList("https://www.googleapis.com/auth/cloud-platform"));
    Storage storage =
        StorageOptions.newBuilder()
            .setProjectId(gcpProjectId)
            .setCredentials(credentials)
            .build()
            .getService();

    // Use the primitive to encrypt/decrypt files.
    if (MODE_ENCRYPT.equals(mode)) {
      // Encrypt the local file
      byte[] input = Files.readAllBytes(Paths.get(args[4]));
      String gcsBlobPath = args[5];
      // This will bind the encryption to the location of the GCS blob. That if, if you rename or
      // move the blob to a different bucket, decryption will fail.
      // See https://developers.google.com/tink/aead#associated_data.
      byte[] associatedData = gcsBlobPath.getBytes(UTF_8);
      byte[] ciphertext = aead.encrypt(input, associatedData);

      // Upload to GCS
      String bucketName = getBucketName(gcsBlobPath);
      String objectName = getObjectName(gcsBlobPath);
      BlobId blobId = BlobId.of(bucketName, objectName);
      BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
      storage.create(blobInfo, ciphertext);
    } else if (MODE_DECRYPT.equals(mode)) {
      // Download the GCS blob
      String gcsBlobPath = args[4];
      String bucketName = getBucketName(gcsBlobPath);
      String objectName = getObjectName(gcsBlobPath);
      byte[] input = storage.readAllBytes(bucketName, objectName);

      // Decrypt to a local file
      byte[] associatedData = gcsBlobPath.getBytes(UTF_8);
      byte[] plaintext = aead.decrypt(input, associatedData);
      File outputFile = new File(args[5]);
      try (FileOutputStream stream = new FileOutputStream(outputFile)) {
        stream.write(plaintext);
      }
    } else {
      System.err.println("The first argument must be either encrypt or decrypt, got: " + mode);
      System.exit(1);
    }

    System.exit(0);
  }

  private static String getBucketName(String gcsBlobPath) {
    if (!gcsBlobPath.startsWith(GCS_PATH_PREFIX)) {
      throw new IllegalArgumentException(
          "GCS blob paths must start with gs://, got " + gcsBlobPath);
    }

    String bucketAndObjectName = gcsBlobPath.substring(GCS_PATH_PREFIX.length());
    int firstSlash = bucketAndObjectName.indexOf("/");
    if (firstSlash == -1) {
      throw new IllegalArgumentException(
          "GCS blob paths must have format gs://my-bucket-name/my-object-name, got " + gcsBlobPath);
    }
    return bucketAndObjectName.substring(0, firstSlash);
  }

  private static String getObjectName(String gcsBlobPath) {
    if (!gcsBlobPath.startsWith(GCS_PATH_PREFIX)) {
      throw new IllegalArgumentException(
          "GCS blob paths must start with gs://, got " + gcsBlobPath);
    }

    String bucketAndObjectName = gcsBlobPath.substring(GCS_PATH_PREFIX.length());
    int firstSlash = bucketAndObjectName.indexOf("/");
    if (firstSlash == -1) {
      throw new IllegalArgumentException(
          "GCS blob paths must have format gs://my-bucket-name/my-object-name, got " + gcsBlobPath);
    }
    return bucketAndObjectName.substring(firstSlash + 1);
  }

  private GcsEnvelopeAeadExample() {}
}

Para mais informações sobre as interfaces e as primitivas compatíveis com a Tink, consulte a página Primeiros passos da Tink.

A seguir