@vizamodo/aws-sts-core 0.3.2 → 0.3.5

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.
Files changed (2) hide show
  1. package/dist/sts/issue.js +100 -62
  2. package/package.json +1 -1
package/dist/sts/issue.js CHANGED
@@ -8,7 +8,7 @@ const ALGORITHM = "AWS4-X509-ECDSA-SHA256";
8
8
  const SERVICE = "rolesanywhere";
9
9
  const PATH = "/sessions";
10
10
  const PROFILE_TTL = {
11
- Runtime: 90 * 60,
11
+ Runtime: 30 * 60,
12
12
  ConsoleReadOnly: 12 * 60 * 60,
13
13
  ConsoleViewOnlyProd: 12 * 60 * 60,
14
14
  BillingReadOnly: 2 * 60 * 60,
@@ -25,6 +25,7 @@ let cachedPrivateKeyBase64 = null;
25
25
  // ---- cached certificate serial (DER parse is expensive, cert rarely changes) ----
26
26
  let cachedCertSerialDec = null;
27
27
  let cachedCertSerialSource = null;
28
+ const stsCredentialCache = new Map();
28
29
  // ---- shared encoder ----
29
30
  const textEncoder = new TextEncoder();
30
31
  // Precomputed hex table for fast byte→hex conversion (no per-byte toString alloc)
@@ -78,22 +79,7 @@ export async function issueAwsCredentials(input) {
78
79
  // Normalize certificate once so every subsystem (cache, DER parse, headers)
79
80
  // uses the exact same canonical string
80
81
  const normalizedCert = normalizeCert(certBase64);
81
- const { signingKey } = await getSigningMaterial({
82
- certBase64: normalizedCert,
83
- privateKeyPkcs8Base64,
84
- });
85
- // 2. Setup constants
86
- const host = `${SERVICE}.${region}.amazonaws.com`;
87
- const path = PATH;
88
- const now = new Date();
89
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
90
- const dateStamp = amzDate.slice(0, 8);
91
- const service = SERVICE;
92
- // 3. Chuẩn bị Body & Cert
93
- const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
94
- const payloadHash = await getPayloadHash(body);
95
- // --- REFACTOR: Làm sạch chứng chỉ chặt chẽ ---
96
- // const normalizedCert = normalizeCert(certBase64);
82
+ let cacheKey = "";
97
83
  // --- REFACTOR: Parser ASN.1 an toàn để lấy Serial Number (DECIMAL) ---
98
84
  // --- MINIMAL DER WALK: extract serial number ---
99
85
  let certSerialDec;
@@ -105,11 +91,15 @@ export async function issueAwsCredentials(input) {
105
91
  const der = base64ToBytes(normalizedCert);
106
92
  let offset = 0;
107
93
  function readLen() {
94
+ if (offset >= der.length)
95
+ throw new Error("DER overflow");
108
96
  const b = der[offset++];
109
97
  if ((b & 0x80) === 0)
110
98
  return b;
111
99
  const n = b & 0x7f;
112
100
  let len = 0;
101
+ if (offset + n > der.length)
102
+ throw new Error("DER overflow");
113
103
  for (let i = 0; i < n; i++) {
114
104
  len = (len << 8) | der[offset++];
115
105
  }
@@ -129,15 +119,18 @@ export async function issueAwsCredentials(input) {
129
119
  if (der[offset++] !== 0x02)
130
120
  throw new Error("bad serial tag");
131
121
  const serialLen = readLen();
122
+ if (offset + serialLen > der.length)
123
+ throw new Error("DER overflow");
132
124
  let serial = der.slice(offset, offset + serialLen);
133
125
  if (serial.length > 1 && serial[0] === 0x00) {
134
126
  serial = serial.slice(1);
135
127
  }
136
- let hex = "";
128
+ // Build BigInt directly from bytes (avoids hex string allocation)
129
+ let serialBig = 0n;
137
130
  for (let i = 0; i < serial.length; i++) {
138
- hex += HEX_TABLE[serial[i]];
131
+ serialBig = (serialBig << 8n) | BigInt(serial[i]);
139
132
  }
140
- certSerialDec = BigInt("0x" + hex).toString();
133
+ certSerialDec = serialBig.toString();
141
134
  cachedCertSerialDec = certSerialDec;
142
135
  cachedCertSerialSource = normalizedCert;
143
136
  }
@@ -146,6 +139,37 @@ export async function issueAwsCredentials(input) {
146
139
  throw new InternalError("invalid_cert_der");
147
140
  }
148
141
  }
142
+ // ---- STS credential cache lookup (using certificate serial) ----
143
+ cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
144
+ const cachedEntry = stsCredentialCache.get(cacheKey);
145
+ // Only reuse cached credentials if they still have >10p remaining
146
+ const MIN_REMAINING_MS = 5 * 60 * 1000;
147
+ if (cachedEntry) {
148
+ // If refresh already in-flight → await the same promise
149
+ if (cachedEntry.expiresAt === 0) {
150
+ return cachedEntry.promise;
151
+ }
152
+ // If credentials still valid with safe remaining window → reuse
153
+ if (cachedEntry.expiresAt > Date.now() + MIN_REMAINING_MS) {
154
+ return cachedEntry.promise;
155
+ }
156
+ }
157
+ const { signingKey } = await getSigningMaterial({
158
+ certBase64: normalizedCert,
159
+ privateKeyPkcs8Base64,
160
+ });
161
+ // 2. Setup constants
162
+ const host = `${SERVICE}.${region}.amazonaws.com`;
163
+ const path = PATH;
164
+ const iso = new Date().toISOString(); // e.g. 2026-03-07T12:00:00.000Z
165
+ const amzDate = `${iso.slice(0, 4)}${iso.slice(5, 7)}${iso.slice(8, 10)}T${iso.slice(11, 13)}${iso.slice(14, 16)}${iso.slice(17, 19)}Z`;
166
+ const dateStamp = iso.slice(0, 4) + iso.slice(5, 7) + iso.slice(8, 10);
167
+ const service = SERVICE;
168
+ // 3. Chuẩn bị Body & Cert
169
+ const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
170
+ const payloadHash = await getPayloadHash(body);
171
+ // --- REFACTOR: Làm sạch chứng chỉ chặt chẽ ---
172
+ // const normalizedCert = normalizeCert(certBase64);
149
173
  // 4. Tính toán Signature
150
174
  const baseHeaders = {
151
175
  "content-type": "application/json",
@@ -178,60 +202,74 @@ export async function issueAwsCredentials(input) {
178
202
  "X-Amz-X509": normalizedCert,
179
203
  "Authorization": `AWS4-X509-ECDSA-SHA256 Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`
180
204
  });
181
- try {
182
- const res = await fetch(`https://${host}${path}`, {
183
- method: "POST",
184
- headers: finalHeaders,
185
- body,
186
- });
187
- if (!res.ok) {
188
- const errorBody = await res.text();
189
- // Debug logs
190
- console.error("[aws-rejected] Request details", {
191
- status: res.status,
192
- response: errorBody,
193
- region,
194
- profile,
195
- amzDate
205
+ const refreshPromise = (async () => {
206
+ try {
207
+ const res = await fetch(`https://${host}${path}`, {
208
+ method: "POST",
209
+ headers: finalHeaders,
210
+ body,
196
211
  });
197
- throw new InternalError("aws_rejected");
212
+ if (!res.ok) {
213
+ console.warn("[aws-rejected]", {
214
+ status: res.status,
215
+ region,
216
+ profile
217
+ });
218
+ throw new InternalError("aws_rejected");
219
+ }
220
+ const json = await res.json();
221
+ const creds = json?.credentialSet?.[0]?.credentials;
222
+ if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
223
+ console.warn("[issueAwsCredentials] malformed AWS credential response");
224
+ throw new InternalError("aws_malformed_credentials");
225
+ }
226
+ const result = {
227
+ accessKeyId: creds.accessKeyId,
228
+ secretAccessKey: creds.secretAccessKey,
229
+ sessionToken: creds.sessionToken,
230
+ expiration: creds.expiration,
231
+ };
232
+ // subtract small clock‑skew safety window (5s)
233
+ const exp = new Date(creds.expiration).getTime() - 5000;
234
+ // update cache expiration after success
235
+ const entry = stsCredentialCache.get(cacheKey);
236
+ if (entry) {
237
+ entry.expiresAt = exp;
238
+ }
239
+ return result;
198
240
  }
199
- const json = await res.json();
200
- const creds = json?.credentialSet?.[0]?.credentials;
201
- if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
202
- console.error("[issueAwsCredentials] malformed AWS credential response", { json });
203
- throw new InternalError("aws_malformed_credentials");
241
+ catch (e) {
242
+ // remove broken cache entry so next call can retry
243
+ stsCredentialCache.delete(cacheKey);
244
+ if (e instanceof InternalError)
245
+ throw e;
246
+ throw new InternalError("aws_unreachable");
204
247
  }
205
- return {
206
- accessKeyId: creds.accessKeyId,
207
- secretAccessKey: creds.secretAccessKey,
208
- sessionToken: creds.sessionToken,
209
- expiration: creds.expiration,
210
- };
211
- }
212
- catch (e) {
213
- if (e instanceof InternalError)
214
- throw e;
215
- throw new InternalError("aws_unreachable");
216
- }
248
+ })();
249
+ // store promise immediately to deduplicate concurrent refresh
250
+ stsCredentialCache.set(cacheKey, {
251
+ promise: refreshPromise,
252
+ expiresAt: 0,
253
+ });
254
+ return refreshPromise;
217
255
  }
218
256
  // ---- helpers ----
219
257
  function normalizeCert(raw) {
220
258
  if (raw.includes("BEGIN CERTIFICATE")) {
221
259
  throw new InternalError("pem_not_allowed");
222
260
  }
223
- return raw.trim();
261
+ // remove whitespace and newlines to ensure stable cache keys
262
+ return raw.replace(/\s+/g, "");
224
263
  }
225
264
  async function sha256Hex(input) {
226
- const data = textEncoder.encode(input);
227
- const hash = await crypto.subtle.digest("SHA-256", data);
265
+ // Tận dụng textEncoder có sẵn
266
+ const hash = await crypto.subtle.digest("SHA-256", textEncoder.encode(input));
228
267
  const bytes = new Uint8Array(hash);
229
- // build hex string with minimal allocations
230
- let out = "";
231
- for (let i = 0; i < bytes.length; i++) {
232
- out += HEX_TABLE[bytes[i]];
268
+ const hexParts = new Array(32); // SHA-256 luôn 32 bytes
269
+ for (let i = 0; i < 32; i++) {
270
+ hexParts[i] = HEX_TABLE[bytes[i]];
233
271
  }
234
- return out;
272
+ return hexParts.join("");
235
273
  }
236
274
  async function getCanonicalRequestHash(canonicalRequest) {
237
275
  return sha256Hex(canonicalRequest);
@@ -241,7 +279,7 @@ async function getPayloadHash(body) {
241
279
  // so caching adds complexity without measurable benefit.
242
280
  return sha256Hex(body);
243
281
  }
244
- // Faster base64 decode → Uint8Array (avoids extra ArrayBuffer wrapping)
282
+ // Faster base64 decode → Uint8Array (V8 optimized path using built‑in vectorized conversion)
245
283
  function base64ToBytes(base64) {
246
284
  const binary = atob(base64);
247
285
  const len = binary.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizamodo/aws-sts-core",
3
- "version": "0.3.2",
3
+ "version": "0.3.5",
4
4
  "description": "Pure AWS STS + SigV4 (X509 Roles Anywhere) core logic",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",