NiceCryptoInvariants.cs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. using System.Security.Cryptography;
  2. using System.Text;
  3. namespace Infrastructure.Kyc.Nice;
  4. /// <summary>
  5. /// <see cref="NiceCrypto"/> 의 invariants self-check.
  6. ///
  7. /// dev 환경 부팅 시 1회 실행해서 PBKDF2 출력 길이, AES-GCM 라운드트립, HMAC 라운드트립이
  8. /// 깨지지 않았음을 확인. 라이브러리/런타임 업데이트로 silently 동작이 바뀌는 회귀를 방지.
  9. ///
  10. /// 진짜 NICE 호환성 검증은 별도 phase (xUnit + NICE 공식 PHP/Node.js sample 의 골든벡터) 에서.
  11. /// </summary>
  12. public static class NiceCryptoInvariants
  13. {
  14. /// <summary>
  15. /// 실패 시 <see cref="CryptographicException"/> throw.
  16. /// </summary>
  17. public static void EnsureOrThrow()
  18. {
  19. EnsureBase64UrlRoundTrip();
  20. EnsureDeriveKeysShape();
  21. EnsureHmacRoundTrip();
  22. EnsureAesGcmRoundTrip();
  23. }
  24. private static void EnsureBase64UrlRoundTrip()
  25. {
  26. // 패딩 길이 0/1/2 byte 모두 라운드트립
  27. var samples = new[]
  28. {
  29. new byte[] { 0x00 },
  30. new byte[] { 0x01, 0x02 },
  31. new byte[] { 0xFF, 0xFE, 0xFD },
  32. Encoding.UTF8.GetBytes("hello world")
  33. };
  34. foreach (var s in samples)
  35. {
  36. var encoded = NiceCrypto.Base64UrlEncode(s);
  37. if (encoded.Contains('+') || encoded.Contains('/') || encoded.EndsWith('='))
  38. {
  39. throw new CryptographicException($"Base64UrlEncode 가 URL-safe + no-padding 규칙을 위반: '{encoded}'");
  40. }
  41. var decoded = NiceCrypto.Base64UrlDecode(encoded);
  42. if (!decoded.AsSpan().SequenceEqual(s))
  43. {
  44. throw new CryptographicException("Base64Url 라운드트립 실패.");
  45. }
  46. }
  47. }
  48. private static void EnsureDeriveKeysShape()
  49. {
  50. // 임의 vector — NICE 호환 vector 아님. 길이/결정성만 검증.
  51. var (aesKey, hmacKey) = NiceCrypto.DeriveKeys("ticket-self-check", "transaction-id-self-check", 1000);
  52. if (aesKey.Length != 32)
  53. {
  54. throw new CryptographicException($"DeriveKeys: AES key length = {aesKey.Length} (expected 32).");
  55. }
  56. if (hmacKey.Length != 32)
  57. {
  58. throw new CryptographicException($"DeriveKeys: HMAC key length = {hmacKey.Length} (expected 32).");
  59. }
  60. // 같은 입력 → 같은 출력 (deterministic)
  61. var (aesKey2, hmacKey2) = NiceCrypto.DeriveKeys("ticket-self-check", "transaction-id-self-check", 1000);
  62. if (!aesKey.AsSpan().SequenceEqual(aesKey2) || !hmacKey.AsSpan().SequenceEqual(hmacKey2))
  63. {
  64. throw new CryptographicException("DeriveKeys 가 결정론적이지 않습니다.");
  65. }
  66. }
  67. private static void EnsureHmacRoundTrip()
  68. {
  69. var hmacKey = new byte[32];
  70. RandomNumberGenerator.Fill(hmacKey);
  71. const string encData = "AbCdEfGhIjKlMnOpQrStUvWxYz0123456789";
  72. var integrity = NiceCrypto.ComputeIntegrity(hmacKey, encData);
  73. if (!NiceCrypto.VerifyIntegrity(hmacKey, encData, integrity))
  74. {
  75. throw new CryptographicException("HMAC ComputeIntegrity ↔ VerifyIntegrity 라운드트립 실패.");
  76. }
  77. // 다른 키로는 fail
  78. var otherKey = new byte[32];
  79. RandomNumberGenerator.Fill(otherKey);
  80. if (NiceCrypto.VerifyIntegrity(otherKey, encData, integrity))
  81. {
  82. throw new CryptographicException("HMAC VerifyIntegrity 가 잘못된 키를 통과시켰습니다.");
  83. }
  84. }
  85. private static void EnsureAesGcmRoundTrip()
  86. {
  87. var aesKey = new byte[32];
  88. RandomNumberGenerator.Fill(aesKey);
  89. const string plaintext = "{\"name\":\"홍길동\",\"birthdate\":\"19800101\",\"gender\":\"1\"}";
  90. // NICE 와 동일한 16 byte IV + ciphertext+tag 레이아웃 (BouncyCastle 기반).
  91. var encData = NiceCrypto.EncryptAesGcm(aesKey, plaintext);
  92. var decrypted = NiceCrypto.DecryptAesGcm(aesKey, encData);
  93. if (decrypted != plaintext)
  94. {
  95. throw new CryptographicException($"AES-GCM 라운드트립 실패. expected='{plaintext}', got='{decrypted}'");
  96. }
  97. // 위변조 검출: 임의 1 byte 변경 시 throw
  98. var tamperedBytes = NiceCrypto.Base64UrlDecode(encData);
  99. tamperedBytes[tamperedBytes.Length - 1] ^= 0x01;
  100. var tampered = NiceCrypto.Base64UrlEncode(tamperedBytes);
  101. try
  102. {
  103. NiceCrypto.DecryptAesGcm(aesKey, tampered);
  104. throw new CryptographicException("AES-GCM 위변조된 데이터를 무사 통과시켰습니다.");
  105. }
  106. catch (Org.BouncyCastle.Crypto.InvalidCipherTextException)
  107. {
  108. // 정상 — GCM tag 가 위변조 감지
  109. }
  110. }
  111. }