ننصح باستخدام دالة التدفّق بتشفير متعدّد الإرسال والاستلام (AEAD) مع نوع مفتاح AES128_GCM_HKDF_1MB لمعظم حالات استخدام تشفير الملفات.
إنّ التشفير المُعتمَد للبث مع البيانات المرتبطة (Streaming AEAD) هو إجراء مفيد لتشفير أحداث البث المباشر للبيانات أو الملفات الكبيرة التي لا تتمكّن من تخزينها في الذاكرة. تمامًا مثل AEAD، يكون التشفير المتماثل ذا اتجاهَين، ويستخدم مفتاحًا واحدًا لكلٍ من التشفير وفك التشفير.
تساعدك الأمثلة التالية في بدء استخدام العنصر الأساسي لتشفير AEAD أثناء البث:
انتقال
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: يتم تشفير النص العادي وفك تشفيره باستخدام المفتاح نفسه.
- التوزيع العشوائي: يتم توزيع التشفير بشكل عشوائي. تؤدي رسالتان تتضمّنان النص العادي نفسه إلى نصوص مشفّرة مختلفة. لا يمكن للمهاجمين معرفة ملف التشفير الذي يتوافق مع نص عادي معيّن.
البيانات المرتبطة
يمكن استخدام العنصر الأساسي لتشفير 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 بايت كحد أقصى لكل منهما. يتم استخدام 1 ميغابايت تقريبًا من الذاكرة أثناء عملية التشفير وفك التشفير.
- يستهلك AES128_GCM_HKDF_4KB ما يقرب من 4 كيلوبايت من الذاكرة، وهو خيار جيد إذا كان نظامك لا يتضمّن الكثير من الذاكرة.
- AES-CTR HMAC
- يُعدّ AES128_CTR_HMAC_SHA256_1MB (أو AES256_CTR_HMAC_SHA256_1MB) خيارًا أكثر تحفظًا.
ضمانات الأمان
توفّر عمليات تنفيذ AEAD للبث ما يلي:
- أمان CCA2
- قوة مصادقة تبلغ 80 بت على الأقل
- إمكانية تشفير 64 رسالة3 على الأقل بإجمالي 251 بايت2 لا يمكن لأي هجوم باستخدام ما يصل إلى 232 نصًا عاديًا أو نصًا مشفَّرًا تم اختيارهما أن يكون احتمال نجاحه أكبر من 2-32.
-
ويعود سبب هذا التقييد إلى استخدام مُشفّر AES-GCM. إنّ تشفير جزء مختلف من النص العادي في الموضع نفسه يعادل إعادة استخدام مبدّل البدء، ما ينتهك ضمانات الأمان لـ AES-GCM. وهناك سبب آخر وهو أنّ ذلك يمنع هجمات "الرجوع إلى الإصدار السابق"، حيث قد يحاول المهاجم استعادة إصدار سابق من الملف بدون رصده. ↩
-
يمكن استخدام 32 مقطعًا، مع احتواء كل مقطع على
segment_size - tag_size
بايت من النص العادي. بالنسبة إلى المقاطع التي تبلغ 1 ميغابايت، يبلغ إجمالي حجم النص العادي 232 * (220-16) ~= 251 بايت. ↩ -
يصبح بروتوكول AEAD للبث غير آمن عند تكرار تركيبة مفتاح مشتق (128 بت) وبادئة عدد عشوائي (قيمة عشوائية مستقلة تبلغ 7 بايت). لدينا مقاومة للتصادم بسعة 184 بت، ما يعادل تقريبًا رسالتَين64 إذا أردنا أن يكون احتمال النجاح أقل من 2-32. ↩