From 9e2025ac4d8bd0e645a00dc686ae6b00c79dbaf6 Mon Sep 17 00:00:00 2001 From: Joel Gallant Date: Sat, 19 Jun 2021 17:34:42 -0600 Subject: [PATCH 01/22] feat(#105): starts on creating environmentOptions for encryption teamMembers and encryptionKeys --- app-config-cli/src/index.ts | 106 ++++++++++-- app-config-encryption/src/encryption.ts | 201 ++++++++++++++++++---- app-config-encryption/src/secret-agent.ts | 14 +- app-config-meta/src/index.ts | 6 +- 4 files changed, 276 insertions(+), 51 deletions(-) diff --git a/app-config-cli/src/index.ts b/app-config-cli/src/index.ts index 340eba86..7988ec58 100644 --- a/app-config-cli/src/index.ts +++ b/app-config-cli/src/index.ts @@ -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, @@ -577,13 +577,23 @@ 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 = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + 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'); }, ), @@ -599,10 +609,20 @@ 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 = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + + const keys = await loadSymmetricKeys(undefined, environmentOptions); + const teamMembers = await loadTeamMembersLazy(environmentOptions); let revision: number; @@ -612,7 +632,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}`); }, ), @@ -670,12 +695,27 @@ 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 = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + 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`); @@ -708,11 +748,21 @@ export const cli = yargs description: 'Filepath of public key', }, }, + options: { + environmentOverride: environmentOverrideOption, + environmentVariableName: environmentVariableNameOption, + }, }, async (opts) => { + const environmentOptions = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + 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(', ')}`); }, @@ -736,10 +786,22 @@ export const cli = yargs description: 'User ID email address', }, }, + options: { + environmentOverride: environmentOverrideOption, + environmentVariableName: environmentVariableNameOption, + }, }, async (opts) => { + const environmentOptions = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + const privateKey = await loadPrivateKeyLazy(); - await untrustTeamMember(opts.email, privateKey); + + // TODO: by default, untrust for all envs? + await untrustTeamMember(opts.email, privateKey, environmentOptions); }, ), ) @@ -761,9 +823,17 @@ export const cli = yargs options: { clipboard: clipboardOption, agent: secretAgentOption, + environmentOverride: environmentOverrideOption, + environmentVariableName: environmentVariableNameOption, }, }, async (opts) => { + const environmentOptions = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + shouldUseSecretAgent(opts.agent); // load these right away, so user unlocks asap @@ -797,7 +867,7 @@ export const cli = yargs } } - const encrypted = await encryptValue(secretValue); + const encrypted = await encryptValue(secretValue, undefined, environmentOptions); if (opts.clipboard) { await clipboardy.write(encrypted); @@ -825,9 +895,17 @@ export const cli = yargs options: { clipboard: clipboardOption, agent: secretAgentOption, + environmentOverride: environmentOverrideOption, + environmentVariableName: environmentVariableNameOption, }, }, async (opts) => { + const environmentOptions = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + shouldUseSecretAgent(opts.agent); // load these right away, so user unlocks asap @@ -855,7 +933,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'); }, ), diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index 0b68f2b3..0e1b8662 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -15,7 +15,12 @@ import { } from '@app-config/core'; import { Json } from '@app-config/utils'; import { checkTTY, logger } from '@app-config/logging'; -import { promptUser, promptUserWithRetry } from '@app-config/node'; +import { + currentEnvironment, + EnvironmentOptions, + promptUser, + promptUserWithRetry, +} from '@app-config/node'; import { loadMetaConfig, loadMetaConfigLazy, @@ -278,32 +283,42 @@ export async function decryptSymmetricKey( return { revision: encrypted.revision, key: data }; } -export async function saveNewSymmetricKey(symmetricKey: DecryptedSymmetricKey, teamMembers: Key[]) { +export async function saveNewSymmetricKey( + symmetricKey: DecryptedSymmetricKey, + teamMembers: Key[], + environmentOptions?: EnvironmentOptions, +) { const encrypted = await encryptSymmetricKey(symmetricKey, teamMembers); + const environment = currentEnvironment(environmentOptions); await saveNewMetaFile(({ encryptionKeys = [], ...meta }) => ({ ...meta, - encryptionKeys: [...encryptionKeys, encrypted], + encryptionKeys: addForEnvironment(encrypted, encryptionKeys, environment), })); } -export async function loadSymmetricKeys(lazy = true): Promise { +export async function loadSymmetricKeys( + lazy = true, + environmentOptions?: EnvironmentOptions, +): Promise { // flag is here mostly for testing const loadMeta = lazy ? loadMetaConfigLazy : loadMetaConfig; + const environment = currentEnvironment(environmentOptions); const { value: { encryptionKeys = [] }, } = await loadMeta(); - return encryptionKeys; + return selectForEnvironment(encryptionKeys, environment); } export async function loadSymmetricKey( revision: number, privateKey: Key, lazyMeta = true, + environmentOptions?: EnvironmentOptions, ): Promise { - const symmetricKeys = await loadSymmetricKeys(lazyMeta); + const symmetricKeys = await loadSymmetricKeys(lazyMeta, environmentOptions); const symmetricKey = symmetricKeys.find((k) => k.revision === revision); if (!symmetricKey) throw new InvalidEncryptionKey(`Could not find symmetric key ${revision}`); @@ -318,35 +333,48 @@ const symmetricKeys = new Map>(); export async function loadSymmetricKeyLazy( revision: number, privateKey: Key, + environmentOptions?: EnvironmentOptions, ): Promise { if (!symmetricKeys.has(revision)) { - symmetricKeys.set(revision, loadSymmetricKey(revision, privateKey, true)); + symmetricKeys.set(revision, loadSymmetricKey(revision, privateKey, true, environmentOptions)); } return symmetricKeys.get(revision)!; } -export async function loadLatestSymmetricKey(privateKey: Key): Promise { - const allKeys = await loadSymmetricKeys(false); +export async function loadLatestSymmetricKey( + privateKey: Key, + environmentOptions?: EnvironmentOptions, +): Promise { + const allKeys = await loadSymmetricKeys(false, environmentOptions); - return loadSymmetricKey(latestSymmetricKeyRevision(allKeys), privateKey, false); + return loadSymmetricKey( + latestSymmetricKeyRevision(allKeys), + privateKey, + false, + environmentOptions, + ); } -export async function loadLatestSymmetricKeyLazy(privateKey: Key): Promise { - const allKeys = await loadSymmetricKeys(); +export async function loadLatestSymmetricKeyLazy( + privateKey: Key, + environmentOptions?: EnvironmentOptions, +): Promise { + const allKeys = await loadSymmetricKeys(true, environmentOptions); - return loadSymmetricKeyLazy(latestSymmetricKeyRevision(allKeys), privateKey); + return loadSymmetricKeyLazy(latestSymmetricKeyRevision(allKeys), privateKey, environmentOptions); } export async function encryptValue( value: Json, symmetricKeyOverride?: DecryptedSymmetricKey, + environmentOptions?: EnvironmentOptions, ): Promise { if (!symmetricKeyOverride && shouldUseSecretAgent()) { - const client = await retrieveSecretAgent(); + const client = await retrieveSecretAgent(environmentOptions); if (client) { - const allKeys = await loadSymmetricKeys(); + const allKeys = await loadSymmetricKeys(true, environmentOptions); const latestRevision = latestSymmetricKeyRevision(allKeys); const symmetricKey = allKeys.find((k) => k.revision === latestRevision)!; @@ -359,7 +387,7 @@ export async function encryptValue( if (symmetricKeyOverride) { symmetricKey = symmetricKeyOverride; } else { - symmetricKey = await loadLatestSymmetricKeyLazy(await loadPrivateKeyLazy()); + symmetricKey = await loadLatestSymmetricKeyLazy(await loadPrivateKeyLazy(), environmentOptions); } // all encrypted data is JSON encoded @@ -386,9 +414,10 @@ export async function encryptValue( export async function decryptValue( text: string, symmetricKeyOverride?: DecryptedSymmetricKey, + environmentOptions?: EnvironmentOptions, ): Promise { if (!symmetricKeyOverride && shouldUseSecretAgent()) { - const client = await retrieveSecretAgent(); + const client = await retrieveSecretAgent(environmentOptions); if (client) { return client.decryptValue(text); @@ -410,7 +439,11 @@ export async function decryptValue( ); } - symmetricKey = await loadSymmetricKeyLazy(revisionNumber, await loadPrivateKeyLazy()); + symmetricKey = await loadSymmetricKeyLazy( + revisionNumber, + await loadPrivateKeyLazy(), + environmentOptions, + ); } const armored = `-----BEGIN PGP MESSAGE-----\nVersion: OpenPGP.js VERSION\n\n${base64}\n-----END PGP PUBLIC KEY BLOCK-----`; @@ -431,13 +464,14 @@ export async function decryptValue( return JSON.parse(data) as Json; } -export async function loadTeamMembers(): Promise { +export async function loadTeamMembers(environmentOptions?: EnvironmentOptions): Promise { + const environment = currentEnvironment(environmentOptions); const { value: { teamMembers = [] }, } = await loadMetaConfig(); return Promise.all( - teamMembers.map(({ keyName, publicKey }) => + selectForEnvironment(teamMembers, environment).map(({ keyName, publicKey }) => loadKey(publicKey).then((key) => Object.assign(key, { keyName })), ), ); @@ -445,16 +479,21 @@ export async function loadTeamMembers(): Promise { let loadedTeamMembers: Promise | undefined; -export async function loadTeamMembersLazy(): Promise { +export async function loadTeamMembersLazy(environmentOptions?: EnvironmentOptions): Promise { if (!loadedTeamMembers) { - loadedTeamMembers = loadTeamMembers(); + loadedTeamMembers = loadTeamMembers(environmentOptions); } return loadedTeamMembers; } -export async function trustTeamMember(newTeamMember: Key, privateKey: Key) { - const teamMembers = await loadTeamMembers(); +export async function trustTeamMember( + newTeamMember: Key, + privateKey: Key, + environmentOptions?: EnvironmentOptions, +) { + const environment = currentEnvironment(environmentOptions); + const teamMembers = await loadTeamMembers(environmentOptions); if (newTeamMember.isPrivate()) { throw new InvalidEncryptionKey( @@ -474,7 +513,7 @@ export async function trustTeamMember(newTeamMember: Key, privateKey: Key) { const newTeamMembers = teamMembers.concat(newTeamMember); const newEncryptionKeys = await reencryptSymmetricKeys( - await loadSymmetricKeys(), + await loadSymmetricKeys(true, environmentOptions), newTeamMembers, privateKey, ); @@ -486,12 +525,22 @@ export async function trustTeamMember(newTeamMember: Key, privateKey: Key) { keyName: key.keyName ?? null, publicKey: key.armor(), })), - encryptionKeys: newEncryptionKeys, + encryptionKeys: addForEnvironment( + newEncryptionKeys, + meta.encryptionKeys ?? [], + environment, + true, + ), })); } -export async function untrustTeamMember(email: string, privateKey: Key) { - const teamMembers = await loadTeamMembers(); +export async function untrustTeamMember( + email: string, + privateKey: Key, + environmentOptions?: EnvironmentOptions, +) { + const environment = currentEnvironment(environmentOptions); + const teamMembers = await loadTeamMembers(environmentOptions); const removalCandidates = new Set(); @@ -542,7 +591,7 @@ export async function untrustTeamMember(email: string, privateKey: Key) { // of course, nothing stops users from having previously copy-pasted secrets, so they should always be rotated when untrusting old users // reason being, they had previous access to the actual private symmetric key const newEncryptionKeys = await reencryptSymmetricKeys( - await loadSymmetricKeys(), + await loadSymmetricKeys(true, environmentOptions), newTeamMembers, privateKey, ); @@ -561,7 +610,12 @@ export async function untrustTeamMember(email: string, privateKey: Key) { keyName: key.keyName ?? null, publicKey: key.armor(), })), - encryptionKeys: newEncryptionKeys, + encryptionKeys: addForEnvironment( + newEncryptionKeys, + meta.encryptionKeys ?? [], + environment, + true, + ), })); } @@ -600,11 +654,11 @@ async function reencryptSymmetricKeys( return newEncryptionKeys; } -async function retrieveSecretAgent() { +async function retrieveSecretAgent(environmentOptions?: EnvironmentOptions) { let client; try { - client = await connectAgentLazy(); + client = await connectAgentLazy(undefined, undefined, environmentOptions); } catch (err: unknown) { if (err && typeof err === 'object' && 'error' in err) { const { error } = err as { error: { errno: string } }; @@ -633,6 +687,89 @@ async function saveNewMetaFile(mutate: (props: MetaProperties) => MetaProperties await fs.writeFile(writeFilePath, stringify(writeMeta, writeFileType)); } +function selectForEnvironment( + values: T[] | Record, + environment: string | undefined, +): T[] { + if (Array.isArray(values)) { + return values; + } + + if (environment === undefined) { + if ('none' in values) { + return values.none; + } + + if ('default' in values) { + return values.default; + } + + const environments = Array.from(Object.keys(values).values()).join(', '); + + throw new AppConfigError(`No current environment selected, found [${environments}}`); + } + + if (environment in values) { + return values[environment]; + } + + const environments = Array.from(Object.keys(values).values()).join(', '); + + throw new AppConfigError(`Current environment was ${environment}, only found [${environments}]`); +} + +function addForEnvironment( + add: T | T[], + values: T[] | Record, + environment: string | undefined, + overwrite = false, +): T[] | Record { + const addArray = Array.isArray(add) ? add : [add]; + const addOrReplace = (orig: T[]) => { + if (overwrite) { + return addArray; + } + + return orig.concat(addArray); + }; + + if (Array.isArray(values)) { + return values.concat(add); + } + + if (environment === undefined) { + if ('none' in values) { + return { + ...values, + none: addOrReplace(values.none), + }; + } + + if ('default' in values) { + return { + ...values, + default: addOrReplace(values.default), + }; + } + + const environments = Array.from(Object.keys(values).values()).join(', '); + + throw new AppConfigError(`No current environment selected, found [${environments}}`); + } + + if (environment in values) { + return { + ...values, + [environment]: addOrReplace(values[environment]), + }; + } + + return { + ...values, + [environment]: addArray, + }; +} + function decodeTypedArray(buf: ArrayBuffer): string { return String.fromCharCode.apply(null, (new Uint16Array(buf) as any) as number[]); } diff --git a/app-config-encryption/src/secret-agent.ts b/app-config-encryption/src/secret-agent.ts index 9c9a3da7..ef55fcd8 100644 --- a/app-config-encryption/src/secret-agent.ts +++ b/app-config-encryption/src/secret-agent.ts @@ -6,6 +6,7 @@ import { AppConfigError } from '@app-config/core'; import { Json } from '@app-config/utils'; import { logger } from '@app-config/logging'; import { loadSettingsLazy, saveSettings } from '@app-config/settings'; +import type { EnvironmentOptions } from '@app-config/node'; import { Key, @@ -81,6 +82,7 @@ export async function connectAgent( closeTimeoutMs = Infinity, socketOrPortOverride?: number | string, loadEncryptedKey: typeof loadSymmetricKey = loadSymmetricKey, + environmentOptions?: EnvironmentOptions, ) { let client: Client; @@ -145,7 +147,7 @@ export async function connectAgent( ); } - const symmetricKey = await loadEncryptedKey(revisionNumber); + const symmetricKey = await loadEncryptedKey(revisionNumber, environmentOptions); const decrypted = await client.Decrypt({ text, symmetricKey }); keepAlive(); @@ -169,11 +171,12 @@ const clients = new Map>(); export async function connectAgentLazy( closeTimeoutMs = 500, socketOrPortOverride?: number | string, + environmentOptions?: EnvironmentOptions, ): ReturnType { const socketOrPort = await getAgentPortOrSocket(socketOrPortOverride); if (!clients.has(socketOrPort)) { - const connection = connectAgent(closeTimeoutMs, socketOrPort); + const connection = connectAgent(closeTimeoutMs, socketOrPort, undefined, environmentOptions); clients.set(socketOrPort, connection); @@ -244,8 +247,11 @@ export async function getAgentPortOrSocket( return defaultPort; } -async function loadSymmetricKey(revision: number): Promise { - const symmetricKeys = await loadSymmetricKeys(true); +async function loadSymmetricKey( + revision: number, + environmentOptions?: EnvironmentOptions, +): Promise { + const symmetricKeys = await loadSymmetricKeys(true, environmentOptions); const symmetricKey = symmetricKeys.find((k) => k.revision === revision); if (!symmetricKey) throw new AppConfigError(`Could not find symmetric key ${revision}`); diff --git a/app-config-meta/src/index.ts b/app-config-meta/src/index.ts index 523391be..ab7b1959 100644 --- a/app-config-meta/src/index.ts +++ b/app-config-meta/src/index.ts @@ -48,8 +48,10 @@ export interface GenerateFile { } export interface MetaProperties { - teamMembers?: TeamMember[]; - encryptionKeys?: EncryptedSymmetricKey[]; + teamMembers?: TeamMember[] | Record; + encryptionKeys?: + | EncryptedSymmetricKey[] + | Record; generate?: GenerateFile[]; parsingExtensions?: (ParsingExtensionWithOptions | string)[]; environmentAliases?: Record; From f748f6171ca1683db19e56a969ccf2834da5c1cc Mon Sep 17 00:00:00 2001 From: Joel Gallant Date: Sat, 19 Jun 2021 17:53:17 -0600 Subject: [PATCH 02/22] feat: environment asliases for env-specific encryption --- app-config-encryption/src/encryption.ts | 61 ++++++++++++++++++++----- app-config-node/src/index.ts | 1 + 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index 0e1b8662..6d793b2f 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -16,6 +16,7 @@ import { import { Json } from '@app-config/utils'; import { checkTTY, logger } from '@app-config/logging'; import { + aliasesFor, currentEnvironment, EnvironmentOptions, promptUser, @@ -289,11 +290,10 @@ export async function saveNewSymmetricKey( environmentOptions?: EnvironmentOptions, ) { const encrypted = await encryptSymmetricKey(symmetricKey, teamMembers); - const environment = currentEnvironment(environmentOptions); await saveNewMetaFile(({ encryptionKeys = [], ...meta }) => ({ ...meta, - encryptionKeys: addForEnvironment(encrypted, encryptionKeys, environment), + encryptionKeys: addForEnvironment(encrypted, encryptionKeys, environmentOptions), })); } @@ -309,7 +309,13 @@ export async function loadSymmetricKeys( value: { encryptionKeys = [] }, } = await loadMeta(); - return selectForEnvironment(encryptionKeys, environment); + const selected = selectForEnvironment(encryptionKeys, environmentOptions); + + logger.verbose( + `Found ${selected.length} symmetric keys for environment: ${environment ?? 'none'}`, + ); + + return selected; } export async function loadSymmetricKey( @@ -470,8 +476,14 @@ export async function loadTeamMembers(environmentOptions?: EnvironmentOptions): value: { teamMembers = [] }, } = await loadMetaConfig(); + const currentTeamMembers = selectForEnvironment(teamMembers, environmentOptions); + + logger.verbose( + `Found ${currentTeamMembers.length} team members for environment: ${environment ?? 'none'}`, + ); + return Promise.all( - selectForEnvironment(teamMembers, environment).map(({ keyName, publicKey }) => + currentTeamMembers.map(({ keyName, publicKey }) => loadKey(publicKey).then((key) => Object.assign(key, { keyName })), ), ); @@ -492,7 +504,6 @@ export async function trustTeamMember( privateKey: Key, environmentOptions?: EnvironmentOptions, ) { - const environment = currentEnvironment(environmentOptions); const teamMembers = await loadTeamMembers(environmentOptions); if (newTeamMember.isPrivate()) { @@ -528,7 +539,7 @@ export async function trustTeamMember( encryptionKeys: addForEnvironment( newEncryptionKeys, meta.encryptionKeys ?? [], - environment, + environmentOptions, true, ), })); @@ -539,7 +550,6 @@ export async function untrustTeamMember( privateKey: Key, environmentOptions?: EnvironmentOptions, ) { - const environment = currentEnvironment(environmentOptions); const teamMembers = await loadTeamMembers(environmentOptions); const removalCandidates = new Set(); @@ -613,7 +623,7 @@ export async function untrustTeamMember( encryptionKeys: addForEnvironment( newEncryptionKeys, meta.encryptionKeys ?? [], - environment, + environmentOptions, true, ), })); @@ -689,12 +699,14 @@ async function saveNewMetaFile(mutate: (props: MetaProperties) => MetaProperties function selectForEnvironment( values: T[] | Record, - environment: string | undefined, + environmentOptions: EnvironmentOptions | undefined, ): T[] { if (Array.isArray(values)) { return values; } + const environment = currentEnvironment(environmentOptions); + if (environment === undefined) { if ('none' in values) { return values.none; @@ -713,15 +725,25 @@ function selectForEnvironment( return values[environment]; } + if (environmentOptions?.aliases) { + for (const alias of aliasesFor(environment, environmentOptions.aliases)) { + if (alias in values) { + return values[alias]; + } + } + } + const environments = Array.from(Object.keys(values).values()).join(', '); - throw new AppConfigError(`Current environment was ${environment}, only found [${environments}]`); + throw new AppConfigError( + `Current environment was ${environment}, only found [${environments}] when selecting environment-specific encryption options from meta file`, + ); } function addForEnvironment( add: T | T[], values: T[] | Record, - environment: string | undefined, + environmentOptions: EnvironmentOptions | undefined, overwrite = false, ): T[] | Record { const addArray = Array.isArray(add) ? add : [add]; @@ -737,6 +759,8 @@ function addForEnvironment( return values.concat(add); } + const environment = currentEnvironment(environmentOptions); + if (environment === undefined) { if ('none' in values) { return { @@ -754,7 +778,9 @@ function addForEnvironment( const environments = Array.from(Object.keys(values).values()).join(', '); - throw new AppConfigError(`No current environment selected, found [${environments}}`); + throw new AppConfigError( + `No current environment selected, found [${environments}] when adding environment-specific encryption options to meta file`, + ); } if (environment in values) { @@ -764,6 +790,17 @@ function addForEnvironment( }; } + if (environmentOptions?.aliases) { + for (const alias of aliasesFor(environment, environmentOptions.aliases)) { + if (alias in values) { + return { + ...values, + [alias]: addOrReplace(values[alias]), + }; + } + } + } + return { ...values, [environment]: addArray, diff --git a/app-config-node/src/index.ts b/app-config-node/src/index.ts index 193c5d54..eef579ee 100644 --- a/app-config-node/src/index.ts +++ b/app-config-node/src/index.ts @@ -1,5 +1,6 @@ export { FileSource, FlexibleFileSource, resolveFilepath } from './file-source'; export { + aliasesFor, asEnvOptions, environmentOptionsFromContext, currentEnvironment, From 2ebb3bc31c367472ef48927b9390af246cb09345 Mon Sep 17 00:00:00 2001 From: Joel Gallant Date: Sat, 19 Jun 2021 17:57:14 -0600 Subject: [PATCH 03/22] fix: addForEnvironment should respect replace when using array input --- app-config-encryption/src/encryption.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index 6d793b2f..84748382 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -756,7 +756,7 @@ function addForEnvironment( }; if (Array.isArray(values)) { - return values.concat(add); + return addOrReplace(values); } const environment = currentEnvironment(environmentOptions); From 315e6e34deec46c88fbf89437a0b35485d61515b Mon Sep 17 00:00:00 2001 From: Joel Gallant Date: Sun, 27 Jun 2021 13:39:52 -0600 Subject: [PATCH 04/22] feat: use meta file environmentOptions when looking up environment for encryption subcommands --- app-config-cli/src/index.ts | 59 +++++++++++++++---------------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/app-config-cli/src/index.ts b/app-config-cli/src/index.ts index 45502753..144a4059 100644 --- a/app-config-cli/src/index.ts +++ b/app-config-cli/src/index.ts @@ -48,6 +48,7 @@ import { import { loadSchema, JSONSchema } from '@app-config/schema'; import { generateTypeFiles } from '@app-config/generate'; import { validateAllConfigVariants } from './validation'; +import { loadMetaConfigLazy } from '@app-config/meta'; enum OptionGroups { Options = 'Options:', @@ -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)) @@ -583,11 +599,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); + const environmentOptions = await loadEnvironmentOptions(opts); const myKey = await loadPublicKeyLazy(); const privateKey = await loadPrivateKeyLazy(); @@ -615,11 +627,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); + const environmentOptions = await loadEnvironmentOptions(opts); const keys = await loadSymmetricKeys(undefined, environmentOptions); const teamMembers = await loadTeamMembersLazy(environmentOptions); @@ -701,11 +709,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); + const environmentOptions = await loadEnvironmentOptions(opts); logger.info('Creating a new trusted CI encryption key'); @@ -754,11 +758,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); + const environmentOptions = await loadEnvironmentOptions(opts); const key = await loadKey(await readFile(opts.keyPath)); const privateKey = await loadPrivateKeyLazy(); @@ -792,12 +792,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); - + const environmentOptions = await loadEnvironmentOptions(opts); const privateKey = await loadPrivateKeyLazy(); // TODO: by default, untrust for all envs? @@ -828,11 +823,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); + const environmentOptions = await loadEnvironmentOptions(opts); shouldUseSecretAgent(opts.agent); @@ -900,11 +891,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); + const environmentOptions = await loadEnvironmentOptions(opts); shouldUseSecretAgent(opts.agent); From 1c21d9506bf4f375a53a754449e8611743bfa9e9 Mon Sep 17 00:00:00 2001 From: Joel Gallant Date: Sun, 27 Jun 2021 13:49:04 -0600 Subject: [PATCH 05/22] fix: lint --- app-config-cli/package.json | 1 + app-config-cli/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app-config-cli/package.json b/app-config-cli/package.json index b1d8dddf..692e91fe 100644 --- a/app-config-cli/package.json +++ b/app-config-cli/package.json @@ -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", diff --git a/app-config-cli/src/index.ts b/app-config-cli/src/index.ts index 144a4059..ae7c1faa 100644 --- a/app-config-cli/src/index.ts +++ b/app-config-cli/src/index.ts @@ -47,8 +47,8 @@ import { } from '@app-config/encryption'; import { loadSchema, JSONSchema } from '@app-config/schema'; import { generateTypeFiles } from '@app-config/generate'; -import { validateAllConfigVariants } from './validation'; import { loadMetaConfigLazy } from '@app-config/meta'; +import { validateAllConfigVariants } from './validation'; enum OptionGroups { Options = 'Options:', From 6b7575a81710d06375b53985aa0d77d8125125ff Mon Sep 17 00:00:00 2001 From: Joel Gallant Date: Tue, 29 Jun 2021 18:41:05 -0600 Subject: [PATCH 06/22] feat: uses environmentOptionsFromContext for decryptValue encryption extension --- app-config-encryption/src/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app-config-encryption/src/index.ts b/app-config-encryption/src/index.ts index 7ad0fc17..1d36af47 100644 --- a/app-config-encryption/src/index.ts +++ b/app-config-encryption/src/index.ts @@ -1,6 +1,7 @@ import type { ParsingExtension } from '@app-config/core'; import { named } from '@app-config/extension-utils'; import { logger } from '@app-config/logging'; +import { environmentOptionsFromContext } from '@app-config/node'; import { DecryptedSymmetricKey, decryptValue } from './encryption'; export * from './encryption'; @@ -12,7 +13,7 @@ export default function encryptedDirective( symmetricKey?: DecryptedSymmetricKey, shouldShowDeprecationNotice?: true, ): ParsingExtension { - return named('encryption', (value) => { + return named('encryption', (value, _, __, ctx) => { if (typeof value === 'string' && value.startsWith('enc:')) { return async (parse) => { if (shouldShowDeprecationNotice) { @@ -21,7 +22,8 @@ export default function encryptedDirective( ); } - const decrypted = await decryptValue(value, symmetricKey); + const environmentOptions = environmentOptionsFromContext(ctx); + const decrypted = await decryptValue(value, symmetricKey, environmentOptions); return parse(decrypted, { fromSecrets: true, parsedFromEncryptedValue: true }); }; From d08f16fb07d9d9762e3a6334ecb5f99743259f40 Mon Sep 17 00:00:00 2001 From: John Brandt Date: Sun, 21 May 2023 13:50:18 -0600 Subject: [PATCH 07/22] feat: support env suffixed encryption env var keys --- app-config-cli/src/index.ts | 14 ++--- app-config-encryption/src/encryption.ts | 75 +++++++++++++++++++++---- 2 files changed, 70 insertions(+), 19 deletions(-) diff --git a/app-config-cli/src/index.ts b/app-config-cli/src/index.ts index ae7c1faa..f07d85da 100644 --- a/app-config-cli/src/index.ts +++ b/app-config-cli/src/index.ts @@ -601,8 +601,8 @@ export const cli = yargs async (opts) => { const environmentOptions = await loadEnvironmentOptions(opts); - const myKey = await loadPublicKeyLazy(); - const privateKey = await loadPrivateKeyLazy(); + const myKey = await loadPublicKeyLazy(environmentOptions); + const privateKey = await loadPrivateKeyLazy(environmentOptions); // we trust ourselves, essentially await trustTeamMember(myKey, privateKey, environmentOptions); @@ -717,7 +717,7 @@ export const cli = yargs await trustTeamMember( await loadKey(publicKeyArmored), - await loadPrivateKeyLazy(), + await loadPrivateKeyLazy(environmentOptions), environmentOptions, ); @@ -761,7 +761,7 @@ export const cli = yargs const environmentOptions = await loadEnvironmentOptions(opts); const key = await loadKey(await readFile(opts.keyPath)); - const privateKey = await loadPrivateKeyLazy(); + const privateKey = await loadPrivateKeyLazy(environmentOptions); await trustTeamMember(key, privateKey, environmentOptions); logger.info(`Trusted ${key.getUserIds().join(', ')}`); @@ -793,7 +793,7 @@ export const cli = yargs }, async (opts) => { const environmentOptions = await loadEnvironmentOptions(opts); - const privateKey = await loadPrivateKeyLazy(); + const privateKey = await loadPrivateKeyLazy(environmentOptions); // TODO: by default, untrust for all envs? await untrustTeamMember(opts.email, privateKey, environmentOptions); @@ -828,7 +828,7 @@ export const cli = yargs shouldUseSecretAgent(opts.agent); // load these right away, so user unlocks asap - if (!shouldUseSecretAgent()) await loadPrivateKeyLazy(); + if (!shouldUseSecretAgent()) await loadPrivateKeyLazy(environmentOptions); let { secretValue }: { secretValue?: Json } = opts; @@ -896,7 +896,7 @@ export const cli = yargs shouldUseSecretAgent(opts.agent); // load these right away, so user unlocks asap - if (!shouldUseSecretAgent()) await loadPrivateKeyLazy(); + if (!shouldUseSecretAgent()) await loadPrivateKeyLazy(environmentOptions); let { encryptedText } = opts; diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index 921bceb5..e53f3bfa 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -147,12 +147,20 @@ export async function loadKey(contents: string | Buffer): Promise { } export async function loadPrivateKey( - override: string | Buffer | undefined = process.env.APP_CONFIG_SECRETS_KEY, + override: string | Buffer | undefined = undefined, + environmentOptions?: EnvironmentOptions, ): Promise { let key: Key; + let overrideKey; if (override) { - key = await loadKey(override); + overrideKey = override; + } else { + overrideKey = getKeyFromEnv('public', environmentOptions); + } + + if (overrideKey) { + key = await loadKey(overrideKey); } else { if (process.env.CI) { logger.info('Warning! Trying to load encryption keys from home folder in a CI environment'); @@ -183,12 +191,20 @@ export async function loadPrivateKey( } export async function loadPublicKey( - override: string | Buffer | undefined = process.env.APP_CONFIG_SECRETS_PUBLIC_KEY, + override: string | Buffer | undefined = undefined, + environmentOptions?: EnvironmentOptions, ): Promise { let key: Key; + let overrideKey; if (override) { - key = await loadKey(override); + overrideKey = override; + } else { + overrideKey = getKeyFromEnv('public', environmentOptions); + } + + if (overrideKey) { + key = await loadKey(overrideKey); } else { if (process.env.CI) { logger.warn('Warning! Trying to load encryption keys from home folder in a CI environment'); @@ -203,17 +219,47 @@ export async function loadPublicKey( return key; } +function getKeyFromEnv(keyType: 'private' | 'public', envOptions?: EnvironmentOptions) { + const env = currentEnvironment(envOptions); + + const envVarPrefix = + keyType === 'private' ? 'APP_CONFIG_SECRETS_KEY' : 'APP_CONFIG_SECRETS_PUBLIC_KEY'; + + if (!envOptions || !env) { + return process.env[envVarPrefix]; + } + + let key = process.env[`${envVarPrefix}_${env.toUpperCase()}`]; + + // try an alias if we didn't find the key first try + if (!key) { + const aliases = aliasesFor(env, envOptions.aliases); + + for (const alias of aliases) { + key = process.env[`${envVarPrefix}_${alias.toUpperCase()}`]; + + if (key) { + break; + } + } + } + + return key; +} + let loadedPrivateKey: Promise | undefined; -export async function loadPrivateKeyLazy(): Promise { +export async function loadPrivateKeyLazy(environmentOptions?: EnvironmentOptions): Promise { if (!loadedPrivateKey) { logger.verbose('Loading local private key'); if (checkTTY()) { // help the end user, if they haven't initialized their local keys yet - loadedPrivateKey = initializeLocalKeys().then(() => loadPrivateKey()); + loadedPrivateKey = initializeLocalKeys().then(() => + loadPrivateKey(undefined, environmentOptions), + ); } else { - loadedPrivateKey = loadPrivateKey(); + loadedPrivateKey = loadPrivateKey(undefined, environmentOptions); } } @@ -222,15 +268,17 @@ export async function loadPrivateKeyLazy(): Promise { let loadedPublicKey: Promise | undefined; -export async function loadPublicKeyLazy(): Promise { +export async function loadPublicKeyLazy(environmentOptions?: EnvironmentOptions): Promise { if (!loadedPublicKey) { logger.verbose('Loading local public key'); if (checkTTY()) { // help the end user, if they haven't initialized their local keys yet - loadedPublicKey = initializeLocalKeys().then(() => loadPublicKey()); + loadedPublicKey = initializeLocalKeys().then(() => + loadPublicKey(undefined, environmentOptions), + ); } else { - loadedPublicKey = loadPublicKey(); + loadedPublicKey = loadPublicKey(undefined, environmentOptions); } } @@ -393,7 +441,10 @@ export async function encryptValue( if (symmetricKeyOverride) { symmetricKey = symmetricKeyOverride; } else { - symmetricKey = await loadLatestSymmetricKeyLazy(await loadPrivateKeyLazy(), environmentOptions); + symmetricKey = await loadLatestSymmetricKeyLazy( + await loadPrivateKeyLazy(environmentOptions), + environmentOptions, + ); } // all encrypted data is JSON encoded @@ -447,7 +498,7 @@ export async function decryptValue( symmetricKey = await loadSymmetricKeyLazy( revisionNumber, - await loadPrivateKeyLazy(), + await loadPrivateKeyLazy(environmentOptions), environmentOptions, ); } From a8ea8c672fad82dae018ad67b7482a90fdbf02b6 Mon Sep 17 00:00:00 2001 From: John Brandt Date: Sun, 21 May 2023 16:29:02 -0600 Subject: [PATCH 08/22] chore: tests for env secret key --- app-config-encryption/src/encryption.test.ts | 72 ++++++++++++++++++++ app-config-encryption/src/encryption.ts | 7 +- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/app-config-encryption/src/encryption.test.ts b/app-config-encryption/src/encryption.test.ts index 5b17f15e..cdf1d18f 100644 --- a/app-config-encryption/src/encryption.test.ts +++ b/app-config-encryption/src/encryption.test.ts @@ -4,6 +4,7 @@ import { SecretsRequireTTYError } from '@app-config/core'; import { loadMetaConfig } from '@app-config/meta'; import { withTempFiles, mockedStdin } from '@app-config/test-utils'; +import { defaultEnvOptions } from '@app-config/node'; import { initializeKeys, initializeKeysManually, @@ -102,6 +103,77 @@ describe('User Keys', () => { }); }); +const createKeys = async () => { + const { privateKeyArmored, publicKeyArmored } = await initializeKeysManually({ + name: 'Tester', + email: 'test@example.com', + }); + + return { + privateKey: await loadPrivateKey(privateKeyArmored), + publicKey: await loadPublicKey(publicKeyArmored), + privateKeyArmored, + publicKeyArmored, + }; +}; + +describe('User keys from environment', () => { + it('loads user keys from environment', async () => { + const keys = await createKeys(); + + process.env.APP_CONFIG_SECRETS_PUBLIC_KEY = keys.publicKeyArmored; + process.env.APP_CONFIG_SECRETS_KEY = keys.privateKeyArmored; + + const privateKey = await loadPrivateKey(); + const publicKey = await loadPublicKey(); + + expect(privateKey.getFingerprint()).toEqual(keys.privateKey.getFingerprint()); + expect(publicKey.getFingerprint()).toEqual(keys.publicKey.getFingerprint()); + }); + + it('loads environment user keys from environment', async () => { + const keys = await createKeys(); + + process.env.APP_CONFIG_SECRETS_PUBLIC_KEY_PRODUCTION = keys.publicKeyArmored; + process.env.APP_CONFIG_SECRETS_KEY_PRODUCTION = keys.privateKeyArmored; + process.env.APP_CONFIG_ENV = 'prod'; + + const privateKey = await loadPrivateKey(undefined, defaultEnvOptions); + const publicKey = await loadPublicKey(undefined, defaultEnvOptions); + + expect(privateKey.getFingerprint()).toEqual(keys.privateKey.getFingerprint()); + expect(publicKey.getFingerprint()).toEqual(keys.publicKey.getFingerprint()); + }); + + it('loads aliased environment user keys from environment', async () => { + const keys = await createKeys(); + + process.env.APP_CONFIG_SECRETS_PUBLIC_KEY_PROD = keys.publicKeyArmored; + process.env.APP_CONFIG_SECRETS_KEY_PROD = keys.privateKeyArmored; + process.env.APP_CONFIG_ENV = 'prod'; + + const privateKey = await loadPrivateKey(undefined, defaultEnvOptions); + const publicKey = await loadPublicKey(undefined, defaultEnvOptions); + + expect(privateKey.getFingerprint()).toEqual(keys.privateKey.getFingerprint()); + expect(publicKey.getFingerprint()).toEqual(keys.publicKey.getFingerprint()); + }); + + it('falls back to key with no environment', async () => { + const keys = await createKeys(); + + process.env.APP_CONFIG_SECRETS_PUBLIC_KEY = keys.publicKeyArmored; + process.env.APP_CONFIG_SECRETS_KEY = keys.privateKeyArmored; + process.env.APP_CONFIG_ENV = 'prod'; + + const privateKey = await loadPrivateKey(undefined, defaultEnvOptions); + const publicKey = await loadPublicKey(undefined, defaultEnvOptions); + + expect(privateKey.getFingerprint()).toEqual(keys.privateKey.getFingerprint()); + expect(publicKey.getFingerprint()).toEqual(keys.publicKey.getFingerprint()); + }); +}); + const createKey = async () => { const { privateKeyArmored } = await initializeKeysManually({ name: 'Tester', diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index e53f3bfa..daa6d499 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -156,7 +156,7 @@ export async function loadPrivateKey( if (override) { overrideKey = override; } else { - overrideKey = getKeyFromEnv('public', environmentOptions); + overrideKey = getKeyFromEnv('private', environmentOptions); } if (overrideKey) { @@ -244,6 +244,11 @@ function getKeyFromEnv(keyType: 'private' | 'public', envOptions?: EnvironmentOp } } + // if we didn't find a key with an environment, fallback on one without if it exists + if (!key) { + key = process.env[envVarPrefix]; + } + return key; } From 534596d93f2b594debc5087e9e3d1dfb2000b53d Mon Sep 17 00:00:00 2001 From: John Brandt Date: Sun, 21 May 2023 18:51:49 -0600 Subject: [PATCH 09/22] feat: encryption key revision includes environment --- app-config-encryption/src/encryption.test.ts | 30 ++++--- app-config-encryption/src/encryption.ts | 84 ++++++++++++++----- app-config-encryption/src/index.test.ts | 4 +- .../src/secret-agent.test.ts | 4 +- app-config-encryption/src/secret-agent.ts | 11 +-- app-config-meta/src/index.ts | 2 +- app-config-schema/src/index.test.ts | 8 +- 7 files changed, 90 insertions(+), 53 deletions(-) diff --git a/app-config-encryption/src/encryption.test.ts b/app-config-encryption/src/encryption.test.ts index cdf1d18f..6c6f4341 100644 --- a/app-config-encryption/src/encryption.test.ts +++ b/app-config-encryption/src/encryption.test.ts @@ -22,6 +22,7 @@ import { loadTeamMembers, trustTeamMember, untrustTeamMember, + getRevisionNumber, } from './encryption'; describe('User Keys', () => { @@ -185,25 +186,25 @@ const createKey = async () => { describe('Symmetric Keys', () => { it('generates a plain symmetric key', async () => { - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); - expect(symmetricKey.revision).toBe(1); + expect(symmetricKey.revision).toBe('1'); expect(symmetricKey.key).toBeInstanceOf(Uint8Array); expect(symmetricKey.key.length).toBeGreaterThan(2048); }); it('encrypts and decrypts a symmetric key', async () => { const privateKey = await createKey(); - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const encryptedKey = await encryptSymmetricKey(symmetricKey, [privateKey]); - expect(encryptedKey.revision).toBe(1); + expect(encryptedKey.revision).toBe('1'); expect(typeof encryptedKey.key).toBe('string'); expect(encryptedKey.key.length).toBeGreaterThan(0); const decryptedKey = await decryptSymmetricKey(encryptedKey, privateKey); - expect(decryptedKey.revision).toBe(1); + expect(decryptedKey.revision).toBe('1'); expect(decryptedKey.key).toEqual(symmetricKey.key); }); @@ -211,7 +212,7 @@ describe('Symmetric Keys', () => { const privateKey = await createKey(); const someoneElsesKey = await createKey(); - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const encryptedKey = await encryptSymmetricKey(symmetricKey, [someoneElsesKey]); await expect(decryptSymmetricKey(encryptedKey, privateKey)).rejects.toThrow(); @@ -221,7 +222,7 @@ describe('Symmetric Keys', () => { const privateKey = await createKey(); const someoneElsesKey = await createKey(); - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const encryptedKey = await encryptSymmetricKey(symmetricKey, [privateKey, someoneElsesKey]); await expect(decryptSymmetricKey(encryptedKey, privateKey)).resolves.toEqual(symmetricKey); @@ -232,7 +233,7 @@ describe('Symmetric Keys', () => { const privateKey = await createKey(); const someoneElsesKey = await createKey(); - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const encryptedKey = await encryptSymmetricKey(symmetricKey, [privateKey]); const decryptedKey = await decryptSymmetricKey(encryptedKey, privateKey); const encryptedKey2 = await encryptSymmetricKey(decryptedKey, [privateKey, someoneElsesKey]); @@ -244,7 +245,7 @@ describe('Symmetric Keys', () => { it('validates encoded revision number in keys', async () => { const privateKey = await createKey(); - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const encryptedKey = await encryptSymmetricKey(symmetricKey, [privateKey]); // really go out of our way to mess with the key - this usually results in integrity check failures either way @@ -261,7 +262,7 @@ describe('Value Encryption', () => { const values = ['hello world', 42.42, null, true, { message: 'hello world', nested: {} }]; for (const value of values) { - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const encrypted = await encryptValue(value, symmetricKey); const decrypted = await decryptValue(encrypted, symmetricKey); @@ -273,8 +274,8 @@ describe('Value Encryption', () => { it('cannot decrypt a value with the wrong key', async () => { const value = 'hello world'; - const symmetricKey = await generateSymmetricKey(1); - const wrongKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); + const wrongKey = await generateSymmetricKey('1'); const encrypted = await encryptValue(value, symmetricKey); await expect(decryptValue(encrypted, wrongKey)).rejects.toThrow(); @@ -377,8 +378,11 @@ describe('E2E Encrypted Repo', () => { // just for test coverage, create a new symmetric key const latestSymmetricKey = await loadLatestSymmetricKey(privateKey); + + const newRevisionNumber = getRevisionNumber(latestSymmetricKey.revision) + 1; + await saveNewSymmetricKey( - await generateSymmetricKey(latestSymmetricKey.revision + 1), + await generateSymmetricKey(newRevisionNumber.toString()), await loadTeamMembers(), ); diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index daa6d499..4e0979eb 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -293,11 +293,11 @@ export async function loadPublicKeyLazy(environmentOptions?: EnvironmentOptions) export { EncryptedSymmetricKey }; export interface DecryptedSymmetricKey { - revision: number; + revision: string; key: Uint8Array; } -export async function generateSymmetricKey(revision: number): Promise { +export async function generateSymmetricKey(revision: string): Promise { // eslint-disable-next-line @typescript-eslint/await-thenable const rawPassword = await crypto.random.getRandomBytes(2048); const passwordWithRevision = encodeRevisionInPassword(rawPassword, revision); @@ -372,7 +372,7 @@ export async function loadSymmetricKeys( } export async function loadSymmetricKey( - revision: number, + revision: string, privateKey: Key, lazyMeta = true, environmentOptions?: EnvironmentOptions, @@ -387,10 +387,10 @@ export async function loadSymmetricKey( return decryptSymmetricKey(symmetricKey, privateKey); } -const symmetricKeys = new Map>(); +const symmetricKeys = new Map>(); export async function loadSymmetricKeyLazy( - revision: number, + revision: string, privateKey: Key, environmentOptions?: EnvironmentOptions, ): Promise { @@ -493,16 +493,8 @@ export async function decryptValue( if (symmetricKeyOverride) { symmetricKey = symmetricKeyOverride; } else { - const revisionNumber = parseFloat(revision); - - if (Number.isNaN(revisionNumber)) { - throw new AppConfigError( - `Encrypted value was invalid, revision was not a number (${revision})`, - ); - } - symmetricKey = await loadSymmetricKeyLazy( - revisionNumber, + revision, await loadPrivateKeyLazy(environmentOptions), environmentOptions, ); @@ -583,6 +575,7 @@ export async function trustTeamMember( await loadSymmetricKeys(true, environmentOptions), newTeamMembers, privateKey, + environmentOptions, ); await saveNewMetaFile((meta) => ({ @@ -606,6 +599,8 @@ export async function untrustTeamMember( privateKey: Key, environmentOptions?: EnvironmentOptions, ) { + const environment = currentEnvironment(environmentOptions); + const teamMembers = await loadTeamMembers(environmentOptions); const removalCandidates = new Set(); @@ -660,10 +655,22 @@ export async function untrustTeamMember( await loadSymmetricKeys(true, environmentOptions), newTeamMembers, privateKey, + environmentOptions, ); + const latestRevision = latestSymmetricKeyRevision(newEncryptionKeys); + const newRevisionNumber = getRevisionNumber(latestRevision) + 1; + + let newRevision; + + if (environment) { + newRevision = `${environment}-${newRevisionNumber}`; + } else { + newRevision = `${newRevisionNumber}`; + } + const newLatestEncryptionKey = await encryptSymmetricKey( - await generateSymmetricKey(latestSymmetricKeyRevision(newEncryptionKeys) + 1), + await generateSymmetricKey(newRevision), newTeamMembers, ); @@ -685,10 +692,32 @@ export async function untrustTeamMember( })); } +export function getRevisionNumber(revision: string) { + const regex = /^(?:\w*-)?(?\d*)$/; + + const match = regex.exec(revision)?.groups?.revisionNumber; + + if (!match) { + throw new AppConfigError( + `Encryption revision is invalid. Got "${revision}" but expected a number or -"`, + ); + } + + const revisionNumber = parseFloat(match); + + if (Number.isNaN(revisionNumber)) { + throw new AppConfigError( + `Encryption revision is invalid. Got "${revision}" but expected a number or -"`, + ); + } + + return revisionNumber; +} + export function latestSymmetricKeyRevision( keys: (EncryptedSymmetricKey | DecryptedSymmetricKey)[], -): number { - keys.sort((a, b) => a.revision - b.revision); +): string { + keys.sort((a, b) => getRevisionNumber(a.revision) - getRevisionNumber(b.revision)); if (keys.length === 0) throw new InvalidEncryptionKey('No symmetric keys were found'); @@ -699,11 +728,22 @@ async function reencryptSymmetricKeys( previousSymmetricKeys: EncryptedSymmetricKey[], newTeamMembers: Key[], privateKey: Key, + environmentOptions?: EnvironmentOptions, ): Promise { const newEncryptionKeys: EncryptedSymmetricKey[] = []; if (previousSymmetricKeys.length === 0) { - const initialKey = await generateSymmetricKey(1); + let newRevision = '1'; + + if (environmentOptions) { + const env = currentEnvironment(environmentOptions); + + if (env) { + newRevision = `${env}-1`; + } + } + + const initialKey = await generateSymmetricKey(newRevision); const encrypted = await encryptSymmetricKey(initialKey, newTeamMembers); newEncryptionKeys.push(encrypted); @@ -879,8 +919,8 @@ function stringAsTypedArray(str: string): Uint16Array { return bufView; } -function encodeRevisionInPassword(password: Uint8Array, revision: number): Uint8Array { - const revisionBytes = stringAsTypedArray(revision.toString()); +function encodeRevisionInPassword(password: Uint8Array, revision: string): Uint8Array { + const revisionBytes = stringAsTypedArray(revision); const passwordWithRevision = new Uint8Array(password.length + revisionBytes.length + 1); // first byte is the revision length, next N bytes is the revision as a string @@ -891,12 +931,12 @@ function encodeRevisionInPassword(password: Uint8Array, revision: number): Uint8 return passwordWithRevision; } -function verifyEncodedRevision(password: Uint8Array, expectedRevision: number) { +function verifyEncodedRevision(password: Uint8Array, expectedRevision: string) { const revisionBytesLength = password[0]; const revisionBytes = password.slice(1, 1 + revisionBytesLength); const revision = decodeTypedArray(revisionBytes); - if (parseFloat(revision) !== expectedRevision) { + if (revision !== expectedRevision) { throw new EncryptionEncoding(oneLine` We detected tampering in the encryption key, revision ${expectedRevision}! This error occurs when the revision in the 'encryptionKeys' does not match the one that was embedded into the key. diff --git a/app-config-encryption/src/index.test.ts b/app-config-encryption/src/index.test.ts index 54ec86a4..e174086d 100644 --- a/app-config-encryption/src/index.test.ts +++ b/app-config-encryption/src/index.test.ts @@ -4,7 +4,7 @@ import encryptedDirective from './index'; describe('encryptedDirective', () => { it('loads an encrypted value', async () => { - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const source = new LiteralSource({ foo: await encryptValue('foobar', symmetricKey), @@ -16,7 +16,7 @@ describe('encryptedDirective', () => { }); it('loads an array of encrypted values', async () => { - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const source = new LiteralSource({ foo: [ diff --git a/app-config-encryption/src/secret-agent.test.ts b/app-config-encryption/src/secret-agent.test.ts index 50f73979..77e28596 100644 --- a/app-config-encryption/src/secret-agent.test.ts +++ b/app-config-encryption/src/secret-agent.test.ts @@ -28,7 +28,7 @@ describe('Decryption', () => { }); const privateKey = await loadPrivateKey(privateKeyArmored); - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const encryptedSymmetricKey = await encryptSymmetricKey(symmetricKey, [privateKey]); const port = await getPort(); @@ -84,7 +84,7 @@ describe('Unix Sockets', () => { }); const privateKey = await loadPrivateKey(privateKeyArmored); - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const encryptedSymmetricKey = await encryptSymmetricKey(symmetricKey, [privateKey]); const socket = resolve('./temporary-socket-file'); diff --git a/app-config-encryption/src/secret-agent.ts b/app-config-encryption/src/secret-agent.ts index ef55fcd8..b627ba93 100644 --- a/app-config-encryption/src/secret-agent.ts +++ b/app-config-encryption/src/secret-agent.ts @@ -139,15 +139,8 @@ export async function connectAgent( keepAlive(); const revision = text.split(':')[1]; - const revisionNumber = parseFloat(revision); - if (Number.isNaN(revisionNumber)) { - throw new AppConfigError( - `Encrypted value was invalid, revision was not a number (${revision})`, - ); - } - - const symmetricKey = await loadEncryptedKey(revisionNumber, environmentOptions); + const symmetricKey = await loadEncryptedKey(revision, environmentOptions); const decrypted = await client.Decrypt({ text, symmetricKey }); keepAlive(); @@ -248,7 +241,7 @@ export async function getAgentPortOrSocket( } async function loadSymmetricKey( - revision: number, + revision: string, environmentOptions?: EnvironmentOptions, ): Promise { const symmetricKeys = await loadSymmetricKeys(true, environmentOptions); diff --git a/app-config-meta/src/index.ts b/app-config-meta/src/index.ts index ab7b1959..65487cc0 100644 --- a/app-config-meta/src/index.ts +++ b/app-config-meta/src/index.ts @@ -27,7 +27,7 @@ export interface TeamMember { } export interface EncryptedSymmetricKey { - revision: number; + revision: string; key: string; } diff --git a/app-config-schema/src/index.test.ts b/app-config-schema/src/index.test.ts index 1638a8b3..1ad2cb9c 100644 --- a/app-config-schema/src/index.test.ts +++ b/app-config-schema/src/index.test.ts @@ -488,7 +488,7 @@ describe('Validation', () => { }, async (inDir) => { const { validate } = await loadSchema({ directory: inDir('.') }); - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const parsed = await ParsedValue.parseLiteral( { @@ -515,7 +515,7 @@ describe('Validation', () => { }, async (inDir) => { const { validate } = await loadSchema({ directory: inDir('.') }); - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const parsed = await ParsedValue.parseLiteral( [ @@ -542,7 +542,7 @@ describe('Validation', () => { }, async (inDir) => { const { validate } = await loadSchema({ directory: inDir('.') }); - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const parsed = await ParsedValue.parseLiteral( [ @@ -573,7 +573,7 @@ describe('Validation', () => { }, async (inDir) => { const { validate } = await loadSchema({ directory: inDir('.') }); - const symmetricKey = await generateSymmetricKey(1); + const symmetricKey = await generateSymmetricKey('1'); const parsed = await ParsedValue.parseLiteral( { From 0d5c00a74e615d838e539b4146e20e9288314717 Mon Sep 17 00:00:00 2001 From: John Brandt Date: Mon, 22 May 2023 16:22:00 -0600 Subject: [PATCH 10/22] fix: add environment team members properly --- app-config-encryption/src/encryption.ts | 82 ++++++++++++++++--------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index 4e0979eb..a7ed1427 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -552,7 +552,14 @@ export async function trustTeamMember( privateKey: Key, environmentOptions?: EnvironmentOptions, ) { - const teamMembers = await loadTeamMembers(environmentOptions); + let teamMembers: Key[] = []; + + try { + teamMembers = await loadTeamMembers(environmentOptions); + } catch { + // if this throws it's just because members for the selected env weren't found + // if the env wasn't found just add it + } if (newTeamMember.isPrivate()) { throw new InvalidEncryptionKey( @@ -571,8 +578,17 @@ export async function trustTeamMember( const newTeamMembers = teamMembers.concat(newTeamMember); + let currentKeys: EncryptedSymmetricKey[] = []; + + try { + currentKeys = await loadSymmetricKeys(true, environmentOptions); + } catch { + // if this throws it's just because keys for the selected env weren't found + // if the env wasn't found just add it + } + const newEncryptionKeys = await reencryptSymmetricKeys( - await loadSymmetricKeys(true, environmentOptions), + currentKeys, newTeamMembers, privateKey, environmentOptions, @@ -580,14 +596,19 @@ export async function trustTeamMember( await saveNewMetaFile((meta) => ({ ...meta, - teamMembers: newTeamMembers.map((key) => ({ - userId: key.getUserIds()[0], - keyName: key.keyName ?? null, - publicKey: key.armor(), - })), + teamMembers: addForEnvironment( + newTeamMembers.map((key) => ({ + userId: key.getUserIds()[0], + keyName: key.keyName ?? null, + publicKey: key.armor(), + })), + meta.teamMembers ?? {}, + environmentOptions, + true, + ), encryptionKeys: addForEnvironment( newEncryptionKeys, - meta.encryptionKeys ?? [], + meta.encryptionKeys ?? {}, environmentOptions, true, ), @@ -678,14 +699,19 @@ export async function untrustTeamMember( await saveNewMetaFile((meta) => ({ ...meta, - teamMembers: newTeamMembers.map((key) => ({ - userId: key.getUserIds()[0], - keyName: key.keyName ?? null, - publicKey: key.armor(), - })), + teamMembers: addForEnvironment( + newTeamMembers.map((key) => ({ + userId: key.getUserIds()[0], + keyName: key.keyName ?? null, + publicKey: key.armor(), + })), + meta.teamMembers ?? {}, + environmentOptions, + true, + ), encryptionKeys: addForEnvironment( newEncryptionKeys, - meta.encryptionKeys ?? [], + meta.encryptionKeys ?? {}, environmentOptions, true, ), @@ -693,7 +719,7 @@ export async function untrustTeamMember( } export function getRevisionNumber(revision: string) { - const regex = /^(?:\w*-)?(?\d*)$/; + const regex = /^(?:\w*-)?(?\d+)$/; const match = regex.exec(revision)?.groups?.revisionNumber; @@ -851,12 +877,18 @@ function addForEnvironment( return orig.concat(addArray); }; + const environment = currentEnvironment(environmentOptions); + + if (Array.isArray(values) && environment) { + throw new AppConfigError( + 'An environment was specified when adding a key but your meta file is not setup to use per environment keys', + ); + } + if (Array.isArray(values)) { return addOrReplace(values); } - const environment = currentEnvironment(environmentOptions); - if (environment === undefined) { if ('none' in values) { return { @@ -865,18 +897,10 @@ function addForEnvironment( }; } - if ('default' in values) { - return { - ...values, - default: addOrReplace(values.default), - }; - } - - const environments = Array.from(Object.keys(values).values()).join(', '); - - throw new AppConfigError( - `No current environment selected, found [${environments}] when adding environment-specific encryption options to meta file`, - ); + return { + ...values, + default: addOrReplace(values.default), + }; } if (environment in values) { From 2598c162b71e294cc2014df474277000190f912c Mon Sep 17 00:00:00 2001 From: John Brandt Date: Mon, 22 May 2023 16:23:24 -0600 Subject: [PATCH 11/22] test: add environments to encrypton tests --- app-config-encryption/src/encryption.test.ts | 184 ++++++++++++++++++- 1 file changed, 176 insertions(+), 8 deletions(-) diff --git a/app-config-encryption/src/encryption.test.ts b/app-config-encryption/src/encryption.test.ts index 6c6f4341..5a55d1d9 100644 --- a/app-config-encryption/src/encryption.test.ts +++ b/app-config-encryption/src/encryption.test.ts @@ -282,11 +282,171 @@ describe('Value Encryption', () => { }); }); +describe('per environment encryption E2E', () => { + it('sets up, trusts and untrusts users correctly', () => { + const cwd = process.cwd(); + + return withTempFiles({}, async (inDir) => { + // run environmentless + delete process.env.NODE_ENV; + + process.chdir(inDir('.')); + process.env.APP_CONFIG_SECRETS_KEYCHAIN_FOLDER = inDir('keychain'); + + const keys = await initializeKeysManually({ + name: 'Tester', + email: 'test@example.com', + }); + + const dirs = { + keychain: inDir('keychain'), + privateKey: inDir('keychain/private-key.asc'), + publicKey: inDir('keychain/public-key.asc'), + revocationCert: inDir('keychain/revocation.asc'), + }; + + expect(await initializeLocalKeys(keys, dirs)).toEqual({ + publicKeyArmored: keys.publicKeyArmored, + }); + + const publicKey = await loadPublicKey(); + const privateKey = await loadPrivateKey(); + + // this is what init-repo does + await trustTeamMember(publicKey, privateKey); + + // at this point, we should have ourselves trusted, and 1 symmetric key + const { value: meta } = await loadMetaConfig(); + + expect(meta.teamMembers).toHaveProperty('default'); + expect(meta.encryptionKeys).toHaveProperty('default'); + expect((meta.teamMembers! as any).default).toHaveLength(1); + expect((meta.encryptionKeys! as any).default).toHaveLength(1); + + const encryptionKey = await loadLatestSymmetricKey(privateKey); + const encrypted = await encryptValue('a secret value', encryptionKey); + await expect(decryptValue(encrypted, encryptionKey)).resolves.toBe('a secret value'); + + // now lets create a new environment + await trustTeamMember(publicKey, privateKey, { ...defaultEnvOptions, override: 'prod' }); + + // at this point, we should have a default and a prod env with 1 trusted member and 1 key each + const { value: prodEnvMeta } = await loadMetaConfig(); + + expect(prodEnvMeta.teamMembers).toHaveProperty('default'); + expect(prodEnvMeta.encryptionKeys).toHaveProperty('default'); + expect((prodEnvMeta.teamMembers! as any).default).toHaveLength(1); + expect((prodEnvMeta.encryptionKeys! as any).default).toHaveLength(1); + expect(prodEnvMeta.teamMembers).toHaveProperty('production'); + expect(prodEnvMeta.encryptionKeys).toHaveProperty('production'); + expect((prodEnvMeta.teamMembers! as any).production).toHaveLength(1); + expect((prodEnvMeta.encryptionKeys! as any).production).toHaveLength(1); + + const prodEncryptionKey = await loadLatestSymmetricKey(privateKey); + const prodEncrypted = await encryptValue('a secret value', prodEncryptionKey); + await expect(decryptValue(prodEncrypted, prodEncryptionKey)).resolves.toBe('a secret value'); + + const teammateKeys = await initializeKeysManually({ + name: 'A Teammate', + email: 'teammate@example.com', + }); + + const teammatePublicKey = await loadPublicKey(teammateKeys.publicKeyArmored); + const teammatePrivateKey = await loadPrivateKey(teammateKeys.privateKeyArmored); + + await trustTeamMember(teammatePublicKey, privateKey); + + // at this point, we should have 2 team members, but still 1 symmetric key + const { value: metaAfterTrustingTeammate } = await loadMetaConfig(); + + expect(metaAfterTrustingTeammate.teamMembers).toHaveProperty('default'); + expect(metaAfterTrustingTeammate.encryptionKeys).toHaveProperty('default'); + expect((metaAfterTrustingTeammate.teamMembers! as any).default).toHaveLength(2); + expect((metaAfterTrustingTeammate.encryptionKeys! as any).default).toHaveLength(1); + expect(metaAfterTrustingTeammate.teamMembers).toHaveProperty('production'); + expect(metaAfterTrustingTeammate.encryptionKeys).toHaveProperty('production'); + expect((metaAfterTrustingTeammate.teamMembers! as any).production).toHaveLength(1); + expect((metaAfterTrustingTeammate.encryptionKeys! as any).production).toHaveLength(1); + + // ensures that the teammate can now encrypt/decrypt values + const encryptedByTeammate = await encryptValue( + 'a secret value', + await loadLatestSymmetricKey(teammatePrivateKey), + ); + await expect( + decryptValue(encryptedByTeammate, await loadLatestSymmetricKey(teammatePrivateKey)), + ).resolves.toBe('a secret value'); + + // ensures that we can still encrypt/decrypt values + const encryptedByUs = await encryptValue( + 'a secret value', + await loadLatestSymmetricKey(privateKey), + ); + await expect( + decryptValue(encryptedByUs, await loadLatestSymmetricKey(privateKey)), + ).resolves.toBe('a secret value'); + + await untrustTeamMember('teammate@example.com', privateKey); + + // at this point, we should have 1 team members, and a newly generated symmetric key + const { value: metaAfterUntrustingTeammate } = await loadMetaConfig(); + + expect(metaAfterUntrustingTeammate.teamMembers).toHaveProperty('default'); + expect(metaAfterUntrustingTeammate.encryptionKeys).toHaveProperty('default'); + expect((metaAfterUntrustingTeammate.teamMembers! as any).default).toHaveLength(1); + expect((metaAfterUntrustingTeammate.encryptionKeys! as any).default).toHaveLength(2); + expect(metaAfterUntrustingTeammate.teamMembers).toHaveProperty('production'); + expect(metaAfterUntrustingTeammate.encryptionKeys).toHaveProperty('production'); + expect((metaAfterUntrustingTeammate.teamMembers! as any).production).toHaveLength(1); + expect((metaAfterUntrustingTeammate.encryptionKeys! as any).production).toHaveLength(1); + + // ensures that we can still encrypt/decrypt values + const newlyEncryptedByUs = await encryptValue( + 'a secret value', + await loadLatestSymmetricKey(privateKey), + ); + await expect( + decryptValue(newlyEncryptedByUs, await loadLatestSymmetricKey(privateKey)), + ).resolves.toBe('a secret value'); + + // now, the teammate should have no access + await expect(loadLatestSymmetricKey(teammatePrivateKey)).rejects.toThrow(); + + // just for test coverage, create a new symmetric key + const latestSymmetricKey = await loadLatestSymmetricKey(privateKey); + + const newRevisionNumber = getRevisionNumber(latestSymmetricKey.revision) + 1; + + await saveNewSymmetricKey( + await generateSymmetricKey(newRevisionNumber.toString()), + await loadTeamMembers(), + ); + + const { value: metaAfterNewSymmetricKey } = await loadMetaConfig(); + + expect(metaAfterNewSymmetricKey.teamMembers).toHaveProperty('default'); + expect(metaAfterNewSymmetricKey.encryptionKeys).toHaveProperty('default'); + expect((metaAfterNewSymmetricKey.teamMembers! as any).default).toHaveLength(1); + expect((metaAfterNewSymmetricKey.encryptionKeys! as any).default).toHaveLength(3); + expect(metaAfterNewSymmetricKey.teamMembers).toHaveProperty('production'); + expect(metaAfterNewSymmetricKey.encryptionKeys).toHaveProperty('production'); + expect((metaAfterNewSymmetricKey.teamMembers! as any).production).toHaveLength(1); + expect((metaAfterNewSymmetricKey.encryptionKeys! as any).production).toHaveLength(1); + + // get out of the directory, Windows doesn't like unlink while cwd + process.chdir(cwd); + }); + }); +}); + describe('E2E Encrypted Repo', () => { it('sets up, trusts and untrusts users correctly', () => { const cwd = process.cwd(); return withTempFiles({}, async (inDir) => { + // run environmentless + delete process.env.NODE_ENV; + process.chdir(inDir('.')); process.env.APP_CONFIG_SECRETS_KEYCHAIN_FOLDER = inDir('keychain'); @@ -315,8 +475,10 @@ describe('E2E Encrypted Repo', () => { // at this point, we should have ourselves trusted, and 1 symmetric key const { value: meta } = await loadMetaConfig(); - expect(meta.teamMembers).toHaveLength(1); - expect(meta.encryptionKeys).toHaveLength(1); + expect(meta.teamMembers).toHaveProperty('default'); + expect(meta.encryptionKeys).toHaveProperty('default'); + expect((meta.teamMembers! as any).default).toHaveLength(1); + expect((meta.encryptionKeys! as any).default).toHaveLength(1); const encryptionKey = await loadLatestSymmetricKey(privateKey); const encrypted = await encryptValue('a secret value', encryptionKey); @@ -335,8 +497,10 @@ describe('E2E Encrypted Repo', () => { // at this point, we should have 2 team members, but still 1 symmetric key const { value: metaAfterTrustingTeammate } = await loadMetaConfig(); - expect(metaAfterTrustingTeammate.teamMembers).toHaveLength(2); - expect(metaAfterTrustingTeammate.encryptionKeys).toHaveLength(1); + expect(metaAfterTrustingTeammate.teamMembers).toHaveProperty('default'); + expect(metaAfterTrustingTeammate.encryptionKeys).toHaveProperty('default'); + expect((metaAfterTrustingTeammate.teamMembers! as any).default).toHaveLength(2); + expect((metaAfterTrustingTeammate.encryptionKeys! as any).default).toHaveLength(1); // ensures that the teammate can now encrypt/decrypt values const encryptedByTeammate = await encryptValue( @@ -361,8 +525,10 @@ describe('E2E Encrypted Repo', () => { // at this point, we should have 1 team members, and a newly generated symmetric key const { value: metaAfterUntrustingTeammate } = await loadMetaConfig(); - expect(metaAfterUntrustingTeammate.teamMembers).toHaveLength(1); - expect(metaAfterUntrustingTeammate.encryptionKeys).toHaveLength(2); + expect(metaAfterUntrustingTeammate.teamMembers).toHaveProperty('default'); + expect(metaAfterUntrustingTeammate.encryptionKeys).toHaveProperty('default'); + expect((metaAfterUntrustingTeammate.teamMembers! as any).default).toHaveLength(1); + expect((metaAfterUntrustingTeammate.encryptionKeys! as any).default).toHaveLength(2); // ensures that we can still encrypt/decrypt values const newlyEncryptedByUs = await encryptValue( @@ -388,8 +554,10 @@ describe('E2E Encrypted Repo', () => { const { value: metaAfterNewSymmetricKey } = await loadMetaConfig(); - expect(metaAfterNewSymmetricKey.teamMembers).toHaveLength(1); - expect(metaAfterNewSymmetricKey.encryptionKeys).toHaveLength(3); + expect(metaAfterNewSymmetricKey.teamMembers).toHaveProperty('default'); + expect(metaAfterNewSymmetricKey.encryptionKeys).toHaveProperty('default'); + expect((metaAfterNewSymmetricKey.teamMembers! as any).default).toHaveLength(1); + expect((metaAfterNewSymmetricKey.encryptionKeys! as any).default).toHaveLength(3); // get out of the directory, Windows doesn't like unlink while cwd process.chdir(cwd); From bd8e1e90440774ed2c6e2afa67a520e2a6857063 Mon Sep 17 00:00:00 2001 From: John Brandt Date: Mon, 22 May 2023 16:23:57 -0600 Subject: [PATCH 12/22] feat: override env with whats in the key revision to allow for key reuse --- app-config-encryption/src/index.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/app-config-encryption/src/index.ts b/app-config-encryption/src/index.ts index 1d36af47..f033cd56 100644 --- a/app-config-encryption/src/index.ts +++ b/app-config-encryption/src/index.ts @@ -1,4 +1,4 @@ -import type { ParsingExtension } from '@app-config/core'; +import { AppConfigError, ParsingExtension } from '@app-config/core'; import { named } from '@app-config/extension-utils'; import { logger } from '@app-config/logging'; import { environmentOptionsFromContext } from '@app-config/node'; @@ -22,8 +22,28 @@ export default function encryptedDirective( ); } + // we override the environment with what's specified in the key revision + // so you can use the same key for multiple environments + + const revision = value.split(':')[1]; + + if (!revision) { + throw new AppConfigError(`Could not find key revision in encrypted value`); + } + + const envRegex = /^(?:(?\w*)-)?(?:\d+)$/; + const env = envRegex.exec(revision)?.groups?.env; const environmentOptions = environmentOptionsFromContext(ctx); - const decrypted = await decryptValue(value, symmetricKey, environmentOptions); + + if (env && environmentOptions) { + environmentOptions.override = env; + } + + const decrypted = await decryptValue( + value, + symmetricKey, + env ? environmentOptions : undefined, + ); return parse(decrypted, { fromSecrets: true, parsedFromEncryptedValue: true }); }; From af20cd24c149f2364a823dfb79a98f78e58ae45d Mon Sep 17 00:00:00 2001 From: John Brandt Date: Mon, 22 May 2023 16:48:10 -0600 Subject: [PATCH 13/22] docs: add multi environment encryption --- docs/guide/intro/encryption.md | 60 ++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/docs/guide/intro/encryption.md b/docs/guide/intro/encryption.md index 7729c8e5..a74d7b33 100644 --- a/docs/guide/intro/encryption.md +++ b/docs/guide/intro/encryption.md @@ -80,6 +80,9 @@ npx @app-config/cli secret trust ./my-key This will re-sign all encryption keys of the current repository with their public key. This gives them access to any previously encrypted secrets as well. +To specify an encryption environment to trust the user on, set the standard App-Config environment variables (`ENV`, `NODE_ENV`, or `APP_CONFIG_ENV`). +If no environment is specified, the user will be trusted on the `default` environment. + ## Untrusting Users You can untrust users as well. Please rotate secrets if they are a security concern. @@ -92,6 +95,11 @@ npx @app-config/cli secret untrust somebody@example.com This does not require re-encrypting any secrets. Any new encrypted values will use a new key that `somebody@example.com` never had access to. +To specify an encryption environment to untrust the user on, set the standard App-Config environment variables (`ENV`, `NODE_ENV`, or `APP_CONFIG_ENV`). +If no environment is specified, the user will be untrusted on the `default` environment. + +To completely untrust a user from your project you must untrust them from all encryption environments they were trusted on. + ::: warning While the above is true, be wary of how the timeline of events interacts with version control. ::: @@ -107,13 +115,20 @@ This key (public + private) can be added as protected environment variables in y The CLI will output both of these with instructions. +To use different keys for different secret environments suffix the environment variable names with the name of the environment. +For example - to specify the keys that will be used in an environment called `production` use these environment variables: + +- `APP_CONFIG_SECRETS_KEY_PRODUCTION` +- `APP_CONFIG_SECRETS_PUBLIC_KEY_PRODUCTION` + ## Implementation Details -- We store a list of team members public keys in meta files -- We store a list of 'encryptionKeys' (symmetric keys) in meta files +- We store a list of team members public keys per encryption environment in meta files +- We store a list of 'encryptionKeys' (symmetric keys) per encryption environment in meta files - Keys are symmetric, but are themselves stored in encrypted form (encrypted by team members' public keys, which allows any team member to decrypt it) - Once the key is given to somebody, they can always decrypt secrets that were encrypted with it - Keys have 'revision' numbers, so we can use the latest one (revision is embedded into the password itself, to prevent tampering in the YAML) + - The encryption environment is included in the revision to determine which symmetric key set to use to decrypt the secret - By keeping revisions, we can untrust a user without having to re-encrypt every secret made before - You'd likely still want to rotate most passwords, but doing so automatically (dumping out YAML files everywhere) would be extremely difficult to do right - The secrets are already compromised, so manual intervention is needed regardless @@ -146,6 +161,29 @@ It serves no logical roles, it's simply there as metadata for you to identify wh

.app-config.meta.yml

+```yaml +teamMembers: + default: + - userId: Joel Gallant + keyName: Desktop + publicKey: '...' + - userId: Joel Gallant + keyName: Laptop + publicKey: '...' +encryptionKeys: + default: + - revision: 1 + key: '...' +``` + +## Multiple Environments + +It can be useful to have different levels of trust on the same repository. For example, it may make sense for your project to have fewer people able to decrypt the secrets used on production then the secrets for a QA environment. + +To add secret environments to an existing project just move your existing `teamMembers` and `encryptionKeys` values under a `default` property. + +For example: + ```yaml teamMembers: - userId: Joel Gallant @@ -158,3 +196,21 @@ encryptionKeys: - revision: 1 key: '...' ``` + +Becomes + +```yaml +teamMembers: + default: + - userId: Joel Gallant + keyName: Desktop + publicKey: '...' + - userId: Joel Gallant + keyName: Laptop + publicKey: '...' +encryptionKeys: + default: + - revision: 1 + key: '...' +``` +To create a new encryption environment use the `init-key` CLI subcommand while setting one of the standard App-Config environment variables (`ENV`, `NODE_ENV`, or `APP_CONFIG_ENV`) with the new encryption environment. From 4620708ac505b4d9ca4ab792862dae4526142259 Mon Sep 17 00:00:00 2001 From: John Brandt Date: Mon, 22 May 2023 16:55:05 -0600 Subject: [PATCH 14/22] fix: init-key creates the right key revision --- app-config-cli/src/index.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app-config-cli/src/index.ts b/app-config-cli/src/index.ts index f07d85da..54130e8e 100644 --- a/app-config-cli/src/index.ts +++ b/app-config-cli/src/index.ts @@ -16,7 +16,7 @@ import { FailedToSelectSubObject, EmptyStdinOrPromptResponse, } from '@app-config/core'; -import { promptUser, consumeStdin, asEnvOptions } from '@app-config/node'; +import { promptUser, consumeStdin, asEnvOptions, currentEnvironment } from '@app-config/node'; import { checkTTY, LogLevel, logger } from '@app-config/logging'; import { LoadedConfiguration, @@ -628,16 +628,19 @@ export const cli = yargs }, async (opts) => { const environmentOptions = await loadEnvironmentOptions(opts); + const environment = currentEnvironment(environmentOptions); const keys = await loadSymmetricKeys(undefined, environmentOptions); const teamMembers = await loadTeamMembersLazy(environmentOptions); - let revision: number; + let revision: string; if (keys.length > 0) { - revision = latestSymmetricKeyRevision(keys) + 1; + revision = latestSymmetricKeyRevision(keys); + } else if (environment) { + revision = `${environment}-1`; } else { - revision = 1; + revision = '1'; } await saveNewSymmetricKey( From ac2d0772d062df75033f52008c7d9b98f0475501 Mon Sep 17 00:00:00 2001 From: John Brandt Date: Mon, 5 Jun 2023 16:54:35 -0600 Subject: [PATCH 15/22] fix: encryption environment adding and reading old key revisions --- app-config-cli/src/index.ts | 17 +++++++++-------- app-config-meta/src/index.ts | 23 +++++++++++++++++++++++ docs/guide/intro/encryption.md | 3 ++- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/app-config-cli/src/index.ts b/app-config-cli/src/index.ts index 54130e8e..5025668d 100644 --- a/app-config-cli/src/index.ts +++ b/app-config-cli/src/index.ts @@ -44,6 +44,7 @@ import { shouldUseSecretAgent, startAgent, disconnectAgents, + getRevisionNumber, } from '@app-config/encryption'; import { loadSchema, JSONSchema } from '@app-config/schema'; import { generateTypeFiles } from '@app-config/generate'; @@ -595,7 +596,6 @@ export const cli = yargs ], options: { environmentOverride: environmentOverrideOption, - environmentVariableName: environmentVariableNameOption, }, }, async (opts) => { @@ -623,7 +623,6 @@ export const cli = yargs ], options: { environmentOverride: environmentOverrideOption, - environmentVariableName: environmentVariableNameOption, }, }, async (opts) => { @@ -636,7 +635,14 @@ export const cli = yargs let revision: string; if (keys.length > 0) { - revision = latestSymmetricKeyRevision(keys); + const latestRevison = latestSymmetricKeyRevision(keys); + const revNumber = getRevisionNumber(latestRevison); + + if (environment) { + revision = `${environment}-${revNumber + 1}`; + } else { + revision = `${revNumber + 1}`; + } } else if (environment) { revision = `${environment}-1`; } else { @@ -708,7 +714,6 @@ export const cli = yargs 'Creates an encryption key that can be used without a passphrase (useful for CI)', options: { environmentOverride: environmentOverrideOption, - environmentVariableName: environmentVariableNameOption, }, }, async (opts) => { @@ -757,7 +762,6 @@ export const cli = yargs }, options: { environmentOverride: environmentOverrideOption, - environmentVariableName: environmentVariableNameOption, }, }, async (opts) => { @@ -791,7 +795,6 @@ export const cli = yargs }, options: { environmentOverride: environmentOverrideOption, - environmentVariableName: environmentVariableNameOption, }, }, async (opts) => { @@ -822,7 +825,6 @@ export const cli = yargs clipboard: clipboardOption, agent: secretAgentOption, environmentOverride: environmentOverrideOption, - environmentVariableName: environmentVariableNameOption, }, }, async (opts) => { @@ -890,7 +892,6 @@ export const cli = yargs clipboard: clipboardOption, agent: secretAgentOption, environmentOverride: environmentOverrideOption, - environmentVariableName: environmentVariableNameOption, }, }, async (opts) => { diff --git a/app-config-meta/src/index.ts b/app-config-meta/src/index.ts index 65487cc0..85f8515f 100644 --- a/app-config-meta/src/index.ts +++ b/app-config-meta/src/index.ts @@ -101,6 +101,9 @@ export async function loadMetaConfig({ const parsed = await source.read(defaultMetaExtensions()); const value = parsed.toJSON() as MetaProperties; + // normalize all revisions to be strings even if they're numbers + normalizeMetaEncryptionKeyRevisions(value); + const fileSources = parsed.sources.filter((s) => s instanceof FileSource) as FileSource[]; const [{ filePath, fileType }] = fileSources.filter((s) => s.filePath.includes(fileNameBase)); @@ -124,6 +127,26 @@ export async function loadMetaConfig({ } } +function normalizeMetaEncryptionKeyRevisions(meta: MetaProperties): MetaProperties { + const stringifyRevision = (keys: EncryptedSymmetricKey[]) => { + for (const key of keys) { + if (typeof key.revision !== 'string') { + key.revision = (key.revision as number).toString(); + } + } + }; + + if (Array.isArray(meta.encryptionKeys)) { + stringifyRevision(meta.encryptionKeys); + } else if (meta.encryptionKeys) { + for (const env of Object.keys(meta.encryptionKeys)) { + stringifyRevision(meta.encryptionKeys[env]); + } + } + + return meta; +} + let metaConfig: Promise | undefined; export async function loadMetaConfigLazy(options?: MetaLoadingOptions): Promise { diff --git a/docs/guide/intro/encryption.md b/docs/guide/intro/encryption.md index a74d7b33..6317b5f5 100644 --- a/docs/guide/intro/encryption.md +++ b/docs/guide/intro/encryption.md @@ -213,4 +213,5 @@ encryptionKeys: - revision: 1 key: '...' ``` -To create a new encryption environment use the `init-key` CLI subcommand while setting one of the standard App-Config environment variables (`ENV`, `NODE_ENV`, or `APP_CONFIG_ENV`) with the new encryption environment. + +To create a new encryption environment use the `init-repo` CLI subcommand while setting one of the standard App-Config environment variables (`ENV`, `NODE_ENV`, or `APP_CONFIG_ENV`) with the new encryption environment. From 3cf016d22bf28ea4d8f48230a7581ac858fefbca Mon Sep 17 00:00:00 2001 From: John Brandt Date: Mon, 5 Jun 2023 19:37:51 -0600 Subject: [PATCH 16/22] fix: select all symmetric keys when no environment is selected --- app-config-encryption/src/encryption.ts | 28 ++++++++++++++++++++----- app-config-encryption/src/index.ts | 27 ++++-------------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index a7ed1427..7af3a4ad 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -362,13 +362,17 @@ export async function loadSymmetricKeys( value: { encryptionKeys = [] }, } = await loadMeta(); - const selected = selectForEnvironment(encryptionKeys, environmentOptions); + if (environmentOptions) { + const selected = selectForEnvironment(encryptionKeys, environmentOptions); - logger.verbose( - `Found ${selected.length} symmetric keys for environment: ${environment ?? 'none'}`, - ); + logger.verbose( + `Found ${selected.length} symmetric keys for environment: ${environment ?? 'none'}`, + ); - return selected; + return selected; + } + + return selectAll(encryptionKeys); } export async function loadSymmetricKey( @@ -819,6 +823,20 @@ async function saveNewMetaFile(mutate: (props: MetaProperties) => MetaProperties await fs.writeFile(writeFilePath, stringify(writeMeta, writeFileType)); } +function selectAll(values: T[] | Record): T[] { + if (Array.isArray(values)) { + return values; + } + + const allValues: T[] = []; + + for (const key of Object.keys(values)) { + allValues.push(...values[key]); + } + + return allValues; +} + function selectForEnvironment( values: T[] | Record, environmentOptions: EnvironmentOptions | undefined, diff --git a/app-config-encryption/src/index.ts b/app-config-encryption/src/index.ts index f033cd56..ffa37b06 100644 --- a/app-config-encryption/src/index.ts +++ b/app-config-encryption/src/index.ts @@ -13,7 +13,7 @@ export default function encryptedDirective( symmetricKey?: DecryptedSymmetricKey, shouldShowDeprecationNotice?: true, ): ParsingExtension { - return named('encryption', (value, _, __, ctx) => { + return named('encryption', (value) => { if (typeof value === 'string' && value.startsWith('enc:')) { return async (parse) => { if (shouldShowDeprecationNotice) { @@ -22,28 +22,9 @@ export default function encryptedDirective( ); } - // we override the environment with what's specified in the key revision - // so you can use the same key for multiple environments - - const revision = value.split(':')[1]; - - if (!revision) { - throw new AppConfigError(`Could not find key revision in encrypted value`); - } - - const envRegex = /^(?:(?\w*)-)?(?:\d+)$/; - const env = envRegex.exec(revision)?.groups?.env; - const environmentOptions = environmentOptionsFromContext(ctx); - - if (env && environmentOptions) { - environmentOptions.override = env; - } - - const decrypted = await decryptValue( - value, - symmetricKey, - env ? environmentOptions : undefined, - ); + // we don't need to pass the environment here - we use the key revision + // to determine which symmetric key to use + const decrypted = await decryptValue(value, symmetricKey); return parse(decrypted, { fromSecrets: true, parsedFromEncryptedValue: true }); }; From fb54e0bb6cb5f6f3f28ef783d15a392111597098 Mon Sep 17 00:00:00 2001 From: John Brandt Date: Mon, 5 Jun 2023 19:59:07 -0600 Subject: [PATCH 17/22] fix: only send the decryption env if one is specified --- app-config-cli/src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app-config-cli/src/index.ts b/app-config-cli/src/index.ts index 5025668d..e7fb9d5e 100644 --- a/app-config-cli/src/index.ts +++ b/app-config-cli/src/index.ts @@ -896,6 +896,7 @@ export const cli = yargs }, async (opts) => { const environmentOptions = await loadEnvironmentOptions(opts); + const environment = currentEnvironment(environmentOptions); shouldUseSecretAgent(opts.agent); @@ -924,7 +925,12 @@ export const cli = yargs throw new EmptyStdinOrPromptResponse('Failed to read from stdin or prompt'); } - const decrypted = await decryptValue(encryptedText, undefined, environmentOptions); + // only use an environment if one was provided - otherwise just find the key to use based on the revision + const decrypted = await decryptValue( + encryptedText, + undefined, + environment ? environmentOptions : undefined, + ); process.stdout.write(JSON.stringify(decrypted)); process.stdout.write('\n'); From ac9cecd1f1551900feda032b56f8d3ca9c66ddc6 Mon Sep 17 00:00:00 2001 From: John Brandt Date: Mon, 5 Jun 2023 20:02:06 -0600 Subject: [PATCH 18/22] chore: encryption docs --- docs/guide/intro/encryption.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/guide/intro/encryption.md b/docs/guide/intro/encryption.md index 6317b5f5..96909112 100644 --- a/docs/guide/intro/encryption.md +++ b/docs/guide/intro/encryption.md @@ -214,4 +214,6 @@ encryptionKeys: key: '...' ``` -To create a new encryption environment use the `init-repo` CLI subcommand while setting one of the standard App-Config environment variables (`ENV`, `NODE_ENV`, or `APP_CONFIG_ENV`) with the new encryption environment. +To create a new encryption environment use the `init-repo` CLI subcommand while setting one of the standard App-Config environment variables (`ENV`, `NODE_ENV`, or `APP_CONFIG_ENV`) with the new encryption environment. You can then use the normal App Config secret CLI commands while specifying the environment to trust and untrust users and encrypt and decrypt secrets for that specific environment. + +It's also possible to reuse encryption keys across environments since App Config secret environments and config environments are not linked. For example, you may have 3 config environments like prod, QA, and staging but only 2 encryption environments prod and QA. The production environment likely has more strict access requirements than staging and QA which may have the same users trusted on them. This allows you to trust a user once on the shared QA/staging encryption environment which will allow them to decrypt secrets used on staging and QA. From c22424c1f2a1a4833cbfe76f7c07877c6d5a0c3c Mon Sep 17 00:00:00 2001 From: John Brandt Date: Wed, 7 Jun 2023 16:33:12 -0600 Subject: [PATCH 19/22] chore: readd key file env var support --- app-config-encryption/src/encryption.ts | 47 ++++++++++++++++++++----- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index 7af3a4ad..4f7378c9 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -156,7 +156,7 @@ export async function loadPrivateKey( if (override) { overrideKey = override; } else { - overrideKey = getKeyFromEnv('private', environmentOptions); + overrideKey = await getKeyFromEnv('private', environmentOptions); } if (overrideKey) { @@ -200,7 +200,7 @@ export async function loadPublicKey( if (override) { overrideKey = override; } else { - overrideKey = getKeyFromEnv('public', environmentOptions); + overrideKey = await getKeyFromEnv('public', environmentOptions); } if (overrideKey) { @@ -219,7 +219,7 @@ export async function loadPublicKey( return key; } -function getKeyFromEnv(keyType: 'private' | 'public', envOptions?: EnvironmentOptions) { +async function getKeyFromEnv(keyType: 'private' | 'public', envOptions?: EnvironmentOptions) { const env = currentEnvironment(envOptions); const envVarPrefix = @@ -231,17 +231,39 @@ function getKeyFromEnv(keyType: 'private' | 'public', envOptions?: EnvironmentOp let key = process.env[`${envVarPrefix}_${env.toUpperCase()}`]; - // try an alias if we didn't find the key first try - if (!key) { + const tryAliases = (envVarName: (alias: string) => string) => { const aliases = aliasesFor(env, envOptions.aliases); for (const alias of aliases) { - key = process.env[`${envVarPrefix}_${alias.toUpperCase()}`]; + const val = process.env[envVarName(alias.toUpperCase())]; - if (key) { - break; + if (val) { + return val; } } + }; + + // try an alias if we didn't find the key first try + if (!key) { + key = tryAliases((alias) => `${envVarPrefix}_${alias}`); + } + + // see if a file was specified for the environment + if (!key) { + const file = process.env[`${envVarPrefix}_${env.toUpperCase()}_FILE`]; + + if (file) { + key = (await fs.readFile(file)).toString(); + } + } + + // try an env alias if we don't have the key from a file + if (!key) { + const file = tryAliases((alias) => `${envVarPrefix}_${alias}_FILE`); + + if (file) { + key = (await fs.readFile(file)).toString(); + } } // if we didn't find a key with an environment, fallback on one without if it exists @@ -249,6 +271,15 @@ function getKeyFromEnv(keyType: 'private' | 'public', envOptions?: EnvironmentOp key = process.env[envVarPrefix]; } + // if a key still wasn't found try read from a file specified + if (!key) { + const file = process.env[`${envVarPrefix}_FILE`]; + + if (file) { + key = (await fs.readFile(file)).toString(); + } + } + return key; } From 0d8cf899f2939a21bf04c96c9364f35be9e6b173 Mon Sep 17 00:00:00 2001 From: John Brandt Date: Wed, 7 Jun 2023 20:58:57 -0600 Subject: [PATCH 20/22] fix: prioritize default keys when no env is selected --- app-config-encryption/src/encryption.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index 4f7378c9..56b36c03 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -778,7 +778,22 @@ export function getRevisionNumber(revision: string) { export function latestSymmetricKeyRevision( keys: (EncryptedSymmetricKey | DecryptedSymmetricKey)[], ): string { - keys.sort((a, b) => getRevisionNumber(a.revision) - getRevisionNumber(b.revision)); + keys.sort((a, b) => { + // sort the default keys first + // this is ok because if we have an environment the keys should be filtered by env first + let aRevNum = getRevisionNumber(a.revision); + let bRevNum = getRevisionNumber(b.revision); + + if (!a.revision.includes('-')) { + aRevNum += 1000; + } + + if (!b.revision.includes('-')) { + bRevNum += 1000; + } + + return aRevNum - bRevNum; + }); if (keys.length === 0) throw new InvalidEncryptionKey('No symmetric keys were found'); From f01a273278128a04c03a5a6a23f652d7e1b61f8f Mon Sep 17 00:00:00 2001 From: John Brandt Date: Thu, 8 Jun 2023 13:07:29 -0600 Subject: [PATCH 21/22] chore: fix lint --- app-config-encryption/src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app-config-encryption/src/index.ts b/app-config-encryption/src/index.ts index ffa37b06..03278e43 100644 --- a/app-config-encryption/src/index.ts +++ b/app-config-encryption/src/index.ts @@ -1,7 +1,6 @@ -import { AppConfigError, ParsingExtension } from '@app-config/core'; +import { ParsingExtension } from '@app-config/core'; import { named } from '@app-config/extension-utils'; import { logger } from '@app-config/logging'; -import { environmentOptionsFromContext } from '@app-config/node'; import { DecryptedSymmetricKey, decryptValue } from './encryption'; export * from './encryption'; From 22d0f46999459338005e669724ec3f283d2b5e56 Mon Sep 17 00:00:00 2001 From: John Brandt Date: Thu, 8 Jun 2023 15:02:50 -0600 Subject: [PATCH 22/22] chore: release v2.9.0-beta.1 --- app-config-cli/package.json | 20 +++++++------- app-config-config/package.json | 20 +++++++------- app-config-core/package.json | 8 +++--- app-config-cypress/package.json | 6 ++--- app-config-default-extensions/package.json | 10 +++---- app-config-electron/package.json | 2 +- app-config-encryption/package.json | 18 ++++++------- app-config-esbuild/package.json | 10 +++---- app-config-exec/package.json | 14 +++++----- app-config-extension-utils/package.json | 4 +-- app-config-extensions/package.json | 14 +++++----- app-config-generate/package.json | 10 +++---- app-config-git/package.json | 10 +++---- app-config-inject/package.json | 10 +++---- app-config-js/package.json | 10 +++---- app-config-logging/package.json | 4 +-- app-config-main/package.json | 26 +++++++++---------- app-config-meta/package.json | 14 +++++----- app-config-node/package.json | 8 +++--- app-config-react-native/package.json | 8 +++--- app-config-rollup/package.json | 8 +++--- app-config-schema/package.json | 16 ++++++------ app-config-settings/package.json | 12 ++++----- app-config-test-utils/package.json | 6 ++--- app-config-utils/package.json | 2 +- app-config-v1-compat/package.json | 16 ++++++------ app-config-vault/package.json | 8 +++--- app-config-vite/package.json | 4 +-- app-config-webpack/package.json | 14 +++++----- lcdev-app-config-inject/package.json | 4 +-- lcdev-app-config-webpack-plugin/package.json | 4 +-- lcdev-app-config/package.json | 6 ++--- .../package.json | 4 +-- 33 files changed, 165 insertions(+), 165 deletions(-) diff --git a/app-config-cli/package.json b/app-config-cli/package.json index 2dc2dc9a..e51bbd3e 100644 --- a/app-config-cli/package.json +++ b/app-config-cli/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/cli", "description": "CLI for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -35,14 +35,14 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/config": "^2.8.7", - "@app-config/core": "^2.8.7", - "@app-config/encryption": "^2.8.7", - "@app-config/generate": "^2.8.7", - "@app-config/logging": "^2.8.7", - "@app-config/node": "^2.8.7", - "@app-config/schema": "^2.8.7", - "@app-config/utils": "^2.8.7", + "@app-config/config": "^2.9.0-beta.1", + "@app-config/core": "^2.9.0-beta.1", + "@app-config/encryption": "^2.9.0-beta.1", + "@app-config/generate": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/node": "^2.9.0-beta.1", + "@app-config/schema": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1", "ajv": "7", "clipboardy": "2", "common-tags": "1", @@ -52,7 +52,7 @@ "yargs": "16" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7", + "@app-config/test-utils": "^2.9.0-beta.1", "@types/common-tags": "1", "@types/fs-extra": "9" }, diff --git a/app-config-config/package.json b/app-config-config/package.json index 584bc1da..da740b4a 100644 --- a/app-config-config/package.json +++ b/app-config-config/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/config", "description": "The config in @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,17 +30,17 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/core": "^2.8.7", - "@app-config/default-extensions": "^2.8.7", - "@app-config/extensions": "^2.8.7", - "@app-config/logging": "^2.8.7", - "@app-config/meta": "^2.8.7", - "@app-config/node": "^2.8.7", - "@app-config/schema": "^2.8.7", - "@app-config/utils": "^2.8.7" + "@app-config/core": "^2.9.0-beta.1", + "@app-config/default-extensions": "^2.9.0-beta.1", + "@app-config/extensions": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/meta": "^2.9.0-beta.1", + "@app-config/node": "^2.9.0-beta.1", + "@app-config/schema": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7" + "@app-config/test-utils": "^2.9.0-beta.1" }, "prettier": "@lcdev/prettier", "jest": { diff --git a/app-config-core/package.json b/app-config-core/package.json index 3b517e1c..e89c4a88 100644 --- a/app-config-core/package.json +++ b/app-config-core/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/core", "description": "Core logic for App Config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,15 +30,15 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/logging": "^2.8.7", - "@app-config/utils": "^2.8.7", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1", "@iarna/toml": "3", "js-yaml": "^3.13.1", "json5": "2", "lodash.merge": "^4.6.2" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7", + "@app-config/test-utils": "^2.9.0-beta.1", "@types/js-yaml": "3", "@types/lodash.merge": "4" }, diff --git a/app-config-cypress/package.json b/app-config-cypress/package.json index 19fab046..d67f5f3f 100644 --- a/app-config-cypress/package.json +++ b/app-config-cypress/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/cypress", "description": "Cypress testing plugin for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -31,11 +31,11 @@ }, "dependencies": {}, "peerDependencies": { - "@app-config/main": "^2.8.7", + "@app-config/main": "^2.9.0-beta.1", "cypress": "6" }, "devDependencies": { - "@app-config/main": "^2.8.7", + "@app-config/main": "^2.9.0-beta.1", "cypress": "6" }, "prettier": "@lcdev/prettier", diff --git a/app-config-default-extensions/package.json b/app-config-default-extensions/package.json index 7658c91a..6031e5c1 100644 --- a/app-config-default-extensions/package.json +++ b/app-config-default-extensions/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/default-extensions", "description": "Default parsing extensions for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -22,10 +22,10 @@ "test": "jest" }, "dependencies": { - "@app-config/core": "^2.8.7", - "@app-config/encryption": "^2.8.7", - "@app-config/extensions": "^2.8.7", - "@app-config/git": "^2.8.7", + "@app-config/core": "^2.9.0-beta.1", + "@app-config/encryption": "^2.9.0-beta.1", + "@app-config/extensions": "^2.9.0-beta.1", + "@app-config/git": "^2.9.0-beta.1", "@app-config/v1-compat": "^2.1.4" }, "devDependencies": {}, diff --git a/app-config-electron/package.json b/app-config-electron/package.json index 94152c74..a583ebbc 100644 --- a/app-config-electron/package.json +++ b/app-config-electron/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/electron", "description": "Exposes app-config values to Electron render processes", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", diff --git a/app-config-encryption/package.json b/app-config-encryption/package.json index 6919a946..f0c4944d 100644 --- a/app-config-encryption/package.json +++ b/app-config-encryption/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/encryption", "description": "Secret value encryption for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,13 +30,13 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/core": "^2.8.7", - "@app-config/extension-utils": "^2.8.7", - "@app-config/logging": "^2.8.7", - "@app-config/meta": "^2.8.7", - "@app-config/node": "^2.8.7", - "@app-config/settings": "^2.8.7", - "@app-config/utils": "^2.8.7", + "@app-config/core": "^2.9.0-beta.1", + "@app-config/extension-utils": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/meta": "^2.9.0-beta.1", + "@app-config/node": "^2.9.0-beta.1", + "@app-config/settings": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1", "@lcdev/ws-rpc": "0.4", "@types/openpgp": "4", "common-tags": "1", @@ -47,7 +47,7 @@ "ws": "^7.4.6" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7", + "@app-config/test-utils": "^2.9.0-beta.1", "get-port": "5" }, "prettier": "@lcdev/prettier", diff --git a/app-config-esbuild/package.json b/app-config-esbuild/package.json index 6961b018..181a6c88 100644 --- a/app-config-esbuild/package.json +++ b/app-config-esbuild/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/esbuild", "description": "esbuild module resolution support for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,12 +30,12 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/config": "^2.8.7", - "@app-config/schema": "^2.8.7", - "@app-config/utils": "^2.8.7" + "@app-config/config": "^2.9.0-beta.1", + "@app-config/schema": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7", + "@app-config/test-utils": "^2.9.0-beta.1", "esbuild": "0.13" }, "prettier": "@lcdev/prettier", diff --git a/app-config-exec/package.json b/app-config-exec/package.json index 9580c048..5bf3361c 100644 --- a/app-config-exec/package.json +++ b/app-config-exec/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/exec", "description": "Generate config by running arbitrary programs", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,14 +30,14 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/core": "^2.8.7", - "@app-config/extension-utils": "^2.8.7", - "@app-config/node": "^2.8.7", - "@app-config/utils": "^2.8.7" + "@app-config/core": "^2.9.0-beta.1", + "@app-config/extension-utils": "^2.9.0-beta.1", + "@app-config/node": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1" }, "devDependencies": { - "@app-config/main": "^2.8.7", - "@app-config/test-utils": "^2.8.7" + "@app-config/main": "^2.9.0-beta.1", + "@app-config/test-utils": "^2.9.0-beta.1" }, "prettier": "@lcdev/prettier", "jest": { diff --git a/app-config-extension-utils/package.json b/app-config-extension-utils/package.json index a1a42b50..012cd1ff 100644 --- a/app-config-extension-utils/package.json +++ b/app-config-extension-utils/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/extension-utils", "description": "Utilities for writing @app-config parsing extensions", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,7 +30,7 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/core": "^2.8.7", + "@app-config/core": "^2.9.0-beta.1", "@serafin/schema-builder": "0.14" }, "devDependencies": {}, diff --git a/app-config-extensions/package.json b/app-config-extensions/package.json index 8a42a799..f3e2ce3d 100644 --- a/app-config-extensions/package.json +++ b/app-config-extensions/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/extensions", "description": "Common parsing extensions for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,15 +30,15 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/core": "^2.8.7", - "@app-config/extension-utils": "^2.8.7", - "@app-config/logging": "^2.8.7", - "@app-config/node": "^2.8.7", - "@app-config/utils": "^2.8.7", + "@app-config/core": "^2.9.0-beta.1", + "@app-config/extension-utils": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/node": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1", "lodash.isequal": "4" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7", + "@app-config/test-utils": "^2.9.0-beta.1", "@types/lodash.isequal": "4" }, "prettier": "@lcdev/prettier", diff --git a/app-config-generate/package.json b/app-config-generate/package.json index 0b6d810b..cad247bb 100644 --- a/app-config-generate/package.json +++ b/app-config-generate/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/generate", "description": "Code generation for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,9 +30,9 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/logging": "^2.8.7", - "@app-config/meta": "^2.8.7", - "@app-config/schema": "^2.8.7", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/meta": "^2.9.0-beta.1", + "@app-config/schema": "^2.9.0-beta.1", "@types/readable-stream": "2", "@types/urijs": "1", "common-tags": "1", @@ -41,7 +41,7 @@ "quicktype-core": "6.0.70" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7" + "@app-config/test-utils": "^2.9.0-beta.1" }, "prettier": "@lcdev/prettier", "jest": { diff --git a/app-config-git/package.json b/app-config-git/package.json index 54b7e967..2dad4c4b 100644 --- a/app-config-git/package.json +++ b/app-config-git/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/git", "description": "$git directive parsing extension for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,13 +30,13 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/core": "^2.8.7", - "@app-config/extension-utils": "^2.8.7", - "@app-config/logging": "^2.8.7", + "@app-config/core": "^2.9.0-beta.1", + "@app-config/extension-utils": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", "simple-git": "3" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7" + "@app-config/test-utils": "^2.9.0-beta.1" }, "prettier": "@lcdev/prettier", "jest": { diff --git a/app-config-inject/package.json b/app-config-inject/package.json index 72c42f18..3f557473 100644 --- a/app-config-inject/package.json +++ b/app-config-inject/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/inject", "description": "Runtime injection of app-config into static HTML files", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -34,10 +34,10 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/config": "^2.8.7", - "@app-config/logging": "^2.8.7", - "@app-config/node": "^2.8.7", - "@app-config/schema": "^2.8.7", + "@app-config/config": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/node": "^2.9.0-beta.1", + "@app-config/schema": "^2.9.0-beta.1", "@types/yargs": "16", "node-html-parser": "1", "yargs": "16" diff --git a/app-config-js/package.json b/app-config-js/package.json index 840b7e3e..7a04bbda 100644 --- a/app-config-js/package.json +++ b/app-config-js/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/js", "description": "Loads a JavaScript module to inject configuration", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,12 +30,12 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/core": "^2.8.7", - "@app-config/extension-utils": "^2.8.7", - "@app-config/node": "^2.8.7" + "@app-config/core": "^2.9.0-beta.1", + "@app-config/extension-utils": "^2.9.0-beta.1", + "@app-config/node": "^2.9.0-beta.1" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7" + "@app-config/test-utils": "^2.9.0-beta.1" }, "prettier": "@lcdev/prettier", "jest": { diff --git a/app-config-logging/package.json b/app-config-logging/package.json index ab923070..e04901ec 100644 --- a/app-config-logging/package.json +++ b/app-config-logging/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/logging", "description": "Logging for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,7 +30,7 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/utils": "^2.8.7" + "@app-config/utils": "^2.9.0-beta.1" }, "devDependencies": {}, "prettier": "@lcdev/prettier", diff --git a/app-config-main/package.json b/app-config-main/package.json index 0668ec00..b6c0a75b 100644 --- a/app-config-main/package.json +++ b/app-config-main/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/main", "description": "Easy to use configuration loader with schema validation", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -35,21 +35,21 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/cli": "^2.8.7", - "@app-config/config": "^2.8.7", - "@app-config/core": "^2.8.7", - "@app-config/default-extensions": "^2.8.7", - "@app-config/encryption": "^2.8.7", - "@app-config/extensions": "^2.8.7", - "@app-config/logging": "^2.8.7", - "@app-config/meta": "^2.8.7", - "@app-config/node": "^2.8.7", - "@app-config/schema": "^2.8.7", - "@app-config/utils": "^2.8.7", + "@app-config/cli": "^2.9.0-beta.1", + "@app-config/config": "^2.9.0-beta.1", + "@app-config/core": "^2.9.0-beta.1", + "@app-config/default-extensions": "^2.9.0-beta.1", + "@app-config/encryption": "^2.9.0-beta.1", + "@app-config/extensions": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/meta": "^2.9.0-beta.1", + "@app-config/node": "^2.9.0-beta.1", + "@app-config/schema": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1", "ajv": "7" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7" + "@app-config/test-utils": "^2.9.0-beta.1" }, "prettier": "@lcdev/prettier", "jest": { diff --git a/app-config-meta/package.json b/app-config-meta/package.json index 2dfb3a72..f5eefbf7 100644 --- a/app-config-meta/package.json +++ b/app-config-meta/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/meta", "description": "Meta file parsing for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,15 +30,15 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/core": "^2.8.7", - "@app-config/default-extensions": "^2.8.7", - "@app-config/logging": "^2.8.7", - "@app-config/node": "^2.8.7", - "@app-config/utils": "^2.8.7", + "@app-config/core": "^2.9.0-beta.1", + "@app-config/default-extensions": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/node": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1", "fs-extra": "7" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7" + "@app-config/test-utils": "^2.9.0-beta.1" }, "prettier": "@lcdev/prettier", "jest": { diff --git a/app-config-node/package.json b/app-config-node/package.json index 7984be13..e0bb8e64 100644 --- a/app-config-node/package.json +++ b/app-config-node/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/node", "description": "Node.js API for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,14 +30,14 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/core": "^2.8.7", - "@app-config/logging": "^2.8.7", + "@app-config/core": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", "@types/prompts": "2", "fs-extra": "9", "prompts": "2" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7", + "@app-config/test-utils": "^2.9.0-beta.1", "@types/fs-extra": "9" }, "prettier": "@lcdev/prettier", diff --git a/app-config-react-native/package.json b/app-config-react-native/package.json index 86b913dd..c9952c70 100644 --- a/app-config-react-native/package.json +++ b/app-config-react-native/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/react-native", "description": "React Native Metro transformer that loads your app-config values into bundles statically", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,12 +30,12 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/node": "^2.8.7", + "@app-config/node": "^2.9.0-beta.1", "semver": "7" }, "peerDependencies": { - "@app-config/config": "^2.8.7", - "@app-config/utils": "^2.8.7", + "@app-config/config": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1", "react-native": ">=0.45.0" }, "devDependencies": { diff --git a/app-config-rollup/package.json b/app-config-rollup/package.json index d341b4fa..dfb9dbba 100644 --- a/app-config-rollup/package.json +++ b/app-config-rollup/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/rollup", "description": "Rollup plugin that resolves @app-config for you", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -34,9 +34,9 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/config": "^2.8.7", - "@app-config/schema": "^2.8.7", - "@app-config/utils": "^2.8.7" + "@app-config/config": "^2.9.0-beta.1", + "@app-config/schema": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1" }, "peerDependencies": {}, "devDependencies": { diff --git a/app-config-schema/package.json b/app-config-schema/package.json index 415594a4..3f88181d 100644 --- a/app-config-schema/package.json +++ b/app-config-schema/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/schema", "description": "Schema validation for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,19 +30,19 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/core": "^2.8.7", - "@app-config/logging": "^2.8.7", - "@app-config/node": "^2.8.7", - "@app-config/utils": "^2.8.7", + "@app-config/core": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/node": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1", "@types/json-schema": "7", "ajv": "7", "ajv-formats": "1", "json-schema-ref-parser": "9" }, "devDependencies": { - "@app-config/encryption": "^2.8.7", - "@app-config/extensions": "^2.8.7", - "@app-config/test-utils": "^2.8.7" + "@app-config/encryption": "^2.9.0-beta.1", + "@app-config/extensions": "^2.9.0-beta.1", + "@app-config/test-utils": "^2.9.0-beta.1" }, "prettier": "@lcdev/prettier", "jest": { diff --git a/app-config-settings/package.json b/app-config-settings/package.json index 4fff8adc..bb546b40 100644 --- a/app-config-settings/package.json +++ b/app-config-settings/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/settings", "description": "User settings for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,15 +30,15 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/core": "^2.8.7", - "@app-config/logging": "^2.8.7", - "@app-config/node": "^2.8.7", - "@app-config/utils": "^2.8.7", + "@app-config/core": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/node": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1", "env-paths": "2", "fs-extra": "7" }, "devDependencies": { - "@app-config/test-utils": "^2.8.7" + "@app-config/test-utils": "^2.9.0-beta.1" }, "prettier": "@lcdev/prettier", "jest": { diff --git a/app-config-test-utils/package.json b/app-config-test-utils/package.json index 0498c006..7bb90377 100644 --- a/app-config-test-utils/package.json +++ b/app-config-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/test-utils", "description": "Internal test utilities", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,8 +30,8 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/logging": "^2.8.7", - "@app-config/utils": "^2.8.7", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1", "@types/fs-extra": "9", "@types/tmp": "0.2", "fs-extra": "9", diff --git a/app-config-utils/package.json b/app-config-utils/package.json index 11a817d0..813dd4b9 100644 --- a/app-config-utils/package.json +++ b/app-config-utils/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/utils", "description": "Common utilities used in @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", diff --git a/app-config-v1-compat/package.json b/app-config-v1-compat/package.json index ad66614e..f418d83f 100644 --- a/app-config-v1-compat/package.json +++ b/app-config-v1-compat/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/v1-compat", "description": "Version 1 compatibility layer for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,16 +30,16 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/core": "^2.8.7", - "@app-config/extension-utils": "^2.8.7", - "@app-config/logging": "^2.8.7", - "@app-config/node": "^2.8.7", - "@app-config/utils": "^2.8.7", + "@app-config/core": "^2.9.0-beta.1", + "@app-config/extension-utils": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/node": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1", "fs-extra": "7" }, "devDependencies": { - "@app-config/extensions": "^2.8.7", - "@app-config/test-utils": "^2.8.7" + "@app-config/extensions": "^2.9.0-beta.1", + "@app-config/test-utils": "^2.9.0-beta.1" }, "prettier": "@lcdev/prettier", "jest": { diff --git a/app-config-vault/package.json b/app-config-vault/package.json index 026e0bf0..f1502153 100644 --- a/app-config-vault/package.json +++ b/app-config-vault/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/vault", "description": "Hashicorp Vault support for App Config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,16 +30,16 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/extension-utils": "^2.8.7", + "@app-config/extension-utils": "^2.9.0-beta.1", "@lcdev/fetch": "^0.1.10", "cross-fetch": "3", "node-vault": "0.9" }, "peerDependencies": { - "@app-config/main": "^2.8.7" + "@app-config/main": "^2.9.0-beta.1" }, "devDependencies": { - "@app-config/main": "^2.8.7" + "@app-config/main": "^2.9.0-beta.1" }, "prettier": "@lcdev/prettier", "jest": { diff --git a/app-config-vite/package.json b/app-config-vite/package.json index 9201cab2..878be5bf 100644 --- a/app-config-vite/package.json +++ b/app-config-vite/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/vite", "description": "Vite plugin for @app-config", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -34,7 +34,7 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/rollup": "^2.8.7" + "@app-config/rollup": "^2.9.0-beta.1" }, "devDependencies": { "vite": "2" diff --git a/app-config-webpack/package.json b/app-config-webpack/package.json index 106a340f..e6f9fcb9 100644 --- a/app-config-webpack/package.json +++ b/app-config-webpack/package.json @@ -1,7 +1,7 @@ { "name": "@app-config/webpack", "description": "Webpack plugin that loads your app-config values into bundles statically", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -31,19 +31,19 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/config": "^2.8.7", - "@app-config/schema": "^2.8.7", - "@app-config/utils": "^2.8.7", - "@app-config/logging": "^2.8.7", + "@app-config/config": "^2.9.0-beta.1", + "@app-config/logging": "^2.9.0-beta.1", + "@app-config/schema": "^2.9.0-beta.1", + "@app-config/utils": "^2.9.0-beta.1", "loader-utils": "2" }, "peerDependencies": { - "@app-config/main": "^2.8.7", + "@app-config/main": "^2.9.0-beta.1", "html-webpack-plugin": "4 || 5", "webpack": "4 || 5" }, "devDependencies": { - "@app-config/main": "^2.8.7", + "@app-config/main": "^2.9.0-beta.1", "@types/loader-utils": "1", "@webpack-cli/serve": "1", "html-webpack-plugin": "5", diff --git a/lcdev-app-config-inject/package.json b/lcdev-app-config-inject/package.json index 722fe8ad..a050ef19 100644 --- a/lcdev-app-config-inject/package.json +++ b/lcdev-app-config-inject/package.json @@ -1,7 +1,7 @@ { "name": "@lcdev/app-config-inject", "description": "Alias for @app-config/inject", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -34,7 +34,7 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/inject": "^2.8.7" + "@app-config/inject": "^2.9.0-beta.1" }, "devDependencies": {}, "prettier": "@lcdev/prettier", diff --git a/lcdev-app-config-webpack-plugin/package.json b/lcdev-app-config-webpack-plugin/package.json index 39927e51..f128a8e0 100644 --- a/lcdev-app-config-webpack-plugin/package.json +++ b/lcdev-app-config-webpack-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@lcdev/app-config-webpack-plugin", "description": "Alias for @app-config/webpack", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,7 +30,7 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/webpack": "^2.8.7" + "@app-config/webpack": "^2.9.0-beta.1" }, "devDependencies": {}, "prettier": "@lcdev/prettier", diff --git a/lcdev-app-config/package.json b/lcdev-app-config/package.json index 0c90428d..c583056f 100644 --- a/lcdev-app-config/package.json +++ b/lcdev-app-config/package.json @@ -1,7 +1,7 @@ { "name": "@lcdev/app-config", "description": "Alias for @app-config/main", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -34,8 +34,8 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/cli": "^2.8.7", - "@app-config/main": "^2.8.7" + "@app-config/cli": "^2.9.0-beta.1", + "@app-config/main": "^2.9.0-beta.1" }, "devDependencies": {}, "prettier": "@lcdev/prettier", diff --git a/lcdev-react-native-app-config-transformer/package.json b/lcdev-react-native-app-config-transformer/package.json index 8f6b9007..0d0680eb 100644 --- a/lcdev-react-native-app-config-transformer/package.json +++ b/lcdev-react-native-app-config-transformer/package.json @@ -1,7 +1,7 @@ { "name": "@lcdev/react-native-app-config-transformer", "description": "Alias for @app-config/react-native", - "version": "2.8.7", + "version": "2.9.0-beta.1", "license": "MPL-2.0", "author": { "name": "Launchcode", @@ -30,7 +30,7 @@ "prepublishOnly": "yarn clean && yarn build && yarn build:es" }, "dependencies": { - "@app-config/react-native": "^2.8.7" + "@app-config/react-native": "^2.9.0-beta.1" }, "devDependencies": {}, "prettier": "@lcdev/prettier",