Tink는 주된 위험의 원인인 부적절한 키 관리를 방지하는 솔루션을 제공합니다.
키 만들기 및 순환
이전 I T to... 섹션에서 사용 사례의 기본 유형과 키 유형을 선택한 후 다음과 같이 외부 키 관리 시스템(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에서 KMS 키를 사용하려면 Uniform Resource Identifier(URI)가 필요합니다.
이 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 Vault | hcvault:// |
hcvault://[key-id] |
키 사용자 인증 정보 가져오기
Tink가 외부 KMS에 인증할 수 있도록 필요한 사용자 인증 정보를 준비합니다.
사용자 인증 정보의 정확한 형식은 KMS마다 다릅니다.
- Google Cloud KMS – Tink에는 서비스 계정 사용자 인증 정보가 필요합니다. 이는 Google Cloud 콘솔에서 생성하고 다운로드할 수 있는 JSON 파일입니다.
- AWS KMS – Tink에는
accessKey
속성의 액세스 키 IDsecretKey
속성의 보안 비밀 액세스 키
- 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
자바
이 예에서는 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 관련 문서에서 키 순환에 대해 자세히 알아보세요.