Skip to content

Commit 534596d

Browse files
committed
feat: encryption key revision includes environment
1 parent a8ea8c6 commit 534596d

File tree

7 files changed

+90
-53
lines changed

7 files changed

+90
-53
lines changed

app-config-encryption/src/encryption.test.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
loadTeamMembers,
2323
trustTeamMember,
2424
untrustTeamMember,
25+
getRevisionNumber,
2526
} from './encryption';
2627

2728
describe('User Keys', () => {
@@ -185,33 +186,33 @@ const createKey = async () => {
185186

186187
describe('Symmetric Keys', () => {
187188
it('generates a plain symmetric key', async () => {
188-
const symmetricKey = await generateSymmetricKey(1);
189+
const symmetricKey = await generateSymmetricKey('1');
189190

190-
expect(symmetricKey.revision).toBe(1);
191+
expect(symmetricKey.revision).toBe('1');
191192
expect(symmetricKey.key).toBeInstanceOf(Uint8Array);
192193
expect(symmetricKey.key.length).toBeGreaterThan(2048);
193194
});
194195

195196
it('encrypts and decrypts a symmetric key', async () => {
196197
const privateKey = await createKey();
197-
const symmetricKey = await generateSymmetricKey(1);
198+
const symmetricKey = await generateSymmetricKey('1');
198199
const encryptedKey = await encryptSymmetricKey(symmetricKey, [privateKey]);
199200

200-
expect(encryptedKey.revision).toBe(1);
201+
expect(encryptedKey.revision).toBe('1');
201202
expect(typeof encryptedKey.key).toBe('string');
202203
expect(encryptedKey.key.length).toBeGreaterThan(0);
203204

204205
const decryptedKey = await decryptSymmetricKey(encryptedKey, privateKey);
205206

206-
expect(decryptedKey.revision).toBe(1);
207+
expect(decryptedKey.revision).toBe('1');
207208
expect(decryptedKey.key).toEqual(symmetricKey.key);
208209
});
209210

210211
it('cannot decrypt a symmetric key that was created by someone else', async () => {
211212
const privateKey = await createKey();
212213
const someoneElsesKey = await createKey();
213214

214-
const symmetricKey = await generateSymmetricKey(1);
215+
const symmetricKey = await generateSymmetricKey('1');
215216
const encryptedKey = await encryptSymmetricKey(symmetricKey, [someoneElsesKey]);
216217

217218
await expect(decryptSymmetricKey(encryptedKey, privateKey)).rejects.toThrow();
@@ -221,7 +222,7 @@ describe('Symmetric Keys', () => {
221222
const privateKey = await createKey();
222223
const someoneElsesKey = await createKey();
223224

224-
const symmetricKey = await generateSymmetricKey(1);
225+
const symmetricKey = await generateSymmetricKey('1');
225226
const encryptedKey = await encryptSymmetricKey(symmetricKey, [privateKey, someoneElsesKey]);
226227

227228
await expect(decryptSymmetricKey(encryptedKey, privateKey)).resolves.toEqual(symmetricKey);
@@ -232,7 +233,7 @@ describe('Symmetric Keys', () => {
232233
const privateKey = await createKey();
233234
const someoneElsesKey = await createKey();
234235

235-
const symmetricKey = await generateSymmetricKey(1);
236+
const symmetricKey = await generateSymmetricKey('1');
236237
const encryptedKey = await encryptSymmetricKey(symmetricKey, [privateKey]);
237238
const decryptedKey = await decryptSymmetricKey(encryptedKey, privateKey);
238239
const encryptedKey2 = await encryptSymmetricKey(decryptedKey, [privateKey, someoneElsesKey]);
@@ -244,7 +245,7 @@ describe('Symmetric Keys', () => {
244245

245246
it('validates encoded revision number in keys', async () => {
246247
const privateKey = await createKey();
247-
const symmetricKey = await generateSymmetricKey(1);
248+
const symmetricKey = await generateSymmetricKey('1');
248249
const encryptedKey = await encryptSymmetricKey(symmetricKey, [privateKey]);
249250

250251
// 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', () => {
261262
const values = ['hello world', 42.42, null, true, { message: 'hello world', nested: {} }];
262263

263264
for (const value of values) {
264-
const symmetricKey = await generateSymmetricKey(1);
265+
const symmetricKey = await generateSymmetricKey('1');
265266
const encrypted = await encryptValue(value, symmetricKey);
266267
const decrypted = await decryptValue(encrypted, symmetricKey);
267268

@@ -273,8 +274,8 @@ describe('Value Encryption', () => {
273274

274275
it('cannot decrypt a value with the wrong key', async () => {
275276
const value = 'hello world';
276-
const symmetricKey = await generateSymmetricKey(1);
277-
const wrongKey = await generateSymmetricKey(1);
277+
const symmetricKey = await generateSymmetricKey('1');
278+
const wrongKey = await generateSymmetricKey('1');
278279
const encrypted = await encryptValue(value, symmetricKey);
279280

280281
await expect(decryptValue(encrypted, wrongKey)).rejects.toThrow();
@@ -377,8 +378,11 @@ describe('E2E Encrypted Repo', () => {
377378

378379
// just for test coverage, create a new symmetric key
379380
const latestSymmetricKey = await loadLatestSymmetricKey(privateKey);
381+
382+
const newRevisionNumber = getRevisionNumber(latestSymmetricKey.revision) + 1;
383+
380384
await saveNewSymmetricKey(
381-
await generateSymmetricKey(latestSymmetricKey.revision + 1),
385+
await generateSymmetricKey(newRevisionNumber.toString()),
382386
await loadTeamMembers(),
383387
);
384388

app-config-encryption/src/encryption.ts

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -293,11 +293,11 @@ export async function loadPublicKeyLazy(environmentOptions?: EnvironmentOptions)
293293
export { EncryptedSymmetricKey };
294294

295295
export interface DecryptedSymmetricKey {
296-
revision: number;
296+
revision: string;
297297
key: Uint8Array;
298298
}
299299

300-
export async function generateSymmetricKey(revision: number): Promise<DecryptedSymmetricKey> {
300+
export async function generateSymmetricKey(revision: string): Promise<DecryptedSymmetricKey> {
301301
// eslint-disable-next-line @typescript-eslint/await-thenable
302302
const rawPassword = await crypto.random.getRandomBytes(2048);
303303
const passwordWithRevision = encodeRevisionInPassword(rawPassword, revision);
@@ -372,7 +372,7 @@ export async function loadSymmetricKeys(
372372
}
373373

374374
export async function loadSymmetricKey(
375-
revision: number,
375+
revision: string,
376376
privateKey: Key,
377377
lazyMeta = true,
378378
environmentOptions?: EnvironmentOptions,
@@ -387,10 +387,10 @@ export async function loadSymmetricKey(
387387
return decryptSymmetricKey(symmetricKey, privateKey);
388388
}
389389

390-
const symmetricKeys = new Map<number, Promise<DecryptedSymmetricKey>>();
390+
const symmetricKeys = new Map<string, Promise<DecryptedSymmetricKey>>();
391391

392392
export async function loadSymmetricKeyLazy(
393-
revision: number,
393+
revision: string,
394394
privateKey: Key,
395395
environmentOptions?: EnvironmentOptions,
396396
): Promise<DecryptedSymmetricKey> {
@@ -493,16 +493,8 @@ export async function decryptValue(
493493
if (symmetricKeyOverride) {
494494
symmetricKey = symmetricKeyOverride;
495495
} else {
496-
const revisionNumber = parseFloat(revision);
497-
498-
if (Number.isNaN(revisionNumber)) {
499-
throw new AppConfigError(
500-
`Encrypted value was invalid, revision was not a number (${revision})`,
501-
);
502-
}
503-
504496
symmetricKey = await loadSymmetricKeyLazy(
505-
revisionNumber,
497+
revision,
506498
await loadPrivateKeyLazy(environmentOptions),
507499
environmentOptions,
508500
);
@@ -583,6 +575,7 @@ export async function trustTeamMember(
583575
await loadSymmetricKeys(true, environmentOptions),
584576
newTeamMembers,
585577
privateKey,
578+
environmentOptions,
586579
);
587580

588581
await saveNewMetaFile((meta) => ({
@@ -606,6 +599,8 @@ export async function untrustTeamMember(
606599
privateKey: Key,
607600
environmentOptions?: EnvironmentOptions,
608601
) {
602+
const environment = currentEnvironment(environmentOptions);
603+
609604
const teamMembers = await loadTeamMembers(environmentOptions);
610605

611606
const removalCandidates = new Set<Key>();
@@ -660,10 +655,22 @@ export async function untrustTeamMember(
660655
await loadSymmetricKeys(true, environmentOptions),
661656
newTeamMembers,
662657
privateKey,
658+
environmentOptions,
663659
);
664660

661+
const latestRevision = latestSymmetricKeyRevision(newEncryptionKeys);
662+
const newRevisionNumber = getRevisionNumber(latestRevision) + 1;
663+
664+
let newRevision;
665+
666+
if (environment) {
667+
newRevision = `${environment}-${newRevisionNumber}`;
668+
} else {
669+
newRevision = `${newRevisionNumber}`;
670+
}
671+
665672
const newLatestEncryptionKey = await encryptSymmetricKey(
666-
await generateSymmetricKey(latestSymmetricKeyRevision(newEncryptionKeys) + 1),
673+
await generateSymmetricKey(newRevision),
667674
newTeamMembers,
668675
);
669676

@@ -685,10 +692,32 @@ export async function untrustTeamMember(
685692
}));
686693
}
687694

695+
export function getRevisionNumber(revision: string) {
696+
const regex = /^(?:\w*-)?(?<revisionNumber>\d*)$/;
697+
698+
const match = regex.exec(revision)?.groups?.revisionNumber;
699+
700+
if (!match) {
701+
throw new AppConfigError(
702+
`Encryption revision is invalid. Got "${revision}" but expected a number or <Environment Name>-<Revision Number>"`,
703+
);
704+
}
705+
706+
const revisionNumber = parseFloat(match);
707+
708+
if (Number.isNaN(revisionNumber)) {
709+
throw new AppConfigError(
710+
`Encryption revision is invalid. Got "${revision}" but expected a number or <Environment Name>-<Revision Number>"`,
711+
);
712+
}
713+
714+
return revisionNumber;
715+
}
716+
688717
export function latestSymmetricKeyRevision(
689718
keys: (EncryptedSymmetricKey | DecryptedSymmetricKey)[],
690-
): number {
691-
keys.sort((a, b) => a.revision - b.revision);
719+
): string {
720+
keys.sort((a, b) => getRevisionNumber(a.revision) - getRevisionNumber(b.revision));
692721

693722
if (keys.length === 0) throw new InvalidEncryptionKey('No symmetric keys were found');
694723

@@ -699,11 +728,22 @@ async function reencryptSymmetricKeys(
699728
previousSymmetricKeys: EncryptedSymmetricKey[],
700729
newTeamMembers: Key[],
701730
privateKey: Key,
731+
environmentOptions?: EnvironmentOptions,
702732
): Promise<EncryptedSymmetricKey[]> {
703733
const newEncryptionKeys: EncryptedSymmetricKey[] = [];
704734

705735
if (previousSymmetricKeys.length === 0) {
706-
const initialKey = await generateSymmetricKey(1);
736+
let newRevision = '1';
737+
738+
if (environmentOptions) {
739+
const env = currentEnvironment(environmentOptions);
740+
741+
if (env) {
742+
newRevision = `${env}-1`;
743+
}
744+
}
745+
746+
const initialKey = await generateSymmetricKey(newRevision);
707747
const encrypted = await encryptSymmetricKey(initialKey, newTeamMembers);
708748

709749
newEncryptionKeys.push(encrypted);
@@ -879,8 +919,8 @@ function stringAsTypedArray(str: string): Uint16Array {
879919
return bufView;
880920
}
881921

882-
function encodeRevisionInPassword(password: Uint8Array, revision: number): Uint8Array {
883-
const revisionBytes = stringAsTypedArray(revision.toString());
922+
function encodeRevisionInPassword(password: Uint8Array, revision: string): Uint8Array {
923+
const revisionBytes = stringAsTypedArray(revision);
884924
const passwordWithRevision = new Uint8Array(password.length + revisionBytes.length + 1);
885925

886926
// 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
891931
return passwordWithRevision;
892932
}
893933

894-
function verifyEncodedRevision(password: Uint8Array, expectedRevision: number) {
934+
function verifyEncodedRevision(password: Uint8Array, expectedRevision: string) {
895935
const revisionBytesLength = password[0];
896936
const revisionBytes = password.slice(1, 1 + revisionBytesLength);
897937
const revision = decodeTypedArray(revisionBytes);
898938

899-
if (parseFloat(revision) !== expectedRevision) {
939+
if (revision !== expectedRevision) {
900940
throw new EncryptionEncoding(oneLine`
901941
We detected tampering in the encryption key, revision ${expectedRevision}!
902942
This error occurs when the revision in the 'encryptionKeys' does not match the one that was embedded into the key.

app-config-encryption/src/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import encryptedDirective from './index';
44

55
describe('encryptedDirective', () => {
66
it('loads an encrypted value', async () => {
7-
const symmetricKey = await generateSymmetricKey(1);
7+
const symmetricKey = await generateSymmetricKey('1');
88

99
const source = new LiteralSource({
1010
foo: await encryptValue('foobar', symmetricKey),
@@ -16,7 +16,7 @@ describe('encryptedDirective', () => {
1616
});
1717

1818
it('loads an array of encrypted values', async () => {
19-
const symmetricKey = await generateSymmetricKey(1);
19+
const symmetricKey = await generateSymmetricKey('1');
2020

2121
const source = new LiteralSource({
2222
foo: [

app-config-encryption/src/secret-agent.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('Decryption', () => {
2828
});
2929

3030
const privateKey = await loadPrivateKey(privateKeyArmored);
31-
const symmetricKey = await generateSymmetricKey(1);
31+
const symmetricKey = await generateSymmetricKey('1');
3232
const encryptedSymmetricKey = await encryptSymmetricKey(symmetricKey, [privateKey]);
3333

3434
const port = await getPort();
@@ -84,7 +84,7 @@ describe('Unix Sockets', () => {
8484
});
8585

8686
const privateKey = await loadPrivateKey(privateKeyArmored);
87-
const symmetricKey = await generateSymmetricKey(1);
87+
const symmetricKey = await generateSymmetricKey('1');
8888
const encryptedSymmetricKey = await encryptSymmetricKey(symmetricKey, [privateKey]);
8989

9090
const socket = resolve('./temporary-socket-file');

app-config-encryption/src/secret-agent.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,8 @@ export async function connectAgent(
139139
keepAlive();
140140

141141
const revision = text.split(':')[1];
142-
const revisionNumber = parseFloat(revision);
143142

144-
if (Number.isNaN(revisionNumber)) {
145-
throw new AppConfigError(
146-
`Encrypted value was invalid, revision was not a number (${revision})`,
147-
);
148-
}
149-
150-
const symmetricKey = await loadEncryptedKey(revisionNumber, environmentOptions);
143+
const symmetricKey = await loadEncryptedKey(revision, environmentOptions);
151144
const decrypted = await client.Decrypt({ text, symmetricKey });
152145

153146
keepAlive();
@@ -248,7 +241,7 @@ export async function getAgentPortOrSocket(
248241
}
249242

250243
async function loadSymmetricKey(
251-
revision: number,
244+
revision: string,
252245
environmentOptions?: EnvironmentOptions,
253246
): Promise<EncryptedSymmetricKey> {
254247
const symmetricKeys = await loadSymmetricKeys(true, environmentOptions);

app-config-meta/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface TeamMember {
2727
}
2828

2929
export interface EncryptedSymmetricKey {
30-
revision: number;
30+
revision: string;
3131
key: string;
3232
}
3333

app-config-schema/src/index.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ describe('Validation', () => {
488488
},
489489
async (inDir) => {
490490
const { validate } = await loadSchema({ directory: inDir('.') });
491-
const symmetricKey = await generateSymmetricKey(1);
491+
const symmetricKey = await generateSymmetricKey('1');
492492

493493
const parsed = await ParsedValue.parseLiteral(
494494
{
@@ -515,7 +515,7 @@ describe('Validation', () => {
515515
},
516516
async (inDir) => {
517517
const { validate } = await loadSchema({ directory: inDir('.') });
518-
const symmetricKey = await generateSymmetricKey(1);
518+
const symmetricKey = await generateSymmetricKey('1');
519519

520520
const parsed = await ParsedValue.parseLiteral(
521521
[
@@ -542,7 +542,7 @@ describe('Validation', () => {
542542
},
543543
async (inDir) => {
544544
const { validate } = await loadSchema({ directory: inDir('.') });
545-
const symmetricKey = await generateSymmetricKey(1);
545+
const symmetricKey = await generateSymmetricKey('1');
546546

547547
const parsed = await ParsedValue.parseLiteral(
548548
[
@@ -573,7 +573,7 @@ describe('Validation', () => {
573573
},
574574
async (inDir) => {
575575
const { validate } = await loadSchema({ directory: inDir('.') });
576-
const symmetricKey = await generateSymmetricKey(1);
576+
const symmetricKey = await generateSymmetricKey('1');
577577

578578
const parsed = await ParsedValue.parseLiteral(
579579
{

0 commit comments

Comments
 (0)