crypto.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. package payment
  2. import (
  3. "crypto/aes"
  4. "crypto/cipher"
  5. "crypto/rand"
  6. "encoding/base64"
  7. "fmt"
  8. "io"
  9. "strings"
  10. )
  11. // AES256KeySize is the required key length (in bytes) for AES-256-GCM.
  12. const AES256KeySize = 32
  13. // Encrypt encrypts plaintext using AES-256-GCM with the given 32-byte key.
  14. // The output format is "iv:authTag:ciphertext" where each component is base64-encoded,
  15. // matching the Node.js crypto.ts format for cross-compatibility.
  16. //
  17. // Deprecated: payment provider configs are now stored as plaintext JSON.
  18. // This function is kept only for seeding legacy ciphertext in tests and for
  19. // the transitional Decrypt fallback. Scheduled for removal after all live
  20. // deployments complete migration by re-saving their configs.
  21. func Encrypt(plaintext string, key []byte) (string, error) {
  22. if len(key) != AES256KeySize {
  23. return "", fmt.Errorf("encryption key must be %d bytes, got %d", AES256KeySize, len(key))
  24. }
  25. block, err := aes.NewCipher(key)
  26. if err != nil {
  27. return "", fmt.Errorf("create AES cipher: %w", err)
  28. }
  29. gcm, err := cipher.NewGCM(block)
  30. if err != nil {
  31. return "", fmt.Errorf("create GCM: %w", err)
  32. }
  33. nonce := make([]byte, gcm.NonceSize()) // 12 bytes for GCM
  34. if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
  35. return "", fmt.Errorf("generate nonce: %w", err)
  36. }
  37. // Seal appends the ciphertext + auth tag
  38. sealed := gcm.Seal(nil, nonce, []byte(plaintext), nil)
  39. // Split sealed into ciphertext and auth tag (last 16 bytes)
  40. tagSize := gcm.Overhead()
  41. ciphertext := sealed[:len(sealed)-tagSize]
  42. authTag := sealed[len(sealed)-tagSize:]
  43. // Format: iv:authTag:ciphertext (all base64)
  44. return fmt.Sprintf("%s:%s:%s",
  45. base64.StdEncoding.EncodeToString(nonce),
  46. base64.StdEncoding.EncodeToString(authTag),
  47. base64.StdEncoding.EncodeToString(ciphertext),
  48. ), nil
  49. }
  50. // Decrypt decrypts a ciphertext string produced by Encrypt.
  51. // The input format is "iv:authTag:ciphertext" where each component is base64-encoded.
  52. //
  53. // Deprecated: payment provider configs are now stored as plaintext JSON.
  54. // This function remains only as a read-path fallback for pre-migration
  55. // ciphertext records. Scheduled for removal once all deployments re-save
  56. // their provider configs through the admin UI.
  57. func Decrypt(ciphertext string, key []byte) (string, error) {
  58. if len(key) != AES256KeySize {
  59. return "", fmt.Errorf("encryption key must be %d bytes, got %d", AES256KeySize, len(key))
  60. }
  61. parts := strings.SplitN(ciphertext, ":", 3)
  62. if len(parts) != 3 {
  63. return "", fmt.Errorf("invalid ciphertext format: expected iv:authTag:ciphertext")
  64. }
  65. nonce, err := base64.StdEncoding.DecodeString(parts[0])
  66. if err != nil {
  67. return "", fmt.Errorf("decode IV: %w", err)
  68. }
  69. authTag, err := base64.StdEncoding.DecodeString(parts[1])
  70. if err != nil {
  71. return "", fmt.Errorf("decode auth tag: %w", err)
  72. }
  73. encrypted, err := base64.StdEncoding.DecodeString(parts[2])
  74. if err != nil {
  75. return "", fmt.Errorf("decode ciphertext: %w", err)
  76. }
  77. block, err := aes.NewCipher(key)
  78. if err != nil {
  79. return "", fmt.Errorf("create AES cipher: %w", err)
  80. }
  81. gcm, err := cipher.NewGCM(block)
  82. if err != nil {
  83. return "", fmt.Errorf("create GCM: %w", err)
  84. }
  85. // Reconstruct the sealed data: ciphertext + authTag
  86. sealed := append(encrypted, authTag...)
  87. plaintext, err := gcm.Open(nil, nonce, sealed, nil)
  88. if err != nil {
  89. return "", fmt.Errorf("decrypt: %w", err)
  90. }
  91. return string(plaintext), nil
  92. }