@faizahmed/secret-keystore 1.1.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/LICENSE +21 -0
- package/README.md +1203 -0
- package/SECURITY.md +505 -0
- package/bin/cli.js +969 -0
- package/package.json +77 -0
- package/src/attestation/attestation-client.js +146 -0
- package/src/attestation/attestation-manager.js +339 -0
- package/src/attestation/cms-unwrap.js +166 -0
- package/src/attestation/index.js +66 -0
- package/src/attestation/key-pair.js +129 -0
- package/src/config.js +130 -0
- package/src/content-operations.js +494 -0
- package/src/errors.js +372 -0
- package/src/index.d.ts +641 -0
- package/src/index.js +438 -0
- package/src/keystore.js +678 -0
- package/src/kms.js +858 -0
- package/src/object-operations.js +232 -0
- package/src/options.js +541 -0
- package/src/path-matcher.js +319 -0
- package/src/rotate.js +92 -0
- package/src/yaml-utils.js +265 -0
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@faizahmed/secret-keystore",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Secure secrets management with AWS KMS encryption, in-memory keystore, and Nitro Enclave attestation support",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"secret-keystore": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18.0.0"
|
|
12
|
+
},
|
|
13
|
+
"packageManager": "pnpm@9.15.9",
|
|
14
|
+
"files": [
|
|
15
|
+
"src/**/*.js",
|
|
16
|
+
"src/**/*.d.ts",
|
|
17
|
+
"bin/**/*.js",
|
|
18
|
+
"README.md",
|
|
19
|
+
"SECURITY.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@aws-sdk/client-kms": "^3.1067.0",
|
|
24
|
+
"asn1js": "^3.0.10",
|
|
25
|
+
"pkijs": "^3.4.0",
|
|
26
|
+
"pvtsutils": "^1.3.5"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@aws-sdk/client-kms": "^3.x",
|
|
30
|
+
"js-yaml": "^4.x"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"js-yaml": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "node --test test/*.test.js",
|
|
39
|
+
"test:coverage": "c8 node --test test/*.test.js",
|
|
40
|
+
"lint": "eslint .",
|
|
41
|
+
"lint:fix": "eslint . --fix",
|
|
42
|
+
"format": "prettier --write \"{src,bin,test}/**/*.js\"",
|
|
43
|
+
"format:check": "prettier --check \"{src,bin,test}/**/*.js\"",
|
|
44
|
+
"typecheck": "tsc --noEmit --strict --lib es2022 src/index.d.ts",
|
|
45
|
+
"pack:local": "pnpm pack"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@eslint/js": "^9.0.0",
|
|
49
|
+
"aws-sdk-client-mock": "^4.1.0",
|
|
50
|
+
"c8": "^10.1.0",
|
|
51
|
+
"eslint": "^9.0.0",
|
|
52
|
+
"globals": "^15.0.0",
|
|
53
|
+
"prettier": "^3.3.0",
|
|
54
|
+
"typescript": "^5.5.0"
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"kms",
|
|
58
|
+
"secrets",
|
|
59
|
+
"keystore",
|
|
60
|
+
"aws",
|
|
61
|
+
"encryption",
|
|
62
|
+
"decryption",
|
|
63
|
+
"dotenv",
|
|
64
|
+
"nitro",
|
|
65
|
+
"attestation"
|
|
66
|
+
],
|
|
67
|
+
"author": "Faiz Ahmed Farooqui",
|
|
68
|
+
"license": "MIT",
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "https://github.com/faizahmedfarooqui/secret-keystore.git"
|
|
72
|
+
},
|
|
73
|
+
"homepage": "https://github.com/faizahmedfarooqui/secret-keystore#readme",
|
|
74
|
+
"bugs": {
|
|
75
|
+
"url": "https://github.com/faizahmedfarooqui/secret-keystore/issues"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @faizahmed/secret-keystore - Attestation Client
|
|
3
|
+
*
|
|
4
|
+
* Fetches attestation documents from AWS Nitro Enclaves or Anjuna endpoints.
|
|
5
|
+
* The attestation document contains the caller's public key and is signed
|
|
6
|
+
* by the enclave's PCR values.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const https = require('node:https');
|
|
10
|
+
const http = require('node:http');
|
|
11
|
+
const { URL } = require('node:url');
|
|
12
|
+
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
// ATTESTATION CLIENT
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default attestation endpoint (Anjuna)
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_ATTESTATION_ENDPOINT = 'http://localhost:50123/api/v1/attestation/report';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fetch attestation document from Nitro/Anjuna endpoint
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} params - Attestation request parameters
|
|
26
|
+
* @param {string} params.publicKey - Base64url-encoded DER public key
|
|
27
|
+
* @param {string} [params.userData] - Base64url-encoded user data
|
|
28
|
+
* @param {string} [params.nonce] - Base64-encoded nonce
|
|
29
|
+
* @param {Object} [options] - Client options
|
|
30
|
+
* @param {string} [options.endpoint] - Attestation endpoint URL
|
|
31
|
+
* @param {number} [options.timeout] - Request timeout in milliseconds (default: 10000)
|
|
32
|
+
* @returns {Promise<{ attestationDocument: string }>} - Base64-encoded attestation document
|
|
33
|
+
* @throws {Error} If the request fails
|
|
34
|
+
*/
|
|
35
|
+
async function fetchAttestationDocument(params, options = {}) {
|
|
36
|
+
const endpoint = options.endpoint || DEFAULT_ATTESTATION_ENDPOINT;
|
|
37
|
+
const timeout = options.timeout || 10000;
|
|
38
|
+
|
|
39
|
+
// Build URL with query parameters
|
|
40
|
+
const url = new URL(endpoint);
|
|
41
|
+
if (params.publicKey) {
|
|
42
|
+
url.searchParams.set('public_key', params.publicKey);
|
|
43
|
+
}
|
|
44
|
+
if (params.userData) {
|
|
45
|
+
url.searchParams.set('user_data', params.userData);
|
|
46
|
+
}
|
|
47
|
+
if (params.nonce) {
|
|
48
|
+
url.searchParams.set('nonce', params.nonce);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Choose http or https based on protocol
|
|
52
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
53
|
+
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const req = client.get(
|
|
56
|
+
url.toString(),
|
|
57
|
+
{
|
|
58
|
+
timeout,
|
|
59
|
+
headers: {
|
|
60
|
+
Accept: 'application/octet-stream'
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
res => {
|
|
64
|
+
const chunks = [];
|
|
65
|
+
|
|
66
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
67
|
+
|
|
68
|
+
res.on('end', () => {
|
|
69
|
+
const buffer = Buffer.concat(chunks);
|
|
70
|
+
|
|
71
|
+
if (res.statusCode !== 200) {
|
|
72
|
+
const errorMessage = buffer.toString('utf8');
|
|
73
|
+
reject(
|
|
74
|
+
new Error(
|
|
75
|
+
`Attestation endpoint returned ${res.statusCode}: ${errorMessage}`
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The response is binary attestation document, convert to base64
|
|
82
|
+
const attestationDocument = buffer.toString('base64');
|
|
83
|
+
|
|
84
|
+
resolve({ attestationDocument });
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
req.on('error', error => {
|
|
90
|
+
reject(new Error(`Attestation request failed: ${error.message}`));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
req.on('timeout', () => {
|
|
94
|
+
req.destroy();
|
|
95
|
+
reject(new Error(`Attestation request timed out after ${timeout}ms`));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if running inside a Nitro Enclave
|
|
102
|
+
* Nitro Enclaves have /dev/nsm device
|
|
103
|
+
*
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
function isNitroEnclave() {
|
|
107
|
+
try {
|
|
108
|
+
const fs = require('node:fs');
|
|
109
|
+
return fs.existsSync('/dev/nsm');
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if attestation endpoint is reachable
|
|
117
|
+
*
|
|
118
|
+
* @param {string} [endpoint] - Attestation endpoint URL
|
|
119
|
+
* @param {number} [timeout] - Request timeout in milliseconds
|
|
120
|
+
* @returns {Promise<boolean>}
|
|
121
|
+
*/
|
|
122
|
+
async function isAttestationAvailable(endpoint = DEFAULT_ATTESTATION_ENDPOINT, timeout = 3000) {
|
|
123
|
+
const url = new URL(endpoint);
|
|
124
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
125
|
+
|
|
126
|
+
return new Promise(resolve => {
|
|
127
|
+
const req = client.get(url.toString(), { timeout }, res => {
|
|
128
|
+
// Any response means the endpoint is available
|
|
129
|
+
res.resume(); // Consume response data to free up memory
|
|
130
|
+
resolve(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
req.on('error', () => resolve(false));
|
|
134
|
+
req.on('timeout', () => {
|
|
135
|
+
req.destroy();
|
|
136
|
+
resolve(false);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
fetchAttestationDocument,
|
|
143
|
+
isNitroEnclave,
|
|
144
|
+
isAttestationAvailable,
|
|
145
|
+
DEFAULT_ATTESTATION_ENDPOINT
|
|
146
|
+
};
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @faizahmed/secret-keystore - Attestation Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages the attestation document lifecycle:
|
|
5
|
+
* - Generates ephemeral RSA key pairs
|
|
6
|
+
* - Fetches attestation documents from Nitro/Anjuna
|
|
7
|
+
* - Caches attestation materials
|
|
8
|
+
* - Handles 5-minute age limit by refreshing on demand
|
|
9
|
+
* - Provides CMS unwrapping for KMS CiphertextForRecipient
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { DecryptCommand } = require('@aws-sdk/client-kms');
|
|
13
|
+
const { generateEphemeralKeyPair, prepareAttestationParams } = require('./key-pair');
|
|
14
|
+
const { fetchAttestationDocument, DEFAULT_ATTESTATION_ENDPOINT } = require('./attestation-client');
|
|
15
|
+
const { unwrapCms } = require('./cms-unwrap');
|
|
16
|
+
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
+
// ATTESTATION MANAGER CLASS
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* AttestationManager handles the full attestation lifecycle
|
|
23
|
+
*/
|
|
24
|
+
class AttestationManager {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.endpoint = options.endpoint || DEFAULT_ATTESTATION_ENDPOINT;
|
|
27
|
+
this.timeout = options.timeout || 10000;
|
|
28
|
+
this.userData = options.userData || '';
|
|
29
|
+
this.logger = options.logger || null;
|
|
30
|
+
|
|
31
|
+
// Cached materials
|
|
32
|
+
this.cachedKeyPair = null;
|
|
33
|
+
this.cachedDocument = null;
|
|
34
|
+
this.cachedTimestamp = null;
|
|
35
|
+
|
|
36
|
+
// Initialization state
|
|
37
|
+
this.initialized = false;
|
|
38
|
+
this.initError = null;
|
|
39
|
+
|
|
40
|
+
// Mutex for concurrent initialization
|
|
41
|
+
this.initPromise = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Initialize attestation (generate key pair, fetch document)
|
|
46
|
+
* @returns {Promise<void>}
|
|
47
|
+
*/
|
|
48
|
+
async initialize() {
|
|
49
|
+
// Prevent concurrent initialization
|
|
50
|
+
if (this.initPromise) {
|
|
51
|
+
return this.initPromise;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.initPromise = this._doInitialize();
|
|
55
|
+
try {
|
|
56
|
+
await this.initPromise;
|
|
57
|
+
} finally {
|
|
58
|
+
this.initPromise = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Internal initialization logic
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
async _doInitialize() {
|
|
67
|
+
this._log('debug', 'Initializing attestation...');
|
|
68
|
+
this.initialized = false;
|
|
69
|
+
this.initError = null;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// 1. Generate ephemeral RSA-4096 key pair
|
|
73
|
+
this._log('debug', 'Generating ephemeral RSA-4096 key pair...');
|
|
74
|
+
this.cachedKeyPair = generateEphemeralKeyPair();
|
|
75
|
+
|
|
76
|
+
// 2. Prepare attestation request parameters
|
|
77
|
+
const params = prepareAttestationParams(this.cachedKeyPair.publicKey, this.userData);
|
|
78
|
+
|
|
79
|
+
// 3. Fetch attestation document
|
|
80
|
+
this._log('debug', `Fetching attestation document from ${this.endpoint}...`);
|
|
81
|
+
const response = await fetchAttestationDocument(params, {
|
|
82
|
+
endpoint: this.endpoint,
|
|
83
|
+
timeout: this.timeout
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// 4. Cache the document
|
|
87
|
+
this.cachedDocument = response.attestationDocument;
|
|
88
|
+
this.cachedTimestamp = Date.now();
|
|
89
|
+
this.initialized = true;
|
|
90
|
+
|
|
91
|
+
this._log('info', 'Attestation initialized successfully');
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.initError = error.message;
|
|
94
|
+
this.initialized = false;
|
|
95
|
+
this._log('error', `Attestation initialization failed: ${error.message}`);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reinitialize attestation (for 5-minute refresh)
|
|
102
|
+
* @returns {Promise<void>}
|
|
103
|
+
*/
|
|
104
|
+
async reinitialize() {
|
|
105
|
+
this._log('warn', 'Reinitializing attestation (5-minute refresh)...');
|
|
106
|
+
// Clear current cache
|
|
107
|
+
this.cachedKeyPair = null;
|
|
108
|
+
this.cachedDocument = null;
|
|
109
|
+
this.cachedTimestamp = null;
|
|
110
|
+
this.initialized = false;
|
|
111
|
+
|
|
112
|
+
// Re-initialize
|
|
113
|
+
await this.initialize();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if attestation is initialized and usable
|
|
118
|
+
* @returns {boolean}
|
|
119
|
+
*/
|
|
120
|
+
isInitialized() {
|
|
121
|
+
return this.initialized && !!this.cachedDocument && !!this.cachedKeyPair;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get cached attestation materials
|
|
126
|
+
* @returns {{ document: string, privateKey: string, publicKey: string } | null}
|
|
127
|
+
*/
|
|
128
|
+
getCachedMaterials() {
|
|
129
|
+
if (!this.isInitialized()) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
document: this.cachedDocument,
|
|
135
|
+
privateKey: this.cachedKeyPair.privateKey,
|
|
136
|
+
publicKey: this.cachedKeyPair.publicKey
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get the age of the cached attestation document in milliseconds
|
|
142
|
+
* @returns {number | null}
|
|
143
|
+
*/
|
|
144
|
+
getDocumentAge() {
|
|
145
|
+
if (!this.cachedTimestamp) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return Date.now() - this.cachedTimestamp;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Perform attested decrypt with KMS
|
|
153
|
+
*
|
|
154
|
+
* This method:
|
|
155
|
+
* 1. Ensures attestation is initialized
|
|
156
|
+
* 2. Calls KMS Decrypt with Recipient parameter
|
|
157
|
+
* 3. Handles CiphertextForRecipient by unwrapping with PKIjs
|
|
158
|
+
* 4. Retries on 5-minute age limit error
|
|
159
|
+
*
|
|
160
|
+
* @param {import('@aws-sdk/client-kms').KMSClient} kmsClient - KMS client
|
|
161
|
+
* @param {Buffer} ciphertextBlob - The encrypted data
|
|
162
|
+
* @param {string} kmsKeyId - The KMS key ID
|
|
163
|
+
* @param {Object} [options] - Additional options
|
|
164
|
+
* @param {string} [options.encryptionAlgorithm] - 'RSAES_OAEP_SHA_256' or 'SYMMETRIC_DEFAULT'
|
|
165
|
+
* @param {Object} [options.encryptionContext] - KMS encryption context
|
|
166
|
+
* @returns {Promise<Buffer>} - Decrypted plaintext
|
|
167
|
+
*/
|
|
168
|
+
async decryptWithAttestation(kmsClient, ciphertextBlob, kmsKeyId, options = {}) {
|
|
169
|
+
// Ensure initialized
|
|
170
|
+
if (!this.isInitialized()) {
|
|
171
|
+
await this.initialize();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const materials = this.getCachedMaterials();
|
|
175
|
+
if (!materials) {
|
|
176
|
+
throw new Error('Failed to get attestation materials after initialization');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
return await this._performAttestedDecrypt(
|
|
181
|
+
kmsClient,
|
|
182
|
+
ciphertextBlob,
|
|
183
|
+
kmsKeyId,
|
|
184
|
+
materials,
|
|
185
|
+
options
|
|
186
|
+
);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
// Check for 5-minute age limit error
|
|
189
|
+
if (this._isAgeLimitError(error)) {
|
|
190
|
+
this._log('warn', 'Attestation document expired (5-minute limit), refreshing...');
|
|
191
|
+
|
|
192
|
+
// Reinitialize to get fresh materials
|
|
193
|
+
await this.reinitialize();
|
|
194
|
+
|
|
195
|
+
const freshMaterials = this.getCachedMaterials();
|
|
196
|
+
if (!freshMaterials) {
|
|
197
|
+
throw new Error('Failed to get fresh attestation materials');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Retry once with fresh materials
|
|
201
|
+
return await this._performAttestedDecrypt(
|
|
202
|
+
kmsClient,
|
|
203
|
+
ciphertextBlob,
|
|
204
|
+
kmsKeyId,
|
|
205
|
+
freshMaterials,
|
|
206
|
+
options
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Perform the actual attested decrypt
|
|
216
|
+
* @private
|
|
217
|
+
*/
|
|
218
|
+
async _performAttestedDecrypt(kmsClient, ciphertextBlob, kmsKeyId, materials, options) {
|
|
219
|
+
const attestationBuffer = Buffer.from(materials.document, 'base64');
|
|
220
|
+
|
|
221
|
+
// Build KMS Decrypt command with Recipient
|
|
222
|
+
const command = new DecryptCommand({
|
|
223
|
+
KeyId: kmsKeyId,
|
|
224
|
+
CiphertextBlob: ciphertextBlob,
|
|
225
|
+
EncryptionAlgorithm: options.encryptionAlgorithm,
|
|
226
|
+
EncryptionContext: options.encryptionContext,
|
|
227
|
+
Recipient: {
|
|
228
|
+
KeyEncryptionAlgorithm: 'RSAES_OAEP_SHA_256',
|
|
229
|
+
AttestationDocument: attestationBuffer
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
this._log('debug', 'Sending KMS Decrypt with Recipient...');
|
|
234
|
+
const response = await kmsClient.send(command);
|
|
235
|
+
|
|
236
|
+
// With Recipient, KMS returns CiphertextForRecipient, NOT Plaintext
|
|
237
|
+
if (!response.CiphertextForRecipient) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
'KMS did not return CiphertextForRecipient - check key policy and Recipient support'
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const ciphertextForRecipient = Buffer.from(response.CiphertextForRecipient);
|
|
244
|
+
this._log(
|
|
245
|
+
'debug',
|
|
246
|
+
`Received CiphertextForRecipient (${ciphertextForRecipient.length} bytes)`
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Unwrap CMS EnvelopedData using our ephemeral private key
|
|
250
|
+
this._log('debug', 'Unwrapping CMS EnvelopedData...');
|
|
251
|
+
const plaintext = await unwrapCms(ciphertextForRecipient, materials.privateKey);
|
|
252
|
+
this._log('debug', `Decrypted plaintext (${plaintext.length} bytes)`);
|
|
253
|
+
|
|
254
|
+
return plaintext;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Check if an error is a 5-minute age limit error
|
|
259
|
+
* @private
|
|
260
|
+
*/
|
|
261
|
+
_isAgeLimitError(error) {
|
|
262
|
+
const message = error.message || '';
|
|
263
|
+
return (
|
|
264
|
+
message.includes('exceeded the five-minute age limit') ||
|
|
265
|
+
message.includes('exceeded the five minute age limit') ||
|
|
266
|
+
message.includes('age limit') ||
|
|
267
|
+
message.includes('cannot parse the attestation document') ||
|
|
268
|
+
message.includes('cannot parse attestation document')
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Log a message
|
|
274
|
+
* @private
|
|
275
|
+
*/
|
|
276
|
+
_log(level, message) {
|
|
277
|
+
if (this.logger && typeof this.logger[level] === 'function') {
|
|
278
|
+
this.logger[level](`[AttestationManager] ${message}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get initialization status
|
|
284
|
+
* @returns {Object}
|
|
285
|
+
*/
|
|
286
|
+
getStatus() {
|
|
287
|
+
return {
|
|
288
|
+
initialized: this.initialized,
|
|
289
|
+
hasError: !!this.initError,
|
|
290
|
+
error: this.initError,
|
|
291
|
+
hasDocument: !!this.cachedDocument,
|
|
292
|
+
hasKeyPair: !!this.cachedKeyPair,
|
|
293
|
+
documentAge: this.getDocumentAge(),
|
|
294
|
+
endpoint: this.endpoint
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Destroy the manager and clear all cached materials
|
|
300
|
+
*/
|
|
301
|
+
destroy() {
|
|
302
|
+
this.cachedKeyPair = null;
|
|
303
|
+
this.cachedDocument = null;
|
|
304
|
+
this.cachedTimestamp = null;
|
|
305
|
+
this.initialized = false;
|
|
306
|
+
this.initError = null;
|
|
307
|
+
this._log('debug', 'Attestation manager destroyed');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
312
|
+
// FACTORY FUNCTION
|
|
313
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Create and initialize an AttestationManager
|
|
317
|
+
*
|
|
318
|
+
* @param {Object} [options] - Options
|
|
319
|
+
* @param {string} [options.endpoint] - Attestation endpoint URL
|
|
320
|
+
* @param {number} [options.timeout] - Request timeout in ms
|
|
321
|
+
* @param {string} [options.userData] - User data to include in attestation
|
|
322
|
+
* @param {Object} [options.logger] - Logger instance
|
|
323
|
+
* @param {boolean} [options.autoInitialize=true] - Initialize on creation
|
|
324
|
+
* @returns {Promise<AttestationManager>}
|
|
325
|
+
*/
|
|
326
|
+
async function createAttestationManager(options = {}) {
|
|
327
|
+
const manager = new AttestationManager(options);
|
|
328
|
+
|
|
329
|
+
if (options.autoInitialize !== false) {
|
|
330
|
+
await manager.initialize();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return manager;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = {
|
|
337
|
+
AttestationManager,
|
|
338
|
+
createAttestationManager
|
|
339
|
+
};
|