Tôi muốn mã hoá các tệp hoặc luồng dữ liệu lớn

Bạn nên dùng nguyên tắc cơ bản Streaming AEAD với loại khoá AES128_GCM_HKDF_1MB cho hầu hết các trường hợp sử dụng mã hoá tệp.

Phương thức gốc Mã hoá truyền trực tuyến đã xác thực với dữ liệu liên kết (Streaming AEAD) rất hữu ích để mã hoá các luồng dữ liệu trực tiếp hoặc các tệp lớn không vừa bộ nhớ. Tương tự như AEAD, nó có tính đối xứng, sử dụng một khoá duy nhất cho cả hoạt động mã hoá và giải mã.

Các ví dụ sau đây giúp bạn bắt đầu sử dụng nguyên thuỷ AEAD truyền phát trực tiếp:

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
}

Java

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 truyền trực tuyến

Phương thức gốc AEAD truyền trực tuyến cung cấp phương thức mã hoá đã xác thực cho dữ liệu truyền trực tuyến. Phương thức này hữu ích khi dữ liệu cần mã hoá quá lớn để xử lý trong một bước duy nhất. Các trường hợp sử dụng điển hình bao gồm mã hoá các tệp lớn hoặc luồng dữ liệu trực tiếp.

Quá trình mã hoá được thực hiện theo các phân đoạn, được liên kết với vị trí của chúng trong một văn bản mã hoá và không thể xoá hoặc sắp xếp lại. Bạn không thể chèn các đoạn từ một văn bản mã hoá vào một văn bản mã hoá khác. Để sửa đổi một văn bản mã hoá hiện có, bạn phải mã hoá lại toàn bộ luồng dữ liệu.1

Quá trình giải mã diễn ra nhanh chóng vì chỉ một phần của văn bản mã hoá được giải mã và xác thực cùng một lúc. Bạn có thể lấy được văn bản thô một phần mà không cần xử lý toàn bộ văn bản mã hoá.

Các phương thức triển khai AEAD truyền trực tuyến đáp ứng định nghĩa AEADbảo mật nOAE. Chúng có các thuộc tính sau:

  • Secrecy: Không ai biết gì về văn bản thuần tuý, ngoại trừ độ dài của văn bản đó.
  • Tính xác thực: Không thể thay đổi văn bản thuần tuý được mã hoá làm cơ sở cho văn bản mã hoá mà không bị phát hiện.
  • Đối xứng: Quá trình mã hoá văn bản thuần tuý và giải mã văn bản mã hoá được thực hiện bằng cùng một khoá.
  • Ngẫu nhiên hoá: Quá trình mã hoá được ngẫu nhiên hoá. Hai thông báo có cùng văn bản thuần tuý sẽ tạo ra các văn bản mã hoá khác nhau. Kẻ tấn công không thể biết văn bản mã hoá nào tương ứng với một văn bản thuần tuý nhất định.

Dữ liệu liên quan

Bạn có thể dùng nguyên thuỷ AEAD truyền trực tuyến để liên kết văn bản mã hoá với dữ liệu được liên kết cụ thể. Giả sử bạn có một cơ sở dữ liệu với các trường user-idencrypted-medical-history: Trong trường hợp này, user-id có thể được dùng làm dữ liệu được liên kết khi mã hoá encrypted-medical-history. Điều này ngăn chặn kẻ tấn công di chuyển nhật ký y tế từ người dùng này sang người dùng khác.

Chọn loại khoá

Bạn nên dùng AES128_GCM_HKDF_1MB cho hầu hết các trường hợp sử dụng. Nói chung:

  • AES-GCM-HKDF
    • AES128_GCM_HKDF_1MB (hoặc AES256_GCM_HKDF_1MB) là lựa chọn nhanh hơn. Ứng dụng này có thể mã hoá 264 tệp, mỗi tệp có dung lượng tối đa 264 byte. Khoảng 1 MB bộ nhớ được dùng trong quá trình mã hoá và giải mã.
    • AES128_GCM_HKDF_4KB tiêu thụ khoảng 4 KB bộ nhớ và là lựa chọn phù hợp nếu hệ thống của bạn không có nhiều bộ nhớ.
  • AES-CTR HMAC
    • AES128_CTR_HMAC_SHA256_1MB (hoặc AES256_CTR_HMAC_SHA256_1MB) là một lựa chọn thận trọng hơn.

Đảm bảo về bảo mật

Các cách triển khai AEAD truyền trực tuyến cung cấp:

  • Bảo mật CCA2.
  • Độ mạnh của chế độ xác thực ít nhất là 80 bit.
  • Khả năng mã hoá ít nhất 264 thông báo3 với tổng cộng 251 byte2 . Không có cuộc tấn công nào với tối đa 232 văn bản gốc hoặc văn bản mã hoá được chọn có xác suất thành công lớn hơn 2-32.

  1. Một lý do cho hạn chế này là việc sử dụng mật mã AES-GCM. Việc mã hoá một đoạn văn bản thuần tuý khác ở cùng một vị trí sẽ tương đương với việc sử dụng lại IV, điều này vi phạm các đảm bảo về tính bảo mật của AES-GCM. Một lý do khác là việc này giúp ngăn chặn các cuộc tấn công quay lui, trong đó kẻ tấn công có thể cố gắng khôi phục một phiên bản trước của tệp mà không bị phát hiện. 

  2. Hỗ trợ 232 đoạn, mỗi đoạn chứa segment_size - tag_size byte văn bản thuần tuý. Đối với các đoạn 1 MB, tổng kích thước văn bản thuần tuý là 232 * (220-16) ~= 251 byte. 

  3. AEAD truyền trực tuyến sẽ trở nên không an toàn khi tổ hợp khoá phái sinh (128 bit) và tiền tố số chỉ dùng một lần (giá trị ngẫu nhiên độc lập 7 byte) được lặp lại. Chúng ta có khả năng chống xung đột 184 bit, tức là khoảng 264 thông báo nếu muốn xác suất thành công nhỏ hơn 2-32