aboutsummaryrefslogtreecommitdiff
path: root/apps/backend/src/utils/cipher.ts
blob: 3ba2e905deba24821b169291536ee481f6c8121d (plain) (blame)
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
async function encrypt(data: string, key: string): Promise<string> {
  try {
    const encoder = new TextEncoder();
    const encodedData = encoder.encode(data);

    const baseForIv = encoder.encode(data + key);
    const ivHash = await crypto.subtle.digest('SHA-256', baseForIv);
    const iv = new Uint8Array(ivHash).slice(0, 12);

    const cryptoKey = await crypto.subtle.importKey(
      "raw",
      encoder.encode(key),
      { name: "AES-GCM", length: 256 },
      false,
      ["encrypt", "decrypt"]
    );

    const encrypted = await crypto.subtle.encrypt(
      { name: "AES-GCM", iv: new Uint8Array(iv).buffer as ArrayBuffer },
      cryptoKey,
      encodedData
    );

    const combined = new Uint8Array([...iv, ...new Uint8Array(encrypted)]);

    // Convert to base64 safely
    const base64 = Buffer.from(combined).toString("base64");

    // Make URL-safe
    return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
  } catch (err) {
    console.error("Encryption error:", err);
    throw err;
  }
}

async function decrypt(encryptedData: string, key: string): Promise<string> {
  try {
    // Restore base64 padding and convert URL-safe chars
    const base64 = encryptedData
      .replace(/-/g, "+")
      .replace(/_/g, "/")
      .padEnd(
        encryptedData.length + ((4 - (encryptedData.length % 4)) % 4),
        "="
      );

    // Use Buffer for safer base64 decoding
    const combined = Buffer.from(base64, "base64");
    const combinedArray = new Uint8Array(combined);

    // Extract the IV that was used for encryption
    const iv = combinedArray.slice(0, 12);
    const encrypted = combinedArray.slice(12);

    // Import the same key used for encryption
    const cryptoKey = await crypto.subtle.importKey(
      "raw",
      new TextEncoder().encode(key),
      { name: "AES-GCM", length: 256 },
      false,
      ["encrypt", "decrypt"]
    );

    // Use the extracted IV and key to decrypt
    const decrypted = await crypto.subtle.decrypt(
      { name: "AES-GCM", iv: new Uint8Array(iv).buffer as ArrayBuffer },
      cryptoKey,
      encrypted.buffer as ArrayBuffer
    );

    return new TextDecoder().decode(decrypted);
  } catch (err) {
    console.error("Decryption error:", err);
    throw err;
  }
}

export { encrypt, decrypt };