File size: 2,727 Bytes
4930c7d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3666265
 
 
 
 
 
 
4930c7d
3666265
4930c7d
 
 
 
 
 
 
 
 
 
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
const crypto = require("node:crypto");

function toBase64Url(buffer) {
  return Buffer.from(buffer).toString("base64url");
}

function fromBase64Url(value) {
  return Buffer.from(value, "base64url");
}

function deriveMasterKey(encryptionKey) {
  return crypto.scryptSync(encryptionKey, "hf-home-master-key", 32);
}

function deriveTokenKey(masterKey, sessionSecret, sessionId) {
  return crypto.hkdfSync(
    "sha256",
    Buffer.from(sessionSecret, "utf8"),
    masterKey,
    Buffer.from(`hf-home:${sessionId}`, "utf8"),
    32,
  );
}

function encryptText(plainText, key, aad = "") {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);

  if (aad) {
    cipher.setAAD(Buffer.from(aad, "utf8"));
  }

  const ciphertext = Buffer.concat([cipher.update(plainText, "utf8"), cipher.final()]);
  const tag = cipher.getAuthTag();
  return `${toBase64Url(iv)}.${toBase64Url(ciphertext)}.${toBase64Url(tag)}`;
}

function decryptText(payload, key, aad = "") {
  const [ivPart, ciphertextPart, tagPart] = String(payload).split(".");
  if (!ivPart || !ciphertextPart || !tagPart) {
    throw new Error("Invalid encrypted payload.");
  }

  const decipher = crypto.createDecipheriv("aes-256-gcm", key, fromBase64Url(ivPart));
  if (aad) {
    decipher.setAAD(Buffer.from(aad, "utf8"));
  }
  decipher.setAuthTag(fromBase64Url(tagPart));

  const plaintext = Buffer.concat([
    decipher.update(fromBase64Url(ciphertextPart)),
    decipher.final(),
  ]);

  return plaintext.toString("utf8");
}

function encryptJson(value, key, aad = "") {
  return encryptText(JSON.stringify(value), key, aad);
}

function decryptJson(payload, key, aad = "") {
  return JSON.parse(decryptText(payload, key, aad));
}

function hashSessionSecret(secret, salt = crypto.randomBytes(16)) {
  const hash = crypto.scryptSync(secret, salt, 64);
  return {
    salt: toBase64Url(salt),
    hash: toBase64Url(hash),
  };
}

function verifySessionSecret(secret, expectedHash, salt) {
  const actual = crypto.scryptSync(secret, fromBase64Url(salt), 64);
  const expected = fromBase64Url(expectedHash);

  if (actual.length !== expected.length) {
    return false;
  }

  return crypto.timingSafeEqual(actual, expected);
}

function generateToken(size = 32) {
  return toBase64Url(crypto.randomBytes(size));
}

function createCsrfToken(masterKey, sessionId, sessionSecret) {
  return crypto
    .createHmac("sha256", masterKey)
    .update(`csrf:${sessionId}:${sessionSecret}`, "utf8")
    .digest("base64url");
}

module.exports = {
  createCsrfToken,
  decryptJson,
  decryptText,
  deriveMasterKey,
  deriveTokenKey,
  encryptJson,
  encryptText,
  generateToken,
  hashSessionSecret,
  verifySessionSecret,
};