Thomas E Lackey
3372ce29f3
This adds a new `/upload/config` endpoint to the API for uploading encrypted configuration files for later use by the deployer. The payload takes the form: ``` authorized: - accounta - accountb config: env: FOO: bar BAR: baz ``` The request is the encrypted using the deployer's public key (discoverable from its `WebappDeployer` record). This is handled automatically by `laconic-so` but can also be handled manually using standard CLI tools like `gpg` and `curl`. For example: ``` # Get the key $ laconic -c ~/.laconic/testnet-a-cercio.yml registry name resolve lrn://laconic/deployers/webapp-deployer-api.dev.vaasl.io | jq -r '.[0].attributes.publicKey' | base64 -d > webapp-deployer-api.dev.vaasl.io.pgp.pub # Import it $ gpg --import webapp-deployer-api.dev.vaasl.io.pgp.pub # Encrypt your config file. $ gpg --yes --encrypt --recipient webapp-deployer-api.dev.vaasl.io --trust-model always config.yaml # Post it $ curl -s -X POST -d '@config.yaml.gpg' https://webapp-deployer-api.dev.vaasl.io/upload/config | jq { "id": "B56C65AB96B741B7B219520A3ABFCD10" } ``` Reviewed-on: #14 Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com> Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
92 lines
2.3 KiB
TypeScript
92 lines
2.3 KiB
TypeScript
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<any> => {
|
|
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<string> {
|
|
let raw: any;
|
|
try {
|
|
raw = b64ToBytes(body);
|
|
} catch {
|
|
raw = body;
|
|
}
|
|
|
|
// We decrypt only to make sure the content is valid.
|
|
// Once we know it is good, we want to store the encrypted copy.
|
|
const obj = await decrypt(raw);
|
|
validateConfig(obj);
|
|
|
|
let id: string;
|
|
let destination: string;
|
|
do {
|
|
id = randomId();
|
|
destination = `${this.directory}/${id}`;
|
|
} while (fs.existsSync(destination));
|
|
|
|
console.log(`Wrote config to: ${destination}`);
|
|
fs.writeFileSync(destination, raw);
|
|
return id;
|
|
}
|
|
}
|