gdc-common-utils-ts 1.0.1
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/PUBLISHING.md +33 -0
- package/__tests__/AesManager.test.ts +53 -0
- package/__tests__/CryptographyService.test.ts +194 -0
- package/__tests__/bundle.test.ts +29 -0
- package/__tests__/content.test.ts +72 -0
- package/__tests__/crypto-encode-decode.test.ts +52 -0
- package/__tests__/crypto-hmac.test.ts +21 -0
- package/__tests__/did-generateServiceId.errors.test.ts +8 -0
- package/__tests__/did-generateServiceId.test.ts +18 -0
- package/__tests__/models-clinical-sections.test.ts +32 -0
- package/__tests__/models-multibase58.test.ts +33 -0
- package/__tests__/multibase58.errors.test.ts +7 -0
- package/__tests__/multibase58.test.ts +28 -0
- package/__tests__/multibasehash.test.ts +25 -0
- package/__tests__/utils-actor.test.ts +22 -0
- package/__tests__/utils-base-convert.test.ts +57 -0
- package/__tests__/utils-baseN.test.ts +40 -0
- package/__tests__/utils-did-extra.test.ts +33 -0
- package/__tests__/utils-format-converter.test.ts +87 -0
- package/__tests__/utils-jwt.test.ts +57 -0
- package/__tests__/utils-manager-error.test.ts +11 -0
- package/__tests__/utils-normalize.test.ts +15 -0
- package/__tests__/utils-object-convert.test.ts +38 -0
- package/__tests__/utils-string-convert.test.ts +20 -0
- package/__tests__/utils-string-utils.test.ts +25 -0
- package/__tests__/utils-url.test.ts +21 -0
- package/babel.config.cjs +5 -0
- package/jest.config.ts +46 -0
- package/package.json +36 -0
- package/src/AesManager.ts +82 -0
- package/src/CryptographyService.ts +461 -0
- package/src/JweManager.ts.txt +365 -0
- package/src/KmsService.txt +493 -0
- package/src/constants/Schemas.ts +61 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/schemaorg.ts +193 -0
- package/src/cryptoDecode.ts +104 -0
- package/src/cryptoEncode.ts +36 -0
- package/src/cryptography.abstract.ts +29 -0
- package/src/hmac.ts +15 -0
- package/src/index.ts +3 -0
- package/src/interfaces/Cryptography.types.ts +131 -0
- package/src/interfaces/ICryptoHelper.ts +33 -0
- package/src/interfaces/ICryptography.ts +177 -0
- package/src/interfaces/IWallet.ts +62 -0
- package/src/interfaces/MlDsa.ts +25 -0
- package/src/interfaces/MlKem.ts +18 -0
- package/src/models/aes.ts +93 -0
- package/src/models/auth.ts +38 -0
- package/src/models/bundle.ts +152 -0
- package/src/models/bundle.txt +93 -0
- package/src/models/clinical-sections.en.ts +82 -0
- package/src/models/clinical-sections.ts +64 -0
- package/src/models/comm.ts +63 -0
- package/src/models/confidential-job.ts +100 -0
- package/src/models/confidential-message.ts +137 -0
- package/src/models/confidential-storage.ts +170 -0
- package/src/models/consent-rule.ts +141 -0
- package/src/models/crypto.ts +43 -0
- package/src/models/device-license.ts +161 -0
- package/src/models/did.ts +81 -0
- package/src/models/index.ts +31 -0
- package/src/models/indexing.ts +20 -0
- package/src/models/issue.ts +85 -0
- package/src/models/jsonapi.ts +19 -0
- package/src/models/jwe.ts +132 -0
- package/src/models/jwk.ts +50 -0
- package/src/models/jws.ts +42 -0
- package/src/models/jwt.ts +15 -0
- package/src/models/multibase58.ts +46 -0
- package/src/models/oidc4ida.common.model.ts +39 -0
- package/src/models/oidc4ida.document.model.ts +61 -0
- package/src/models/oidc4ida.electronicRecord.model.ts +86 -0
- package/src/models/oidc4ida.evidence.model.ts +69 -0
- package/src/models/openid-device.ts +146 -0
- package/src/models/operation-outcome.ts +34 -0
- package/src/models/params.ts +142 -0
- package/src/models/resource-document.ts +21 -0
- package/src/models/response.ts +5 -0
- package/src/models/urlPath.ts +76 -0
- package/src/models/verifiable-credential.ts +52 -0
- package/src/types/noble-hashes.d.ts +4 -0
- package/src/utils/actor.ts +52 -0
- package/src/utils/base-convert.ts +77 -0
- package/src/utils/baseN.ts +203 -0
- package/src/utils/bundle.ts +30 -0
- package/src/utils/content.ts +66 -0
- package/src/utils/did.ts +155 -0
- package/src/utils/format-converter.ts +119 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/jwt.ts +165 -0
- package/src/utils/manager-error.ts +27 -0
- package/src/utils/multibase58.ts +46 -0
- package/src/utils/multibasehash.ts +28 -0
- package/src/utils/normalize.ts +43 -0
- package/src/utils/object-convert.ts +57 -0
- package/src/utils/string-convert.ts +71 -0
- package/src/utils/string-utils.ts +70 -0
- package/src/utils/url.ts +46 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
crypto-ts/KmsService.txt
|
|
2
|
+
|
|
3
|
+
import { VerificationMethod } from './models/did';// src/services/KmsService.ts
|
|
4
|
+
// Copyright 2025 Antifraud Services Inc. under the Apache License, Version 2.0.
|
|
5
|
+
|
|
6
|
+
import { JWK, JwkSet } from './models/jwk';
|
|
7
|
+
import { JwsMultiSign } from './models/jws';
|
|
8
|
+
import { ConfidentialStorageDoc, IndexedAttribute } from './models/confidential-storage';
|
|
9
|
+
import { IKmsService } from './interfaces/IKmsService';
|
|
10
|
+
import { ICryptography } from './interfaces/ICryptography';
|
|
11
|
+
import { JobRequest } from './models/request';
|
|
12
|
+
import { MldsaPublicJwk, MlkemPrivateJwk, MlkemPublicJwk } from './interfaces/Cryptography.types';
|
|
13
|
+
import { Content } from './utils/content';
|
|
14
|
+
import { createHash, randomBytes } from 'crypto';
|
|
15
|
+
import { computeHmacSha256Base64Url } from './hmac';
|
|
16
|
+
import { ProtectedDataAES } from './models/aes';
|
|
17
|
+
import { ParameterData } from './models/params';
|
|
18
|
+
|
|
19
|
+
import { TenantsCacheManager } from '../managers/TenantsCacheManager';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @file Implements the Key Management Service, the central facade for all internal cryptographic operations.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Implements the Key Management Service interface.
|
|
27
|
+
* This service is the high-level facade for all internal cryptographic operations,
|
|
28
|
+
* abstracting away the underlying cryptographic engine and key storage mechanism.
|
|
29
|
+
*
|
|
30
|
+
* @architecture
|
|
31
|
+
* This in-memory implementation simulates the multi-level key hierarchy defined in
|
|
32
|
+
* `ARCHITECTURE_PATTERNS.md` under "Key Hierarchy and Envelope Encryption".
|
|
33
|
+
*
|
|
34
|
+
* - **KEK Simulation**: In a production system, a Key Encryption Key (KEK) would be fetched
|
|
35
|
+
* from a secure secret manager at startup. This KEK would then decrypt the Host DEK.
|
|
36
|
+
* This implementation simulates the state *after* this initial decryption has occurred.
|
|
37
|
+
*
|
|
38
|
+
* - **In-Memory Key Storage**: The `_managedKeys` map holds all Data Encryption Keys (DEKs)
|
|
39
|
+
* and private keys in a decrypted, ready-to-use state for the lifecycle of the server.
|
|
40
|
+
* This is suitable for development and testing. A production implementation would replace
|
|
41
|
+
* this map with a secure adapter that performs just-in-time decryption of keys using
|
|
42
|
+
* the Host DEK or a session key.
|
|
43
|
+
*
|
|
44
|
+
* The `entityId` (e.g., 'host', or a tenant's UUID) is the primary identifier for a key set.
|
|
45
|
+
*/
|
|
46
|
+
type EntityKeysSet = {
|
|
47
|
+
verificationKeyPair: {
|
|
48
|
+
publicJWKey: MldsaPublicJwk & { kid: string; };
|
|
49
|
+
secretKeyBytes: Uint8Array;
|
|
50
|
+
};
|
|
51
|
+
encryptionKeyPair: {
|
|
52
|
+
publicJWKey: MlkemPublicJwk & { kid: string; };
|
|
53
|
+
secretKeyBytes: Uint8Array;
|
|
54
|
+
};
|
|
55
|
+
/** The entity's 32-byte Data Encryption Key (DEK), used for symmetric encryption of data at rest. */
|
|
56
|
+
dataEncryptionKey: Uint8Array;
|
|
57
|
+
/** The entity's 32-byte HMAC key, used for creating keyed hashes of searchable attributes. */
|
|
58
|
+
hmacKey: Uint8Array;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Implements the Key Management Service interface.
|
|
63
|
+
* This service is the high-level facade for all internal cryptographic operations,
|
|
64
|
+
* abstracting away the underlying cryptographic engine and key storage mechanism.
|
|
65
|
+
*
|
|
66
|
+
* In this implementation, keys are stored in an in-memory Map, acting as a simple "vault".
|
|
67
|
+
* The Map's key is the `entityId` (e.g., 'host', 'tenant-urn-123'), which functions as a primary,
|
|
68
|
+
* human-readable identifier for the key set. In a production system, this Map would be replaced
|
|
69
|
+
* by a secure database adapter (e.g., Firestore, PostgreSQL) that handles KEK-wrapping of DEKs.
|
|
70
|
+
*/
|
|
71
|
+
export class KmsService implements IKmsService {
|
|
72
|
+
private crypto: ICryptography;
|
|
73
|
+
private tenantsCacheManager: TenantsCacheManager;
|
|
74
|
+
/** In-memory key storage. Key: entityId, Value: KeyPairSet. */
|
|
75
|
+
private _managedKeys: Map<string, EntityKeysSet>;
|
|
76
|
+
private isHostInitialized: boolean = false;
|
|
77
|
+
|
|
78
|
+
constructor(
|
|
79
|
+
cryptographyService: ICryptography,
|
|
80
|
+
tenantsCacheManager: TenantsCacheManager,
|
|
81
|
+
) {
|
|
82
|
+
this.crypto = cryptographyService;
|
|
83
|
+
this.tenantsCacheManager = tenantsCacheManager;
|
|
84
|
+
this._managedKeys = new Map();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Initializes the KmsService by provisioning the essential keys for the 'host' entity.
|
|
89
|
+
* This method MUST be called before any other methods are used.
|
|
90
|
+
*/
|
|
91
|
+
async init(): Promise<void> {
|
|
92
|
+
if (this.isHostInitialized) {
|
|
93
|
+
// console.log('[KmsService] Host keys already initialized.');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// console.log('[KmsService] Initializing host keys...');
|
|
97
|
+
await this.provisionKeys('host');
|
|
98
|
+
this.isHostInitialized = true;
|
|
99
|
+
// console.log('[KmsService] Host keys initialized successfully.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private checkInitialized(): void {
|
|
103
|
+
if (!this.isHostInitialized) {
|
|
104
|
+
throw new Error('KmsService has not been initialized. Call init() before using.');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Key Lifecycle Management ---
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generates a full set of cryptographic keys (signing, encryption, DEK) for a given entity
|
|
112
|
+
* and stores them in the internal vault, associated with the entity's ID.
|
|
113
|
+
*
|
|
114
|
+
* In development/testing environments, if `NODE_ENV` is 'development' AND the `DEV_SEED`
|
|
115
|
+
* environment variable is set to 'true', this method will generate keys deterministically
|
|
116
|
+
* using the `entityId` as a seed. This ensures that tests are reproducible.
|
|
117
|
+
* In production, it uses a cryptographically secure random source.
|
|
118
|
+
*
|
|
119
|
+
* @param entityId A unique identifier for the entity (e.g., 'host', 'tenant-urn-123').
|
|
120
|
+
* @returns A JWKSet containing the public keys (signing and encryption).
|
|
121
|
+
*/
|
|
122
|
+
async provisionKeys(entityVaultId: string): Promise<JwkSet> {
|
|
123
|
+
let dsaSeed: Uint8Array;
|
|
124
|
+
let kemSeed: Uint8Array;
|
|
125
|
+
let dataEncryptionKey: Uint8Array;
|
|
126
|
+
let hmacKey: Uint8Array;
|
|
127
|
+
|
|
128
|
+
// Use deterministic keys only in development and when explicitly requested.
|
|
129
|
+
if (process.env.NODE_ENV === 'development' && process.env.DEV_SEED === 'true') {
|
|
130
|
+
// Deterministic generation for development and testing.
|
|
131
|
+
// Use the modern `.subarr000 está ay()` which is the safe replacement for the deprecated `.slice()` on Buffers.
|
|
132
|
+
dsaSeed = createHash('sha256').update(entityVaultId + '-dsa').digest().subarray(0, 32);
|
|
133
|
+
kemSeed = createHash('sha512').update(entityVaultId + '-kem').digest().subarray(0, 64);
|
|
134
|
+
dataEncryptionKey = createHash('sha256').update(entityVaultId + '-dek').digest().subarray(0, 32);
|
|
135
|
+
hmacKey = createHash('sha256').update(entityVaultId + '-hmac').digest().subarray(0, 32);
|
|
136
|
+
} else {
|
|
137
|
+
// Secure random generation for production
|
|
138
|
+
dsaSeed = randomBytes(32);
|
|
139
|
+
kemSeed = randomBytes(64);
|
|
140
|
+
dataEncryptionKey = randomBytes(32);
|
|
141
|
+
hmacKey = randomBytes(32);
|
|
142
|
+
}
|
|
143
|
+
const verificationKeyPair = await this.crypto.generateKeyPairMlDsa(dsaSeed);
|
|
144
|
+
const encryptionKeyPair = await this.crypto.generateKeyPairMlKem(kemSeed);
|
|
145
|
+
|
|
146
|
+
// In a production vault, the `dataEncryptionKey` would be encrypted with the Host KEK here before storage.
|
|
147
|
+
this._managedKeys.set(entityVaultId, {
|
|
148
|
+
verificationKeyPair: { publicJWKey: verificationKeyPair.publicJWKey, secretKeyBytes: verificationKeyPair.secretKeyBytes },
|
|
149
|
+
encryptionKeyPair: { publicJWKey: encryptionKeyPair.publicJWKey, secretKeyBytes: encryptionKeyPair.secretKeyBytes },
|
|
150
|
+
dataEncryptionKey: dataEncryptionKey,
|
|
151
|
+
hmacKey: hmacKey
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const publicJwkSet = {
|
|
155
|
+
keys: [verificationKeyPair.publicJWKey as JWK, encryptionKeyPair.publicJWKey as JWK]
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// // console.log(`[KmsService] Provisioned new JWKSet for entity: ${entityVaultId}`, publicJwkSet);
|
|
159
|
+
|
|
160
|
+
return publicJwkSet;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Retrieves the public parts of an entity's asymmetric keys.
|
|
165
|
+
* @param entityId The unique identifier for the entity.
|
|
166
|
+
* @returns A JWKSet of the entity's public keys.
|
|
167
|
+
*/
|
|
168
|
+
async getPublicJwks(entityVaultId: string): Promise<JwkSet> {
|
|
169
|
+
const keyPairSet = this._managedKeys.get(entityVaultId);
|
|
170
|
+
if (!keyPairSet) {
|
|
171
|
+
throw new Error(`Keys not found for entity: ${entityVaultId}`);
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
keys: [keyPairSet.verificationKeyPair.publicJWKey as JWK, keyPairSet.encryptionKeyPair.publicJWKey as JWK]
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async getPublicVerificationKey(entityVaultId: string, alg?: string): Promise<MldsaPublicJwk | undefined> {
|
|
179
|
+
const keySet = this._managedKeys.get(entityVaultId);
|
|
180
|
+
if (!keySet) return undefined;
|
|
181
|
+
|
|
182
|
+
// For now, we only support one key type, so we ignore the 'alg' parameter.
|
|
183
|
+
// In the future, this would filter a list of keys.
|
|
184
|
+
// Default to ML-DSA-44 if no alg is provided.
|
|
185
|
+
if (!alg || alg === 'ML-DSA-44') {
|
|
186
|
+
return keySet.verificationKeyPair.publicJWKey;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Placeholder for legacy ECDSA key lookup
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async getPublicEncryptionKey(entityVaultId: string, crv?: string): Promise<MlkemPublicJwk | undefined> {
|
|
194
|
+
const keySet = this._managedKeys.get(entityVaultId);
|
|
195
|
+
if (!keySet) return undefined;
|
|
196
|
+
|
|
197
|
+
// For now, we only support one key type, so we ignore the 'crv' parameter.
|
|
198
|
+
// In the future, this would filter a list of keys.
|
|
199
|
+
// Default to ML-KEM-768 if no crv is provided.
|
|
200
|
+
if (!crv || crv === 'ML-KEM-768') {
|
|
201
|
+
return keySet.encryptionKeyPair.publicJWKey;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Placeholder for legacy P-256 key lookup
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async getHostPublicJwkSet(): Promise<JwkSet> {
|
|
209
|
+
this.checkInitialized();
|
|
210
|
+
const hostKeys = this._managedKeys.get('host');
|
|
211
|
+
if (!hostKeys) {
|
|
212
|
+
// This state should be impossible if init() was called successfully.
|
|
213
|
+
throw new Error('Host keys not found despite service being initialized.');
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
keys: [
|
|
217
|
+
hostKeys.verificationKeyPair.publicJWKey as JWK,
|
|
218
|
+
hostKeys.encryptionKeyPair.publicJWKey as JWK,
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- Inbound Request Processing ---
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Decrypts an incoming JWE message to reveal its inner JWS payload.
|
|
227
|
+
* This is the primary entry point for the asynchronous API. It does not perform signature validation.
|
|
228
|
+
*
|
|
229
|
+
* It works by:
|
|
230
|
+
* 1. Inspecting the `kid` (Key ID) in the header of each JWE recipient.
|
|
231
|
+
* 2. Searching the internal vault for a managed private key corresponding to one of those `kid`s.
|
|
232
|
+
* 3. Using the found private key to decrypt the message via `crypto.decryptJwe`.
|
|
233
|
+
* @param message A compact JWE string.
|
|
234
|
+
* @returns A `JobRequest` object containing the parsed payload and metadata.
|
|
235
|
+
*/
|
|
236
|
+
async decodeRequest(message: string): Promise<JobRequest> {
|
|
237
|
+
this.checkInitialized();
|
|
238
|
+
const recipientKids = this.crypto.getRecipientKidsFromJwe(message);
|
|
239
|
+
if (recipientKids.length === 0) {
|
|
240
|
+
throw new Error('JWE does not contain any recipient key identifiers (kid).');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let foundKey: MlkemPrivateJwk | undefined;
|
|
244
|
+
for (const [entityId, keySet] of Array.from(this._managedKeys.entries())) {
|
|
245
|
+
if (recipientKids.includes(keySet.encryptionKeyPair.publicJWKey.kid)) {
|
|
246
|
+
foundKey = { ...keySet.encryptionKeyPair.publicJWKey, dBytes: keySet.encryptionKeyPair.secretKeyBytes };
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!foundKey) {
|
|
252
|
+
throw new Error(`No managed key found for any of the JWE recipients: ${recipientKids.join(', ')}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const { decryptedBytes, protectedHeader } = await this.crypto.decryptJwe(message, foundKey);
|
|
256
|
+
const decryptedPayload = Content.bytesToStringUTF8(decryptedBytes);
|
|
257
|
+
|
|
258
|
+
// ARCHITECTURE KEEPER: Adhere to JOSE standards for nested tokens.
|
|
259
|
+
// The `cty` (Content Type) header in the JWE's protected header indicates the
|
|
260
|
+
// nature of the encrypted payload.
|
|
261
|
+
if ((protectedHeader as { cty?: string }).cty === 'JWS') {
|
|
262
|
+
// Case 1: The payload is a JWS string, as indicated by the standard header.
|
|
263
|
+
const dataJwt = this.crypto.parseCompactJws(decryptedPayload);
|
|
264
|
+
return {
|
|
265
|
+
content: dataJwt.payload,
|
|
266
|
+
meta: { jws: dataJwt, jwe: { header: protectedHeader } },
|
|
267
|
+
} as JobRequest;
|
|
268
|
+
} else {
|
|
269
|
+
// Case 2: No `cty` header is present. We assume the payload is a direct JSON object.
|
|
270
|
+
// This is the case for job responses generated by our own worker.
|
|
271
|
+
return {
|
|
272
|
+
content: JSON.parse(decryptedPayload),
|
|
273
|
+
meta: { jwe: { header: protectedHeader } },
|
|
274
|
+
} as JobRequest;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --- Signing Operations ---
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Signs a payload using a key directly managed by the KMS.
|
|
282
|
+
* The key is located using the `entityId` (e.g., 'host').
|
|
283
|
+
* @param payload The raw byte array to be signed.
|
|
284
|
+
* @param entityId The identifier of the signing entity.
|
|
285
|
+
* @returns A `JwsMultiSign` object containing the signature.
|
|
286
|
+
*/
|
|
287
|
+
async signWithManagedKey(payload: Uint8Array, entityVaultId: string): Promise<JwsMultiSign> {
|
|
288
|
+
const keyPairSet = this._managedKeys.get(entityVaultId);
|
|
289
|
+
if (!keyPairSet) {
|
|
290
|
+
throw new Error(`Verification key not found for entity: ${entityVaultId}`);
|
|
291
|
+
}
|
|
292
|
+
const { publicJWKey, secretKeyBytes } = keyPairSet.verificationKeyPair;
|
|
293
|
+
const protectedHeader = { alg: publicJWKey.alg, kid: publicJWKey.kid };
|
|
294
|
+
const jwsParts = await this.crypto.signDataJws({ data: Content.bytesToRawBase64UrlSafe(payload) }, protectedHeader, secretKeyBytes);
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
payload: jwsParts.payload,
|
|
298
|
+
signatures: [{ protected: jwsParts.protected, signature: jwsParts.signature }],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Signs a payload by first reconstructing a signing key from two seed parts.
|
|
304
|
+
* This is used for keys that are not stored directly, like an employee's key.
|
|
305
|
+
* It works by decrypting `encryptedSeedPartB` using the `protectorEntityId`'s Data Encryption Key (DEK).
|
|
306
|
+
* @param payload The raw byte array to sign.
|
|
307
|
+
* @param seedPartA The user-provided part of the seed.
|
|
308
|
+
* @param encryptedSeedPartB A byte array representing the JSON-stringified `ProtectedDataAES` of the encrypted second seed part.
|
|
309
|
+
* @param protectorEntityId The ID of the entity (e.g., the Tenant) whose DEK protects `seedPartB`.
|
|
310
|
+
* @returns A `JwsMultiSign` object containing the signature.
|
|
311
|
+
*/
|
|
312
|
+
async signWithReconstructedKey(
|
|
313
|
+
payload: Uint8Array,
|
|
314
|
+
seedPartA: Uint8Array,
|
|
315
|
+
encryptedSeedPartB: Uint8Array,
|
|
316
|
+
protectorEntityId: string
|
|
317
|
+
): Promise<JwsMultiSign> {
|
|
318
|
+
this.checkInitialized();
|
|
319
|
+
const protectorKeys = this._managedKeys.get(protectorEntityId);
|
|
320
|
+
if (!protectorKeys) {
|
|
321
|
+
throw new Error(`Protector entity's keys not found: ${protectorEntityId}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const encryptedDataString = Content.bytesToStringUTF8(encryptedSeedPartB);
|
|
325
|
+
const encryptedDataObject: ProtectedDataAES = JSON.parse(encryptedDataString);
|
|
326
|
+
|
|
327
|
+
const decryptedSeedPartBString = await this.crypto.decrypt(encryptedDataObject, protectorKeys.dataEncryptionKey, protectorEntityId);
|
|
328
|
+
const decryptedSeedPartB = Content.stringToBytesUTF8(decryptedSeedPartBString);
|
|
329
|
+
|
|
330
|
+
// FIX: Use Buffer.concat for robust Uint8Array concatenation
|
|
331
|
+
const fullSeed = Buffer.concat([seedPartA, decryptedSeedPartB]);
|
|
332
|
+
|
|
333
|
+
const { publicJWKey, secretKeyBytes } = await this.crypto.generateKeyPairMlDsa(fullSeed);
|
|
334
|
+
|
|
335
|
+
const protectedHeader = { alg: publicJWKey.alg, kid: publicJWKey.kid };
|
|
336
|
+
const jwsParts = await this.crypto.signDataJws({ data: Content.bytesToRawBase64UrlSafe(payload) }, protectedHeader, secretKeyBytes);
|
|
337
|
+
return {
|
|
338
|
+
payload: jwsParts.payload,
|
|
339
|
+
signatures: [{ protected: jwsParts.protected, signature: jwsParts.signature }],
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// --- Outbound Encryption ---
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Encrypts a response payload for one or more external recipients.
|
|
347
|
+
* This is typically used by an asynchronous Worker to prepare a response for the original caller.
|
|
348
|
+
* The response is a JWE, encrypted using the recipients' public keys.
|
|
349
|
+
* @param payload The JSON object to encrypt.
|
|
350
|
+
* @param recipientJwks An array of public ML-KEM keys for the recipients.
|
|
351
|
+
* @param senderId The `entityId` of the internal entity sending the response (e.g., 'host'),
|
|
352
|
+
* used to include its `skid` (sender key id) in the protected header.
|
|
353
|
+
* @returns The encrypted JWE as a compact string (for a single recipient) or a JSON string (for multiple).
|
|
354
|
+
*/
|
|
355
|
+
async encodeResponse(payload: any, recipientJwks: JWK[], senderVaultId: string): Promise<string> {
|
|
356
|
+
const senderKeys = this._managedKeys.get(senderVaultId);
|
|
357
|
+
if (!senderKeys) {
|
|
358
|
+
throw new Error(`Sender keys not found for entity: ${senderVaultId}`);
|
|
359
|
+
}
|
|
360
|
+
const senderPrivKey: MlkemPrivateJwk = {
|
|
361
|
+
...senderKeys.encryptionKeyPair.publicJWKey,
|
|
362
|
+
dBytes: senderKeys.encryptionKeyPair.secretKeyBytes,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const protectedHeader = {
|
|
366
|
+
enc: 'A256GCM',
|
|
367
|
+
alg: 'ML-KEM-768',
|
|
368
|
+
skid: senderPrivKey.kid,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const kemRecipientJwks = recipientJwks as MlkemPublicJwk[];
|
|
372
|
+
|
|
373
|
+
// For a single recipient, we should use the compact serialization format.
|
|
374
|
+
if (kemRecipientJwks.length === 1) {
|
|
375
|
+
return this.crypto.encryptJweToCompact(payload, protectedHeader, senderPrivKey, kemRecipientJwks[0]);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// For multiple recipients, we use the general JSON serialization format.
|
|
379
|
+
const jweObject = await this.crypto.encryptJwe(payload, protectedHeader, senderPrivKey, kemRecipientJwks);
|
|
380
|
+
return JSON.stringify(jweObject);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// --- At-Rest Data Protection ---
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Encrypts the `.content` of a document using the entity's managed Data Encryption Key (DEK).
|
|
387
|
+
* @param doc The document to protect.
|
|
388
|
+
* @param entityId The ID of the entity whose DEK will be used.
|
|
389
|
+
* @returns A new document object where `.content` is replaced by a `.jwe` property
|
|
390
|
+
* containing the JSON-stringified `ProtectedDataAES` object. The `entityId` is used
|
|
391
|
+
* as the AAD to prevent cross-tenant decryption attacks.
|
|
392
|
+
*/
|
|
393
|
+
async protectConfidentialData(doc: ConfidentialStorageDoc, entityVaultId: string): Promise<ConfidentialStorageDoc> {
|
|
394
|
+
this.checkInitialized();
|
|
395
|
+
if (!doc.content) {
|
|
396
|
+
throw new Error('Document has no "content" to protect.');
|
|
397
|
+
}
|
|
398
|
+
const protectorKeys = this._managedKeys.get(entityVaultId);
|
|
399
|
+
if (!protectorKeys) {
|
|
400
|
+
throw new Error(`Protector entity's keys not found: ${entityVaultId}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const contentString = JSON.stringify(doc.content);
|
|
404
|
+
// The AAD is crucial here to bind the ciphertext to the entity.
|
|
405
|
+
const encryptedData = await this.crypto.encrypt(contentString, protectorKeys.dataEncryptionKey, entityVaultId);
|
|
406
|
+
|
|
407
|
+
const { content, ...docWithoutContent } = doc;
|
|
408
|
+
// FIX: The result of encryption is an object, it should be stringified for storage.
|
|
409
|
+
return { ...docWithoutContent, jwe: encryptedData };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Decrypts the `.jwe` property of a document using the entity's managed Data Encryption Key (DEK).
|
|
414
|
+
* @param doc The protected document containing the `.jwe` property, which is an object of type `ProtectedDataAES`.
|
|
415
|
+
* @param entityId The ID of the entity whose DEK was used. This is passed as the AAD
|
|
416
|
+
* to ensure the ciphertext was intended for this entity.
|
|
417
|
+
* @returns The decrypted content of the document.
|
|
418
|
+
*/
|
|
419
|
+
async unprotectConfidentialData<T>(doc: ConfidentialStorageDoc, entityVaultId: string): Promise<T> {
|
|
420
|
+
this.checkInitialized();
|
|
421
|
+
if (!doc.jwe || typeof doc.jwe !== 'object') {
|
|
422
|
+
throw new Error('Document has no valid "jwe" property to unprotect.');
|
|
423
|
+
}
|
|
424
|
+
const protectorKeys = this._managedKeys.get(entityVaultId);
|
|
425
|
+
if (!protectorKeys) {
|
|
426
|
+
throw new Error(`Protector entity's keys not found: ${entityVaultId}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const encryptedDataObject = doc.jwe as ProtectedDataAES;
|
|
430
|
+
// The AAD must match the one used during encryption.
|
|
431
|
+
const decryptedString = await this.crypto.decrypt(encryptedDataObject, protectorKeys.dataEncryptionKey, entityVaultId);
|
|
432
|
+
return JSON.parse(decryptedString) as T;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Computes a keyed hash (HMAC) of a plaintext string using the specified entity's secret HMAC key.
|
|
437
|
+
* @param plaintext The string to hash.
|
|
438
|
+
* @param entityVaultId The vault ID of the entity (e.g., 'host', 'health-care_acme') whose HMAC key should be used.
|
|
439
|
+
* @returns The resulting HMAC as a Base64UrlSafe string.
|
|
440
|
+
*/
|
|
441
|
+
async getHmacBase64Url(plaintext: string, entityVaultId: string): Promise<string> {
|
|
442
|
+
this.checkInitialized();
|
|
443
|
+
const keys = this._managedKeys.get(entityVaultId);
|
|
444
|
+
if (!keys) {
|
|
445
|
+
throw new Error(`Keys not found for entity: ${entityVaultId}`);
|
|
446
|
+
}
|
|
447
|
+
if (!keys.hmacKey) {
|
|
448
|
+
// This should not happen if provisionKeys is always used.
|
|
449
|
+
throw new Error(`HMAC key is missing for entity: ${entityVaultId}`);
|
|
450
|
+
}
|
|
451
|
+
return computeHmacSha256Base64Url(plaintext, keys.hmacKey);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Takes an array of plaintext attributes (with type information) and returns a new array where
|
|
456
|
+
* both the `name` and `value` properties of each attribute have been protected with HMAC.
|
|
457
|
+
* This is used to create the `indexed` array for a `ConfidentialStorageDoc`.
|
|
458
|
+
* The implementation MUST use domain separation based on the attribute `type` to prevent collisions.
|
|
459
|
+
*
|
|
460
|
+
* @param attributes The array of plaintext `ParameterData` objects.
|
|
461
|
+
* @param entityVaultId The security context for key selection.
|
|
462
|
+
* @returns A promise that resolves to the array of protected `IndexedAttribute` objects, ready for storage.
|
|
463
|
+
*/
|
|
464
|
+
async protectAttributesNameAndValue(attributes: ParameterData[], entityVaultId: string): Promise<IndexedAttribute[]> {
|
|
465
|
+
const protectedAttributes: IndexedAttribute[] = [];
|
|
466
|
+
for (const attribute of attributes) {
|
|
467
|
+
if (attribute.value === undefined) {
|
|
468
|
+
// Do not index undefined values. Log a warning.
|
|
469
|
+
console.warn(`[KmsService] Skipping HMAC protection for attribute "${attribute.name}" because its value is undefined.`);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
// Coerce number to string for canonical representation before HMAC.
|
|
473
|
+
const valueAsString = String(attribute.value);
|
|
474
|
+
|
|
475
|
+
const protectedName = await this.getHmacBase64Url(attribute.name, entityVaultId);
|
|
476
|
+
const protectedValue = await this.getHmacBase64Url(valueAsString, entityVaultId);
|
|
477
|
+
|
|
478
|
+
const indexedAttr: IndexedAttribute = {
|
|
479
|
+
name: protectedName,
|
|
480
|
+
value: protectedValue,
|
|
481
|
+
unique: attribute.unique,
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// Only add the 'type' if it's not the default 'string'
|
|
485
|
+
if (attribute.type && attribute.type !== 'string') {
|
|
486
|
+
indexedAttr.type = attribute.type;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
protectedAttributes.push(indexedAttr);
|
|
490
|
+
}
|
|
491
|
+
return protectedAttributes;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// constants/Schemas.ts
|
|
2
|
+
// Based on the backend's src/models/schemaorg.ts
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Defines the types of form requests that can be sent in a batch.
|
|
6
|
+
*/
|
|
7
|
+
export const FormRequestType = {
|
|
8
|
+
IndividualTerms: 'IndividualTerms',
|
|
9
|
+
PersonalIdentity: 'PersonalIdentity',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
// --- Enums for URL construction and validation ---
|
|
13
|
+
|
|
14
|
+
export const Sector = {
|
|
15
|
+
EMERGENCY: 'emergency',
|
|
16
|
+
HEALTH_CARE: 'health-care',
|
|
17
|
+
HEALTH_INSURANCE: 'health-insurance',
|
|
18
|
+
RESEARCH: 'research',
|
|
19
|
+
} as const;
|
|
20
|
+
export type Sector = typeof Sector[keyof typeof Sector];
|
|
21
|
+
|
|
22
|
+
export const Section = {
|
|
23
|
+
REGISTRY: 'registry',
|
|
24
|
+
ENTITY: 'entity',
|
|
25
|
+
INDIVIDUAL: 'individual',
|
|
26
|
+
NETWORK: 'network',
|
|
27
|
+
} as const;
|
|
28
|
+
export type Section = typeof Section[keyof typeof Section];
|
|
29
|
+
|
|
30
|
+
export const Format = {
|
|
31
|
+
SCHEMA: 'org.schema',
|
|
32
|
+
FHIR_API: 'org.hl7.fhir.api',
|
|
33
|
+
} as const;
|
|
34
|
+
export type Format = typeof Format[keyof typeof Format];
|
|
35
|
+
|
|
36
|
+
export const Resource = {
|
|
37
|
+
PERSON: 'Person',
|
|
38
|
+
RELATED_PERSON: 'RelatedPerson',
|
|
39
|
+
EMPLOYEE: 'Employee',
|
|
40
|
+
EMPLOYEE_ROLE: 'EmployeeRole',
|
|
41
|
+
PRACTITIONER: 'Practitioner',
|
|
42
|
+
PRACTITIONER_ROLE: 'PractitionerRole',
|
|
43
|
+
ORGANIZATION: 'Organization',
|
|
44
|
+
LOCATION: 'Location',
|
|
45
|
+
GROUP: 'Group',
|
|
46
|
+
} as const;
|
|
47
|
+
export type Resource = typeof Resource[keyof typeof Resource];
|
|
48
|
+
|
|
49
|
+
export const JobAction = {
|
|
50
|
+
BATCH: '_batch',
|
|
51
|
+
CREATE: '_create',
|
|
52
|
+
DISCOVERY: '_discovery',
|
|
53
|
+
} as const;
|
|
54
|
+
export type JobAction = typeof JobAction[keyof typeof JobAction];
|
|
55
|
+
|
|
56
|
+
export const knownDomainsReversed = [
|
|
57
|
+
'org.schema',
|
|
58
|
+
'org.hl7.fhir',
|
|
59
|
+
'org.ilo.isco',
|
|
60
|
+
'net.openid',
|
|
61
|
+
] as const;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './schemaorg';
|