mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
123 lines
3.6 KiB
TypeScript
123 lines
3.6 KiB
TypeScript
import crypto from "crypto";
|
|
|
|
/**
|
|
* Derives a 32-byte encryption key from the environment variable using SHA-256
|
|
* @param keyMaterial - The raw key material from environment
|
|
* @returns 32-byte Buffer for AES-256
|
|
*/
|
|
function deriveKey(keyMaterial: string): Buffer {
|
|
return crypto.createHash("sha256").update(keyMaterial, "utf8").digest();
|
|
}
|
|
|
|
/**
|
|
* Checks if a token is already encrypted by detecting the encrypted format
|
|
* @param token - Token to check
|
|
* @returns true if already encrypted
|
|
*/
|
|
function isTokenEncrypted(token: string): boolean {
|
|
if (!token) return false;
|
|
|
|
// Check for encrypted format (iv:encrypted:authTag - 3 parts, all hex)
|
|
const parts = token.split(":");
|
|
if (parts.length === 3) {
|
|
const [iv, encrypted, authTag] = parts;
|
|
// New format: 24-char hex IV (12 bytes), hex encrypted data, 32-char hex authTag (16 bytes)
|
|
return (
|
|
/^[0-9a-f]{24}$/.test(iv) &&
|
|
/^[0-9a-f]+$/.test(encrypted) &&
|
|
/^[0-9a-f]{32}$/.test(authTag)
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function encryptSlackToken(token: string): string {
|
|
if (!token) return "";
|
|
|
|
// Avoid double-encryption with improved detection
|
|
if (isTokenEncrypted(token)) {
|
|
return token;
|
|
}
|
|
|
|
const encryptionKey = process.env.NEXT_PRIVATE_SLACK_ENCRYPTION_KEY;
|
|
if (!encryptionKey) {
|
|
throw new Error(
|
|
"NEXT_PRIVATE_SLACK_ENCRYPTION_KEY environment variable is required for token encryption",
|
|
);
|
|
}
|
|
|
|
// Derive key using raw SHA-256 digest Buffer
|
|
const key = deriveKey(encryptionKey);
|
|
|
|
// Use 12-byte IV for GCM (recommended size)
|
|
const iv = crypto.randomBytes(12);
|
|
|
|
// Create AES-256-GCM cipher
|
|
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
|
|
// Encrypt the token
|
|
let encrypted = cipher.update(token, "utf8", "hex");
|
|
encrypted += cipher.final("hex");
|
|
|
|
// Get authentication tag
|
|
const authTag = cipher.getAuthTag();
|
|
|
|
// Format: iv:encrypted:authTag (all hex-encoded)
|
|
return `${iv.toString("hex")}:${encrypted}:${authTag.toString("hex")}`;
|
|
}
|
|
|
|
export function decryptSlackToken(encryptedToken: string): string {
|
|
if (!encryptedToken) return "";
|
|
|
|
const encryptionKey = process.env.NEXT_PRIVATE_SLACK_ENCRYPTION_KEY;
|
|
if (!encryptionKey) {
|
|
throw new Error(
|
|
"NEXT_PRIVATE_SLACK_ENCRYPTION_KEY environment variable is required for token decryption",
|
|
);
|
|
}
|
|
|
|
// Derive key using raw SHA-256 digest Buffer
|
|
const key = deriveKey(encryptionKey);
|
|
|
|
const parts = encryptedToken.split(":");
|
|
|
|
// Handle new GCM format: iv:encrypted:authTag
|
|
if (parts.length === 3) {
|
|
try {
|
|
const [ivHex, encryptedHex, authTagHex] = parts;
|
|
|
|
// Parse components
|
|
const iv = Buffer.from(ivHex, "hex");
|
|
const encrypted = Buffer.from(encryptedHex, "hex");
|
|
const authTag = Buffer.from(authTagHex, "hex");
|
|
|
|
// Validate sizes
|
|
if (iv.length !== 12) {
|
|
throw new Error("Invalid IV length for GCM");
|
|
}
|
|
if (authTag.length !== 16) {
|
|
throw new Error("Invalid auth tag length for GCM");
|
|
}
|
|
|
|
// Create decipher and set auth tag
|
|
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
decipher.setAuthTag(authTag);
|
|
|
|
// Decrypt
|
|
let decrypted = decipher.update(encrypted, undefined, "utf8");
|
|
decrypted += decipher.final("utf8");
|
|
|
|
return decrypted;
|
|
} catch (error) {
|
|
// Surface authentication failures as decryption errors
|
|
throw new Error(
|
|
`Token decryption failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// If not in expected format, return as-is (might be plaintext)
|
|
return encryptedToken;
|
|
}
|