대용량 파일 또는 데이터 스트림을 암호화하고 싶음

대부분의 파일 암호화 사용 사례에는 AES128_GCM_HKDF_1MB 키 유형이 있는 스트리밍 AEAD 원시를 사용하는 것이 좋습니다.

연결된 데이터와 함께 스트리밍 인증 암호화 (스트리밍 AEAD) 프리미티브는 실시간 데이터 스트림이나 메모리에 맞지 않는 대용량 파일을 암호화하는 데 유용합니다. AEAD와 마찬가지로 대칭이며 암호화와 복호화에 단일 키를 사용합니다.

다음 예를 통해 스트리밍 AEAD 기본을 사용해 보세요.

Go

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"

	"github.com/tink-crypto/tink-go/v2/insecurecleartextkeyset"
	"github.com/tink-crypto/tink-go/v2/keyset"
	"github.com/tink-crypto/tink-go/v2/streamingaead"
)

func Example() {
	// A keyset created with "tinkey create-keyset --key-template=AES256_CTR_HMAC_SHA256_1MB". Note
	// that this keyset has the secret key information in cleartext.
	jsonKeyset := `{
    "primaryKeyId": 1720777699,
    "key": [{
        "keyData": {
            "typeUrl": "type.googleapis.com/google.crypto.tink.AesCtrHmacStreamingKey",
            "keyMaterialType": "SYMMETRIC",
            "value": "Eg0IgCAQIBgDIgQIAxAgGiDtesd/4gCnQdTrh+AXodwpm2b6BFJkp043n+8mqx0YGw=="
        },
        "outputPrefixType": "RAW",
        "keyId": 1720777699,
        "status": "ENABLED"
    }]
	}`

	// Create a keyset handle from the cleartext keyset in the previous
	// step. The keyset handle provides abstract access to the underlying keyset to
	// limit the exposure of accessing the raw key material. WARNING: In practice,
	// it is unlikely you will want to use an insecurecleartextkeyset, as it implies
	// that your key material is passed in cleartext, which is a security risk.
	// Consider encrypting it with a remote key in Cloud KMS, AWS KMS or HashiCorp Vault.
	// See https://github.com/google/tink/blob/master/docs/GOLANG-HOWTO.md#storing-and-loading-existing-keysets.
	keysetHandle, err := insecurecleartextkeyset.Read(
		keyset.NewJSONReader(bytes.NewBufferString(jsonKeyset)))
	if err != nil {
		log.Fatal(err)
	}

	// Retrieve the StreamingAEAD primitive we want to use from the keyset handle.
	primitive, err := streamingaead.New(keysetHandle)
	if err != nil {
		log.Fatal(err)
	}

	// Create a file with the plaintext.
	dir, err := os.MkdirTemp("", "streamingaead")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(dir)
	plaintextPath := filepath.Join(dir, "plaintext")
	if err := os.WriteFile(plaintextPath, []byte("this data needs to be encrypted"), 0666); err != nil {
		log.Fatal(err)
	}
	plaintextFile, err := os.Open(plaintextPath)
	if err != nil {
		log.Fatal(err)
	}

	// associatedData defines the context of the encryption. Here, we include the path of the
	// plaintext file.
	associatedData := []byte("associatedData for " + plaintextPath)

	// Encrypt the plaintext file and write the output to the ciphertext file. In this case the
	// primary key of the keyset will be used (which is also the only key in this example).
	ciphertextPath := filepath.Join(dir, "ciphertext")
	ciphertextFile, err := os.Create(ciphertextPath)
	if err != nil {
		log.Fatal(err)
	}
	w, err := primitive.NewEncryptingWriter(ciphertextFile, associatedData)
	if err != nil {
		log.Fatal(err)
	}
	if _, err := io.Copy(w, plaintextFile); err != nil {
		log.Fatal(err)
	}
	if err := w.Close(); err != nil {
		log.Fatal(err)
	}
	if err := ciphertextFile.Close(); err != nil {
		log.Fatal(err)
	}
	if err := plaintextFile.Close(); err != nil {
		log.Fatal(err)
	}

	// Decrypt the ciphertext file and write the output to the decrypted file. The
	// decryption finds the correct key in the keyset and decrypts the ciphertext.
	// If no key is found or decryption fails, it returns an error.
	ciphertextFile, err = os.Open(ciphertextPath)
	if err != nil {
		log.Fatal(err)
	}
	decryptedPath := filepath.Join(dir, "decrypted")
	decryptedFile, err := os.Create(decryptedPath)
	if err != nil {
		log.Fatal(err)
	}
	r, err := primitive.NewDecryptingReader(ciphertextFile, associatedData)
	if err != nil {
		log.Fatal(err)
	}
	if _, err := io.Copy(decryptedFile, r); err != nil {
		log.Fatal(err)
	}
	if err := decryptedFile.Close(); err != nil {
		log.Fatal(err)
	}
	if err := ciphertextFile.Close(); err != nil {
		log.Fatal(err)
	}

	// Print the content of the decrypted file.
	b, err := os.ReadFile(decryptedPath)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(b))
	// Output: this data needs to be encrypted
}

