Tink 提供解決方案,可避免不當的金鑰管理作業,因為這類作業是主要風險來源。
建立及輪替金鑰
為用途選取原始類型和金鑰類型 (在前述的「我想...」部分) 後,請使用您選擇的外部金鑰管理系統 (KMS) 管理金鑰:
在 KMS 中建立金鑰加密金鑰 (KEK),以保護金鑰。
從 KMS 擷取金鑰 URI 和金鑰憑證,並傳送至 Tink。
使用 Tink 的 API 或 Tinkey 產生加密的鍵組。金鑰經過加密後,您可以將金鑰儲存在任何位置。
輪替金鑰,避免重複使用金鑰,並在金鑰遭到盜用時復原。
如果您需要匯出金鑰,請參閱「以程式輔助方式匯出金鑰素材」,瞭解如何安全地執行這項操作。
步驟 1:在外部 KMS 中建立 KEK
在外部 KMS 中建立金鑰加密金鑰 (KEK)。KEK 會加密金鑰來保護金鑰,為安全性增添一層防護。
如要建立 KEK,請參閱 KMS 專用說明文件:
- Google Cloud KMS
- Amazon KMS
- HashiCorp Vault (目前僅支援 Go 語言)
步驟 2:取得金鑰 URI 和憑證
您可以從 KMS 擷取金鑰 URI 和金鑰憑證。
取得金鑰 URI
Tink 需要統一資源識別碼 (URI) 才能搭配 KMS 金鑰運作。
如要建構這個 URI,請使用 KMS 在建立金鑰時指派給金鑰的專屬 ID。新增適當的 KMS 專屬前置字串,並遵循下表所述的支援金鑰 URI 格式:
KMS | KMS ID 前置字串 | 金鑰 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 Vault | hcvault:// |
hcvault://[key-id] |
取得主要憑證
準備必要的憑證,讓 Tink 能夠向外部金鑰管理服務進行驗證。
憑證的確切格式取決於 KMS:
- Google Cloud KMS - Tink 需要服務帳戶憑證。這些是可以透過 Google Cloud 控制台建立及下載的 JSON 檔案。
- AWS KMS – Tink 需要憑證檔案,其中包含
- HashiCorp Vault – Tink 需要服務權杖,可透過 vault token create 指令建立。
如果您未提供憑證,Tink 會嘗試載入預設憑證。詳情請參閱 KMS 專用說明文件:
步驟 3:建立並儲存加密的金鑰組
使用 Tink 的 API (適用於 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 專用說明文件: