Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 121 additions & 1 deletion graphile.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<string>;
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<Buffer>((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<string> {
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<string> {
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,
Expand All @@ -32,6 +151,7 @@ const preset: GraphileConfig.Preset = {
PgManyToManyPreset,
PgAggregatesPreset,
// PgSimplifyInflectionPreset
SystemCredentialCryptoPreset,
],
plugins: [PersistedPlugin.default, PgOmitArchivedPlugin, TagsFilePlugin],
pgServices: [
Expand Down
8 changes: 7 additions & 1 deletion schema.sql
Original file line number Diff line number Diff line change
@@ -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 -*';