자바

package streamingaead;

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

import com.google.crypto.tink.InsecureSecretKeyAccess;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.RegistryConfiguration;
import com.google.crypto.tink.StreamingAead;
import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
import com.google.crypto.tink.streamingaead.StreamingAeadConfig;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.GeneralSecurityException;

/**
 * A command-line utility for encrypting files with Streaming AEAD.
 *
 * <p>It loads cleartext keys from disk - this is not recommended!
 *
 * <p>It requires the following arguments:
 *
 * <ul>
 *   <li>mode: Can be "encrypt" or "decrypt" to encrypt/decrypt the input to the output.
 *   <li>key-file: Read the key material from this file.
 *   <li>input-file: Read the input from this file.
 *   <li>output-file: Write the result to this file.
 *   <li>[optional] associated-data: Associated data used for the encryption or decryption.
 */
public final class StreamingAeadExample {
  private static final String MODE_ENCRYPT = "encrypt";
  private static final String MODE_DECRYPT = "decrypt";
  private static final int BLOCK_SIZE_IN_BYTES = 8 * 1024;

  public static void main(String[] args) throws Exception {
    if (args.length != 4 && args.length != 5) {
      System.err.printf("Expected 4 or 5 parameters, got %d\n", args.length);
      System.err.println(
          "Usage: java StreamingAeadExample encrypt/decrypt key-file input-file output-file"
              + " [associated-data]");
      System.exit(1);
    }
    String mode = args[0];
    Path keyFile = Paths.get(args[1]);
    Path inputFile = Paths.get(args[2]);
    Path outputFile = Paths.get(args[3]);
    byte[] associatedData = new byte[0];
    if (args.length == 5) {
      associatedData = args[4].getBytes(UTF_8);
    }

    // Initialize Tink: register all Streaming AEAD key types with the Tink runtime
    StreamingAeadConfig.register();

    // Read the keyset into a KeysetHandle
    KeysetHandle handle =
        TinkJsonProtoKeysetFormat.parseKeyset(
            new String(Files.readAllBytes(keyFile), UTF_8), InsecureSecretKeyAccess.get());

    // Get the primitive
    StreamingAead streamingAead =
        handle.getPrimitive(RegistryConfiguration.get(), StreamingAead.class);

    // Use the primitive to encrypt/decrypt files
    if (mode.equals(MODE_ENCRYPT)) {
      encryptFile(streamingAead, inputFile, outputFile, associatedData);
    } else if (mode.equals(MODE_DECRYPT)) {
      decryptFile(streamingAead, inputFile, outputFile, associatedData);
    } else {
      System.err.println(
          "The first argument must be either "
              + MODE_ENCRYPT
              + " or "
              + MODE_DECRYPT
              + ", got: "
              + mode);
      System.exit(1);
    }
  }

  private static void encryptFile(
      StreamingAead streamingAead, Path inputFile, Path outputFile, byte[] associatedData)
      throws GeneralSecurityException, IOException {
    try (WritableByteChannel encryptingChannel =
            streamingAead.newEncryptingChannel(
                FileChannel.open(outputFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE),
                associatedData);
        FileChannel inputChannel = FileChannel.open(inputFile, StandardOpenOption.READ)) {
      ByteBuffer byteBuffer = ByteBuffer.allocate(BLOCK_SIZE_IN_BYTES);
      while (true) {
        int read = inputChannel.read(byteBuffer);
        if (read <= 0) {
          return;
        }
        byteBuffer.flip();
        while (byteBuffer.hasRemaining()) {
          encryptingChannel.write(byteBuffer);
        }
        byteBuffer.clear();
      }
    }
  }

