ほとんどのファイル暗号化ユースケースでは、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 }
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
ストリーミング AEAD プリミティブは、ストリーミング データの認証付き暗号化を提供します。暗号化するデータが 1 つのステップで処理できないほど大きい場合に便利です。一般的なユースケースとしては、大規模なファイルやライブ データ ストリームの暗号化などがあります。
暗号化はセグメント単位で行われ、セグメントは暗号文内の位置にバインドされているため、削除や並べ替えはできません。1 つの暗号文のセグメントを別の暗号文に挿入することはできません。既存の暗号文を変更するには、データ ストリーム全体を再暗号化する必要があります。1
暗号テキストの一部のみが一度に復号されて認証されるため、復号は高速です。暗号文全体を処理しなくても、部分的な平文を取得できます。
ストリーミング AEAD 実装は AEAD の定義を満たし、nOAE で安全です。プロパティは次のとおりです。
- Secrecy: 平文については、長さ以外は何もわかりません。
- Authenticity: 暗号テキストの基になる暗号化された平文を検出されることなく変更することはできません。
- Symmetric: 平文の暗号化と暗号テキストの復号は、同じ鍵で行われます。
- ランダム化: 暗号化はランダム化されます。同じ平文の 2 つのメッセージがあっても、暗号テキストは異なります。攻撃者は、特定の平文に対応する暗号テキストを認識できません。
関連データ
ストリーミング AEAD プリミティブを使用すると、暗号テキストを特定の関連データに関連付けることができます。フィールド user-id
と encrypted-medical-history
を持つデータベースがあるとします。このシナリオでは、encrypted-medical-history
を暗号化するときに、user-id
を関連データとして使用できます。これにより、攻撃者がユーザー間で医療履歴を移動させることはできません。
キーの種類を選択する
ほとんどの用途には AES128_GCM_HKDF_1MB をおすすめします。原則:
- AES-GCM-HKDF
- AES128_GCM_HKDF_1MB(または AES256_GCM_HKDF_1MB)の方が高速です。最大 264 バイトの 264 ファイルを暗号化できます。暗号化と復号のプロセスで約 1 MB のメモリが消費されます。
- AES128_GCM_HKDF_4KB は約 4 KB のメモリを消費します。システムにメモリがあまりない場合に適しています。
- AES-CTR HMAC
- AES128_CTR_HMAC_SHA256_1MB(または AES256_CTR_HMAC_SHA256_1MB)は、より保守的なオプションです。
セキュリティ保証
ストリーミング AEAD 実装は、次を提供します。
- CCA2 セキュリティ。
- 80 ビット以上の認証強度。
- 合計 251 バイト2 の 264 個以上のメッセージ3 を暗号化できる。最大 232 個の平文または暗号文を選択する攻撃で、成功確率が 2-32 を超えることはありません。
-
この制限の理由は、AES-GCM 暗号を使用しているためです。同じ場所で別の平文セグメントを暗号化することは、IV を再利用することと同等であり、AES-GCM のセキュリティ保証に違反します。もう 1 つの理由は、攻撃者が検出されることなく以前のバージョンのファイルを復元しようとするロールバック攻撃を防ぐことです。 ↩
-
2 つの32 セグメントがサポートされており、各セグメントには
segment_size - tag_size
バイトの平文が含まれます。1 MB セグメントの場合、プレーンテキストの合計サイズは 232 *(220-16)~= 251 バイトです。 ↩ -
導出鍵(128 ビット)とノンス プレフィックス(独立したランダムな 7 バイト値)の組み合わせが繰り返されると、ストリーミング AEAD は安全でなくなります。184 ビットの衝突耐性があり、成功確率を 2-32 未満にするには、264 個のメッセージが必要です。 ↩