/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import {getKeyManager, KeyManagerError} from '@fluxer/api/src/federation/KeyManager'; import type {HonoEnv} from '@fluxer/api/src/types/HonoEnv'; import {DecryptionFailedError} from '@fluxer/errors/src/domains/federation/DecryptionFailedError'; import {EmptyEncryptedBodyError} from '@fluxer/errors/src/domains/federation/EmptyEncryptedBodyError'; import {EncryptionFailedError} from '@fluxer/errors/src/domains/federation/EncryptionFailedError'; import {InvalidDecryptedJsonError} from '@fluxer/errors/src/domains/federation/InvalidDecryptedJsonError'; import {InvalidEphemeralKeyError} from '@fluxer/errors/src/domains/federation/InvalidEphemeralKeyError'; import {InvalidIvError} from '@fluxer/errors/src/domains/federation/InvalidIvError'; import {MissingEphemeralKeyError} from '@fluxer/errors/src/domains/federation/MissingEphemeralKeyError'; import {MissingIvError} from '@fluxer/errors/src/domains/federation/MissingIvError'; import {createMiddleware} from 'hono/factory'; const ENCRYPTED_CONTENT_TYPE = 'application/x-fluxer-encrypted'; const EPHEMERAL_KEY_HEADER = 'x-fluxer-ephemeral-key'; const IV_HEADER = 'x-fluxer-iv'; const ENCRYPTED_RESPONSE_HEADER = 'x-fluxer-encrypted-response'; export interface EncryptionContext { decryptedBody: Uint8Array | null; isEncrypted: boolean; ephemeralPublicKey: Uint8Array | null; } declare module 'hono' { interface ContextVariableMap { encryption: EncryptionContext; } } export const EncryptionMiddleware = createMiddleware(async (ctx, next) => { const contentType = ctx.req.header('content-type'); if (contentType !== ENCRYPTED_CONTENT_TYPE) { ctx.set('encryption', { decryptedBody: null, isEncrypted: false, ephemeralPublicKey: null, }); return next(); } const ephemeralKeyBase64 = ctx.req.header(EPHEMERAL_KEY_HEADER); const ivBase64 = ctx.req.header(IV_HEADER); if (!ephemeralKeyBase64) { throw new MissingEphemeralKeyError(); } if (!ivBase64) { throw new MissingIvError(); } let ephemeralPublicKey: Uint8Array; let iv: Uint8Array; try { ephemeralPublicKey = new Uint8Array(Buffer.from(ephemeralKeyBase64, 'base64')); } catch { throw new InvalidEphemeralKeyError(); } try { iv = new Uint8Array(Buffer.from(ivBase64, 'base64')); } catch { throw new InvalidIvError(); } const encryptedBody = await ctx.req.arrayBuffer(); const ciphertext = new Uint8Array(encryptedBody); if (ciphertext.length === 0) { throw new EmptyEncryptedBodyError(); } let decryptedBody: Uint8Array; try { const keyManager = getKeyManager(); decryptedBody = await keyManager.decrypt(ciphertext, ephemeralPublicKey, iv); } catch (error) { if (error instanceof KeyManagerError) { throw new DecryptionFailedError(); } throw error; } ctx.set('encryption', { decryptedBody, isEncrypted: true, ephemeralPublicKey, }); await next(); const encryptionContext = ctx.get('encryption'); if (encryptionContext?.isEncrypted && encryptionContext.ephemeralPublicKey) { const responseBody = await ctx.res.arrayBuffer(); if (responseBody.byteLength > 0) { try { const keyManager = getKeyManager(); const {ciphertext: encryptedResponse, iv: responseIv} = await keyManager.encrypt( new Uint8Array(responseBody), encryptionContext.ephemeralPublicKey, ); const newHeaders = new Headers(ctx.res.headers); newHeaders.set('content-type', ENCRYPTED_CONTENT_TYPE); newHeaders.set(IV_HEADER, Buffer.from(responseIv).toString('base64')); newHeaders.set(ENCRYPTED_RESPONSE_HEADER, 'true'); ctx.res = new Response(encryptedResponse, { status: ctx.res.status, statusText: ctx.res.statusText, headers: newHeaders, }); } catch (error) { if (error instanceof KeyManagerError) { throw new EncryptionFailedError(); } throw error; } } } }); export function getDecryptedBody(ctx: {get: (key: 'encryption') => EncryptionContext | undefined}): Uint8Array | null { const encryptionContext = ctx.get('encryption'); return encryptionContext?.decryptedBody ?? null; } export function getDecryptedBodyAsJson(ctx: {get: (key: 'encryption') => EncryptionContext | undefined}): T | null { const decryptedBody = getDecryptedBody(ctx); if (!decryptedBody) { return null; } try { const text = new TextDecoder().decode(decryptedBody); return JSON.parse(text) as T; } catch { throw new InvalidDecryptedJsonError(); } } export function isEncryptedRequest(ctx: {get: (key: 'encryption') => EncryptionContext | undefined}): boolean { const encryptionContext = ctx.get('encryption'); return encryptionContext?.isEncrypted ?? false; }