  private static void decryptFile(
      StreamingAead streamingAead, Path inputFile, Path outputFile, byte[] associatedData)
      throws GeneralSecurityException, IOException {
    try (ReadableByteChannel decryptingChannel =
            streamingAead.newDecryptingChannel(
                FileChannel.open(inputFile, StandardOpenOption.READ), associatedData);
        FileChannel outputChannel =
            FileChannel.open(outputFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
      ByteBuffer byteBuffer = ByteBuffer.allocate(BLOCK_SIZE_IN_BYTES);
      while (true) {
        int read = decryptingChannel.read(byteBuffer);
        if (read <= 0) {
          return;
        }
        byteBuffer.flip();
        while (byteBuffer.hasRemaining()) {
          outputChannel.write(byteBuffer);
        }
        byteBuffer.clear();
      }
    }
  }

  private StreamingAeadExample() {}
}

Python

"""A command-line utility for using streaming AEAD for a file.

It loads cleartext keys from disk - this is not recommended!

It requires 4 arguments (and one optional one):
  mode: either 'encrypt' or 'decrypt'
  keyset_path: name of the file with the keyset to be used for encryption or
    decryption
  input_path: name of the file with the input data to be encrypted or decrypted
  output_path: name of the file to write the ciphertext respectively plaintext
    to
  [optional] associated_data: the associated data used for encryption/decryption
    provided as a string.
"""

from typing import BinaryIO

from absl import app
from absl import flags
from absl import logging
import tink
from tink import secret_key_access
from tink import streaming_aead

FLAGS = flags.FLAGS
BLOCK_SIZE = 1024 * 1024  # The CLI tool will read/write at most 1 MB at once.

flags.DEFINE_enum('mode', None, ['encrypt', 'decrypt'],
                  'Selects if the file should be encrypted or decrypted.')
flags.DEFINE_string('keyset_path', None,
                    'Path to the keyset used for encryption or decryption.')
flags.DEFINE_string('input_path', None, 'Path to the input file.')
flags.DEFINE_string('output_path', None, 'Path to the output file.')
flags.DEFINE_string('associated_data', None,
                    'Associated data used for the encryption or decryption.')


def read_as_blocks(file: BinaryIO):
  """Generator function to read from a file BLOCK_SIZE bytes.

  Args:
    file: The file object to read from.

  Yields:
    Returns up to BLOCK_SIZE bytes from the file.
  """
  while True:
    data = file.read(BLOCK_SIZE)
    # If file was opened in rawIO, EOF is only reached when b'' is returned.
    # pylint: disable=g-explicit-bool-comparison
    if data == b'':
      break
    # pylint: enable=g-explicit-bool-comparison
    yield data


def encrypt_file(input_file: BinaryIO, output_file: BinaryIO,
                 associated_data: bytes,
                 primitive: streaming_aead.StreamingAead):
  """Encrypts a file with the given streaming AEAD primitive.

  Args:
    input_file: File to read from.
    output_file: File to write to.
    associated_data: Associated data provided for the AEAD.
    primitive: The streaming AEAD primitive used for encryption.
  """
  with primitive.new_encrypting_stream(output_file,
                                       associated_data) as enc_stream:
    for data_block in read_as_blocks(input_file):
      enc_stream.write(data_block)


def decrypt_file(input_file: BinaryIO, output_file: BinaryIO,
                 associated_data: bytes,
                 primitive: streaming_aead.StreamingAead):
  """Decrypts a file with the given streaming AEAD primitive.

  This function will cause the program to exit with 1 if the decryption fails.

  Args:
    input_file: File to read from.
    output_file: File to write to.
    associated_data: Associated data provided for the AEAD.
    primitive: The streaming AEAD primitive used for decryption.
  """
  try:
    with primitive.new_decrypting_stream(input_file,
                                         associated_data) as dec_stream:
      for data_block in read_as_blocks(dec_stream):
        output_file.write(data_block)
  except tink.TinkError as e:
    logging.exception('Error decrypting ciphertext: %s', e)
    exit(1)


def main(argv):
  del argv

  associated_data = b'' if not FLAGS.associated_data else bytes(
      FLAGS.associated_data, 'utf-8')

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

  # Read the keyset into a keyset_handle.
  with open(FLAGS.keyset_path, 'rt') as keyset_file:
    try:
      text = keyset_file.read()
      keyset_handle = tink.json_proto_keyset_format.parse(
          text, secret_key_access.TOKEN
      )
    except tink.TinkError as e:
      logging.exception('Error reading key: %s', e)
      return 1

  # Get the primitive.
  try:
    streaming_aead_primitive = keyset_handle.primitive(
        streaming_aead.StreamingAead)
  except tink.TinkError as e:
    logging.exception('Error creating streaming AEAD primitive from keyset: %s',
                      e)
    return 1

