@carecard/auth-util 3.19.0 → 3.21.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.
@@ -1,22 +1,25 @@
1
1
  const crypto = require('crypto');
2
2
 
3
+ const supportedVerificationHashes = new Set(crypto.getHashes().map(hash => hash.toLowerCase()));
4
+
5
+ // Pattern: Normalizer - preserves explicit registered times while supplying omitted defaults.
3
6
  const _normalizePayload = payloadObject => {
4
7
  const payload = { ...payloadObject };
5
8
  const now = Math.floor(Date.now() / 1000);
6
9
  const fieldsToNormalize = ['iat', 'exp', 'nbf', 'auth_time'];
7
10
  const msThreshold = 1000000000000;
8
11
 
9
- if (!payload.iat) {
12
+ if (!Number.isFinite(payload.iat)) {
10
13
  payload.iat = now;
11
14
  }
12
15
 
13
16
  fieldsToNormalize.forEach(field => {
14
- if (payload[field] && payload[field] > msThreshold) {
17
+ if (Number.isFinite(payload[field]) && payload[field] > msThreshold) {
15
18
  payload[field] = Math.floor(payload[field] / 1000);
16
19
  }
17
20
  });
18
21
 
19
- if (!payload.exp) {
22
+ if (!Number.isFinite(payload.exp)) {
20
23
  payload.exp = payload.iat + 3600;
21
24
  }
22
25
  return payload;
@@ -52,6 +55,19 @@ const _verify = (token, signature, alg, publicKey) => {
52
55
  return verify.verify(publicKey, signature, 'base64url');
53
56
  };
54
57
 
58
+ // Pattern: Allow List - rejects algorithms that the runtime cannot safely verify.
59
+ const isSupportedVerificationAlgorithm = algorithm => {
60
+ if (algorithm === 'EdDSA' || algorithm === 'Ed25519') {
61
+ return true;
62
+ }
63
+ return typeof algorithm === 'string' && supportedVerificationHashes.has(algorithm.toLowerCase());
64
+ };
65
+
66
+ // Pattern: Type Guard - decoded JWT parts must honor their documented object contract.
67
+ const isDecodedJwtObject = value => {
68
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
69
+ };
70
+
55
71
  const isNonEmptyString = value => {
56
72
  return typeof value === 'string' && value.trim().length > 0;
57
73
  };
@@ -155,6 +171,7 @@ function createServiceAuthorizationHeader(options = {}) {
155
171
  * @param publicKey
156
172
  * @return {boolean}
157
173
  */
174
+ // Pattern: Fail-Closed Verification - malformed token metadata returns false before crypto use.
158
175
  const verifyJwtSignature = (jwt, publicKey) => {
159
176
  try {
160
177
  if (!jwt || !publicKey) {
@@ -168,6 +185,9 @@ const verifyJwtSignature = (jwt, publicKey) => {
168
185
  const [header, payload, signature] = splitJWT;
169
186
  const token = header + '.' + payload;
170
187
  const headerObject = _decode(header);
188
+ if (!isDecodedJwtObject(headerObject) || !isSupportedVerificationAlgorithm(headerObject.alg)) {
189
+ return false;
190
+ }
171
191
 
172
192
  return _verify(token, signature, headerObject.alg, publicKey);
173
193
  } catch (error) {
@@ -183,6 +203,7 @@ const verifyJwtSignature = (jwt, publicKey) => {
183
203
  * @param jwt
184
204
  * @return {{payload: *, header: *}|null}
185
205
  */
206
+ // Pattern: Boundary Validation - returns only decoded header and payload objects.
186
207
  const getHeaderPayloadFromJwt = jwt => {
187
208
  try {
188
209
  if (typeof jwt !== 'string') {
@@ -195,6 +216,9 @@ const getHeaderPayloadFromJwt = jwt => {
195
216
 
196
217
  const headerObject = _decode(splitJWT[0]);
197
218
  const payloadObject = _decode(splitJWT[1]);
219
+ if (!isDecodedJwtObject(headerObject) || !isDecodedJwtObject(payloadObject)) {
220
+ return null;
221
+ }
198
222
 
199
223
  return { header: headerObject, payload: payloadObject };
200
224
  } catch (error) {
@@ -1,26 +1,45 @@
1
1
  const crypto = require('crypto');
2
2
 
3
- /**
4
- * Automatically adds random salt.
5
- * @param password
6
- * @param secret
7
- * @param algorithm
8
- * @return {string}
9
- */
3
+ const PASSWORD_HASH_VERSION = '1';
4
+
5
+ // Pattern: Pure Function - creates a digest in which both the pepper and random salt participate.
6
+ function createSaltedPasswordDigest(password, secret, algorithm, salt) {
7
+ return crypto
8
+ .createHmac(algorithm, secret)
9
+ .update(Buffer.from(salt, 'base64'))
10
+ .update(password)
11
+ .digest('base64');
12
+ }
13
+
14
+ // Pattern: Pure Function - recreates the historical digest for existing stored credentials.
15
+ function createLegacyPasswordDigest(password, secret, algorithm) {
16
+ return crypto.createHmac(algorithm, secret).update(password).digest('base64');
17
+ }
18
+
19
+ // Pattern: Serializer - preserves the published password-hash storage contract.
20
+ function serializePasswordHash(algorithmBase64, hashBase64, salt) {
21
+ return `$${PASSWORD_HASH_VERSION}$${algorithmBase64}$${hashBase64}$${salt}$`;
22
+ }
23
+
24
+ // Pattern: Constant-Time Comparison - avoids credential-dependent string comparison timing.
25
+ function passwordHashesMatch(candidateHash, savedPasswordHash) {
26
+ const candidateBuffer = Buffer.from(candidateHash);
27
+ const savedBuffer = Buffer.from(savedPasswordHash);
28
+ return (
29
+ candidateBuffer.length === savedBuffer.length &&
30
+ crypto.timingSafeEqual(candidateBuffer, savedBuffer)
31
+ );
32
+ }
33
+
34
+ // Pattern: Factory - creates a password hash whose random salt changes the credential digest.
10
35
  const createPasswordHashWithRandomSalt = (password, secret, algorithm) => {
11
36
  const salt = crypto.randomBytes(32).toString('base64');
12
37
  const algorithmBase64 = Buffer.from(algorithm).toString('base64');
13
- const hashBase64 = crypto.createHmac(algorithm, secret).update(password).digest('base64');
14
- return '$1$' + algorithmBase64 + '$' + hashBase64 + '$' + salt + '$';
38
+ const hashBase64 = createSaltedPasswordDigest(password, secret, algorithm, salt);
39
+ return serializePasswordHash(algorithmBase64, hashBase64, salt);
15
40
  };
16
41
 
17
- /**
18
- * Creates hash based on saved hash in database.
19
- * @param password
20
- * @param savedPasswordHash
21
- * @param secret
22
- * @return {string}
23
- */
42
+ // Pattern: Compatibility Verification - verifies salted hashes while retaining existing credentials.
24
43
  const createPasswordHashBasedOnSavedAlgorithmSalt = (password, savedPasswordHash, secret) => {
25
44
  if (typeof savedPasswordHash !== 'string') {
26
45
  return null;
@@ -31,12 +50,18 @@ const createPasswordHashBasedOnSavedAlgorithmSalt = (password, savedPasswordHash
31
50
  return null;
32
51
  }
33
52
 
53
+ const version = splitStringArray[1];
34
54
  const algBase64 = splitStringArray[2];
35
55
  const salt = splitStringArray[4];
56
+ if (version !== PASSWORD_HASH_VERSION || !algBase64 || !salt) {
57
+ return null;
58
+ }
36
59
  const algorithm = Buffer.from(algBase64, 'base64').toString('utf8');
37
-
38
- const hashBase64 = crypto.createHmac(algorithm, secret).update(password).digest('base64');
39
- return '$1$' + algBase64 + '$' + hashBase64 + '$' + salt + '$';
60
+ const saltedDigest = createSaltedPasswordDigest(password, secret, algorithm, salt);
61
+ const saltedHash = serializePasswordHash(algBase64, saltedDigest, salt);
62
+ const legacyDigest = createLegacyPasswordDigest(password, secret, algorithm);
63
+ const legacyHash = serializePasswordHash(algBase64, legacyDigest, salt);
64
+ return passwordHashesMatch(legacyHash, savedPasswordHash) ? legacyHash : saltedHash;
40
65
  };
41
66
 
42
67
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carecard/auth-util",
3
- "version": "3.19.0",
3
+ "version": "3.21.0",
4
4
  "repository": "https://github.com/CareCard-ca/pkg-auth-util.git",
5
5
  "description": "Auth utility functions",
6
6
  "main": "index.js",
@@ -47,8 +47,8 @@
47
47
  "typescript": "6.0.3"
48
48
  },
49
49
  "dependencies": {
50
- "@carecard/common-util": "3.18.0",
51
- "@carecard/validate": "3.18.0",
50
+ "@carecard/common-util": "3.21.0",
51
+ "@carecard/validate": "3.21.0",
52
52
  "@types/express": "5.0.6"
53
53
  },
54
54
  "overrides": {