こんにちは、サブスクペイstdの開発担当・谷口です。
今回は、弊社でPCI DSS4.0に準拠するにあたり、クレジットカード番号の暗号化対応について、技術的な観点からご紹介します。
PCI DSS4.0では、クレジットカード番号(PAN)を保持する場合、暗号化と鍵管理が必須となり、要件3.5~3.7でその詳細が定義されています。弊社ではこれまでデータベースのディスク暗号化は実施していましたが、クレジットカード番号自体の暗号化は未対応でした。今回の対応で、PCI DSS4.0の要件を満たすための暗号化方式や運用方法を検討・実装しました。
PCI DSS4.0の要件概要
- 要件3.5: プライマリアカウント番号(PAN)は、保存場所に関わらず、保護されていること
- 要件3.6: 保存されているアカウントデータを保護するための暗号化鍵が適切に保護されていること
- 要件3.7: 鍵のライフサイクル全体を網羅する鍵管理プロセスおよび手順が定義・実施されていること
クレジットカード番号の暗号化対応で検討したこと
- 前提
- 弊社インフラはAWS上に構築されています。
- AWS KMSはPCI DSS準拠のため、今回の対応ではAWS KMSを利用しています。
- 暗号化・復号化方式の検討
- エンベロープ暗号方式
- AWS KMSでデータ暗号化鍵(DEK)を生成し、DEKでクレジットカード番号を暗号化。DEK自体はKMSのマスターキーで暗号化して保存。
- 懸念点
- データ暗号化鍵の管理
- 復号時のパフォーマンス
- エンベロープ暗号方式の詳細
- KMS直接暗号化方式
- クレジットカード番号を直接KMSにリクエストし、KMS側で暗号化・復号化を実施。鍵管理もKMSに一元化。
- 懸念点
- 特になし
- KMS直接暗号化の詳細
- 共通の懸念事項
- KMS APIのリクエストクォータ制限(1秒間に20,000リクエスト超でスロットリングの可能性)
- KMSリクエスト制限
- エンベロープ暗号方式
- 検討結果
- 鍵管理のシンプルさとPCI DSS要件への対応を重視し、「KMS直接暗号化方式」を採用しました。
- 想定されるリクエスト数ではスロットリングの心配もありません。
サンプルコード
1. エンベロープ暗号方式
void Main() { var client = new AmazonKeyManagementServiceClient("XXXXXXXXXXXXXXXXXXXX", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", Amazon.RegionEndpoint.APNortheast1); var keyId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; var dataKeyRes = client.GenerateDataKeyAsync(new GenerateDataKeyRequest { KeySpec = DataKeySpec.AES_256, KeyId = keyId }); var cardnumber = "4444333322221111"; Console.WriteLine($"暗号化前文字列:{cardnumber}"); var manager = new AesManager(); var iv = manager.CreateIv(); var encStr = manager.Encrypt(Convert.ToBase64String(dataKeyRes.Result.Plaintext.ToArray()), iv, cardnumber); Console.WriteLine($"AES-256で暗号化後の文字列:{encStr}"); var encDataKey = Convert.ToBase64String(dataKeyRes.Result.CiphertextBlob.ToArray()); Console.WriteLine($"暗号化されたデータキー:{encDataKey}"); var ciphertextBlob = Convert.FromBase64String(encDataKey); var stream = new MemoryStream(); stream.Write(ciphertextBlob, 0, ciphertextBlob.Length); var decRes = client.DecryptAsync(new DecryptRequest { KeyId = keyId, EncryptionAlgorithm = EncryptionAlgorithmSpec.SYMMETRIC_DEFAULT, CiphertextBlob = stream }); var decStr = manager.Decrypt(Convert.ToBase64String(decRes.Result.Plaintext.ToArray()), iv, encStr); Console.WriteLine($"AES-256で復号化後の文字列:{decStr}"); } public class AesManager { private Aes aes { get; set; } public AesManager() { aes = Aes.Create(); } public string Encrypt(string encryptDataKey, string strIV, string srcStr) { // 暗号化した文字列格納用 string encrypted_str; // Aesオブジェクトを作成 using (var encryptor = aes.CreateEncryptor(Convert.FromBase64String(encryptDataKey), Encoding.UTF8.GetBytes(strIV))) // 出力ストリームを作成 using (var out_stream = new MemoryStream()) { using (var cs = new CryptoStream(out_stream, encryptor, CryptoStreamMode.Write)) { using (var sw = new StreamWriter(cs)) { // 出力ストリームに書き出し sw.Write(srcStr); } } // Base64文字列にする var result = out_stream.ToArray(); encrypted_str = Convert.ToBase64String(result); } return encrypted_str; } public string Decrypt(string descryptionDataKey, string strIV, string base64_text) { string plain_text; // Base64文字列をバイト型配列に変換 var cipher = Convert.FromBase64String(base64_text); // 復号器を作成 using (var in_stream = new MemoryStream(cipher)) using (var decryptor = aes.CreateDecryptor(Convert.FromBase64String(descryptionDataKey), Encoding.UTF8.GetBytes(strIV))) { using (var cs = new CryptoStream(in_stream, decryptor, CryptoStreamMode.Read)) using (var sr = new StreamReader(cs)) { plain_text = sr.ReadToEnd(); } } return plain_text; } public string CreateIv() { var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; var seed = (int)DateTime.Now.Ticks; var rand = new Random(seed); return string.Join("", Enumerable.Range(0, 16).Select(x => { var index = rand.Next(0, 62); return characters[index]; })); } }
2. KMS直接暗号化方式
void Main() { var input = "4444333322221111"; var client = new AmazonKeyManagementServiceClient("XXXXXXXXXXXXXXXXXXXX", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", Amazon.RegionEndpoint.APNortheast1); var tasks = Enumerable.Range(1, 10000).Select((index) => { return Task.Run(() => { try { using (var stream = new MemoryStream()) { var data = Encoding.UTF8.GetBytes(input); stream.Write(data, 0, data.Length); var encResponse = client.EncryptAsync(new EncryptRequest { EncryptionAlgorithm = EncryptionAlgorithmSpec.SYMMETRIC_DEFAULT, Plaintext = stream, KeyId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }); encResponse.Wait(); var decResponse = client.DecryptAsync(new DecryptRequest { KeyId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", EncryptionAlgorithm = EncryptionAlgorithmSpec.SYMMETRIC_DEFAULT, CiphertextBlob = encResponse.Result.CiphertextBlob }); decResponse.Wait(); var num = Encoding.UTF8.GetString(decResponse.Result.Plaintext.ToArray()); Console.WriteLine($"入力値:{input}"); Console.WriteLine($"出力値:{num}"); } } catch (Exception ex) { Console.WriteLine(ex); throw; } }); }).ToArray(); Task.WaitAll(tasks); }
まとめ
PCI DSS4.0対応におけるクレジットカード番号の暗号化は、AWS KMSを活用することで、セキュリティと運用の両立が可能となりました。今後もセキュリティ要件の変化に柔軟に対応し、より安全なサービス提供を目指していきます。
We are hiring!!
ROBOT PAYMENTでは一緒に働く仲間を募集しています!!!