ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
1import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
2import { ApiError } from "../errors";
3
4/**
5 * Token Encryption Service
6 * Encrypts sensitive OAuth tokens at rest using AES-256-GCM
7 */
8
9function getEncryptionKey(): Buffer {
10 const key = process.env.TOKEN_ENCRYPTION_KEY;
11
12 if (!key) {
13 throw new ApiError(
14 "Encryption key not configured",
15 500,
16 "TOKEN_ENCRYPTION_KEY environment variable is required",
17 );
18 }
19
20 // Expect 64-char hex string (32 bytes)
21 if (key.length !== 64) {
22 throw new ApiError(
23 "Invalid encryption key",
24 500,
25 "TOKEN_ENCRYPTION_KEY must be 64 hex characters (32 bytes)",
26 );
27 }
28
29 return Buffer.from(key, "hex");
30}
31
32interface EncryptedPayload {
33 iv: string;
34 data: string;
35 tag: string;
36}
37
38/**
39 * Encrypt sensitive data using AES-256-GCM
40 * @param data - Data to encrypt (will be JSON stringified)
41 * @returns Encrypted payload as JSON string
42 */
43export function encryptToken<T = unknown>(data: T): string {
44 try {
45 const key = getEncryptionKey();
46 const iv = randomBytes(16);
47
48 const cipher = createCipheriv("aes-256-gcm", key, iv);
49
50 const jsonData = JSON.stringify(data);
51 const encrypted = Buffer.concat([
52 cipher.update(jsonData, "utf8"),
53 cipher.final(),
54 ]);
55
56 const authTag = cipher.getAuthTag();
57
58 const payload: EncryptedPayload = {
59 iv: iv.toString("hex"),
60 data: encrypted.toString("hex"),
61 tag: authTag.toString("hex"),
62 };
63
64 return JSON.stringify(payload);
65 } catch (error) {
66 console.error("Token encryption failed:", error);
67 throw new ApiError(
68 "Failed to encrypt token",
69 500,
70 error instanceof Error ? error.message : "Unknown encryption error",
71 );
72 }
73}
74
75/**
76 * Decrypt sensitive data
77 * @param encrypted - Encrypted payload as JSON string
78 * @returns Decrypted data
79 */
80export function decryptToken<T = unknown>(encrypted: string): T {
81 try {
82 const key = getEncryptionKey();
83 const payload: EncryptedPayload = JSON.parse(encrypted);
84
85 const decipher = createDecipheriv(
86 "aes-256-gcm",
87 key,
88 Buffer.from(payload.iv, "hex"),
89 );
90
91 decipher.setAuthTag(Buffer.from(payload.tag, "hex"));
92
93 const decrypted = Buffer.concat([
94 decipher.update(Buffer.from(payload.data, "hex")),
95 decipher.final(),
96 ]);
97
98 return JSON.parse(decrypted.toString("utf8"));
99 } catch (error) {
100 console.error("Token decryption failed:", error);
101 throw new ApiError(
102 "Failed to decrypt token",
103 500,
104 error instanceof Error ? error.message : "Unknown decryption error",
105 );
106 }
107}
108
109/**
110 * Generate a new encryption key (for initial setup)
111 * Run this once and store in environment variables
112 */
113export function generateEncryptionKey(): string {
114 return randomBytes(32).toString("hex");
115}
116
117/**
118 * Check if encryption is properly configured
119 * Returns false in development if key is missing (with warning)
120 */
121export function isEncryptionConfigured(): boolean {
122 const key = process.env.TOKEN_ENCRYPTION_KEY;
123
124 if (!key) {
125 if (process.env.NODE_ENV === "production") {
126 throw new ApiError(
127 "Encryption key not configured in production",
128 500,
129 "TOKEN_ENCRYPTION_KEY is required in production",
130 );
131 }
132 console.warn(
133 "⚠️ TOKEN_ENCRYPTION_KEY not set - tokens will NOT be encrypted",
134 );
135 return false;
136 }
137
138 return true;
139}