Spaces:
Running
on
Inf2
Running
on
Inf2
Andrew
commited on
Commit
·
70025fa
1
Parent(s):
b346517
feat(security): add AES-256-GCM encryption for user tokens
Browse files
src/lib/server/tokenEncryption.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import crypto from "crypto";
|
| 2 |
+
import { config } from "./config";
|
| 3 |
+
import { logger } from "./logger";
|
| 4 |
+
|
| 5 |
+
const ALGORITHM = "aes-256-gcm";
|
| 6 |
+
const IV_LENGTH = 16;
|
| 7 |
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
| 8 |
+
const AUTH_TAG_LENGTH = 16;
|
| 9 |
+
const KEY_LENGTH = 32;
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Gets the encryption key from config or generates a warning
|
| 13 |
+
*/
|
| 14 |
+
function getEncryptionKey(): Buffer {
|
| 15 |
+
const key = config.HF_TOKEN_ENCRYPTION_KEY;
|
| 16 |
+
if (!key) {
|
| 17 |
+
logger.warn(
|
| 18 |
+
"HF_TOKEN_ENCRYPTION_KEY not set. Tokens will be stored unencrypted. Set this to a 32-byte hex string for production."
|
| 19 |
+
);
|
| 20 |
+
// For development, use a default key (not secure, but allows testing)
|
| 21 |
+
const defaultKey = crypto.randomBytes(KEY_LENGTH).toString("hex");
|
| 22 |
+
logger.warn(`Using temporary encryption key: ${defaultKey}`);
|
| 23 |
+
return Buffer.from(defaultKey.slice(0, KEY_LENGTH * 2), "hex");
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
if (key.length !== KEY_LENGTH * 2) {
|
| 27 |
+
throw new Error(`HF_TOKEN_ENCRYPTION_KEY must be exactly ${KEY_LENGTH * 2} characters`);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
return Buffer.from(key, "hex");
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Encrypts a token using AES-256-GCM with a random IV
|
| 35 |
+
*/
|
| 36 |
+
export function encryptToken(token: string): string {
|
| 37 |
+
const key = getEncryptionKey();
|
| 38 |
+
const iv = crypto.randomBytes(IV_LENGTH);
|
| 39 |
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
| 40 |
+
|
| 41 |
+
let encrypted = cipher.update(token, "utf8", "hex");
|
| 42 |
+
encrypted += cipher.final("hex");
|
| 43 |
+
|
| 44 |
+
const authTag = cipher.getAuthTag();
|
| 45 |
+
|
| 46 |
+
// Combine IV + authTag + encrypted data
|
| 47 |
+
return iv.toString("hex") + ":" + authTag.toString("hex") + ":" + encrypted;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/**
|
| 51 |
+
* Decrypts a token that was encrypted with encryptToken
|
| 52 |
+
*/
|
| 53 |
+
export function decryptToken(encryptedToken: string): string {
|
| 54 |
+
const key = getEncryptionKey();
|
| 55 |
+
const parts = encryptedToken.split(":");
|
| 56 |
+
|
| 57 |
+
if (parts.length !== 3) {
|
| 58 |
+
throw new Error("Invalid encrypted token format");
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
const iv = Buffer.from(parts[0], "hex");
|
| 62 |
+
const authTag = Buffer.from(parts[1], "hex");
|
| 63 |
+
const encrypted = parts[2];
|
| 64 |
+
|
| 65 |
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
| 66 |
+
decipher.setAuthTag(authTag);
|
| 67 |
+
|
| 68 |
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
| 69 |
+
decrypted += decipher.final("utf8");
|
| 70 |
+
|
| 71 |
+
return decrypted;
|
| 72 |
+
}
|