kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 39e2dfae265f26c8d6d888a560f50ab2d5d58b3f 117 lines 2.8 kB view raw
1import { 2 createCipheriv, 3 createDecipheriv, 4 createHash, 5 randomBytes, 6} from "node:crypto"; 7import { HTTPException } from "hono/http-exception"; 8 9const SECRET_PREFIX = "enc:v1:"; 10const SECRET_ALGORITHM = "aes-256-gcm"; 11const SECRET_IV_BYTES = 12; 12 13function getSecretEncryptionKey() { 14 const rawKey = process.env.NOTIFICATION_SECRET_ENCRYPTION_KEY?.trim(); 15 if (!rawKey) { 16 return null; 17 } 18 19 return createHash("sha256").update(rawKey).digest(); 20} 21 22function requireSecretEncryptionKey() { 23 const key = getSecretEncryptionKey(); 24 if (!key) { 25 throw new HTTPException(500, { 26 message: 27 "NOTIFICATION_SECRET_ENCRYPTION_KEY is required to store encrypted notification secrets", 28 }); 29 } 30 31 return key; 32} 33 34function encodePart(value: Buffer) { 35 return value.toString("base64url"); 36} 37 38function decodePart(value: string) { 39 return Buffer.from(value, "base64url"); 40} 41 42export function isEncryptedSecret(value: string | null | undefined): boolean { 43 return typeof value === "string" && value.startsWith(SECRET_PREFIX); 44} 45 46function isValidEncryptedSecret(value: string): boolean { 47 try { 48 decryptSecret(value); 49 return true; 50 } catch { 51 return false; 52 } 53} 54 55export function encryptSecret( 56 value: string | null | undefined, 57): string | null | undefined { 58 if (value === undefined || value === null) { 59 return value; 60 } 61 62 if (isEncryptedSecret(value) && isValidEncryptedSecret(value)) { 63 return value; 64 } 65 66 const iv = randomBytes(SECRET_IV_BYTES); 67 const cipher = createCipheriv( 68 SECRET_ALGORITHM, 69 requireSecretEncryptionKey(), 70 iv, 71 ); 72 const encrypted = Buffer.concat([ 73 cipher.update(value, "utf8"), 74 cipher.final(), 75 ]); 76 const authTag = cipher.getAuthTag(); 77 78 return `${SECRET_PREFIX}${encodePart(iv)}.${encodePart(authTag)}.${encodePart(encrypted)}`; 79} 80 81export function decryptSecret( 82 value: string | null | undefined, 83): string | null | undefined { 84 if (value === undefined || value === null || !isEncryptedSecret(value)) { 85 return value; 86 } 87 88 const payload = value.slice(SECRET_PREFIX.length); 89 const [iv, authTag, encrypted] = payload.split("."); 90 91 if (!iv || !authTag || !encrypted) { 92 throw new HTTPException(500, { 93 message: "Invalid encrypted notification secret payload", 94 }); 95 } 96 97 try { 98 const decipher = createDecipheriv( 99 SECRET_ALGORITHM, 100 requireSecretEncryptionKey(), 101 decodePart(iv), 102 ); 103 decipher.setAuthTag(decodePart(authTag)); 104 105 return Buffer.concat([ 106 decipher.update(decodePart(encrypted)), 107 decipher.final(), 108 ]).toString("utf8"); 109 } catch (error) { 110 if (error instanceof HTTPException) { 111 throw error; 112 } 113 throw new HTTPException(500, { 114 message: "Failed to decrypt notification secret", 115 }); 116 } 117}