Skip to content

Multi-environment encryption key & team member separation #158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions app-config-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@app-config/generate": "^2.6.0",
"@app-config/logging": "^2.6.0",
"@app-config/node": "^2.6.0",
"@app-config/meta": "^2.6.0",
"@app-config/schema": "^2.6.0",
"@app-config/utils": "^2.6.0",
"ajv": "7",
Expand Down
93 changes: 80 additions & 13 deletions app-config-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
FailedToSelectSubObject,
EmptyStdinOrPromptResponse,
} from '@app-config/core';
import { promptUser, consumeStdin } from '@app-config/node';
import { promptUser, consumeStdin, asEnvOptions } from '@app-config/node';
import { checkTTY, LogLevel, logger } from '@app-config/logging';
import {
LoadedConfiguration,
Expand Down Expand Up @@ -47,6 +47,7 @@ import {
} from '@app-config/encryption';
import { loadSchema, JSONSchema } from '@app-config/schema';
import { generateTypeFiles } from '@app-config/generate';
import { loadMetaConfigLazy } from '@app-config/meta';
import { validateAllConfigVariants } from './validation';

enum OptionGroups {
Expand Down Expand Up @@ -318,6 +319,21 @@ function fileTypeForFormatOption(option: string): FileType {
}
}

async function loadEnvironmentOptions(opts: {
environmentOverride?: string;
environmentVariableName?: string;
}) {
const {
value: { environmentAliases, environmentSourceNames },
} = await loadMetaConfigLazy();

return asEnvOptions(
opts.environmentOverride,
environmentAliases,
opts.environmentVariableName ?? environmentSourceNames,
);
}

export const cli = yargs
.scriptName('app-config')
.wrap(Math.max(yargs.terminalWidth() - 5, 80))
Expand Down Expand Up @@ -577,13 +593,19 @@ export const cli = yargs
'Creates properties in meta file, making you the first trusted user',
],
],
options: {
environmentOverride: environmentOverrideOption,
environmentVariableName: environmentVariableNameOption,
},
},
async () => {
async (opts) => {
const environmentOptions = await loadEnvironmentOptions(opts);

const myKey = await loadPublicKeyLazy();
const privateKey = await loadPrivateKeyLazy();

// we trust ourselves, essentially
await trustTeamMember(myKey, privateKey);
await trustTeamMember(myKey, privateKey, environmentOptions);
logger.info('Initialized team members and a symmetric key');
},
),
Expand All @@ -599,10 +621,16 @@ export const cli = yargs
'Sets up a new symmetric key with the latest revision number',
],
],
options: {
environmentOverride: environmentOverrideOption,
environmentVariableName: environmentVariableNameOption,
},
},
async () => {
const keys = await loadSymmetricKeys();
const teamMembers = await loadTeamMembersLazy();
async (opts) => {
const environmentOptions = await loadEnvironmentOptions(opts);

const keys = await loadSymmetricKeys(undefined, environmentOptions);
const teamMembers = await loadTeamMembersLazy(environmentOptions);

let revision: number;

Expand All @@ -612,7 +640,12 @@ export const cli = yargs
revision = 1;
}

await saveNewSymmetricKey(await generateSymmetricKey(revision), teamMembers);
await saveNewSymmetricKey(
await generateSymmetricKey(revision),
teamMembers,
environmentOptions,
);

logger.info(`Saved a new symmetric key, revision ${revision}`);
},
),
Expand Down Expand Up @@ -670,12 +703,23 @@ export const cli = yargs
name: 'ci',
description:
'Creates an encryption key that can be used without a passphrase (useful for CI)',
options: {
environmentOverride: environmentOverrideOption,
environmentVariableName: environmentVariableNameOption,
},
},
async () => {
async (opts) => {
const environmentOptions = await loadEnvironmentOptions(opts);

logger.info('Creating a new trusted CI encryption key');

const { privateKeyArmored, publicKeyArmored } = await initializeKeys(false);
await trustTeamMember(await loadKey(publicKeyArmored), await loadPrivateKeyLazy());

await trustTeamMember(
await loadKey(publicKeyArmored),
await loadPrivateKeyLazy(),
environmentOptions,
);

process.stdout.write(`\n${publicKeyArmored}\n\n${privateKeyArmored}\n\n`);

Expand Down Expand Up @@ -708,11 +752,17 @@ export const cli = yargs
description: 'Filepath of public key',
},
},
options: {
environmentOverride: environmentOverrideOption,
environmentVariableName: environmentVariableNameOption,
},
},
async (opts) => {
const environmentOptions = await loadEnvironmentOptions(opts);

const key = await loadKey(await readFile(opts.keyPath));
const privateKey = await loadPrivateKeyLazy();
await trustTeamMember(key, privateKey);
await trustTeamMember(key, privateKey, environmentOptions);

logger.info(`Trusted ${key.getUserIds().join(', ')}`);
},
Expand All @@ -736,10 +786,17 @@ export const cli = yargs
description: 'User ID email address',
},
},
options: {
environmentOverride: environmentOverrideOption,
environmentVariableName: environmentVariableNameOption,
},
},
async (opts) => {
const environmentOptions = await loadEnvironmentOptions(opts);
const privateKey = await loadPrivateKeyLazy();
await untrustTeamMember(opts.email, privateKey);

// TODO: by default, untrust for all envs?
await untrustTeamMember(opts.email, privateKey, environmentOptions);
},
),
)
Expand All @@ -761,9 +818,13 @@ export const cli = yargs
options: {
clipboard: clipboardOption,
agent: secretAgentOption,
environmentOverride: environmentOverrideOption,
environmentVariableName: environmentVariableNameOption,
},
},
async (opts) => {
const environmentOptions = await loadEnvironmentOptions(opts);

shouldUseSecretAgent(opts.agent);

// load these right away, so user unlocks asap
Expand Down Expand Up @@ -797,7 +858,7 @@ export const cli = yargs
}
}

const encrypted = await encryptValue(secretValue);
const encrypted = await encryptValue(secretValue, undefined, environmentOptions);

if (opts.clipboard) {
await clipboardy.write(encrypted);
Expand Down Expand Up @@ -825,9 +886,13 @@ export const cli = yargs
options: {
clipboard: clipboardOption,
agent: secretAgentOption,
environmentOverride: environmentOverrideOption,
environmentVariableName: environmentVariableNameOption,
},
},
async (opts) => {
const environmentOptions = await loadEnvironmentOptions(opts);

shouldUseSecretAgent(opts.agent);

// load these right away, so user unlocks asap
Expand Down Expand Up @@ -855,7 +920,9 @@ export const cli = yargs
throw new EmptyStdinOrPromptResponse('Failed to read from stdin or prompt');
}

process.stdout.write(JSON.stringify(await decryptValue(encryptedText)));
const decrypted = await decryptValue(encryptedText, undefined, environmentOptions);

process.stdout.write(JSON.stringify(decrypted));
process.stdout.write('\n');
},
),
Expand Down
Loading