@tstdl/base 0.93.178 → 0.93.180
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/api/response.js +4 -3
- package/api/server/gateway.js +9 -3
- package/audit/auditor.d.ts +1 -2
- package/audit/drizzle/{0000_lumpy_thunderball.sql → 0000_shallow_elektra.sql} +1 -1
- package/audit/drizzle/meta/0000_snapshot.json +2 -2
- package/audit/drizzle/meta/_journal.json +2 -2
- package/authentication/README.md +87 -42
- package/authentication/authentication.api.d.ts +392 -53
- package/authentication/authentication.api.js +133 -28
- package/authentication/client/api.client.d.ts +3 -3
- package/authentication/client/api.client.js +4 -4
- package/authentication/client/authentication.service.d.ts +93 -23
- package/authentication/client/authentication.service.js +113 -28
- package/authentication/client/http-client.middleware.d.ts +1 -1
- package/authentication/client/http-client.middleware.js +5 -4
- package/authentication/client/module.d.ts +1 -1
- package/authentication/client/module.js +2 -2
- package/authentication/errors/index.d.ts +1 -1
- package/authentication/errors/index.js +1 -1
- package/authentication/errors/password-requirements.error.d.ts +5 -0
- package/authentication/errors/{secret-requirements.error.js → password-requirements.error.js} +2 -2
- package/authentication/models/authentication-password.model.d.ts +8 -0
- package/authentication/models/{authentication-credentials.model.js → authentication-password.model.js} +11 -17
- package/authentication/models/authentication-session.model.d.ts +0 -2
- package/authentication/models/authentication-session.model.js +1 -7
- package/authentication/models/authentication-totp-recovery-code.model.d.ts +6 -0
- package/authentication/models/authentication-totp-recovery-code.model.js +34 -0
- package/authentication/models/authentication-totp.model.d.ts +19 -0
- package/authentication/models/authentication-totp.model.js +51 -0
- package/authentication/models/authentication-used-totp-token.model.d.ts +5 -0
- package/authentication/models/authentication-used-totp-token.model.js +32 -0
- package/authentication/models/index.d.ts +6 -3
- package/authentication/models/index.js +6 -3
- package/authentication/models/{init-secret-reset-data.model.d.ts → init-password-reset-data.model.d.ts} +3 -3
- package/authentication/models/{init-secret-reset-data.model.js → init-password-reset-data.model.js} +5 -5
- package/authentication/models/password-check-result.model.d.ts +3 -0
- package/authentication/models/{secret-check-result.model.js → password-check-result.model.js} +6 -6
- package/authentication/models/subject.model.d.ts +0 -6
- package/authentication/models/subject.model.js +0 -6
- package/authentication/models/token.model.d.ts +16 -2
- package/authentication/server/authentication-ancillary.service.d.ts +6 -6
- package/authentication/server/authentication-ancillary.service.js +1 -1
- package/authentication/server/authentication-password-requirements.validator.d.ts +55 -0
- package/authentication/server/{authentication-secret-requirements.validator.js → authentication-password-requirements.validator.js} +22 -22
- package/authentication/server/authentication.api-controller.d.ts +55 -27
- package/authentication/server/authentication.api-controller.js +214 -39
- package/authentication/server/authentication.audit.d.ts +42 -5
- package/authentication/server/authentication.service.d.ts +182 -93
- package/authentication/server/authentication.service.js +628 -206
- package/authentication/server/drizzle/{0000_soft_tag.sql → 0000_odd_echo.sql} +59 -13
- package/authentication/server/drizzle/meta/0000_snapshot.json +345 -32
- package/authentication/server/drizzle/meta/_journal.json +2 -2
- package/authentication/server/helper.d.ts +16 -16
- package/authentication/server/helper.js +33 -34
- package/authentication/server/index.d.ts +1 -1
- package/authentication/server/index.js +1 -1
- package/authentication/server/module.d.ts +2 -2
- package/authentication/server/module.js +4 -2
- package/authentication/server/schemas.d.ts +11 -7
- package/authentication/server/schemas.js +7 -3
- package/authentication/tests/authentication-password-requirements.validator.test.js +29 -0
- package/authentication/tests/authentication.api-controller.test.js +49 -15
- package/authentication/tests/authentication.client-error-handling.test.js +3 -2
- package/authentication/tests/authentication.client-middleware.test.js +5 -5
- package/authentication/tests/authentication.client-service-methods.test.js +28 -14
- package/authentication/tests/authentication.client-service-refresh.test.js +7 -6
- package/authentication/tests/authentication.client-service.test.js +10 -8
- package/authentication/tests/authentication.service.test.js +37 -29
- package/authentication/tests/authentication.test-ancillary-service.d.ts +1 -1
- package/authentication/tests/authentication.test-ancillary-service.js +1 -1
- package/authentication/tests/brute-force-protection.test.js +211 -0
- package/authentication/tests/helper.test.js +25 -21
- package/authentication/tests/password-requirements.error.test.js +14 -0
- package/authentication/tests/remember.api.test.js +22 -14
- package/authentication/tests/remember.service.test.js +23 -16
- package/authentication/tests/subject.service.test.js +2 -2
- package/authentication/tests/suspended-subject.test.d.ts +1 -0
- package/authentication/tests/suspended-subject.test.js +120 -0
- package/authentication/tests/totp.enrollment.test.d.ts +1 -0
- package/authentication/tests/totp.enrollment.test.js +123 -0
- package/authentication/tests/totp.login.test.d.ts +1 -0
- package/authentication/tests/totp.login.test.js +213 -0
- package/authentication/tests/totp.recovery-codes.test.d.ts +1 -0
- package/authentication/tests/totp.recovery-codes.test.js +97 -0
- package/authentication/tests/totp.status.test.d.ts +1 -0
- package/authentication/tests/totp.status.test.js +72 -0
- package/circuit-breaker/postgres/drizzle/{0000_cooing_korath.sql → 0000_same_captain_cross.sql} +1 -1
- package/circuit-breaker/postgres/drizzle/meta/0000_snapshot.json +2 -2
- package/circuit-breaker/postgres/drizzle/meta/_journal.json +2 -2
- package/cryptography/cryptography.d.ts +336 -0
- package/cryptography/cryptography.js +328 -0
- package/cryptography/index.d.ts +4 -0
- package/cryptography/index.js +4 -0
- package/{utils → cryptography}/jwt.d.ts +22 -4
- package/{utils → cryptography}/jwt.js +36 -18
- package/cryptography/module.d.ts +35 -0
- package/cryptography/module.js +148 -0
- package/cryptography/tests/cryptography.test.d.ts +1 -0
- package/cryptography/tests/cryptography.test.js +175 -0
- package/cryptography/tests/jwt.test.d.ts +1 -0
- package/cryptography/tests/jwt.test.js +54 -0
- package/cryptography/tests/modern.test.d.ts +1 -0
- package/cryptography/tests/modern.test.js +105 -0
- package/cryptography/tests/module.test.d.ts +1 -0
- package/cryptography/tests/module.test.js +100 -0
- package/cryptography/tests/totp.test.d.ts +1 -0
- package/cryptography/tests/totp.test.js +108 -0
- package/cryptography/totp.d.ts +96 -0
- package/cryptography/totp.js +123 -0
- package/document-management/server/drizzle/{0000_curious_nighthawk.sql → 0000_sharp_scream.sql} +21 -21
- package/document-management/server/drizzle/meta/0000_snapshot.json +22 -22
- package/document-management/server/drizzle/meta/_journal.json +2 -2
- package/document-management/server/services/document-file.service.js +1 -1
- package/errors/errors.localization.d.ts +2 -2
- package/errors/errors.localization.js +2 -2
- package/errors/index.d.ts +1 -0
- package/errors/index.js +1 -0
- package/errors/too-many-requests.error.d.ts +5 -0
- package/errors/too-many-requests.error.js +7 -0
- package/examples/api/authentication.js +5 -5
- package/examples/api/custom-authentication.js +4 -3
- package/file/server/mime-type.js +1 -1
- package/http/http-body.d.ts +1 -0
- package/http/http-body.js +3 -0
- package/image-service/imgproxy/imgproxy-image-service.d.ts +0 -1
- package/image-service/imgproxy/imgproxy-image-service.js +9 -27
- package/key-value-store/postgres/drizzle/{0000_shocking_slipstream.sql → 0000_moaning_calypso.sql} +1 -1
- package/key-value-store/postgres/drizzle/meta/0000_snapshot.json +2 -2
- package/key-value-store/postgres/drizzle/meta/_journal.json +2 -2
- package/lock/postgres/drizzle/{0000_busy_tattoo.sql → 0000_nappy_wraith.sql} +1 -1
- package/lock/postgres/drizzle/meta/0000_snapshot.json +2 -2
- package/lock/postgres/drizzle/meta/_journal.json +2 -2
- package/logger/formatters/json.js +1 -1
- package/logger/formatters/pretty-print.js +1 -1
- package/mail/drizzle/{0000_numerous_the_watchers.sql → 0000_cultured_quicksilver.sql} +2 -2
- package/mail/drizzle/meta/0000_snapshot.json +4 -4
- package/mail/drizzle/meta/_journal.json +2 -9
- package/notification/server/drizzle/{0000_wise_pyro.sql → 0000_new_tenebrous.sql} +6 -6
- package/notification/server/drizzle/meta/0000_snapshot.json +7 -7
- package/notification/server/drizzle/meta/_journal.json +2 -2
- package/notification/tests/notification-flow.test.js +1 -8
- package/notification/tests/notification-type.service.test.js +3 -3
- package/openid-connect/oidc.service.js +2 -3
- package/orm/data-types/common.js +1 -1
- package/orm/server/drizzle/schema-converter.js +9 -4
- package/orm/server/encryption.js +1 -1
- package/orm/server/module.d.ts +0 -1
- package/orm/server/module.js +0 -4
- package/orm/server/repository.d.ts +2 -1
- package/orm/server/repository.js +7 -10
- package/orm/tests/encryption.test.js +4 -6
- package/orm/tests/repository-extra-coverage.test.js +0 -2
- package/orm/tests/repository-regression.test.js +0 -3
- package/package.json +9 -8
- package/password/README.md +1 -1
- package/password/have-i-been-pwned.js +1 -1
- package/rate-limit/postgres/drizzle/{0000_watery_rage.sql → 0000_serious_sauron.sql} +1 -1
- package/rate-limit/postgres/drizzle/meta/0000_snapshot.json +2 -2
- package/rate-limit/postgres/drizzle/meta/_journal.json +2 -2
- package/rate-limit/postgres/postgres-rate-limiter.d.ts +1 -1
- package/rate-limit/postgres/postgres-rate-limiter.js +1 -1
- package/rate-limit/rate-limiter.d.ts +1 -1
- package/rpc/tests/rpc.integration.test.js +25 -31
- package/supports.d.ts +1 -0
- package/supports.js +1 -0
- package/task-queue/postgres/drizzle/{0000_faithful_daimon_hellstrom.sql → 0000_dark_ronan.sql} +5 -5
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +10 -10
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -9
- package/task-queue/postgres/task-queue.js +2 -2
- package/task-queue/tests/coverage-enhancement.test.js +2 -2
- package/test/drizzle/{0000_natural_cannonball.sql → 0000_organic_gamora.sql} +2 -2
- package/test/drizzle/meta/0000_snapshot.json +3 -4
- package/test/drizzle/meta/_journal.json +2 -9
- package/testing/integration-setup.d.ts +7 -3
- package/testing/integration-setup.js +119 -96
- package/utils/alphabet.d.ts +1 -0
- package/utils/alphabet.js +1 -0
- package/utils/base32.d.ts +4 -0
- package/utils/base32.js +49 -0
- package/utils/base64.d.ts +0 -2
- package/utils/base64.js +6 -70
- package/utils/equals.d.ts +13 -3
- package/utils/equals.js +29 -9
- package/utils/index.d.ts +1 -2
- package/utils/index.js +1 -2
- package/utils/random.d.ts +1 -0
- package/utils/random.js +14 -8
- package/authentication/errors/secret-requirements.error.d.ts +0 -5
- package/authentication/models/authentication-credentials.model.d.ts +0 -10
- package/authentication/models/secret-check-result.model.d.ts +0 -3
- package/authentication/server/authentication-secret-requirements.validator.d.ts +0 -55
- package/authentication/tests/authentication-ancillary.service.test.js +0 -13
- package/authentication/tests/authentication-secret-requirements.validator.test.js +0 -29
- package/authentication/tests/secret-requirements.error.test.js +0 -14
- package/mail/drizzle/0001_married_tarantula.sql +0 -12
- package/mail/drizzle/meta/0001_snapshot.json +0 -69
- package/orm/server/tokens.d.ts +0 -1
- package/orm/server/tokens.js +0 -2
- package/task-queue/postgres/drizzle/0001_rapid_infant_terrible.sql +0 -16
- package/task-queue/postgres/drizzle/meta/0001_snapshot.json +0 -753
- package/test/drizzle/0001_closed_the_captain.sql +0 -2
- package/test/drizzle/meta/0001_snapshot.json +0 -117
- package/utils/cryptography.d.ts +0 -137
- package/utils/cryptography.js +0 -201
- /package/authentication/tests/{authentication-ancillary.service.test.d.ts → authentication-password-requirements.validator.test.d.ts} +0 -0
- /package/authentication/tests/{authentication-secret-requirements.validator.test.d.ts → brute-force-protection.test.d.ts} +0 -0
- /package/authentication/tests/{secret-requirements.error.test.d.ts → password-requirements.error.test.d.ts} +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { OneOrMany, Record } from '../types/index.js';
|
|
2
|
-
|
|
3
|
-
export type JwtTokenAlgorithm = 'HS256' | 'HS384' | 'HS512';
|
|
2
|
+
export type JwtTokenAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'KMAC128' | 'KMAC256';
|
|
4
3
|
export type JwtTokenHeader<T extends Record<string> = Record<string>> = {
|
|
5
4
|
alg: JwtTokenAlgorithm;
|
|
6
5
|
typ: 'JWT';
|
|
@@ -27,6 +26,25 @@ export type JwtTokenParseResult<T extends JwtToken = JwtToken> = {
|
|
|
27
26
|
payload: string;
|
|
28
27
|
};
|
|
29
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Parses a JWT token string into its components. This function does not perform any validation of the token's signature.
|
|
31
|
+
* @param tokenString JWT token string.
|
|
32
|
+
* @returns Parsed JWT token result.
|
|
33
|
+
*/
|
|
30
34
|
export declare function parseJwtTokenString<T extends JwtToken = JwtToken>(tokenString: string): JwtTokenParseResult<T>;
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Creates a JWT token string.
|
|
37
|
+
* @param jwtToken The JWT token object.
|
|
38
|
+
* @param key The CryptoKey to sign the token with.
|
|
39
|
+
* @returns Signed JWT token string.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createJwtTokenString(jwtToken: JwtToken, key: CryptoKey): Promise<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Parses and validates a JWT token string.
|
|
44
|
+
* @param tokenString The JWT token string.
|
|
45
|
+
* @param allowedAlgorithms One or more allowed algorithms.
|
|
46
|
+
* @param key The CryptoKey to verify the signature with.
|
|
47
|
+
* @returns The parsed token object if valid.
|
|
48
|
+
* @throws {InvalidTokenError} If the token is invalid or the signature fails verification.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseAndValidateJwtTokenString<T extends JwtToken = JwtToken>(tokenString: string, allowedAlgorithms: OneOrMany<JwtTokenAlgorithm>, key: CryptoKey): Promise<T>;
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { InvalidTokenError } from '../errors/invalid-token.error.js';
|
|
2
|
-
import { toArray } from '
|
|
3
|
-
import { decodeBase64Url, encodeBase64Url } from '
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
2
|
+
import { toArray } from '../utils/array/array.js';
|
|
3
|
+
import { decodeBase64Url, encodeBase64Url } from '../utils/base64.js';
|
|
4
|
+
import { encodeUtf8 } from '../utils/encoding.js';
|
|
5
|
+
import { sign, verify } from './cryptography.js';
|
|
6
|
+
const jwtAlgorithmMap = {
|
|
7
|
+
HS256: { name: 'HMAC', hash: 'SHA-256' },
|
|
8
|
+
HS384: { name: 'HMAC', hash: 'SHA-384' },
|
|
9
|
+
HS512: { name: 'HMAC', hash: 'SHA-512' },
|
|
10
|
+
KMAC128: { name: 'KMAC128', outputLength: 32 },
|
|
11
|
+
KMAC256: { name: 'KMAC256', outputLength: 64 },
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Parses a JWT token string into its components. This function does not perform any validation of the token's signature.
|
|
15
|
+
* @param tokenString JWT token string.
|
|
16
|
+
* @returns Parsed JWT token result.
|
|
17
|
+
*/
|
|
7
18
|
export function parseJwtTokenString(tokenString) {
|
|
8
19
|
const splits = tokenString.split('.');
|
|
9
20
|
if (splits.length != 3) {
|
|
@@ -45,6 +56,12 @@ export function parseJwtTokenString(tokenString) {
|
|
|
45
56
|
throw new InvalidTokenError('Invalid token format');
|
|
46
57
|
}
|
|
47
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Creates a JWT token string.
|
|
61
|
+
* @param jwtToken The JWT token object.
|
|
62
|
+
* @param key The CryptoKey to sign the token with.
|
|
63
|
+
* @returns Signed JWT token string.
|
|
64
|
+
*/
|
|
48
65
|
export async function createJwtTokenString(jwtToken, key) {
|
|
49
66
|
const headerBuffer = encodeUtf8(JSON.stringify(jwtToken.header));
|
|
50
67
|
const payloadBuffer = encodeUtf8(JSON.stringify(jwtToken.payload));
|
|
@@ -52,20 +69,30 @@ export async function createJwtTokenString(jwtToken, key) {
|
|
|
52
69
|
const encodedPayload = encodeBase64Url(payloadBuffer, 0, payloadBuffer.byteLength);
|
|
53
70
|
const headerPayloadDataString = `${encodedHeader}.${encodedPayload}`;
|
|
54
71
|
const headerPayloadData = encodeUtf8(headerPayloadDataString);
|
|
55
|
-
const
|
|
72
|
+
const algorithm = jwtAlgorithmMap[jwtToken.header.alg];
|
|
73
|
+
const signature = await sign(algorithm, key, headerPayloadData).toBuffer();
|
|
56
74
|
const encodedSignature = encodeBase64Url(signature);
|
|
57
75
|
const tokenString = `${headerPayloadDataString}.${encodedSignature}`;
|
|
58
76
|
return tokenString;
|
|
59
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Parses and validates a JWT token string.
|
|
80
|
+
* @param tokenString The JWT token string.
|
|
81
|
+
* @param allowedAlgorithms One or more allowed algorithms.
|
|
82
|
+
* @param key The CryptoKey to verify the signature with.
|
|
83
|
+
* @returns The parsed token object if valid.
|
|
84
|
+
* @throws {InvalidTokenError} If the token is invalid or the signature fails verification.
|
|
85
|
+
*/
|
|
60
86
|
export async function parseAndValidateJwtTokenString(tokenString, allowedAlgorithms, key) {
|
|
61
87
|
try {
|
|
62
88
|
const { encoded, bytes, token } = parseJwtTokenString(tokenString);
|
|
63
89
|
if (!toArray(allowedAlgorithms).includes(token.header.alg)) {
|
|
64
90
|
throw new InvalidTokenError('Invalid signature algorithm');
|
|
65
91
|
}
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
92
|
+
const encodedHeaderPayload = encodeUtf8(`${encoded.header}.${encoded.payload}`);
|
|
93
|
+
const algorithm = jwtAlgorithmMap[token.header.alg];
|
|
94
|
+
const isValid = await verify(algorithm, key, bytes.signature, encodedHeaderPayload);
|
|
95
|
+
if (!isValid) {
|
|
69
96
|
throw new InvalidTokenError('Invalid token signature');
|
|
70
97
|
}
|
|
71
98
|
return token;
|
|
@@ -77,12 +104,3 @@ export async function parseAndValidateJwtTokenString(tokenString, allowedAlgorit
|
|
|
77
104
|
throw new InvalidTokenError('Invalid token');
|
|
78
105
|
}
|
|
79
106
|
}
|
|
80
|
-
async function getSignature(data, algorithm, secret) {
|
|
81
|
-
const hashAlgorithm = getHmacHashAlgorithm(algorithm);
|
|
82
|
-
const hmacKey = await importHmacKey(hashAlgorithm, secret, false);
|
|
83
|
-
const hmacSignature = sign('HMAC', hmacKey, data);
|
|
84
|
-
return await hmacSignature.toBuffer();
|
|
85
|
-
}
|
|
86
|
-
function getHmacHashAlgorithm(algorithm) {
|
|
87
|
-
return algorithm.replace('HS', 'SHA-');
|
|
88
|
-
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
|
|
2
|
+
import { Injector } from '../injector/index.js';
|
|
3
|
+
import { type ImportAlgorithm, type KeyData, type KmacImportParams, type KmacParams } from './cryptography.js';
|
|
4
|
+
export type SecretOptions = {
|
|
5
|
+
salt?: string | BufferSource;
|
|
6
|
+
};
|
|
7
|
+
export declare class SecretsModuleConfig {
|
|
8
|
+
key: CryptoKey | BufferSource | string;
|
|
9
|
+
}
|
|
10
|
+
export declare const masterSecretAlgorithm: KmacImportParams;
|
|
11
|
+
export declare const masterSecretKeyUsages: KeyUsage[];
|
|
12
|
+
/**
|
|
13
|
+
* Configure secrets module
|
|
14
|
+
*/
|
|
15
|
+
export declare function configureSecrets({ injector, ...config }: SecretsModuleConfig & {
|
|
16
|
+
injector?: Injector;
|
|
17
|
+
}): void;
|
|
18
|
+
export declare class DerivedKey {
|
|
19
|
+
#private;
|
|
20
|
+
constructor(baseKey: CryptoKey | Promise<CryptoKey>, params: KmacParams, salt: BufferSource, length: number);
|
|
21
|
+
constructor(baseKey: CryptoKey | Promise<CryptoKey>, params: KmacParams, salt: BufferSource, keyOptions: ImportAlgorithm, keyUsages: KeyUsage[]);
|
|
22
|
+
getBytes(): Promise<ArrayBuffer>;
|
|
23
|
+
getKey(): Promise<CryptoKey>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Derive raw bytes using KMAC256
|
|
27
|
+
* @param customization The context/label (used as KMAC customization string)
|
|
28
|
+
*/
|
|
29
|
+
export declare function injectDerivedBytes(customization: string | BufferSource, length: number, options?: SecretOptions): DerivedKey;
|
|
30
|
+
/**
|
|
31
|
+
* Derive a CryptoKey (e.g. AES-GCM) using KMAC256
|
|
32
|
+
* @param customization The context/label (used as KMAC customization string)
|
|
33
|
+
*/
|
|
34
|
+
export declare function injectDerivedCryptoKey(customization: string | BufferSource, derivedKeyAlgorithm: ImportAlgorithm, keyUsages: KeyUsage[], options?: SecretOptions): DerivedKey;
|
|
35
|
+
export declare function importMasterKey(keyMaterial: KeyData | string): Promise<CryptoKey>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
|
|
2
|
+
import { getCurrentInjector, Injector } from '../injector/index.js';
|
|
3
|
+
import { encodeUtf8 } from '../utils/encoding.js';
|
|
4
|
+
import { hasOwnProperty } from '../utils/object/object.js';
|
|
5
|
+
import { assertDefined, assertDefinedPass, isDefined, isNotNull, isNumber, isString, isUndefined } from '../utils/type-guards.js';
|
|
6
|
+
import { importKey, importKmacKey, isBufferSource, sign } from './cryptography.js';
|
|
7
|
+
export class SecretsModuleConfig {
|
|
8
|
+
key;
|
|
9
|
+
}
|
|
10
|
+
export const masterSecretAlgorithm = { name: 'KMAC256' };
|
|
11
|
+
export const masterSecretKeyUsages = ['sign', 'verify'];
|
|
12
|
+
const masterKeyMap = new WeakMap();
|
|
13
|
+
const hashSizeMap = {
|
|
14
|
+
'SHA-1': 20,
|
|
15
|
+
'SHA-256': 32,
|
|
16
|
+
'SHA3-256': 32,
|
|
17
|
+
'SHA-384': 48,
|
|
18
|
+
'SHA3-384': 48,
|
|
19
|
+
'SHA-512': 64,
|
|
20
|
+
'SHA3-512': 64,
|
|
21
|
+
'cSHAKE128': 16,
|
|
22
|
+
'cSHAKE256': 32,
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Configure secrets module
|
|
26
|
+
*/
|
|
27
|
+
export function configureSecrets({ injector, ...config }) {
|
|
28
|
+
const targetInjector = injector ?? Injector;
|
|
29
|
+
let baseKeyPromise;
|
|
30
|
+
if (config.key instanceof CryptoKey) {
|
|
31
|
+
baseKeyPromise = config.key;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const keyMaterial = isString(config.key) ? encodeUtf8(config.key) : config.key;
|
|
35
|
+
// Import the key immediately. The raw material is only held in the local scope of this function.
|
|
36
|
+
baseKeyPromise = importKmacKey('raw-secret', 'KMAC256', keyMaterial, false);
|
|
37
|
+
}
|
|
38
|
+
masterKeyMap.set(targetInjector, baseKeyPromise);
|
|
39
|
+
}
|
|
40
|
+
export class DerivedKey {
|
|
41
|
+
#baseKey;
|
|
42
|
+
#kmacParams;
|
|
43
|
+
#salt;
|
|
44
|
+
#isCryptoKey;
|
|
45
|
+
#keyOptions;
|
|
46
|
+
#keyUsages;
|
|
47
|
+
#derivedBytes;
|
|
48
|
+
#derivedKey;
|
|
49
|
+
constructor(baseKey, params, salt, lengthOrKeyOptions, keyUsages) {
|
|
50
|
+
this.#baseKey = baseKey;
|
|
51
|
+
this.#kmacParams = params;
|
|
52
|
+
this.#salt = salt;
|
|
53
|
+
if (isNumber(lengthOrKeyOptions)) {
|
|
54
|
+
this.#isCryptoKey = false;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this.#isCryptoKey = true;
|
|
58
|
+
this.#keyOptions = lengthOrKeyOptions;
|
|
59
|
+
this.#keyUsages = keyUsages;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async getBytes() {
|
|
63
|
+
assertDefined(this.#isCryptoKey ? undefined : true, 'This key was not configured to derive bytes. Use injectDerivedBytes().');
|
|
64
|
+
if (isUndefined(this.#derivedBytes)) {
|
|
65
|
+
this.#derivedBytes = this.#computeRawBytes();
|
|
66
|
+
this.#derivedBytes = await this.#derivedBytes;
|
|
67
|
+
}
|
|
68
|
+
return this.#derivedBytes; // eslint-disable-line @typescript-eslint/return-await
|
|
69
|
+
}
|
|
70
|
+
async getKey() {
|
|
71
|
+
assertDefined(this.#keyOptions, 'This key was not configured to derive a CryptoKey. Use injectDerivedCryptoKey().');
|
|
72
|
+
assertDefined(this.#keyUsages, 'This key was not configured to derive a CryptoKey. Use injectDerivedCryptoKey().');
|
|
73
|
+
if (isUndefined(this.#derivedKey)) {
|
|
74
|
+
this.#derivedKey = (async () => {
|
|
75
|
+
const rawBytes = await this.#computeRawBytes();
|
|
76
|
+
try {
|
|
77
|
+
return await importKey('raw-secret', rawBytes, this.#keyOptions, false, this.#keyUsages);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
crypto.getRandomValues(new Uint8Array(rawBytes));
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
this.#derivedKey = await this.#derivedKey;
|
|
84
|
+
}
|
|
85
|
+
return this.#derivedKey; // eslint-disable-line @typescript-eslint/return-await
|
|
86
|
+
}
|
|
87
|
+
async #computeRawBytes() {
|
|
88
|
+
const baseKey = await this.#baseKey;
|
|
89
|
+
return await sign(this.#kmacParams, baseKey, this.#salt).toBuffer();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function getBaseKeyFromInjector() {
|
|
93
|
+
let current = getCurrentInjector(true);
|
|
94
|
+
while (isNotNull(current)) {
|
|
95
|
+
const secret = masterKeyMap.get(current);
|
|
96
|
+
if (isDefined(secret)) {
|
|
97
|
+
return secret;
|
|
98
|
+
}
|
|
99
|
+
current = current.parent;
|
|
100
|
+
}
|
|
101
|
+
const globalSecret = masterKeyMap.get(Injector);
|
|
102
|
+
if (isDefined(globalSecret)) {
|
|
103
|
+
return globalSecret;
|
|
104
|
+
}
|
|
105
|
+
throw new Error('No secret key configured. Call configureSecrets() first.');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Derive raw bytes using KMAC256
|
|
109
|
+
* @param customization The context/label (used as KMAC customization string)
|
|
110
|
+
*/
|
|
111
|
+
export function injectDerivedBytes(customization, length, options) {
|
|
112
|
+
const { params, salt } = buildKmacDerivation(customization, length, options);
|
|
113
|
+
return new DerivedKey(getBaseKeyFromInjector(), params, salt, length);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Derive a CryptoKey (e.g. AES-GCM) using KMAC256
|
|
117
|
+
* @param customization The context/label (used as KMAC customization string)
|
|
118
|
+
*/
|
|
119
|
+
export function injectDerivedCryptoKey(customization, derivedKeyAlgorithm, keyUsages, options) {
|
|
120
|
+
const algorithmName = isString(derivedKeyAlgorithm) ? derivedKeyAlgorithm : derivedKeyAlgorithm.name;
|
|
121
|
+
const length = hasOwnProperty(derivedKeyAlgorithm, 'length') && isNumber(derivedKeyAlgorithm.length)
|
|
122
|
+
? derivedKeyAlgorithm.length / 8
|
|
123
|
+
: hasOwnProperty(derivedKeyAlgorithm, 'hash') && isString(derivedKeyAlgorithm.hash)
|
|
124
|
+
? assertDefinedPass(hashSizeMap[derivedKeyAlgorithm.hash], 'Unsupported hash function specified in derived key algorithm.')
|
|
125
|
+
: (algorithmName == 'KMAC128')
|
|
126
|
+
? 16
|
|
127
|
+
: (algorithmName == 'KMAC256')
|
|
128
|
+
? 32
|
|
129
|
+
: undefined;
|
|
130
|
+
assertDefined(length, 'Derived key algorithm must specify a key length either directly or via the hash function.');
|
|
131
|
+
const { params, salt } = buildKmacDerivation(customization, length, options);
|
|
132
|
+
return new DerivedKey(getBaseKeyFromInjector(), params, salt, derivedKeyAlgorithm, keyUsages);
|
|
133
|
+
}
|
|
134
|
+
export async function importMasterKey(keyMaterial) {
|
|
135
|
+
const keyFormat = isBufferSource(keyMaterial) ? 'raw-secret' : 'jwk';
|
|
136
|
+
const keyData = isString(keyMaterial) ? encodeUtf8(keyMaterial) : keyMaterial;
|
|
137
|
+
return await importKey(keyFormat, keyData, masterSecretAlgorithm, false, masterSecretKeyUsages);
|
|
138
|
+
}
|
|
139
|
+
function buildKmacDerivation(customization, length, options) {
|
|
140
|
+
const customizationBytes = isString(customization) ? encodeUtf8(customization) : customization;
|
|
141
|
+
const salt = (isString(options?.salt) ? encodeUtf8(options.salt) : options?.salt) ?? new Uint8Array();
|
|
142
|
+
const params = {
|
|
143
|
+
name: 'KMAC256',
|
|
144
|
+
outputLength: length,
|
|
145
|
+
customization: customizationBytes,
|
|
146
|
+
};
|
|
147
|
+
return { params, salt };
|
|
148
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { encodeUtf8 } from '../../utils/encoding.js';
|
|
3
|
+
import { decrypt, deriveBytes, digest, encrypt, generateEcdsaKey, generateHmacKey, generatePbkdf2Key, generateSymmetricKey, importEcdsaKey, importHkdfKey, importHmacKey, importPbkdf2Key, importSymmetricKey, sign, verify } from '../cryptography.js';
|
|
4
|
+
describe('Cryptography Utilities', () => {
|
|
5
|
+
test('encrypt and decrypt with AES-GCM (string)', async () => {
|
|
6
|
+
const key = await generateSymmetricKey({ name: 'AES-GCM', length: 256 });
|
|
7
|
+
const data = 'Hello World';
|
|
8
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
9
|
+
const algorithm = { name: 'AES-GCM', iv };
|
|
10
|
+
const encryptionResult = encrypt(algorithm, key, data);
|
|
11
|
+
const encryptedBuffer = await encryptionResult.toBuffer();
|
|
12
|
+
const decryptionResult = decrypt(algorithm, key, encryptedBuffer);
|
|
13
|
+
const decryptedText = await decryptionResult.toUtf8();
|
|
14
|
+
expect(decryptedText).toBe(data);
|
|
15
|
+
expect(await encryptionResult.toHex()).toBeTypeOf('string');
|
|
16
|
+
expect(await encryptionResult.toBase64()).toBeTypeOf('string');
|
|
17
|
+
expect(await encryptionResult.toBase64Url()).toBeTypeOf('string');
|
|
18
|
+
expect(await encryptionResult.toZBase32()).toBeTypeOf('string');
|
|
19
|
+
});
|
|
20
|
+
test('encrypt and decrypt with AES-CBC (Uint8Array)', async () => {
|
|
21
|
+
const key = await generateSymmetricKey({ name: 'AES-CBC', length: 256 });
|
|
22
|
+
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
|
23
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
24
|
+
const algorithm = { name: 'AES-CBC', iv };
|
|
25
|
+
const encryptionResult = encrypt(algorithm, key, data);
|
|
26
|
+
const encryptedBuffer = await encryptionResult.toBuffer();
|
|
27
|
+
const decryptionResult = decrypt(algorithm, key, encryptedBuffer);
|
|
28
|
+
const decryptedBuffer = await decryptionResult.toBuffer();
|
|
29
|
+
expect(new Uint8Array(decryptedBuffer)).toEqual(data);
|
|
30
|
+
});
|
|
31
|
+
test('digest with SHA-256', async () => {
|
|
32
|
+
const data = 'Hello World';
|
|
33
|
+
const result = digest('SHA-256', data);
|
|
34
|
+
const hex = await result.toHex();
|
|
35
|
+
// SHA-256 of "Hello World"
|
|
36
|
+
expect(hex).toBe('a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e');
|
|
37
|
+
const bufferResult = digest('SHA-256', encodeUtf8(data));
|
|
38
|
+
expect(await bufferResult.toHex()).toBe(hex);
|
|
39
|
+
});
|
|
40
|
+
test('sign and verify with HMAC', async () => {
|
|
41
|
+
const key = await generateHmacKey('SHA-256');
|
|
42
|
+
const data = 'Hello World';
|
|
43
|
+
const algorithm = { name: 'HMAC', hash: 'SHA-256' };
|
|
44
|
+
const signatureResult = sign(algorithm, key, data);
|
|
45
|
+
const signature = await signatureResult.toBuffer();
|
|
46
|
+
// Test with mixed string/binary
|
|
47
|
+
expect(await verify(algorithm, key, signature, data)).toBe(true);
|
|
48
|
+
expect(await verify(algorithm, key, new Uint8Array(signature), encodeUtf8(data))).toBe(true);
|
|
49
|
+
const isInvalid = await verify(algorithm, key, signature, 'Wrong Data');
|
|
50
|
+
expect(isInvalid).toBe(false);
|
|
51
|
+
const isInvalidSignature = await verify(algorithm, key, new Uint8Array(signature.byteLength), data);
|
|
52
|
+
expect(isInvalidSignature).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
test('import and export HMAC key (raw)', async () => {
|
|
55
|
+
const rawKey = encodeUtf8('my-secret-key');
|
|
56
|
+
const key = await importHmacKey('raw-secret', 'SHA-256', rawKey, true);
|
|
57
|
+
expect(key.algorithm.name).toBe('HMAC');
|
|
58
|
+
expect(key.algorithm.hash.name).toBe('SHA-256');
|
|
59
|
+
const exportedKey = await globalThis.crypto.subtle.exportKey('raw', key);
|
|
60
|
+
expect(new Uint8Array(exportedKey)).toEqual(rawKey);
|
|
61
|
+
// Test import with string
|
|
62
|
+
const keyFromString = await importHmacKey('raw-secret', 'SHA-256', encodeUtf8('my-secret-key'));
|
|
63
|
+
expect(keyFromString.algorithm.name).toBe('HMAC');
|
|
64
|
+
});
|
|
65
|
+
test('import HMAC key (JWK)', async () => {
|
|
66
|
+
const jwk = {
|
|
67
|
+
kty: 'oct',
|
|
68
|
+
k: 'bXktc2VjcmV0LWtleQ', // "my-secret-key" in base64url
|
|
69
|
+
alg: 'HS256',
|
|
70
|
+
ext: true,
|
|
71
|
+
};
|
|
72
|
+
const key = await importHmacKey('jwk', 'SHA-256', jwk, true);
|
|
73
|
+
expect(key.algorithm.name).toBe('HMAC');
|
|
74
|
+
const exportedKey = await globalThis.crypto.subtle.exportKey('raw', key);
|
|
75
|
+
expect(new Uint8Array(exportedKey)).toEqual(encodeUtf8('my-secret-key'));
|
|
76
|
+
});
|
|
77
|
+
test('import symmetric key (raw)', async () => {
|
|
78
|
+
const rawKey = globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
79
|
+
const key = await importSymmetricKey('raw-secret', { name: 'AES-GCM', length: 256 }, rawKey, true);
|
|
80
|
+
expect(key.algorithm.name).toBe('AES-GCM');
|
|
81
|
+
expect(key.algorithm.length).toBe(256);
|
|
82
|
+
const exportedKey = await globalThis.crypto.subtle.exportKey('raw', key);
|
|
83
|
+
expect(new Uint8Array(exportedKey)).toEqual(rawKey);
|
|
84
|
+
// Test import with encoded string (exactly 32 chars)
|
|
85
|
+
const keyFromString = await importSymmetricKey('raw-secret', { name: 'AES-GCM', length: 256 }, encodeUtf8('01234567890123456789012345678901'));
|
|
86
|
+
expect(keyFromString.algorithm.name).toBe('AES-GCM');
|
|
87
|
+
});
|
|
88
|
+
test('import symmetric key (JWK)', async () => {
|
|
89
|
+
const jwk = {
|
|
90
|
+
kty: 'oct',
|
|
91
|
+
k: 'AAECAwQFBgcICQoLDA0ODw', // [0, 1, ..., 15]
|
|
92
|
+
alg: 'A128GCM',
|
|
93
|
+
ext: true,
|
|
94
|
+
};
|
|
95
|
+
const key = await importSymmetricKey('jwk', { name: 'AES-GCM', length: 128 }, jwk, true);
|
|
96
|
+
expect(key.algorithm.name).toBe('AES-GCM');
|
|
97
|
+
const exportedJwk = await globalThis.crypto.subtle.exportKey('jwk', key);
|
|
98
|
+
expect(exportedJwk.k).toBe(jwk.k);
|
|
99
|
+
});
|
|
100
|
+
test('sign and verify with ECDSA', async () => {
|
|
101
|
+
const keyPair = await generateEcdsaKey('P-256');
|
|
102
|
+
const data = 'Hello World';
|
|
103
|
+
const algorithm = { name: 'ECDSA', hash: 'SHA-256' };
|
|
104
|
+
const signatureResult = sign(algorithm, keyPair.privateKey, data);
|
|
105
|
+
const signature = await signatureResult.toBuffer();
|
|
106
|
+
const isValid = await verify(algorithm, keyPair.publicKey, signature, data);
|
|
107
|
+
expect(isValid).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
test('import ECDSA public key (SPKI)', async () => {
|
|
110
|
+
const keyPair = await generateEcdsaKey('P-256', true);
|
|
111
|
+
const spki = await globalThis.crypto.subtle.exportKey('spki', keyPair.publicKey);
|
|
112
|
+
const importedKey = await importEcdsaKey('spki', 'P-256', spki);
|
|
113
|
+
expect(importedKey.type).toBe('public');
|
|
114
|
+
expect(importedKey.algorithm.name).toBe('ECDSA');
|
|
115
|
+
});
|
|
116
|
+
test('import ECDSA public key (JWK)', async () => {
|
|
117
|
+
const keyPair = await generateEcdsaKey('P-256', true);
|
|
118
|
+
const jwk = await globalThis.crypto.subtle.exportKey('jwk', keyPair.publicKey);
|
|
119
|
+
const importedKey = await importEcdsaKey('jwk', 'P-256', jwk);
|
|
120
|
+
expect(importedKey.type).toBe('public');
|
|
121
|
+
expect(importedKey.algorithm.name).toBe('ECDSA');
|
|
122
|
+
});
|
|
123
|
+
test('import HKDF key', async () => {
|
|
124
|
+
const baseKeyMaterial = encodeUtf8('base-secret');
|
|
125
|
+
const baseKey = await importHkdfKey('raw', baseKeyMaterial);
|
|
126
|
+
expect(baseKey.algorithm.name).toBe('HKDF');
|
|
127
|
+
// Test import with string
|
|
128
|
+
const keyFromString = await importHkdfKey('raw', encodeUtf8('base-secret'));
|
|
129
|
+
expect(keyFromString.algorithm.name).toBe('HKDF');
|
|
130
|
+
});
|
|
131
|
+
test('PBKDF2 derivation', async () => {
|
|
132
|
+
const baseKey = await generatePbkdf2Key();
|
|
133
|
+
const salt = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
134
|
+
const algorithm = {
|
|
135
|
+
name: 'PBKDF2',
|
|
136
|
+
salt,
|
|
137
|
+
iterations: 1000,
|
|
138
|
+
hash: 'SHA-256',
|
|
139
|
+
};
|
|
140
|
+
const derivedBytes = await deriveBytes(algorithm, baseKey, 32);
|
|
141
|
+
expect(derivedBytes.byteLength).toBe(32);
|
|
142
|
+
const importedKey = await importPbkdf2Key('raw-secret', encodeUtf8('password'));
|
|
143
|
+
const derivedBytes2 = await deriveBytes(algorithm, importedKey, 32);
|
|
144
|
+
expect(derivedBytes2.byteLength).toBe(32);
|
|
145
|
+
// Test import with string
|
|
146
|
+
const keyFromString = await importPbkdf2Key('raw-secret', encodeUtf8('password'));
|
|
147
|
+
expect(keyFromString.algorithm.name).toBe('PBKDF2');
|
|
148
|
+
});
|
|
149
|
+
test('CryptionResult helper methods', async () => {
|
|
150
|
+
const data = 'Hello World';
|
|
151
|
+
const result = digest('SHA-256', data);
|
|
152
|
+
expect(await result.toBuffer()).toBeInstanceOf(ArrayBuffer);
|
|
153
|
+
expect(await result.toHex()).toBeTypeOf('string');
|
|
154
|
+
expect(await result.toBase64()).toBeTypeOf('string');
|
|
155
|
+
expect(await result.toBase64Url()).toBeTypeOf('string');
|
|
156
|
+
expect(await result.toZBase32()).toBeTypeOf('string');
|
|
157
|
+
const key = await generateHmacKey('SHA-256');
|
|
158
|
+
const signResult = sign({ name: 'HMAC', hash: 'SHA-256' }, key, data);
|
|
159
|
+
expect(await signResult.toHex()).toBeTypeOf('string');
|
|
160
|
+
expect(await signResult.toBase64()).toBeTypeOf('string');
|
|
161
|
+
expect(await signResult.toBase64Url()).toBeTypeOf('string');
|
|
162
|
+
expect(await signResult.toZBase32()).toBeTypeOf('string');
|
|
163
|
+
const symKey = await generateSymmetricKey({ name: 'AES-GCM', length: 256 });
|
|
164
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
165
|
+
const encResult = encrypt({ name: 'AES-GCM', iv }, symKey, data);
|
|
166
|
+
const encrypted = await encResult.toBuffer();
|
|
167
|
+
expect(await encResult.toHex()).toBeTypeOf('string');
|
|
168
|
+
const decResult = decrypt({ name: 'AES-GCM', iv }, symKey, encrypted);
|
|
169
|
+
expect(await decResult.toHex()).toBeTypeOf('string');
|
|
170
|
+
expect(await decResult.toBase64()).toBeTypeOf('string');
|
|
171
|
+
expect(await decResult.toBase64Url()).toBeTypeOf('string');
|
|
172
|
+
expect(await decResult.toZBase32()).toBeTypeOf('string');
|
|
173
|
+
expect(await decResult.toUtf8()).toBe(data);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { InvalidTokenError } from '../../errors/invalid-token.error.js';
|
|
3
|
+
import { generateKmacKey, generateSymmetricKey } from '../cryptography.js';
|
|
4
|
+
import { createJwtTokenString, parseAndValidateJwtTokenString, parseJwtTokenString } from '../jwt.js';
|
|
5
|
+
describe('JWT Utilities', () => {
|
|
6
|
+
test('create, parse and validate JWT', async () => {
|
|
7
|
+
const key = await generateKmacKey('KMAC256');
|
|
8
|
+
const token = {
|
|
9
|
+
header: { alg: 'KMAC256', typ: 'JWT' },
|
|
10
|
+
payload: { sub: '1234567890', name: 'John Doe', iat: 1516239022 },
|
|
11
|
+
};
|
|
12
|
+
const tokenString = await createJwtTokenString(token, key);
|
|
13
|
+
expect(tokenString).toBeTypeOf('string');
|
|
14
|
+
expect(tokenString.split('.').length).toBe(3);
|
|
15
|
+
const parsed = parseJwtTokenString(tokenString);
|
|
16
|
+
expect(parsed.token.payload).toEqual(token.payload);
|
|
17
|
+
const validated = await parseAndValidateJwtTokenString(tokenString, 'KMAC256', key);
|
|
18
|
+
expect(validated.payload).toEqual(token.payload);
|
|
19
|
+
});
|
|
20
|
+
test('fail on invalid signature', async () => {
|
|
21
|
+
const key = await generateKmacKey('KMAC256');
|
|
22
|
+
const otherKey = await generateKmacKey('KMAC256');
|
|
23
|
+
const token = {
|
|
24
|
+
header: { alg: 'KMAC256', typ: 'JWT' },
|
|
25
|
+
payload: { sub: '1234567890' },
|
|
26
|
+
};
|
|
27
|
+
const tokenString = await createJwtTokenString(token, key);
|
|
28
|
+
await expect(parseAndValidateJwtTokenString(tokenString, 'KMAC256', otherKey)).rejects.toThrow(InvalidTokenError);
|
|
29
|
+
});
|
|
30
|
+
test('fail on disallowed algorithm', async () => {
|
|
31
|
+
const key = await generateKmacKey('KMAC256');
|
|
32
|
+
const token = {
|
|
33
|
+
header: { alg: 'KMAC256', typ: 'JWT' },
|
|
34
|
+
payload: { sub: '1234567890' },
|
|
35
|
+
};
|
|
36
|
+
const tokenString = await createJwtTokenString(token, key);
|
|
37
|
+
await expect(parseAndValidateJwtTokenString(tokenString, 'KMAC128', key)).rejects.toThrow(InvalidTokenError);
|
|
38
|
+
});
|
|
39
|
+
test('fail on malformed token', () => {
|
|
40
|
+
expect(() => parseJwtTokenString('a.b')).toThrow('Invalid token format');
|
|
41
|
+
expect(() => parseJwtTokenString('')).toThrow('Missing authorization token');
|
|
42
|
+
});
|
|
43
|
+
test('fail on generic error in parseAndValidateJwtTokenString', async () => {
|
|
44
|
+
const key = await generateKmacKey('KMAC256');
|
|
45
|
+
const token = {
|
|
46
|
+
header: { alg: 'KMAC256', typ: 'JWT' },
|
|
47
|
+
payload: { sub: '1234567890' },
|
|
48
|
+
};
|
|
49
|
+
const tokenString = await createJwtTokenString(token, key);
|
|
50
|
+
// Use an incompatible key type to trigger a generic error in Web Crypto
|
|
51
|
+
const aesKey = await generateSymmetricKey({ name: 'AES-GCM', length: 256 });
|
|
52
|
+
await expect(parseAndValidateJwtTokenString(tokenString, 'KMAC256', aesKey)).rejects.toThrow('Invalid token');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { encodeUtf8 } from '../../utils/encoding.js';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { decrypt, deriveBytes, encrypt, generateAsymmetricKey, generateSymmetricKey, getPublicKey, importKey, importSymmetricKey, sign, supports, verify } from '../cryptography.js';
|
|
4
|
+
describe('Modern Cryptography Features', () => {
|
|
5
|
+
test('SubtleCrypto.supports', async () => {
|
|
6
|
+
expect(await supports('generateKey', { name: 'AES-GCM', length: 256 })).toBe(true);
|
|
7
|
+
expect(await supports('sign', { name: 'HMAC', hash: 'SHA-256' })).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
test('KMAC128 signing and verification', async () => {
|
|
10
|
+
const isSupported = await supports('generateKey', { name: 'KMAC128' });
|
|
11
|
+
if (!isSupported) {
|
|
12
|
+
throw new Error('KMAC128 not supported in this environment');
|
|
13
|
+
}
|
|
14
|
+
const key = await importSymmetricKey('raw-secret', { name: 'KMAC128' }, encodeUtf8('my-secret-key-128bits'));
|
|
15
|
+
const data = 'Hello KMAC';
|
|
16
|
+
const algorithm = { name: 'KMAC128', length: 128, outputLength: 32 };
|
|
17
|
+
const signatureResult = sign(algorithm, key, data);
|
|
18
|
+
const signature = await signatureResult.toBuffer();
|
|
19
|
+
expect(await verify(algorithm, key, signature, data)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
test('KMAC256 signing and verification', async () => {
|
|
22
|
+
const isSupported = await supports('generateKey', { name: 'KMAC256' });
|
|
23
|
+
console.log('KMAC256 supported:', isSupported);
|
|
24
|
+
if (!isSupported) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const key = await importSymmetricKey('raw-secret', { name: 'KMAC256' }, encodeUtf8('my-secret-key-256bits-long-enough-for-kmac256'));
|
|
28
|
+
const data = 'Hello KMAC256';
|
|
29
|
+
const algorithm = { name: 'KMAC256', length: 256, outputLength: 64 };
|
|
30
|
+
const signatureResult = sign(algorithm, key, data);
|
|
31
|
+
const signature = await signatureResult.toBuffer();
|
|
32
|
+
expect(await verify(algorithm, key, signature, data)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
test('Argon2id derivation', async () => {
|
|
35
|
+
const isSupported = await supports('importKey', 'Argon2id');
|
|
36
|
+
if (!isSupported) {
|
|
37
|
+
throw new Error('Argon2id not supported in this environment');
|
|
38
|
+
}
|
|
39
|
+
const password = 'my-password';
|
|
40
|
+
const salt = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
41
|
+
const key = await importKey('raw-secret', encodeUtf8(password), { name: 'Argon2id' }, false, ['deriveBits']);
|
|
42
|
+
const argon2Params = {
|
|
43
|
+
name: 'Argon2id',
|
|
44
|
+
nonce: salt,
|
|
45
|
+
memory: 8,
|
|
46
|
+
parallelism: 1,
|
|
47
|
+
passes: 1,
|
|
48
|
+
};
|
|
49
|
+
const derivedBits = await deriveBytes(argon2Params, key, 32);
|
|
50
|
+
expect(derivedBits.byteLength).toBe(32);
|
|
51
|
+
});
|
|
52
|
+
test('ML-DSA-44 signing and verification', async () => {
|
|
53
|
+
const isSupported = await supports('generateKey', 'ML-DSA-44');
|
|
54
|
+
if (!isSupported) {
|
|
55
|
+
throw new Error('ML-DSA-44 not supported in this environment');
|
|
56
|
+
}
|
|
57
|
+
const keyPair = await generateAsymmetricKey({ name: 'ML-DSA-44', namedCurve: 'ML-DSA-44' }, true, ['sign', 'verify']);
|
|
58
|
+
const data = 'Hello ML-DSA';
|
|
59
|
+
const algorithm = { name: 'ML-DSA-44' };
|
|
60
|
+
const signatureResult = sign(algorithm, keyPair.privateKey, data);
|
|
61
|
+
const signature = await signatureResult.toBuffer();
|
|
62
|
+
expect(await verify(algorithm, keyPair.publicKey, signature, data)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
test('ChaCha20-Poly1305 encryption and decryption', async () => {
|
|
65
|
+
const isSupported = await supports('generateKey', { name: 'ChaCha20-Poly1305' });
|
|
66
|
+
if (!isSupported) {
|
|
67
|
+
throw new Error('ChaCha20-Poly1305 not supported in this environment');
|
|
68
|
+
}
|
|
69
|
+
const key = await generateSymmetricKey({ name: 'ChaCha20-Poly1305', length: 256 });
|
|
70
|
+
const data = 'Hello ChaCha20';
|
|
71
|
+
const nonce = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
72
|
+
const algorithm = { name: 'ChaCha20-Poly1305', iv: nonce };
|
|
73
|
+
const encryptionResult = encrypt(algorithm, key, data);
|
|
74
|
+
const encryptedBuffer = await encryptionResult.toBuffer();
|
|
75
|
+
const decryptionResult = decrypt(algorithm, key, encryptedBuffer);
|
|
76
|
+
const decryptedText = await decryptionResult.toUtf8();
|
|
77
|
+
expect(decryptedText).toBe(data);
|
|
78
|
+
});
|
|
79
|
+
test('AES-OCB encryption and decryption', async () => {
|
|
80
|
+
const isSupported = await supports('generateKey', { name: 'AES-OCB', length: 256 });
|
|
81
|
+
if (!isSupported) {
|
|
82
|
+
throw new Error('AES-OCB not supported in this environment');
|
|
83
|
+
}
|
|
84
|
+
const key = await generateSymmetricKey({ name: 'AES-OCB', length: 256 });
|
|
85
|
+
const data = 'Hello AES-OCB';
|
|
86
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
87
|
+
const algorithm = { name: 'AES-OCB', iv };
|
|
88
|
+
const encryptionResult = encrypt(algorithm, key, data);
|
|
89
|
+
const encryptedBuffer = await encryptionResult.toBuffer();
|
|
90
|
+
const decryptionResult = decrypt(algorithm, key, encryptedBuffer);
|
|
91
|
+
const decryptedText = await decryptionResult.toUtf8();
|
|
92
|
+
expect(decryptedText).toBe(data);
|
|
93
|
+
});
|
|
94
|
+
test('getPublicKey from private key', async () => {
|
|
95
|
+
const isSupported = await supports('getPublicKey', 'Ed25519'); // getPublicKey support is often generic but checking Ed25519
|
|
96
|
+
if (!isSupported) {
|
|
97
|
+
// Fallback check if getPublicKey is supported but supports doesn't report it well for Ed25519
|
|
98
|
+
throw new Error('getPublicKey not supported in this environment');
|
|
99
|
+
}
|
|
100
|
+
const keyPair = await generateAsymmetricKey({ name: 'Ed25519', namedCurve: 'Ed25519' }, true, ['sign', 'verify']);
|
|
101
|
+
const publicKey = await getPublicKey(keyPair.privateKey, ['verify']);
|
|
102
|
+
expect(publicKey.type).toBe('public');
|
|
103
|
+
expect(publicKey.algorithm.name).toBe('Ed25519');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|