@shipfox/api-secrets 15.0.0 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +2 -0
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/crypto.d.ts.map +1 -1
- package/dist/core/crypto.js +9 -51
- package/dist/core/crypto.js.map +1 -1
- package/dist/core/dek-manager.d.ts +0 -5
- package/dist/core/dek-manager.d.ts.map +1 -1
- package/dist/core/dek-manager.js +23 -62
- package/dist/core/dek-manager.js.map +1 -1
- package/dist/core/key-provider.d.ts.map +1 -1
- package/dist/core/key-provider.js +16 -27
- package/dist/core/key-provider.js.map +1 -1
- package/dist/core/rotate-kek.d.ts.map +1 -1
- package/dist/core/rotate-kek.js +33 -87
- package/dist/core/rotate-kek.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/config.ts +1 -1
- package/src/core/crypto.ts +14 -58
- package/src/core/dek-manager.ts +23 -74
- package/src/core/key-provider.ts +18 -34
- package/src/core/rotate-kek.ts +29 -82
- package/tsconfig.build.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
$ shipfox-swc
|
|
2
|
-
Successfully compiled: 53 files with swc (
|
|
2
|
+
Successfully compiled: 53 files with swc (319.1ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @shipfox/api-secrets
|
|
2
2
|
|
|
3
|
+
## 17.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9f898d9: Persists encrypted agent session transcripts to object storage, adds a retention sweep that deletes expired sessions, and adds KEK rotation for envelope-encrypted artifacts. Deployments must set `AGENT_SESSION_ENCRYPTION_KEK` to a unique base64-encoded 32-byte key before upgrading.
|
|
8
|
+
- Updated dependencies [a4f56ff]
|
|
9
|
+
- Updated dependencies [a591e8a]
|
|
10
|
+
- Updated dependencies [9f898d9]
|
|
11
|
+
- Updated dependencies [9f898d9]
|
|
12
|
+
- @shipfox/api-auth-context@17.0.0
|
|
13
|
+
- @shipfox/node-postgres@0.5.1
|
|
14
|
+
- @shipfox/node-envelope-encryption@0.2.0
|
|
15
|
+
- @shipfox/node-outbox@0.2.6
|
|
16
|
+
|
|
3
17
|
## 15.0.0
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -135,6 +135,8 @@ Secret management routes should keep secret values write-only at the HTTP bounda
|
|
|
135
135
|
|
|
136
136
|
Secret values use envelope encryption.
|
|
137
137
|
|
|
138
|
+
`@shipfox/node-envelope-encryption` owns the shared cipher, key-provider, cache, and rotation mechanics. This module owns its KEK configuration, AAD, tables, metrics, and domain errors.
|
|
139
|
+
|
|
138
140
|
- Each workspace has one 32-byte data-encryption key (DEK).
|
|
139
141
|
- The DEK is generated with `crypto.randomBytes(32)` on first use.
|
|
140
142
|
- The DEK is wrapped with the configured key-encryption key (KEK) and stored in `secrets_data_keys`.
|
package/dist/config.js
CHANGED
|
@@ -4,7 +4,7 @@ import { decodeBase64Key } from '#core/crypto.js';
|
|
|
4
4
|
export const MAX_VALUE_BYTES = 64 * 1024;
|
|
5
5
|
export const config = createConfig({
|
|
6
6
|
SECRETS_ENCRYPTION_KEK: str({
|
|
7
|
-
desc: 'Master key used to protect all stored secrets. Required. Generate a unique value per environment with openssl rand -base64 32 and provide it from a secret manager. The committed .env value is only for local development. Losing this key makes stored secrets unrecoverable. To rotate it, set SECRETS_ENCRYPTION_KEK_PREVIOUS to the old value during the rotation window.'
|
|
7
|
+
desc: 'Master key used to protect all stored secrets. Required. Generate a unique value per environment with openssl rand -base64 32 and provide it from a secret manager. Do not reuse AGENT_SESSION_ENCRYPTION_KEK. The committed .env value is only for local development. Losing this key makes stored secrets unrecoverable. To rotate it, set SECRETS_ENCRYPTION_KEK_PREVIOUS to the old value during the rotation window.'
|
|
8
8
|
}),
|
|
9
9
|
SECRETS_ENCRYPTION_KEK_PREVIOUS: str({
|
|
10
10
|
desc: 'Previous master key used only while rotating stored secret data keys. Optional. Set it to the old SECRETS_ENCRYPTION_KEK value until rotation has completed.',
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {SECRETS_MAX_LIST_LIMIT} from '@shipfox/api-secrets-dto';\nimport {createConfig, num, str} from '@shipfox/config';\nimport {decodeBase64Key} from '#core/crypto.js';\n\nexport const MAX_VALUE_BYTES = 64 * 1024;\n\nexport const config = createConfig({\n SECRETS_ENCRYPTION_KEK: str({\n desc: 'Master key used to protect all stored secrets. Required. Generate a unique value per environment with openssl rand -base64 32 and provide it from a secret manager. The committed .env value is only for local development. Losing this key makes stored secrets unrecoverable. To rotate it, set SECRETS_ENCRYPTION_KEK_PREVIOUS to the old value during the rotation window.',\n }),\n SECRETS_ENCRYPTION_KEK_PREVIOUS: str({\n desc: 'Previous master key used only while rotating stored secret data keys. Optional. Set it to the old SECRETS_ENCRYPTION_KEK value until rotation has completed.',\n default: undefined,\n }),\n SECRETS_MAX_PER_WORKSPACE: num({\n desc: 'Maximum number of secrets and variables allowed per workspace across all project scopes and namespaces.',\n default: 10000,\n }),\n SECRETS_SHORT_VALUE_WARN_LENGTH: num({\n desc: 'Secret values shorter than this many characters are classified for a write warning by management routes.',\n default: 12,\n }),\n});\n\ndecodeBase64Key(config.SECRETS_ENCRYPTION_KEK, 'SECRETS_ENCRYPTION_KEK');\nif (config.SECRETS_ENCRYPTION_KEK_PREVIOUS) {\n decodeBase64Key(config.SECRETS_ENCRYPTION_KEK_PREVIOUS, 'SECRETS_ENCRYPTION_KEK_PREVIOUS');\n}\nif (config.SECRETS_MAX_PER_WORKSPACE < 1) {\n throw new Error('SECRETS_MAX_PER_WORKSPACE must be greater than 0.');\n}\nif (config.SECRETS_MAX_PER_WORKSPACE > SECRETS_MAX_LIST_LIMIT) {\n // The settings UI lists the whole bounded set in one call (limit = SECRETS_MAX_LIST_LIMIT).\n // A cap above that would let the list silently truncate, so fail fast instead.\n throw new Error(\n `SECRETS_MAX_PER_WORKSPACE (${config.SECRETS_MAX_PER_WORKSPACE}) cannot exceed SECRETS_MAX_LIST_LIMIT (${SECRETS_MAX_LIST_LIMIT}).`,\n );\n}\nif (config.SECRETS_SHORT_VALUE_WARN_LENGTH < 1) {\n throw new Error('SECRETS_SHORT_VALUE_WARN_LENGTH must be greater than 0.');\n}\n"],"names":["SECRETS_MAX_LIST_LIMIT","createConfig","num","str","decodeBase64Key","MAX_VALUE_BYTES","config","SECRETS_ENCRYPTION_KEK","desc","SECRETS_ENCRYPTION_KEK_PREVIOUS","default","undefined","SECRETS_MAX_PER_WORKSPACE","SECRETS_SHORT_VALUE_WARN_LENGTH","Error"],"mappings":"AAAA,SAAQA,sBAAsB,QAAO,2BAA2B;AAChE,SAAQC,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AACvD,SAAQC,eAAe,QAAO,kBAAkB;AAEhD,OAAO,MAAMC,kBAAkB,KAAK,KAAK;AAEzC,OAAO,MAAMC,SAASL,aAAa;IACjCM,wBAAwBJ,IAAI;QAC1BK,MAAM;IACR;IACAC,iCAAiCN,IAAI;QACnCK,MAAM;QACNE,SAASC;IACX;IACAC,2BAA2BV,IAAI;QAC7BM,MAAM;QACNE,SAAS;IACX;IACAG,iCAAiCX,IAAI;QACnCM,MAAM;QACNE,SAAS;IACX;AACF,GAAG;AAEHN,gBAAgBE,OAAOC,sBAAsB,EAAE;AAC/C,IAAID,OAAOG,+BAA+B,EAAE;IAC1CL,gBAAgBE,OAAOG,+BAA+B,EAAE;AAC1D;AACA,IAAIH,OAAOM,yBAAyB,GAAG,GAAG;IACxC,MAAM,IAAIE,MAAM;AAClB;AACA,IAAIR,OAAOM,yBAAyB,GAAGZ,wBAAwB;IAC7D,4FAA4F;IAC5F,+EAA+E;IAC/E,MAAM,IAAIc,MACR,CAAC,2BAA2B,EAAER,OAAOM,yBAAyB,CAAC,wCAAwC,EAAEZ,uBAAuB,EAAE,CAAC;AAEvI;AACA,IAAIM,OAAOO,+BAA+B,GAAG,GAAG;IAC9C,MAAM,IAAIC,MAAM;AAClB"}
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {SECRETS_MAX_LIST_LIMIT} from '@shipfox/api-secrets-dto';\nimport {createConfig, num, str} from '@shipfox/config';\nimport {decodeBase64Key} from '#core/crypto.js';\n\nexport const MAX_VALUE_BYTES = 64 * 1024;\n\nexport const config = createConfig({\n SECRETS_ENCRYPTION_KEK: str({\n desc: 'Master key used to protect all stored secrets. Required. Generate a unique value per environment with openssl rand -base64 32 and provide it from a secret manager. Do not reuse AGENT_SESSION_ENCRYPTION_KEK. The committed .env value is only for local development. Losing this key makes stored secrets unrecoverable. To rotate it, set SECRETS_ENCRYPTION_KEK_PREVIOUS to the old value during the rotation window.',\n }),\n SECRETS_ENCRYPTION_KEK_PREVIOUS: str({\n desc: 'Previous master key used only while rotating stored secret data keys. Optional. Set it to the old SECRETS_ENCRYPTION_KEK value until rotation has completed.',\n default: undefined,\n }),\n SECRETS_MAX_PER_WORKSPACE: num({\n desc: 'Maximum number of secrets and variables allowed per workspace across all project scopes and namespaces.',\n default: 10000,\n }),\n SECRETS_SHORT_VALUE_WARN_LENGTH: num({\n desc: 'Secret values shorter than this many characters are classified for a write warning by management routes.',\n default: 12,\n }),\n});\n\ndecodeBase64Key(config.SECRETS_ENCRYPTION_KEK, 'SECRETS_ENCRYPTION_KEK');\nif (config.SECRETS_ENCRYPTION_KEK_PREVIOUS) {\n decodeBase64Key(config.SECRETS_ENCRYPTION_KEK_PREVIOUS, 'SECRETS_ENCRYPTION_KEK_PREVIOUS');\n}\nif (config.SECRETS_MAX_PER_WORKSPACE < 1) {\n throw new Error('SECRETS_MAX_PER_WORKSPACE must be greater than 0.');\n}\nif (config.SECRETS_MAX_PER_WORKSPACE > SECRETS_MAX_LIST_LIMIT) {\n // The settings UI lists the whole bounded set in one call (limit = SECRETS_MAX_LIST_LIMIT).\n // A cap above that would let the list silently truncate, so fail fast instead.\n throw new Error(\n `SECRETS_MAX_PER_WORKSPACE (${config.SECRETS_MAX_PER_WORKSPACE}) cannot exceed SECRETS_MAX_LIST_LIMIT (${SECRETS_MAX_LIST_LIMIT}).`,\n );\n}\nif (config.SECRETS_SHORT_VALUE_WARN_LENGTH < 1) {\n throw new Error('SECRETS_SHORT_VALUE_WARN_LENGTH must be greater than 0.');\n}\n"],"names":["SECRETS_MAX_LIST_LIMIT","createConfig","num","str","decodeBase64Key","MAX_VALUE_BYTES","config","SECRETS_ENCRYPTION_KEK","desc","SECRETS_ENCRYPTION_KEK_PREVIOUS","default","undefined","SECRETS_MAX_PER_WORKSPACE","SECRETS_SHORT_VALUE_WARN_LENGTH","Error"],"mappings":"AAAA,SAAQA,sBAAsB,QAAO,2BAA2B;AAChE,SAAQC,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AACvD,SAAQC,eAAe,QAAO,kBAAkB;AAEhD,OAAO,MAAMC,kBAAkB,KAAK,KAAK;AAEzC,OAAO,MAAMC,SAASL,aAAa;IACjCM,wBAAwBJ,IAAI;QAC1BK,MAAM;IACR;IACAC,iCAAiCN,IAAI;QACnCK,MAAM;QACNE,SAASC;IACX;IACAC,2BAA2BV,IAAI;QAC7BM,MAAM;QACNE,SAAS;IACX;IACAG,iCAAiCX,IAAI;QACnCM,MAAM;QACNE,SAAS;IACX;AACF,GAAG;AAEHN,gBAAgBE,OAAOC,sBAAsB,EAAE;AAC/C,IAAID,OAAOG,+BAA+B,EAAE;IAC1CL,gBAAgBE,OAAOG,+BAA+B,EAAE;AAC1D;AACA,IAAIH,OAAOM,yBAAyB,GAAG,GAAG;IACxC,MAAM,IAAIE,MAAM;AAClB;AACA,IAAIR,OAAOM,yBAAyB,GAAGZ,wBAAwB;IAC7D,4FAA4F;IAC5F,+EAA+E;IAC/E,MAAM,IAAIc,MACR,CAAC,2BAA2B,EAAER,OAAOM,yBAAyB,CAAC,wCAAwC,EAAEZ,uBAAuB,EAAE,CAAC;AAEvI;AACA,IAAIM,OAAOO,+BAA+B,GAAG,GAAG;IAC9C,MAAM,IAAIC,MAAM;AAClB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/core/crypto.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/core/crypto.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAE3D;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAM3D;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOlF;AAED,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAIhE"}
|
package/dist/core/crypto.js
CHANGED
|
@@ -1,61 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { decodeBase64Key as decodeEnvelopeKey, KeyConfigurationError, openEnvelopeText, sealEnvelopeText } from '@shipfox/node-envelope-encryption';
|
|
2
2
|
import { KekConfigurationError, SecretDecryptionError } from './errors.js';
|
|
3
|
-
const CIPHER = 'aes-256-gcm';
|
|
4
|
-
const ENCODED_PREFIX = 'v1:';
|
|
5
|
-
const IV_BYTES = 12;
|
|
6
|
-
const AUTH_TAG_BYTES = 16;
|
|
7
|
-
const KEY_BYTES = 32;
|
|
8
|
-
const BASE64_KEY_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
9
|
-
const BASE64_PADDING_SUFFIX = /=+$/;
|
|
10
3
|
export function aesGcmSeal(params) {
|
|
11
|
-
|
|
12
|
-
const cipher = crypto.createCipheriv(CIPHER, params.key, iv);
|
|
13
|
-
cipher.setAAD(Buffer.from(params.aad, 'utf8'));
|
|
14
|
-
const ciphertext = Buffer.concat([
|
|
15
|
-
cipher.update(params.plaintext),
|
|
16
|
-
cipher.final()
|
|
17
|
-
]);
|
|
18
|
-
const authTag = cipher.getAuthTag();
|
|
19
|
-
return `${ENCODED_PREFIX}${Buffer.concat([
|
|
20
|
-
iv,
|
|
21
|
-
authTag,
|
|
22
|
-
ciphertext
|
|
23
|
-
]).toString('base64')}`;
|
|
4
|
+
return sealEnvelopeText(params);
|
|
24
5
|
}
|
|
25
6
|
export function aesGcmOpen(params) {
|
|
26
|
-
if (!params.encoded.startsWith(ENCODED_PREFIX)) throw new SecretDecryptionError();
|
|
27
|
-
const encodedPayload = params.encoded.slice(ENCODED_PREFIX.length);
|
|
28
|
-
const payload = Buffer.from(encodedPayload, 'base64');
|
|
29
|
-
const canonical = payload.toString('base64');
|
|
30
|
-
if (!BASE64_KEY_PATTERN.test(encodedPayload) || canonical.replace(BASE64_PADDING_SUFFIX, '') !== encodedPayload.replace(BASE64_PADDING_SUFFIX, '')) {
|
|
31
|
-
throw new SecretDecryptionError();
|
|
32
|
-
}
|
|
33
|
-
if (payload.length < IV_BYTES + AUTH_TAG_BYTES) throw new SecretDecryptionError();
|
|
34
7
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const ciphertext = payload.subarray(IV_BYTES + AUTH_TAG_BYTES);
|
|
38
|
-
const decipher = crypto.createDecipheriv(CIPHER, params.key, iv);
|
|
39
|
-
decipher.setAAD(Buffer.from(params.aad, 'utf8'));
|
|
40
|
-
decipher.setAuthTag(authTag);
|
|
41
|
-
return Buffer.concat([
|
|
42
|
-
decipher.update(ciphertext),
|
|
43
|
-
decipher.final()
|
|
44
|
-
]);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
if (error instanceof SecretDecryptionError) throw error;
|
|
8
|
+
return openEnvelopeText(params);
|
|
9
|
+
} catch {
|
|
47
10
|
throw new SecretDecryptionError();
|
|
48
11
|
}
|
|
49
12
|
}
|
|
50
13
|
export function decodeBase64Key(encoded, label) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw new KekConfigurationError(`${label} must be a canonical base64-encoded 32-byte key. Strip whitespace and generate a new value with openssl rand -base64 32 if needed.`);
|
|
14
|
+
try {
|
|
15
|
+
return decodeEnvelopeKey(encoded, label);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error instanceof KeyConfigurationError) throw new KekConfigurationError(error.message);
|
|
18
|
+
throw error;
|
|
57
19
|
}
|
|
58
|
-
return key;
|
|
59
20
|
}
|
|
60
21
|
export function aadForDek(workspaceId, kekVersion) {
|
|
61
22
|
return JSON.stringify([
|
|
@@ -78,8 +39,5 @@ export function aadForValue(params) {
|
|
|
78
39
|
params.key
|
|
79
40
|
]);
|
|
80
41
|
}
|
|
81
|
-
function isCanonicalBase64Key(encoded, key) {
|
|
82
|
-
return BASE64_KEY_PATTERN.test(encoded) && key.toString('base64') === encoded;
|
|
83
|
-
}
|
|
84
42
|
|
|
85
43
|
//# sourceMappingURL=crypto.js.map
|
package/dist/core/crypto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/crypto.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"sources":["../../src/core/crypto.ts"],"sourcesContent":["import {\n decodeBase64Key as decodeEnvelopeKey,\n KeyConfigurationError,\n openEnvelopeText,\n sealEnvelopeText,\n} from '@shipfox/node-envelope-encryption';\nimport {KekConfigurationError, SecretDecryptionError} from './errors.js';\n\nexport interface AesGcmSealParams {\n key: Buffer;\n plaintext: Buffer;\n aad: string;\n}\n\nexport interface AesGcmOpenParams {\n key: Buffer;\n encoded: string;\n aad: string;\n}\n\nexport interface SecretScope {\n projectId?: string | null | undefined;\n}\n\nexport interface SecretValueAadParams {\n workspaceId: string;\n scope?: SecretScope | undefined;\n namespace: string;\n key: string;\n}\n\nexport function aesGcmSeal(params: AesGcmSealParams): string {\n return sealEnvelopeText(params);\n}\n\nexport function aesGcmOpen(params: AesGcmOpenParams): Buffer {\n try {\n return openEnvelopeText(params);\n } catch {\n throw new SecretDecryptionError();\n }\n}\n\nexport function decodeBase64Key(encoded: string | undefined, label: string): Buffer {\n try {\n return decodeEnvelopeKey(encoded, label);\n } catch (error) {\n if (error instanceof KeyConfigurationError) throw new KekConfigurationError(error.message);\n throw error;\n }\n}\n\nexport function aadForDek(workspaceId: string, kekVersion: string): string {\n return JSON.stringify([workspaceId, kekVersion]);\n}\n\nexport function aadForValue(params: SecretValueAadParams): string {\n const projectId = params.scope?.projectId ?? null;\n const scopeTuple = projectId !== null ? ['project', projectId] : ['workspace'];\n return JSON.stringify([params.workspaceId, scopeTuple, params.namespace, params.key]);\n}\n"],"names":["decodeBase64Key","decodeEnvelopeKey","KeyConfigurationError","openEnvelopeText","sealEnvelopeText","KekConfigurationError","SecretDecryptionError","aesGcmSeal","params","aesGcmOpen","encoded","label","error","message","aadForDek","workspaceId","kekVersion","JSON","stringify","aadForValue","projectId","scope","scopeTuple","namespace","key"],"mappings":"AAAA,SACEA,mBAAmBC,iBAAiB,EACpCC,qBAAqB,EACrBC,gBAAgB,EAChBC,gBAAgB,QACX,oCAAoC;AAC3C,SAAQC,qBAAqB,EAAEC,qBAAqB,QAAO,cAAc;AAyBzE,OAAO,SAASC,WAAWC,MAAwB;IACjD,OAAOJ,iBAAiBI;AAC1B;AAEA,OAAO,SAASC,WAAWD,MAAwB;IACjD,IAAI;QACF,OAAOL,iBAAiBK;IAC1B,EAAE,OAAM;QACN,MAAM,IAAIF;IACZ;AACF;AAEA,OAAO,SAASN,gBAAgBU,OAA2B,EAAEC,KAAa;IACxE,IAAI;QACF,OAAOV,kBAAkBS,SAASC;IACpC,EAAE,OAAOC,OAAO;QACd,IAAIA,iBAAiBV,uBAAuB,MAAM,IAAIG,sBAAsBO,MAAMC,OAAO;QACzF,MAAMD;IACR;AACF;AAEA,OAAO,SAASE,UAAUC,WAAmB,EAAEC,UAAkB;IAC/D,OAAOC,KAAKC,SAAS,CAAC;QAACH;QAAaC;KAAW;AACjD;AAEA,OAAO,SAASG,YAAYX,MAA4B;IACtD,MAAMY,YAAYZ,OAAOa,KAAK,EAAED,aAAa;IAC7C,MAAME,aAAaF,cAAc,OAAO;QAAC;QAAWA;KAAU,GAAG;QAAC;KAAY;IAC9E,OAAOH,KAAKC,SAAS,CAAC;QAACV,OAAOO,WAAW;QAAEO;QAAYd,OAAOe,SAAS;QAAEf,OAAOgB,GAAG;KAAC;AACtF"}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import type { KeyProvider } from './key-provider.js';
|
|
2
|
-
/**
|
|
3
|
-
* Plaintext DEKs live in memory by design so hot secret reads avoid unwrapping on
|
|
4
|
-
* every access. Node cannot reliably zeroize Buffers, so residency is bounded by
|
|
5
|
-
* LRU size and lazy TTL instead of pretending wipes are a complete mitigation.
|
|
6
|
-
*/
|
|
7
2
|
export declare class DekManager {
|
|
8
3
|
#private;
|
|
9
4
|
constructor(keyProvider: KeyProvider, options: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dek-manager.d.ts","sourceRoot":"","sources":["../../src/core/dek-manager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"dek-manager.d.ts","sourceRoot":"","sources":["../../src/core/dek-manager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAEnD,qBAAa,UAAU;;IAGrB,YAAY,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,EAkBjF;IAEK,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAa1D;IAED,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAEpC;CACF"}
|
package/dist/core/dek-manager.js
CHANGED
|
@@ -1,63 +1,35 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { DataKeyManager } from '@shipfox/node-envelope-encryption';
|
|
2
2
|
import { getDataKey, insertDataKeyIfAbsent } from '#db/index.js';
|
|
3
3
|
import { classifyDekAccessError, recordSecretsDekAccess } from '#metrics/instance.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* Plaintext DEKs live in memory by design so hot secret reads avoid unwrapping on
|
|
7
|
-
* every access. Node cannot reliably zeroize Buffers, so residency is bounded by
|
|
8
|
-
* LRU size and lazy TTL instead of pretending wipes are a complete mitigation.
|
|
9
|
-
*/ export class DekManager {
|
|
10
|
-
#cache;
|
|
11
|
-
#keyProvider;
|
|
12
|
-
#options;
|
|
4
|
+
export class DekManager {
|
|
5
|
+
#manager;
|
|
13
6
|
constructor(keyProvider, options){
|
|
14
|
-
this.#
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
this.#manager = new DataKeyManager(keyProvider, {
|
|
8
|
+
async get (workspaceId) {
|
|
9
|
+
const record = await getDataKey(workspaceId);
|
|
10
|
+
return record ? {
|
|
11
|
+
keyId: workspaceId,
|
|
12
|
+
...record
|
|
13
|
+
} : undefined;
|
|
14
|
+
},
|
|
15
|
+
insertIfAbsent (record) {
|
|
16
|
+
return insertDataKeyIfAbsent({
|
|
17
|
+
workspaceId: record.keyId,
|
|
18
|
+
wrappedDek: record.wrappedDek,
|
|
19
|
+
kekVersion: record.kekVersion
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}, options);
|
|
17
23
|
}
|
|
18
24
|
async getPlaintextDek(workspaceId) {
|
|
19
25
|
const startedAt = Date.now();
|
|
20
26
|
try {
|
|
21
|
-
const
|
|
22
|
-
if (cached && cached.expiresAt > Date.now()) {
|
|
23
|
-
this.#cache.delete(workspaceId);
|
|
24
|
-
this.#cache.set(workspaceId, cached);
|
|
25
|
-
recordSecretsDekAccess({
|
|
26
|
-
outcome: 'cache_hit',
|
|
27
|
-
durationMs: Date.now() - startedAt
|
|
28
|
-
});
|
|
29
|
-
return Buffer.from(cached.dek);
|
|
30
|
-
}
|
|
31
|
-
const hadExpiredCache = Boolean(cached);
|
|
32
|
-
if (cached) this.#cache.delete(workspaceId);
|
|
33
|
-
const existing = await getDataKey(workspaceId);
|
|
34
|
-
if (existing) {
|
|
35
|
-
const dek = this.#keyProvider.unwrapDek(workspaceId, existing.wrappedDek, existing.kekVersion);
|
|
36
|
-
this.#set(workspaceId, dek);
|
|
37
|
-
recordSecretsDekAccess({
|
|
38
|
-
outcome: hadExpiredCache ? 'cache_expired' : 'db_unwrapped',
|
|
39
|
-
durationMs: Date.now() - startedAt
|
|
40
|
-
});
|
|
41
|
-
return Buffer.from(dek);
|
|
42
|
-
}
|
|
43
|
-
const generatedDek = crypto.randomBytes(DEK_BYTES);
|
|
44
|
-
const wrapped = this.#keyProvider.wrapDek(workspaceId, generatedDek);
|
|
45
|
-
const inserted = await insertDataKeyIfAbsent({
|
|
46
|
-
workspaceId,
|
|
47
|
-
wrappedDek: wrapped.wrappedDek,
|
|
48
|
-
kekVersion: wrapped.kekVersion
|
|
49
|
-
});
|
|
50
|
-
// The DEK row commits before value writes. If concurrent first-use inserts race,
|
|
51
|
-
// the primary key decides the winner and every caller re-reads the persisted row.
|
|
52
|
-
const persisted = await getDataKey(workspaceId);
|
|
53
|
-
if (!persisted) throw new Error(`Data key was not persisted for workspace ${workspaceId}`);
|
|
54
|
-
const dek = this.#keyProvider.unwrapDek(workspaceId, persisted.wrappedDek, persisted.kekVersion);
|
|
55
|
-
this.#set(workspaceId, dek);
|
|
27
|
+
const result = await this.#manager.getPlaintextDataKey(workspaceId);
|
|
56
28
|
recordSecretsDekAccess({
|
|
57
|
-
outcome:
|
|
29
|
+
outcome: result.outcome,
|
|
58
30
|
durationMs: Date.now() - startedAt
|
|
59
31
|
});
|
|
60
|
-
return
|
|
32
|
+
return result.dek;
|
|
61
33
|
} catch (error) {
|
|
62
34
|
recordSecretsDekAccess({
|
|
63
35
|
outcome: classifyDekAccessError(error),
|
|
@@ -67,18 +39,7 @@ const DEK_BYTES = 32;
|
|
|
67
39
|
}
|
|
68
40
|
}
|
|
69
41
|
invalidate(workspaceId) {
|
|
70
|
-
this.#
|
|
71
|
-
}
|
|
72
|
-
#set(workspaceId, dek) {
|
|
73
|
-
this.#cache.set(workspaceId, {
|
|
74
|
-
dek: Buffer.from(dek),
|
|
75
|
-
expiresAt: Date.now() + this.#options.ttlMs
|
|
76
|
-
});
|
|
77
|
-
while(this.#cache.size > this.#options.maxEntries){
|
|
78
|
-
const oldest = this.#cache.keys().next().value;
|
|
79
|
-
if (!oldest) break;
|
|
80
|
-
this.#cache.delete(oldest);
|
|
81
|
-
}
|
|
42
|
+
this.#manager.invalidate(workspaceId);
|
|
82
43
|
}
|
|
83
44
|
}
|
|
84
45
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/dek-manager.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"sources":["../../src/core/dek-manager.ts"],"sourcesContent":["import {DataKeyManager} from '@shipfox/node-envelope-encryption';\nimport {getDataKey, insertDataKeyIfAbsent} from '#db/index.js';\nimport {classifyDekAccessError, recordSecretsDekAccess} from '#metrics/instance.js';\nimport type {KeyProvider} from './key-provider.js';\n\nexport class DekManager {\n readonly #manager: DataKeyManager;\n\n constructor(keyProvider: KeyProvider, options: {maxEntries: number; ttlMs: number}) {\n this.#manager = new DataKeyManager(\n keyProvider,\n {\n async get(workspaceId) {\n const record = await getDataKey(workspaceId);\n return record ? {keyId: workspaceId, ...record} : undefined;\n },\n insertIfAbsent(record) {\n return insertDataKeyIfAbsent({\n workspaceId: record.keyId,\n wrappedDek: record.wrappedDek,\n kekVersion: record.kekVersion,\n });\n },\n },\n options,\n );\n }\n\n async getPlaintextDek(workspaceId: string): Promise<Buffer> {\n const startedAt = Date.now();\n try {\n const result = await this.#manager.getPlaintextDataKey(workspaceId);\n recordSecretsDekAccess({outcome: result.outcome, durationMs: Date.now() - startedAt});\n return result.dek;\n } catch (error) {\n recordSecretsDekAccess({\n outcome: classifyDekAccessError(error),\n durationMs: Date.now() - startedAt,\n });\n throw error;\n }\n }\n\n invalidate(workspaceId: string): void {\n this.#manager.invalidate(workspaceId);\n }\n}\n"],"names":["DataKeyManager","getDataKey","insertDataKeyIfAbsent","classifyDekAccessError","recordSecretsDekAccess","DekManager","keyProvider","options","get","workspaceId","record","keyId","undefined","insertIfAbsent","wrappedDek","kekVersion","getPlaintextDek","startedAt","Date","now","result","getPlaintextDataKey","outcome","durationMs","dek","error","invalidate"],"mappings":"AAAA,SAAQA,cAAc,QAAO,oCAAoC;AACjE,SAAQC,UAAU,EAAEC,qBAAqB,QAAO,eAAe;AAC/D,SAAQC,sBAAsB,EAAEC,sBAAsB,QAAO,uBAAuB;AAGpF,OAAO,MAAMC;IACF,CAAA,OAAQ,CAAiB;IAElC,YAAYC,WAAwB,EAAEC,OAA4C,CAAE;QAClF,IAAI,CAAC,CAAA,OAAQ,GAAG,IAAIP,eAClBM,aACA;YACE,MAAME,KAAIC,WAAW;gBACnB,MAAMC,SAAS,MAAMT,WAAWQ;gBAChC,OAAOC,SAAS;oBAACC,OAAOF;oBAAa,GAAGC,MAAM;gBAAA,IAAIE;YACpD;YACAC,gBAAeH,MAAM;gBACnB,OAAOR,sBAAsB;oBAC3BO,aAAaC,OAAOC,KAAK;oBACzBG,YAAYJ,OAAOI,UAAU;oBAC7BC,YAAYL,OAAOK,UAAU;gBAC/B;YACF;QACF,GACAR;IAEJ;IAEA,MAAMS,gBAAgBP,WAAmB,EAAmB;QAC1D,MAAMQ,YAAYC,KAAKC,GAAG;QAC1B,IAAI;YACF,MAAMC,SAAS,MAAM,IAAI,CAAC,CAAA,OAAQ,CAACC,mBAAmB,CAACZ;YACvDL,uBAAuB;gBAACkB,SAASF,OAAOE,OAAO;gBAAEC,YAAYL,KAAKC,GAAG,KAAKF;YAAS;YACnF,OAAOG,OAAOI,GAAG;QACnB,EAAE,OAAOC,OAAO;YACdrB,uBAAuB;gBACrBkB,SAASnB,uBAAuBsB;gBAChCF,YAAYL,KAAKC,GAAG,KAAKF;YAC3B;YACA,MAAMQ;QACR;IACF;IAEAC,WAAWjB,WAAmB,EAAQ;QACpC,IAAI,CAAC,CAAA,OAAQ,CAACiB,UAAU,CAACjB;IAC3B;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key-provider.d.ts","sourceRoot":"","sources":["../../src/core/key-provider.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"key-provider.d.ts","sourceRoot":"","sources":["../../src/core/key-provider.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,UAAU,CAAC;IAC/D,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;CAChF;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,GAAG,WAAW,CAsBlF;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzD"}
|
|
@@ -1,45 +1,34 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { aadForDek, aesGcmOpen, aesGcmSeal } from './crypto.js';
|
|
1
|
+
import { createLocalKeyProvider as createEnvelopeKeyProvider, DataKeyUnwrapError, DataKeyWrapError, deriveLocalKeyVersion } from '@shipfox/node-envelope-encryption';
|
|
3
2
|
import { DekUnwrapError, DekWrapError } from './errors.js';
|
|
4
3
|
const KEK_VERSION_DOMAIN = 'shipfox-secrets-kek-version';
|
|
5
4
|
export function createLocalKeyProvider(params) {
|
|
6
|
-
const
|
|
7
|
-
|
|
5
|
+
const provider = createEnvelopeKeyProvider({
|
|
6
|
+
...params,
|
|
7
|
+
keyVersionDomain: KEK_VERSION_DOMAIN
|
|
8
|
+
});
|
|
8
9
|
return {
|
|
9
|
-
currentKeyVersion,
|
|
10
|
-
previousKeyVersion,
|
|
10
|
+
currentKeyVersion: provider.currentKeyVersion,
|
|
11
|
+
previousKeyVersion: provider.previousKeyVersion,
|
|
11
12
|
wrapDek (workspaceId, plaintextDek) {
|
|
12
13
|
try {
|
|
13
|
-
return
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
aad: aadForDek(workspaceId, currentKeyVersion)
|
|
18
|
-
}),
|
|
19
|
-
kekVersion: currentKeyVersion
|
|
20
|
-
};
|
|
21
|
-
} catch {
|
|
22
|
-
throw new DekWrapError();
|
|
14
|
+
return provider.wrapDek(workspaceId, plaintextDek);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof DataKeyWrapError) throw new DekWrapError();
|
|
17
|
+
throw error;
|
|
23
18
|
}
|
|
24
19
|
},
|
|
25
20
|
unwrapDek (workspaceId, wrappedDek, kekVersion) {
|
|
26
|
-
const key = kekVersion === currentKeyVersion ? params.currentKek : kekVersion === previousKeyVersion ? params.previousKek : undefined;
|
|
27
|
-
if (!key) throw new DekUnwrapError();
|
|
28
21
|
try {
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
});
|
|
34
|
-
} catch {
|
|
35
|
-
throw new DekUnwrapError();
|
|
22
|
+
return provider.unwrapDek(workspaceId, wrappedDek, kekVersion);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error instanceof DataKeyUnwrapError) throw new DekUnwrapError();
|
|
25
|
+
throw error;
|
|
36
26
|
}
|
|
37
27
|
}
|
|
38
28
|
};
|
|
39
29
|
}
|
|
40
30
|
export function deriveLocalKekVersion(kek) {
|
|
41
|
-
|
|
42
|
-
return `local:${hash.slice(0, 16)}`;
|
|
31
|
+
return deriveLocalKeyVersion(kek, KEK_VERSION_DOMAIN);
|
|
43
32
|
}
|
|
44
33
|
|
|
45
34
|
//# sourceMappingURL=key-provider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/key-provider.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"sources":["../../src/core/key-provider.ts"],"sourcesContent":["import {\n createLocalKeyProvider as createEnvelopeKeyProvider,\n DataKeyUnwrapError,\n DataKeyWrapError,\n deriveLocalKeyVersion,\n} from '@shipfox/node-envelope-encryption';\nimport {DekUnwrapError, DekWrapError} from './errors.js';\n\nconst KEK_VERSION_DOMAIN = 'shipfox-secrets-kek-version';\n\nexport interface WrappedDek {\n wrappedDek: string;\n kekVersion: string;\n}\n\nexport interface KeyProvider {\n readonly currentKeyVersion: string;\n readonly previousKeyVersion: string | null;\n wrapDek(workspaceId: string, plaintextDek: Buffer): WrappedDek;\n unwrapDek(workspaceId: string, wrappedDek: string, kekVersion: string): Buffer;\n}\n\nexport interface LocalKeyProviderParams {\n currentKek: Buffer;\n previousKek?: Buffer | undefined;\n}\n\nexport function createLocalKeyProvider(params: LocalKeyProviderParams): KeyProvider {\n const provider = createEnvelopeKeyProvider({...params, keyVersionDomain: KEK_VERSION_DOMAIN});\n return {\n currentKeyVersion: provider.currentKeyVersion,\n previousKeyVersion: provider.previousKeyVersion,\n wrapDek(workspaceId, plaintextDek) {\n try {\n return provider.wrapDek(workspaceId, plaintextDek);\n } catch (error) {\n if (error instanceof DataKeyWrapError) throw new DekWrapError();\n throw error;\n }\n },\n unwrapDek(workspaceId, wrappedDek, kekVersion) {\n try {\n return provider.unwrapDek(workspaceId, wrappedDek, kekVersion);\n } catch (error) {\n if (error instanceof DataKeyUnwrapError) throw new DekUnwrapError();\n throw error;\n }\n },\n };\n}\n\nexport function deriveLocalKekVersion(kek: Buffer): string {\n return deriveLocalKeyVersion(kek, KEK_VERSION_DOMAIN);\n}\n"],"names":["createLocalKeyProvider","createEnvelopeKeyProvider","DataKeyUnwrapError","DataKeyWrapError","deriveLocalKeyVersion","DekUnwrapError","DekWrapError","KEK_VERSION_DOMAIN","params","provider","keyVersionDomain","currentKeyVersion","previousKeyVersion","wrapDek","workspaceId","plaintextDek","error","unwrapDek","wrappedDek","kekVersion","deriveLocalKekVersion","kek"],"mappings":"AAAA,SACEA,0BAA0BC,yBAAyB,EACnDC,kBAAkB,EAClBC,gBAAgB,EAChBC,qBAAqB,QAChB,oCAAoC;AAC3C,SAAQC,cAAc,EAAEC,YAAY,QAAO,cAAc;AAEzD,MAAMC,qBAAqB;AAmB3B,OAAO,SAASP,uBAAuBQ,MAA8B;IACnE,MAAMC,WAAWR,0BAA0B;QAAC,GAAGO,MAAM;QAAEE,kBAAkBH;IAAkB;IAC3F,OAAO;QACLI,mBAAmBF,SAASE,iBAAiB;QAC7CC,oBAAoBH,SAASG,kBAAkB;QAC/CC,SAAQC,WAAW,EAAEC,YAAY;YAC/B,IAAI;gBACF,OAAON,SAASI,OAAO,CAACC,aAAaC;YACvC,EAAE,OAAOC,OAAO;gBACd,IAAIA,iBAAiBb,kBAAkB,MAAM,IAAIG;gBACjD,MAAMU;YACR;QACF;QACAC,WAAUH,WAAW,EAAEI,UAAU,EAAEC,UAAU;YAC3C,IAAI;gBACF,OAAOV,SAASQ,SAAS,CAACH,aAAaI,YAAYC;YACrD,EAAE,OAAOH,OAAO;gBACd,IAAIA,iBAAiBd,oBAAoB,MAAM,IAAIG;gBACnD,MAAMW;YACR;QACF;IACF;AACF;AAEA,OAAO,SAASI,sBAAsBC,GAAW;IAC/C,OAAOjB,sBAAsBiB,KAAKd;AACpC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rotate-kek.d.ts","sourceRoot":"","sources":["../../src/core/rotate-kek.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rotate-kek.d.ts","sourceRoot":"","sources":["../../src/core/rotate-kek.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,8BAA8B;IAC7C,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CACrC;AAED,wBAAgB,mCAAmC,CACjD,WAAW,EAAE,WAAW,EACxB,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,6BAA6B,CAAC,CA4BxC"}
|
package/dist/core/rotate-kek.js
CHANGED
|
@@ -1,94 +1,40 @@
|
|
|
1
|
+
import { rotateDataKeysWithTelemetry } from '@shipfox/node-envelope-encryption';
|
|
1
2
|
import { listDataKeysPage, listDataKeyVersions, updateDataKeyWrapCas } from '#db/index.js';
|
|
2
3
|
import { classifyKekRotationError, recordSecretsKekRotation } from '#metrics/instance.js';
|
|
3
4
|
import { KekVersionStrandedError } from './errors.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
skipped += 1;
|
|
32
|
-
skippedCurrent += 1;
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const plaintextDek = keyProvider.unwrapDek(row.workspaceId, row.wrappedDek, row.kekVersion);
|
|
36
|
-
try {
|
|
37
|
-
const wrapped = keyProvider.wrapDek(row.workspaceId, plaintextDek);
|
|
38
|
-
const updated = await updateDataKeyWrapCas({
|
|
39
|
-
workspaceId: row.workspaceId,
|
|
40
|
-
oldKekVersion: row.kekVersion,
|
|
41
|
-
wrappedDek: wrapped.wrappedDek,
|
|
42
|
-
kekVersion: wrapped.kekVersion
|
|
43
|
-
});
|
|
44
|
-
if (updated) rotated += 1;
|
|
45
|
-
else {
|
|
46
|
-
skipped += 1;
|
|
47
|
-
skippedRace += 1;
|
|
48
|
-
}
|
|
49
|
-
} finally{
|
|
50
|
-
plaintextDek.fill(0);
|
|
51
|
-
}
|
|
5
|
+
export function rotateWorkspaceDataKeysWithProvider(keyProvider, options = {}) {
|
|
6
|
+
return rotateDataKeysWithTelemetry({
|
|
7
|
+
keyProvider,
|
|
8
|
+
repository: {
|
|
9
|
+
listUnknownKeyVersions (knownVersions) {
|
|
10
|
+
return listDataKeyVersions(knownVersions, {
|
|
11
|
+
workspaceIds: options.workspaceIds
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
async listPage (params) {
|
|
15
|
+
const rows = await listDataKeysPage({
|
|
16
|
+
afterWorkspaceId: params.afterKeyId,
|
|
17
|
+
limit: params.limit,
|
|
18
|
+
workspaceIds: options.workspaceIds
|
|
19
|
+
});
|
|
20
|
+
return rows.map((row)=>({
|
|
21
|
+
keyId: row.workspaceId,
|
|
22
|
+
...row
|
|
23
|
+
}));
|
|
24
|
+
},
|
|
25
|
+
updateWrapCas (params) {
|
|
26
|
+
return updateDataKeyWrapCas({
|
|
27
|
+
workspaceId: params.keyId,
|
|
28
|
+
oldKekVersion: params.oldKekVersion,
|
|
29
|
+
wrappedDek: params.wrappedDek,
|
|
30
|
+
kekVersion: params.kekVersion
|
|
31
|
+
});
|
|
52
32
|
}
|
|
53
|
-
}
|
|
54
|
-
recordSecretsKekRotation
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
recordSecretsKekRotation({
|
|
59
|
-
outcome: 'skipped_current',
|
|
60
|
-
count: skippedCurrent
|
|
61
|
-
});
|
|
62
|
-
recordSecretsKekRotation({
|
|
63
|
-
outcome: 'skipped_race',
|
|
64
|
-
count: skippedRace
|
|
65
|
-
});
|
|
66
|
-
recordSecretsKekRotation({
|
|
67
|
-
outcome: rotationDurationOutcome({
|
|
68
|
-
rotated,
|
|
69
|
-
skippedCurrent,
|
|
70
|
-
skippedRace
|
|
71
|
-
}),
|
|
72
|
-
count: 0,
|
|
73
|
-
durationMs: Date.now() - startedAt
|
|
74
|
-
});
|
|
75
|
-
return {
|
|
76
|
-
rotated,
|
|
77
|
-
skipped
|
|
78
|
-
};
|
|
79
|
-
} catch (error) {
|
|
80
|
-
recordSecretsKekRotation({
|
|
81
|
-
outcome: classifyKekRotationError(error),
|
|
82
|
-
durationMs: Date.now() - startedAt
|
|
83
|
-
});
|
|
84
|
-
throw error;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
function rotationDurationOutcome(params) {
|
|
88
|
-
if (params.rotated > 0) return 'rotated';
|
|
89
|
-
if (params.skippedRace > 0) return 'skipped_race';
|
|
90
|
-
if (params.skippedCurrent === 0) return 'none';
|
|
91
|
-
return 'skipped_current';
|
|
33
|
+
},
|
|
34
|
+
record: recordSecretsKekRotation,
|
|
35
|
+
classifyError: classifyKekRotationError,
|
|
36
|
+
strandedError: (keyVersion)=>new KekVersionStrandedError(keyVersion)
|
|
37
|
+
});
|
|
92
38
|
}
|
|
93
39
|
|
|
94
40
|
//# sourceMappingURL=rotate-kek.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/rotate-kek.ts"],"sourcesContent":["import {listDataKeysPage, listDataKeyVersions, updateDataKeyWrapCas} from '#db/index.js';\nimport {classifyKekRotationError, recordSecretsKekRotation} from '#metrics/instance.js';\nimport {KekVersionStrandedError} from './errors.js';\nimport type {KeyProvider} from './key-provider.js';\n\
|
|
1
|
+
{"version":3,"sources":["../../src/core/rotate-kek.ts"],"sourcesContent":["import {rotateDataKeysWithTelemetry} from '@shipfox/node-envelope-encryption';\nimport {listDataKeysPage, listDataKeyVersions, updateDataKeyWrapCas} from '#db/index.js';\nimport {classifyKekRotationError, recordSecretsKekRotation} from '#metrics/instance.js';\nimport {KekVersionStrandedError} from './errors.js';\nimport type {KeyProvider} from './key-provider.js';\n\nexport interface RotateWorkspaceDataKeysResult {\n rotated: number;\n skipped: number;\n}\n\nexport interface RotateWorkspaceDataKeysOptions {\n workspaceIds?: string[] | undefined;\n}\n\nexport function rotateWorkspaceDataKeysWithProvider(\n keyProvider: KeyProvider,\n options: RotateWorkspaceDataKeysOptions = {},\n): Promise<RotateWorkspaceDataKeysResult> {\n return rotateDataKeysWithTelemetry({\n keyProvider,\n repository: {\n listUnknownKeyVersions(knownVersions) {\n return listDataKeyVersions(knownVersions, {workspaceIds: options.workspaceIds});\n },\n async listPage(params) {\n const rows = await listDataKeysPage({\n afterWorkspaceId: params.afterKeyId,\n limit: params.limit,\n workspaceIds: options.workspaceIds,\n });\n return rows.map((row) => ({keyId: row.workspaceId, ...row}));\n },\n updateWrapCas(params) {\n return updateDataKeyWrapCas({\n workspaceId: params.keyId,\n oldKekVersion: params.oldKekVersion,\n wrappedDek: params.wrappedDek,\n kekVersion: params.kekVersion,\n });\n },\n },\n record: recordSecretsKekRotation,\n classifyError: classifyKekRotationError,\n strandedError: (keyVersion) => new KekVersionStrandedError(keyVersion),\n });\n}\n"],"names":["rotateDataKeysWithTelemetry","listDataKeysPage","listDataKeyVersions","updateDataKeyWrapCas","classifyKekRotationError","recordSecretsKekRotation","KekVersionStrandedError","rotateWorkspaceDataKeysWithProvider","keyProvider","options","repository","listUnknownKeyVersions","knownVersions","workspaceIds","listPage","params","rows","afterWorkspaceId","afterKeyId","limit","map","row","keyId","workspaceId","updateWrapCas","oldKekVersion","wrappedDek","kekVersion","record","classifyError","strandedError","keyVersion"],"mappings":"AAAA,SAAQA,2BAA2B,QAAO,oCAAoC;AAC9E,SAAQC,gBAAgB,EAAEC,mBAAmB,EAAEC,oBAAoB,QAAO,eAAe;AACzF,SAAQC,wBAAwB,EAAEC,wBAAwB,QAAO,uBAAuB;AACxF,SAAQC,uBAAuB,QAAO,cAAc;AAYpD,OAAO,SAASC,oCACdC,WAAwB,EACxBC,UAA0C,CAAC,CAAC;IAE5C,OAAOT,4BAA4B;QACjCQ;QACAE,YAAY;YACVC,wBAAuBC,aAAa;gBAClC,OAAOV,oBAAoBU,eAAe;oBAACC,cAAcJ,QAAQI,YAAY;gBAAA;YAC/E;YACA,MAAMC,UAASC,MAAM;gBACnB,MAAMC,OAAO,MAAMf,iBAAiB;oBAClCgB,kBAAkBF,OAAOG,UAAU;oBACnCC,OAAOJ,OAAOI,KAAK;oBACnBN,cAAcJ,QAAQI,YAAY;gBACpC;gBACA,OAAOG,KAAKI,GAAG,CAAC,CAACC,MAAS,CAAA;wBAACC,OAAOD,IAAIE,WAAW;wBAAE,GAAGF,GAAG;oBAAA,CAAA;YAC3D;YACAG,eAAcT,MAAM;gBAClB,OAAOZ,qBAAqB;oBAC1BoB,aAAaR,OAAOO,KAAK;oBACzBG,eAAeV,OAAOU,aAAa;oBACnCC,YAAYX,OAAOW,UAAU;oBAC7BC,YAAYZ,OAAOY,UAAU;gBAC/B;YACF;QACF;QACAC,QAAQvB;QACRwB,eAAezB;QACf0B,eAAe,CAACC,aAAe,IAAIzB,wBAAwByB;IAC7D;AACF"}
|