From 0865508a7a931ec5d6c27b6bb4df62e5480a1d8e Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 12 Sep 2025 12:03:05 +0100 Subject: [PATCH] Example of replacing a field with an encrypted (or otherwise processed) version --- graphile.config.ts | 122 ++++++++++++++++++++++++++++++++++++++++++++- schema.sql | 8 ++- 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/graphile.config.ts b/graphile.config.ts index 7567391..c36807c 100644 --- a/graphile.config.ts +++ b/graphile.config.ts @@ -3,7 +3,16 @@ import "graphile-config"; import { makePgService } from "@dataplan/pg/adaptors/pg"; import AmberPreset from "postgraphile/presets/amber"; import { makeV4Preset } from "postgraphile/presets/v4"; -import { makePgSmartTagsFromFilePlugin } from "postgraphile/utils"; +import { + extendSchema, + makePgSmartTagsFromFilePlugin, + wrapPlans, +} from "postgraphile/utils"; +import type { Step } from "postgraphile/grafast"; +import type { + PgInsertSingleStep, + PgUpdateSingleStep, +} from "postgraphile/@dataplan/pg"; import { PostGraphileConnectionFilterPreset } from "postgraphile-plugin-connection-filter"; import { PgAggregatesPreset } from "@graphile/pg-aggregates"; import { PgManyToManyPreset } from "@graphile-contrib/pg-many-to-many"; @@ -20,6 +29,116 @@ const __dirname = dirname(__filename); const TagsFilePlugin = makePgSmartTagsFromFilePlugin(`${__dirname}/tags.json5`); +const SystemCredentialCryptoPreset: GraphileConfig.Preset = { + plugins: [ + extendSchema((build) => { + const { + grafast: { lambda, constant }, + } = build; + return { + typeDefs: /* GraphQL */ ` + extend input SystemCredentialInput { + value: String + } + extend input SystemCredentialPatch { + value: String + } + extend type SystemCredential { + value: String + } + `, + objects: { + SystemCredential: { + plans: { + value($sysCred) { + const $secret = constant("SECRET"); // Pull from wherever makes sense + const $encrypted = $sysCred.get("value") as Step; + return lambda([$encrypted, $secret], decrypt); + }, + }, + }, + }, + }; + }, "SystemCredentialCryptoPlugin"), + wrapPlans( + (context, build, field) => { + if ( + context.scope.pgFieldResource?.name === "system_credentials" && + (context.scope.isPgCreateMutation || context.scope.isPgUpdateMutation) + ) { + return { + grafast: build.grafast, + isPatch: context.scope.isPgUpdateMutation, + }; + } + return null; + }, + (match) => (plan, _, fieldArgs) => { + const { + grafast: { constant, lambda }, + isPatch, + } = match; + + const $secret = constant("SECRET"); // Pull from wherever makes sense + + const $value = fieldArgs.getRaw([ + "input", + isPatch ? "systemCredentialPatch" : "systemCredential", + "value", + ]); + const $encrypted = lambda([$value, $secret], encrypt); + const $payload = plan(); + const $insert = $payload.get("result") as + | PgInsertSingleStep + | PgUpdateSingleStep; + $insert.set("value", $encrypted); + return $payload; + } + ), + ], +}; + +import { createCipheriv, createDecipheriv, randomBytes, scrypt } from "crypto"; + +const scryptAsync = (secret: string, salt: string, size: number) => + new Promise((resolve, reject) => + scrypt(secret, salt, size, (err, data) => + err ? reject(err) : resolve(data) + ) + ); + +const salt = "salt"; // TODO + +async function encrypt([value, secret]: readonly [ + string, + string +]): Promise { + const iv = randomBytes(16); + const key = await scryptAsync(secret, salt, 32); + const cipher = createCipheriv("aes-256-ctr", key, iv, {}); + const encrypted = Buffer.concat([ + cipher.update(value, "utf8"), + cipher.final(), + ]); + return iv.toString("hex") + ":" + encrypted.toString("hex"); +} + +async function decrypt([payload, secret]: readonly [ + string, + string +]): Promise { + const [ivHex, dataHex] = payload.split(":"); + const iv = Buffer.from(ivHex, "hex"); + const encrypted = Buffer.from(dataHex, "hex"); + const key = await scryptAsync(secret, salt, 32); + const decipher = createDecipheriv("aes-256-ctr", key, iv); + const decrypted = Buffer.concat([ + decipher.update(encrypted), + decipher.final(), + ]); + return decrypted.toString("utf8"); +} + const preset: GraphileConfig.Preset = { extends: [ AmberPreset.default ?? AmberPreset, @@ -32,6 +151,7 @@ const preset: GraphileConfig.Preset = { PgManyToManyPreset, PgAggregatesPreset, // PgSimplifyInflectionPreset + SystemCredentialCryptoPreset, ], plugins: [PersistedPlugin.default, PgOmitArchivedPlugin, TagsFilePlugin], pgServices: [ diff --git a/schema.sql b/schema.sql index 9fa576e..098602a 100644 --- a/schema.sql +++ b/schema.sql @@ -1 +1,7 @@ --- Create your database schema here +create table system_credentials ( + id int primary key generated always as identity, + name text not null unique, + value text not null +); + +comment on column system_credentials.value is '@behavior -*';