From 2cce37ef1fa8a2f05495906bc4c3f92f8963fbdc Mon Sep 17 00:00:00 2001 From: Thomas E Lackey Date: Sat, 24 Aug 2024 00:04:23 -0500 Subject: [PATCH] WIP: Config upload --- src/upload.ts | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/upload.ts diff --git a/src/upload.ts b/src/upload.ts new file mode 100644 index 0000000..7b254fc --- /dev/null +++ b/src/upload.ts @@ -0,0 +1,90 @@ +import crypto from 'crypto'; +import fs from 'fs'; +import assert from 'node:assert'; +import openpgp from 'openpgp'; +import YAML from 'yaml'; +import { atob } from 'node:buffer'; + +import { Config } from './config.js'; + +let privateKey: openpgp.PrivateKey | null = null; + +const loadPrivateKey = async () => { + if (null == privateKey) { + privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readPrivateKey({ + binaryKey: fs.readFileSync(Config.OPENPGP_PRIVATE_KEY_FILE) + }), + passphrase: Config.OPENPGP_PASSPHRASE, + }); + } + return privateKey; +} + +const randomId = (): string => + crypto + .randomUUID({ disableEntropyCache: true }) + .replaceAll('-', '') + .toUpperCase(); + +const validateConfig = (obj): undefined => { + assert(obj.authorized, "'authorized' is required"); + assert(Array.isArray(obj.authorized), "'authorized' must be an array"); + assert(obj.authorized.length >= 1, "'authorized' cannot be empty"); + assert(obj.config, "'config' is required"); +}; + +export const b64ToBytes = (base64): Uint8Array => { + const binaryString = atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; +}; + + + + +const decrypt = async (binaryMessage: Uint8Array): Promise => { + const message = await openpgp.readMessage({ + binaryMessage, + }); + + const { data } = await openpgp.decrypt({ + message, + decryptionKeys: await loadPrivateKey(), + }); + + const config = data.toString(); + return config.charAt(0) === '{' ? JSON.parse(config) : YAML.parse(config); +}; + +export class Uploader { + directory: string; + + constructor(dir: string) { + this.directory = dir; + } + + async upload(body: string | Uint8Array): Promise { + let raw: any; + try { + raw = b64ToBytes(body); + } catch { + raw = body; + } + const obj = await decrypt(raw); + validateConfig(obj); + + let id = randomId(); + let destination: string; + do { + id = randomId(); + destination = `${this.directory}/${id}`; + } while (fs.existsSync(destination)); + + fs.writeFileSync(destination, raw); + return id; + } +}