File size: 2,239 Bytes
6e13570
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const crypto = require("crypto");
const { env } = require("../config/env");

const ALGORITHM = "aes-256-gcm";
const IV_LENGTH = 12; // 96-bit IV recommended for GCM

function getKey() {
  const key = Buffer.from(env.fieldEncryptionKey || "", "hex");
  if (key.length !== 32) {
    throw new Error(
      "FIELD_ENCRYPTION_KEY must be a 32-byte value encoded as 64 hex characters. " +
        "Generate one with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\""
    );
  }
  return key;
}

/**

 * Encrypts a plaintext string for storage at rest (e.g. bank wire

 * reference numbers, Payoneer transaction IDs). Returns a single

 * base64 payload containing iv + authTag + ciphertext so it can be

 * stored as one string field in MongoDB.

 */
function encryptField(plaintext) {
  if (plaintext === undefined || plaintext === null || plaintext === "") return null;
  const iv = crypto.randomBytes(IV_LENGTH);
  const cipher = crypto.createCipheriv(ALGORITHM, getKey(), iv);
  const ciphertext = Buffer.concat([cipher.update(String(plaintext), "utf8"), cipher.final()]);
  const authTag = cipher.getAuthTag();
  return Buffer.concat([iv, authTag, ciphertext]).toString("base64");
}

/**

 * Decrypts a payload produced by encryptField. Returns null if the

 * input is empty rather than throwing, so callers can treat "no

 * payment reference yet" gracefully.

 */
function decryptField(payload) {
  if (!payload) return null;
  const raw = Buffer.from(payload, "base64");
  const iv = raw.subarray(0, IV_LENGTH);
  const authTag = raw.subarray(IV_LENGTH, IV_LENGTH + 16);
  const ciphertext = raw.subarray(IV_LENGTH + 16);
  const decipher = crypto.createDecipheriv(ALGORITHM, getKey(), iv);
  decipher.setAuthTag(authTag);
  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
  return plaintext.toString("utf8");
}

/** One-way hash used for equality lookups on encrypted fields (e.g. duplicate detection) without decrypting. */
function hmacIndex(value) {
  if (!value) return null;
  return crypto.createHmac("sha256", getKey()).update(String(value)).digest("hex");
}

module.exports = { encryptField, decryptField, hmacIndex };