เราขอแนะนําให้ใช้ Streaming AEAD พื้นฐานที่มีประเภทคีย์ AES128_GCM_HKDF_1MB สําหรับกรณีการใช้งานการเข้ารหัสไฟล์ส่วนใหญ่
รูปแบบการเข้ารหัสที่ตรวจสอบสิทธิ์แบบสตรีมมิงพร้อมข้อมูลที่เกี่ยวข้อง (Streaming 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
การถอดรหัสจะรวดเร็วเนื่องจากระบบจะถอดรหัสและตรวจสอบสิทธิ์เฉพาะบางส่วนของข้อความที่เข้ารหัสในแต่ละครั้ง คุณสามารถดูข้อความธรรมดาบางส่วนได้โดยไม่ต้องประมวลผลข้อความที่เข้ารหัสทั้งข้อความ
การใช้งาน AEAD แบบสตรีมเป็นไปตามคำจำกัดความของ AEAD และปลอดภัยแบบ NOAE โดยจะมีพร็อพเพอร์ตี้ต่อไปนี้
- Secrecy: ไม่มีใครทราบข้อมูลใดๆ เกี่ยวกับข้อความธรรมดา ยกเว้นความยาว
- Authenticity: ไม่สามารถเปลี่ยนแปลงข้อความธรรมดาที่เข้ารหัสซึ่งอยู่ภายใต้ข้อความที่เข้ารหัสได้โดยที่ไม่มีการตรวจพบ
- Symmetric: การเข้ารหัสข้อความธรรมดาและการถอดรหัสข้อความที่เข้ารหัสจะใช้คีย์เดียวกัน
- การสุ่ม: การเข้ารหัสเป็นแบบสุ่ม ข้อความ 2 รายการที่มีข้อความธรรมดาเหมือนกันจะให้ข้อความที่เข้ารหัสต่างกัน ผู้โจมตีจะไม่ทราบว่ามีการเข้ารหัสใดที่สอดคล้องกับข้อความธรรมดาที่ระบุ
ข้อมูลที่เกี่ยวข้อง
สามารถใช้พรอมต์ AEAD แบบสตรีมเพื่อเชื่อมโยงข้อความที่เข้ารหัสกับข้อมูลที่เชื่อมโยงที่เฉพาะเจาะจง สมมติว่าคุณมีฐานข้อมูลที่ประกอบด้วยฟิลด์ user-id
และ encrypted-medical-history
ในกรณีนี้ user-id
สามารถใช้เป็นข้อมูลที่เชื่อมโยงเมื่อเข้ารหัส encrypted-medical-history
ซึ่งจะช่วยป้องกันไม่ให้ผู้โจมตีย้ายประวัติทางการแพทย์จากผู้ใช้รายหนึ่งไปยังอีกรายหนึ่ง
เลือกประเภทคีย์
เราขอแนะนําให้ใช้ 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 บิต
- ความสามารถในการเข้ารหัสข้อความอย่างน้อย 264 รายการ3 โดยรวมมีความยาว 251 ไบต์2 การโจมตีที่มีข้อความธรรมดาหรือข้อความที่เข้ารหัสซึ่งเลือกไว้ไม่เกิน 232 รายการไม่มีความน่าจะเป็นที่จะสำเร็จมากกว่า 2-32
-
สาเหตุของข้อจํากัดนี้คือการใช้การเข้ารหัส AES-GCM การเข้ารหัสส่วนของข้อความธรรมดาอื่นในตำแหน่งเดียวกันจะเทียบเท่ากับการใช้ IV ซ้ำ ซึ่งละเมิดการรับประกันความปลอดภัยของ AES-GCM อีกเหตุผลหนึ่งคือการป้องกันการโจมตีแบบย้อนกลับ ซึ่งผู้โจมตีอาจพยายามกู้คืนไฟล์เวอร์ชันก่อนหน้าโดยที่ตรวจไม่พบ ↩
-
ระบบรองรับกลุ่ม 232 กลุ่ม โดยแต่ละกลุ่มมีข้อความธรรมดา
segment_size - tag_size
ไบต์ สำหรับกลุ่ม 1 MB ขนาดข้อความธรรมดาทั้งหมดคือ 232 * (220-16) ~= 251 ไบต์ ↩ -
AEAD แบบสตรีมมิงจะกลายเป็นไม่ปลอดภัยเมื่อใช้ชุดค่าผสมของคีย์ที่ได้จากข้อมูล (128 บิต) และคำนำหน้า Nonce (ค่า 7 ไบต์แบบสุ่มอิสระ) ซ้ำ เรามีความต้านทานการชนกัน 184 บิต ซึ่งประมาณเท่ากับ 264 ข้อความหากต้องการให้ความน่าจะเป็นในการประสบความสำเร็จน้อยกว่า 2-32 ↩