  # Encrypt or decrypt the file.
  with open(FLAGS.input_path, 'rb') as input_file:
    with open(FLAGS.output_path, 'wb') as output_file:
      if FLAGS.mode == 'encrypt':
        encrypt_file(input_file, output_file, associated_data,
                     streaming_aead_primitive)
      elif FLAGS.mode == 'decrypt':
        decrypt_file(input_file, output_file, associated_data,
                     streaming_aead_primitive)


if __name__ == '__main__':
  flags.mark_flag_as_required('mode')
  flags.mark_flag_as_required('keyset_path')
  flags.mark_flag_as_required('input_path')
  flags.mark_flag_as_required('output_path')
  app.run(main)

스트리밍 AEAD

스트리밍 AEAD 프리미티브는 스트리밍 데이터에 인증된 암호화를 제공합니다. 암호화할 데이터가 너무 커서 한 번에 처리할 수 없는 경우에 유용합니다. 일반적인 사용 사례로는 대용량 파일 또는 실시간 데이터 스트림 암호화가 있습니다.

암호화는 세그먼트 단위로 이루어지며, 세그먼트는 암호문 내 위치에 연결되어 있으며 삭제하거나 재정렬할 수 없습니다. 한 암호문의 세그먼트를 다른 암호문에 삽입할 수 없습니다. 기존 암호문을 수정하려면 전체 데이터 스트림을 다시 암호화해야 합니다.1

한 번에 암호문의 일부만 복호화되고 인증되므로 복호화 속도가 빠릅니다. 전체 암호문을 처리하지 않고도 부분 암호문을 가져올 수 있습니다.

스트리밍 AEAD 구현은 AEAD 정의를 충족하며 nOAE 보안을 제공합니다. 속성은 다음과 같습니다.

  • Secrecy: 길이를 제외하고는 일반 텍스트에 관한 정보는 아무것도 알려지지 않습니다.
  • Authenticity: 감지되지 않고는 암호문의 기본이 되는 암호화된 일반 텍스트를 변경할 수 없습니다.
  • Symmetric: 일반 텍스트를 암호화하고 암호문을 복호화하는 데 동일한 키가 사용됩니다.
  • 무작위 순서 지정: 암호화가 무작위로 지정됩니다. 평문이 동일한 두 메시지는 서로 다른 암호문을 생성합니다. 공격자는 어떤 암호문이 특정 일반 텍스트에 해당하는지 알 수 없습니다.

관련 데이터

스트리밍 AEAD 원시 함수를 사용하여 암호문을 특정 연결된 데이터에 연결할 수 있습니다. user-idencrypted-medical-history 필드가 있는 데이터베이스가 있다고 가정해 보겠습니다. 이 시나리오에서 user-idencrypted-medical-history를 암호화할 때 연결된 데이터로 사용될 수 있습니다. 이렇게 하면 공격자가 한 사용자의 의료 기록을 다른 사용자로 이동할 수 없습니다.

키 유형 선택

대부분의 경우 AES128_GCM_HKDF_1MB를 사용하는 것이 좋습니다. 일반적으로 다음과 같습니다.

  • AES-GCM-HKDF
    • AES128_GCM_HKDF_1MB (또는 AES256_GCM_HKDF_1MB)가 더 빠른 옵션입니다. 최대 264바이트의 264 파일을 암호화할 수 있습니다. 암호화 및 복호화 프로세스 중에 약 1MB의 메모리가 사용됩니다.
    • AES128_GCM_HKDF_4KB는 약 4KB의 메모리를 사용하며 시스템에 메모리가 많지 않은 경우 적합합니다.
  • AES-CTR HMAC
    • AES128_CTR_HMAC_SHA256_1MB (또는 AES256_CTR_HMAC_SHA256_1MB)는 더 보수적인 옵션입니다.

보안 보장

스트리밍 AEAD 구현은 다음을 제공합니다.

  • CCA2 보안
  • 인증 강도가 80비트 이상이어야 합니다.
  • 총 251바이트2로 264개 이상의 메시지3를 암호화할 수 있는 기능입니다 . 선택된 평문 또는 선택된 암호문이 최대 232개인 공격의 성공 확률은 2-32보다 큽니다.

  1. 이러한 제한은 AES-GCM 암호를 사용하기 때문입니다. 동일한 위치에서 다른 일반 텍스트 세그먼트를 암호화하는 것은 IV를 재사용하는 것과 같으며 이는 AES-GCM의 보안 보장을 위반합니다. 또 다른 이유는 이렇게 하면 공격자가 감지되지 않도록 이전 버전의 파일을 복원하려고 시도하는 롤백 공격을 방지할 수 있기 때문입니다. 

  2. 232개의 세그먼트가 지원되며 각 세그먼트는 segment_size - tag_size바이트의 일반 텍스트를 포함합니다. 1MB 세그먼트의 경우 총 일반 텍스트 크기는 232 * (220-16) ~= 251바이트입니다. 

  3. 파생 키 (128비트)와 nonce 접두사 (독립적인 임의의 7바이트 값) 조합이 반복되면 스트리밍 AEAD가 안전하지 않게 됩니다. 184비트 충돌 저항이 있으며, 성공 확률을 2-32 미만으로 하려면 대략 264 메시지로 변환됩니다.