mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
Add an opt-in to download the full backup as an encrypted archive instead of a plaintext JSON file. The user supplies a passphrase over the request; the server derives a 256-bit key with Argon2id and seals the export bytes with AES-256-GCM into a self-describing HLX1 archive (magic, version, KDF params, salt, iv, tag, ciphertext), mirroring the off-host backup envelope. POST /api/export/encrypted returns the binary archive and is step-up gated for accounts with a second factor; single-factor accounts use a normal session. The passphrase is never stored and never logged, and there is no server-side recovery — the UI states this. A standalone decryptor (scripts/decrypt-export.ts) opens the archive with the passphrase outside the app. The plaintext full-backup route now shares one payload builder so both variants emit the same restore-compatible shape.
52 lines
2.2 KiB
TypeScript
52 lines
2.2 KiB
TypeScript
/**
|
|
* OpenAPI route table — generic data export (passphrase-encrypted variant).
|
|
*
|
|
* The plaintext `GET /api/export/full-backup` + per-type CSV routes predate the
|
|
* OpenAPI registry and are not iOS-contract surfaces; this module documents the
|
|
* v1.23 passphrase-encrypted variant because it adds a request flag + a binary
|
|
* response shape the iOS client may consume.
|
|
*
|
|
* Part of the OpenAPI route table; aggregated in `./index.ts`.
|
|
*/
|
|
import { z } from "zod/v4";
|
|
import type { ZodOpenApiObject } from "zod-openapi";
|
|
import { stdResponses } from "./shared";
|
|
|
|
const encryptedExportRequest = z
|
|
.object({
|
|
passphrase: z.string().min(12).max(1024).meta({
|
|
description:
|
|
"User-chosen passphrase (>= 12 chars). Derives the archive key via Argon2id; never stored, never logged, no server-side recovery.",
|
|
}),
|
|
})
|
|
.meta({ id: "EncryptedExportRequest" });
|
|
|
|
export const exportPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
|
"/api/export/encrypted": {
|
|
post: {
|
|
tags: ["Export"],
|
|
summary: "Download the full backup as a passphrase-encrypted archive",
|
|
description:
|
|
"v1.23. Same restore-compatible payload as the plaintext full backup, sealed into an `HLX1` archive (Argon2id-derived key + AES-256-GCM) under the supplied `passphrase`. Returns `application/octet-stream` (a `.hlx` file). When the account has a second factor enrolled, the call is step-up gated (`requireFreshMfa`, cookie-only) and returns 401 `auth.stepup.required` without a fresh factor; single-factor accounts use a normal session or Bearer. Shared `export:<userId>` rate bucket (10/h). The passphrase is never stored — a forgotten passphrase makes the archive unrecoverable.",
|
|
requestBody: {
|
|
required: true,
|
|
content: {
|
|
"application/json": { schema: encryptedExportRequest },
|
|
},
|
|
},
|
|
responses: {
|
|
"200": {
|
|
description:
|
|
"Encrypted archive. `application/octet-stream`; the body is the `HLX1` binary (magic | version | Argon2 params | salt | iv | tag | ciphertext).",
|
|
content: {
|
|
"application/octet-stream": {
|
|
schema: z.string().meta({ format: "binary" }),
|
|
},
|
|
},
|
|
},
|
|
...stdResponses,
|
|
},
|
|
},
|
|
},
|
|
};
|