ב-Tink מציעים פתרונות כדי למנוע ניהול מפתחות לא תקין, שהוא מקור סיכון משמעותי.
יצירה ורוטציה של מפתחות
אחרי שבוחרים את סוג המפתח והפרימיטיב לתרחיש לדוגמה (בקטע הקודם אני רוצה…), מנהלים את המפתחות באמצעות מערכת ניהול המפתחות (KMS) החיצונית שבחרתם:
יצירת מפתח להצפנת מפתחות הצפנה (KEK) ב-KMS כדי להגן על המפתחות.
מאחזרים URI של מפתח ופרטי כניסה של מפתח מ-KMS כדי להעביר ל-Tink.
משתמשים ב-API של Tink או ב-Tinkey כדי ליצור קבוצת מפתחות מוצפנת. אחרי שהמפתחות מוצפנים, אפשר לאחסן אותם בכל מקום שרוצים.
רוטציה של המפתחות כדי להימנע משימוש מוגזם במפתחות ולאפשר שחזור במקרה של פריצה למפתחות.
אם אתם צריכים לייצא מפתחות, ראו חומר מפתח לייצוא פרוגרמטי כדי ללמוד איך לעשות זאת בצורה בטוחה.
שלב 1: יצירת מפתח KEK ב-KMS החיצוני
יוצרים מפתח להצפנת מפתחות (KEK) ב-KMS החיצוני. ה-KEK מגן על המפתחות שלכם על ידי הצפנתם, ומוסיף עוד שכבת אבטחה.
אפשר להיעזר במסמכים ספציפיים ל-KMS כדי ליצור KEK:
- Google Cloud KMS
- Amazon KMS
- HashiCorp Vault (כרגע זמין רק בשפת Go)
שלב 2: קבלת URI של מפתח ופרטי כניסה
אפשר לאחזר מ-KMS גם URI של מפתח וגם פרטי כניסה של מפתח.
איך מקבלים את ה-URI של המפתח
כדי לעבוד עם מפתחות KMS, נדרש מזהה משאב אחיד (URI) ב-Tink.
כדי ליצור את כתובת ה-URI הזו, משתמשים במזהה הייחודי שהוקצה למפתח על ידי KMS כשהוא נוצר. מוסיפים את הקידומת המתאימה ל-KMS ופועלים לפי הפורמט של מזהי URI נתמכים למפתחות, כפי שמתואר בטבלה הבאה:
KMS | קידומת המזהה של KMS | פורמט URI של מפתח |
---|---|---|
AWS KMS | aws-kms:// |
aws-kms://arn:aws:kms:[region]:[account-id]:key/[key-id] |
GCP KMS | gcp-kms:// |
gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/* |
הכספת של HashiCorp | hcvault:// |
hcvault://[key-id] |
קבלת פרטי הכניסה למפתח
מכינים את פרטי הכניסה הנדרשים כדי ש-Tink תוכל לבצע אימות ל-KMS החיצוני.
פרטי הכניסה מופיעים בתבנית ספציפית ל-KMS:
- Google Cloud KMS – ל-Tink נדרשים פרטי כניסה של חשבונות שירות. זהו קובץ JSON שאפשר ליצור ולהוריד דרך Google Cloud Console.
- AWS KMS – ל-Tink נדרש קובץ פרטי כניסה שמכיל את הפרטים הבאים:
- מזהה מפתח הגישה בנכס
accessKey
- מפתח הגישה הסודי בנכס
secretKey
.
- מזהה מפתח הגישה בנכס
- HashiCorp Vault – Tink דורשים אסימוני שירות שאפשר ליצור באמצעות פקודת היצירה של אסימון Vault.
אם לא תספקו פרטי כניסה, Tink תנסה לטעון את פרטי הכניסה שמוגדרים כברירת מחדל. מידע נוסף זמין במשאבי עזרה ספציפיים ל-KMS:
שלב 3: יצירת קבוצת מפתחות מוצפנת ושמירתה
משתמשים בממשקי ה-API של Tink (ל-Google Cloud KMS, ל-AWS KMS או ל-HashiCorp Vault) או ב-Tinkey כדי ליצור קבוצת מפתחות, להצפין אותה באמצעות ה-KMS החיצוני ולאחסן אותה במקום כלשהו.
Tinkey
tinkey create-keyset --key-template AES128_GCM \
--out-format json --out encrypted_aead_keyset.json \
--master-key-uri gcp-kms://projects/tink-examples/locations/global/keyRings/foo/cryptoKeys/bar \
--credential gcp_credentials.json
Java
בדוגמה הזו אתם צריכים את התוסף Google Cloud KMS tink-java-gcpkms
.
package encryptedkeyset; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.crypto.tink.Aead; import com.google.crypto.tink.KeysetHandle; import com.google.crypto.tink.TinkJsonProtoKeysetFormat; import com.google.crypto.tink.aead.AeadConfig; import com.google.crypto.tink.aead.PredefinedAeadParameters; import com.google.crypto.tink.integration.gcpkms.GcpKmsClient; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; /** * A command-line utility for working with encrypted keysets. * * <p>It requires the following arguments: * * <ul> * <li>mode: Can be "generate", "encrypt" or "decrypt". If mode is "generate", it will generate a * keyset, encrypt it and store it in the key-file argument. If mode is "encrypt" or * "decrypt", it will read and decrypt an keyset from the key-file argument, and use it to * encrypt or decrypt the input-file argument. * <li>kek-uri: Use this Cloud KMS' key as the key-encrypting-key for envelope encryption. * <li>gcp-credential-file: Use this JSON credential file to connect to Cloud KMS. * <li>input-file: If mode is "encrypt" or "decrypt", read the input from this file. * <li>output-file: If mode is "encrypt" or "decrypt", write the result to this file. */ public final class EncryptedKeysetExample { private static final String MODE_ENCRYPT = "encrypt"; private static final String MODE_DECRYPT = "decrypt"; private static final String MODE_GENERATE = "generate"; private static final byte[] EMPTY_ASSOCIATED_DATA = new byte[0]; public static void main(String[] args) throws Exception { if (args.length != 4 && args.length != 6) { System.err.printf("Expected 4 or 6 parameters, got %d\n", args.length); System.err.println( "Usage: java EncryptedKeysetExample generate/encrypt/decrypt key-file kek-uri" + " gcp-credential-file input-file output-file"); System.exit(1); } String mode = args[0]; if (!mode.equals(MODE_ENCRYPT) && !mode.equals(MODE_DECRYPT) && !mode.equals(MODE_GENERATE)) { System.err.print("The first argument should be either encrypt, decrypt or generate"); System.exit(1); } Path keyFile = Paths.get(args[1]); String kekUri = args[2]; String gcpCredentialFilename = args[3]; // Initialise Tink: register all AEAD key types with the Tink runtime AeadConfig.register(); // From the key-encryption key (KEK) URI, create a remote AEAD primitive for encrypting Tink // keysets. Aead kekAead = new GcpKmsClient().withCredentials(gcpCredentialFilename).getAead(kekUri); if (mode.equals(MODE_GENERATE)) { KeysetHandle handle = KeysetHandle.generateNew(PredefinedAeadParameters.AES128_GCM); String serializedEncryptedKeyset = TinkJsonProtoKeysetFormat.serializeEncryptedKeyset( handle, kekAead, EMPTY_ASSOCIATED_DATA); Files.write(keyFile, serializedEncryptedKeyset.getBytes(UTF_8)); return; } // Use the primitive to encrypt/decrypt files // Read the encrypted keyset KeysetHandle handle = TinkJsonProtoKeysetFormat.parseEncryptedKeyset( new String(Files.readAllBytes(keyFile), UTF_8), kekAead, EMPTY_ASSOCIATED_DATA); // Get the primitive Aead aead = handle.getPrimitive(Aead.class); Path inputFile = Paths.get(args[4]); Path outputFile = Paths.get(args[5]); if (mode.equals(MODE_ENCRYPT)) { byte[] plaintext = Files.readAllBytes(inputFile); byte[] ciphertext = aead.encrypt(plaintext, EMPTY_ASSOCIATED_DATA); Files.write(outputFile, ciphertext); } else if (mode.equals(MODE_DECRYPT)) { byte[] ciphertext = Files.readAllBytes(inputFile); byte[] plaintext = aead.decrypt(ciphertext, EMPTY_ASSOCIATED_DATA); Files.write(outputFile, plaintext); } } private EncryptedKeysetExample() {} }
Go
import ( "bytes" "fmt" "log" "github.com/tink-crypto/tink-go/v2/aead" "github.com/tink-crypto/tink-go/v2/keyset" "github.com/tink-crypto/tink-go/v2/testing/fakekms" ) // The fake KMS should only be used in tests. It is not secure. const keyURI = "fake-kms://CM2b3_MDElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EhIaEIK75t5L-adlUwVhWvRuWUwYARABGM2b3_MDIAE" func Example_encryptedKeyset() { // Get a KEK (key encryption key) AEAD. This is usually a remote AEAD to a KMS. In this example, // we use a fake KMS to avoid making RPCs. client, err := fakekms.NewClient(keyURI) if err != nil { log.Fatal(err) } kekAEAD, err := client.GetAEAD(keyURI) if err != nil { log.Fatal(err) } // Generate a new keyset handle for the primitive we want to use. newHandle, err := keyset.NewHandle(aead.AES256GCMKeyTemplate()) if err != nil { log.Fatal(err) } // Choose some associated data. This is the context in which the keyset will be used. keysetAssociatedData := []byte("keyset encryption example") // Encrypt the keyset with the KEK AEAD and the associated data. buf := new(bytes.Buffer) writer := keyset.NewBinaryWriter(buf) err = newHandle.WriteWithAssociatedData(writer, kekAEAD, keysetAssociatedData) if err != nil { log.Fatal(err) } encryptedKeyset := buf.Bytes() // The encrypted keyset can now be stored. // To use the primitive, we first need to decrypt the keyset. We use the same // KEK AEAD and the same associated data that we used to encrypt it. reader := keyset.NewBinaryReader(bytes.NewReader(encryptedKeyset)) handle, err := keyset.ReadWithAssociatedData(reader, kekAEAD, keysetAssociatedData) if err != nil { log.Fatal(err) } // Get the primitive. primitive, err := aead.New(handle) if err != nil { log.Fatal(err) } // Use the primitive. plaintext := []byte("message") associatedData := []byte("example encryption") ciphertext, err := primitive.Encrypt(plaintext, associatedData) if err != nil { log.Fatal(err) } decrypted, err := primitive.Decrypt(ciphertext, associatedData) if err != nil { log.Fatal(err) } fmt.Println(string(decrypted)) // Output: message }
Python
"""A command-line utility for generating, encrypting and storing keysets.""" from absl import app from absl import flags from absl import logging import tink from tink import aead from tink.integration import gcpkms FLAGS = flags.FLAGS flags.DEFINE_enum('mode', None, ['generate', 'encrypt', 'decrypt'], 'The operation to perform.') flags.DEFINE_string('keyset_path', None, 'Path to the keyset used for encryption.') flags.DEFINE_string('kek_uri', None, 'The Cloud KMS URI of the key encryption key.') flags.DEFINE_string('gcp_credential_path', None, 'Path to the GCP credentials JSON file.') 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, 'Optional associated data to use with the ' 'encryption operation.') def main(argv): del argv # Unused. associated_data = b'' if not FLAGS.associated_data else bytes( FLAGS.associated_data, 'utf-8') # Initialise Tink aead.register() try: # Read the GCP credentials and setup client client = gcpkms.GcpKmsClient(FLAGS.kek_uri, FLAGS.gcp_credential_path) except tink.TinkError as e: logging.exception('Error creating GCP KMS client: %s', e) return 1 # Create envelope AEAD primitive using AES256 GCM for encrypting the data try: remote_aead = client.get_aead(FLAGS.kek_uri) except tink.TinkError as e: logging.exception('Error creating primitive: %s', e) return 1 if FLAGS.mode == 'generate': # Generate a new keyset try: key_template = aead.aead_key_templates.AES128_GCM keyset_handle = tink.new_keyset_handle(key_template) except tink.TinkError as e: logging.exception('Error creating primitive: %s', e) return 1 # Encrypt the keyset_handle with the remote key-encryption key (KEK) with open(FLAGS.keyset_path, 'wt') as keyset_file: try: keyset_encryption_associated_data = 'encrypted keyset example' serialized_encrypted_keyset = ( tink.json_proto_keyset_format.serialize_encrypted( keyset_handle, remote_aead, keyset_encryption_associated_data ) ) keyset_file.write(serialized_encrypted_keyset) except tink.TinkError as e: logging.exception('Error writing key: %s', e) return 1 return 0 # Use the keyset to encrypt/decrypt data # Read the encrypted keyset into a keyset_handle with open(FLAGS.keyset_path, 'rt') as keyset_file: try: serialized_encrypted_keyset = keyset_file.read() keyset_encryption_associated_data = 'encrypted keyset example' keyset_handle = tink.json_proto_keyset_format.parse_encrypted( serialized_encrypted_keyset, remote_aead, keyset_encryption_associated_data, ) except tink.TinkError as e: logging.exception('Error reading key: %s', e) return 1 # Get the primitive try: cipher = keyset_handle.primitive(aead.Aead) except tink.TinkError as e: logging.exception('Error creating primitive: %s', e) return 1 with open(FLAGS.input_path, 'rb') as input_file: input_data = input_file.read() if FLAGS.mode == 'decrypt': output_data = cipher.decrypt(input_data, associated_data) elif FLAGS.mode == 'encrypt': output_data = cipher.encrypt(input_data, associated_data) else: logging.error( 'Unsupported mode %s. Please choose "encrypt" or "decrypt".', FLAGS.mode, ) return 1 with open(FLAGS.output_path, 'wb') as output_file: output_file.write(output_data) if __name__ == '__main__': flags.mark_flags_as_required([ 'mode', 'keyset_path', 'kek_uri', 'gcp_credential_path']) app.run(main)
שלב 4: סיבוב המקשים
כדי לשמור על אבטחת המערכת, צריך לבצע רוטציית מפתחות.
- מפעילים רוטציית מפתחות אוטומטית ב-KMS.
קובעים תדירות מתאימה להחלפת מפתחות. הזמן הזה תלוי ברמת הרגישות של הנתונים, במספר ההודעות שצריך להצפין ובצורך לתאם את הרוטציה עם שותפים חיצוניים.
- להצפנה סימטרית, משתמשים במפתחות ל-30 עד 90 יום.
- בהצפנה אסימטרית, תדירות הרוטציה יכולה להיות נמוכה יותר, אבל רק אם אפשר לבטל מפתחות בצורה מאובטחת.
מידע נוסף על רוטציית מפתחות זמין במסמכי התיעוד הספציפיים ל-KMS: