···11+import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
22+import { ApiError } from "../errors";
33+44+/**
55+ * Token Encryption Service
66+ * Encrypts sensitive OAuth tokens at rest using AES-256-GCM
77+ */
88+99+function getEncryptionKey(): Buffer {
1010+ const key = process.env.TOKEN_ENCRYPTION_KEY;
1111+1212+ if (!key) {
1313+ throw new ApiError(
1414+ "Encryption key not configured",
1515+ 500,
1616+ "TOKEN_ENCRYPTION_KEY environment variable is required",
1717+ );
1818+ }
1919+2020+ // Expect 64-char hex string (32 bytes)
2121+ if (key.length !== 64) {
2222+ throw new ApiError(
2323+ "Invalid encryption key",
2424+ 500,
2525+ "TOKEN_ENCRYPTION_KEY must be 64 hex characters (32 bytes)",
2626+ );
2727+ }
2828+2929+ return Buffer.from(key, "hex");
3030+}
3131+3232+interface EncryptedPayload {
3333+ iv: string;
3434+ data: string;
3535+ tag: string;
3636+}
3737+3838+/**
3939+ * Encrypt sensitive data using AES-256-GCM
4040+ * @param data - Data to encrypt (will be JSON stringified)
4141+ * @returns Encrypted payload as JSON string
4242+ */
4343+export function encryptToken(data: any): string {
4444+ try {
4545+ const key = getEncryptionKey();
4646+ const iv = randomBytes(16);
4747+4848+ const cipher = createCipheriv("aes-256-gcm", key, iv);
4949+5050+ const jsonData = JSON.stringify(data);
5151+ const encrypted = Buffer.concat([
5252+ cipher.update(jsonData, "utf8"),
5353+ cipher.final(),
5454+ ]);
5555+5656+ const authTag = cipher.getAuthTag();
5757+5858+ const payload: EncryptedPayload = {
5959+ iv: iv.toString("hex"),
6060+ data: encrypted.toString("hex"),
6161+ tag: authTag.toString("hex"),
6262+ };
6363+6464+ return JSON.stringify(payload);
6565+ } catch (error) {
6666+ console.error("Token encryption failed:", error);
6767+ throw new ApiError(
6868+ "Failed to encrypt token",
6969+ 500,
7070+ error instanceof Error ? error.message : "Unknown encryption error",
7171+ );
7272+ }
7373+}
7474+7575+/**
7676+ * Decrypt sensitive data
7777+ * @param encrypted - Encrypted payload as JSON string
7878+ * @returns Decrypted data
7979+ */
8080+export function decryptToken(encrypted: string): any {
8181+ try {
8282+ const key = getEncryptionKey();
8383+ const payload: EncryptedPayload = JSON.parse(encrypted);
8484+8585+ const decipher = createDecipheriv(
8686+ "aes-256-gcm",
8787+ key,
8888+ Buffer.from(payload.iv, "hex"),
8989+ );
9090+9191+ decipher.setAuthTag(Buffer.from(payload.tag, "hex"));
9292+9393+ const decrypted = Buffer.concat([
9494+ decipher.update(Buffer.from(payload.data, "hex")),
9595+ decipher.final(),
9696+ ]);
9797+9898+ return JSON.parse(decrypted.toString("utf8"));
9999+ } catch (error) {
100100+ console.error("Token decryption failed:", error);
101101+ throw new ApiError(
102102+ "Failed to decrypt token",
103103+ 500,
104104+ error instanceof Error ? error.message : "Unknown decryption error",
105105+ );
106106+ }
107107+}
108108+109109+/**
110110+ * Generate a new encryption key (for initial setup)
111111+ * Run this once and store in environment variables
112112+ */
113113+export function generateEncryptionKey(): string {
114114+ return randomBytes(32).toString("hex");
115115+}
116116+117117+/**
118118+ * Check if encryption is properly configured
119119+ * Returns false in development if key is missing (with warning)
120120+ */
121121+export function isEncryptionConfigured(): boolean {
122122+ const key = process.env.TOKEN_ENCRYPTION_KEY;
123123+124124+ if (!key) {
125125+ if (process.env.NODE_ENV === "production") {
126126+ throw new ApiError(
127127+ "Encryption key not configured in production",
128128+ 500,
129129+ "TOKEN_ENCRYPTION_KEY is required in production",
130130+ );
131131+ }
132132+ console.warn(
133133+ "⚠️ TOKEN_ENCRYPTION_KEY not set - tokens will NOT be encrypted",
134134+ );
135135+ return false;
136136+ }
137137+138138+ return true;
139139+}
+4
packages/api/src/utils/index.ts
···11+export * from "./response.utils";
22+export * from "./string.utils";
33+export * from "./encryption.utils";
44+export * from "./validation.utils";