trails/apps/journal/app/lib/crypto.server.ts
Ullrich Schäfer 03304c354b
Add Komoot import with public (bio verification) and authenticated modes
Two-mode import: public mode verifies Komoot account ownership by checking
that the user's trails.cool profile URL appears in their Komoot bio — no
credentials stored. Authenticated mode uses email + password (AES-256-GCM
encrypted) to import private tours as well.

Includes unit tests for crypto/komoot client and E2E tests for the full
connect + import flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 10:18:46 +02:00

41 lines
1.7 KiB
TypeScript

// AES-256-GCM encrypt/decrypt for storing sensitive integration credentials.
// Key is derived from INTEGRATION_SECRET env var using scrypt.
//
// Encoded format: base64(salt_hex:iv_hex:ciphertext_hex:authtag_hex)
// All fields are hex-encoded before joining so the separator is unambiguous.
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
const SALT = "trails-cool-integrations";
const KEY_LEN = 32; // AES-256
const IV_LEN = 12; // GCM standard
function getKey(): Buffer {
const secret = process.env.INTEGRATION_SECRET;
if (!secret) throw new Error("INTEGRATION_SECRET env var is not set");
return scryptSync(secret, SALT, KEY_LEN) as Buffer;
}
export function encrypt(plaintext: string): string {
const key = getKey();
const iv = randomBytes(IV_LEN);
const cipher = createCipheriv("aes-256-gcm", key, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
const authTag = cipher.getAuthTag();
const payload = [iv.toString("hex"), encrypted.toString("hex"), authTag.toString("hex")].join(":");
return Buffer.from(payload).toString("base64");
}
export function decrypt(encoded: string): string {
const key = getKey();
const payload = Buffer.from(encoded, "base64").toString("utf8");
const parts = payload.split(":");
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
const [ivHex, encryptedHex, authTagHex] = parts as [string, string, string];
const iv = Buffer.from(ivHex, "hex");
const encrypted = Buffer.from(encryptedHex, "hex");
const authTag = Buffer.from(authTagHex, "hex");
const decipher = createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(authTag);
return decipher.update(encrypted) + decipher.final("utf8");
}