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.

fix: harden secret encryption guard and normalize error handling

- Validate enc:v1: prefixed values by attempting decryption before
skipping encryption, preventing users from injecting fake ciphertext
- Replace raw Error throws with HTTPException (500) so crypto failures
return proper API responses instead of leaking internal errors
- Wrap decipher block in try-catch to normalize unexpected crypto errors
- Add Gotify to troubleshooting docs alongside ntfy and webhook

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Tin 9634b592 4f349ec8

+39 -17
+37 -15
apps/api/src/notification-preferences/secrets.ts
··· 4 4 createHash, 5 5 randomBytes, 6 6 } from "node:crypto"; 7 + import { HTTPException } from "hono/http-exception"; 7 8 8 9 const SECRET_PREFIX = "enc:v1:"; 9 10 const SECRET_ALGORITHM = "aes-256-gcm"; ··· 21 22 function requireSecretEncryptionKey() { 22 23 const key = getSecretEncryptionKey(); 23 24 if (!key) { 24 - throw new Error( 25 - "NOTIFICATION_SECRET_ENCRYPTION_KEY is required to store encrypted notification secrets", 26 - ); 25 + throw new HTTPException(500, { 26 + message: 27 + "NOTIFICATION_SECRET_ENCRYPTION_KEY is required to store encrypted notification secrets", 28 + }); 27 29 } 28 30 29 31 return key; ··· 41 43 return typeof value === "string" && value.startsWith(SECRET_PREFIX); 42 44 } 43 45 46 + function isValidEncryptedSecret(value: string): boolean { 47 + try { 48 + decryptSecret(value); 49 + return true; 50 + } catch { 51 + return false; 52 + } 53 + } 54 + 44 55 export function encryptSecret( 45 56 value: string | null | undefined, 46 57 ): string | null | undefined { ··· 48 59 return value; 49 60 } 50 61 51 - if (isEncryptedSecret(value)) { 62 + if (isEncryptedSecret(value) && isValidEncryptedSecret(value)) { 52 63 return value; 53 64 } 54 65 ··· 78 89 const [iv, authTag, encrypted] = payload.split("."); 79 90 80 91 if (!iv || !authTag || !encrypted) { 81 - throw new Error("Invalid encrypted notification secret payload"); 92 + throw new HTTPException(500, { 93 + message: "Invalid encrypted notification secret payload", 94 + }); 82 95 } 83 96 84 - const decipher = createDecipheriv( 85 - SECRET_ALGORITHM, 86 - requireSecretEncryptionKey(), 87 - decodePart(iv), 88 - ); 89 - decipher.setAuthTag(decodePart(authTag)); 97 + try { 98 + const decipher = createDecipheriv( 99 + SECRET_ALGORITHM, 100 + requireSecretEncryptionKey(), 101 + decodePart(iv), 102 + ); 103 + decipher.setAuthTag(decodePart(authTag)); 90 104 91 - return Buffer.concat([ 92 - decipher.update(decodePart(encrypted)), 93 - decipher.final(), 94 - ]).toString("utf8"); 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 + } 95 117 }
+2 -2
apps/docs/core/functional/account-notifications.mdx
··· 130 130 - `Gotify` server URLs 131 131 - custom webhook URLs 132 132 133 - If you are using a self-hosted ntfy instance or webhook receiver, it must be reachable from the API with an accepted public or routable hostname. 133 + If you are using a self-hosted ntfy instance, Gotify instance, or webhook receiver, it must be reachable from the API with an accepted public or routable hostname. 134 134 135 135 ## Troubleshooting 136 136 ··· 152 152 - the signed-in user has an email address 153 153 - the server can reach the SMTP provider 154 154 155 - ### ntfy or webhook save fails 155 + ### ntfy, Gotify, or webhook save fails 156 156 157 157 Check that: 158 158