@particle/esim-tooling 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -0
- package/dist/activation-code.d.ts +11 -0
- package/dist/activation-code.d.ts.map +1 -0
- package/dist/activation-code.js +56 -0
- package/dist/activation-code.js.map +1 -0
- package/dist/apdu.d.ts +73 -0
- package/dist/apdu.d.ts.map +1 -0
- package/dist/apdu.js +357 -0
- package/dist/apdu.js.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +52 -0
- package/dist/errors.js.map +1 -0
- package/dist/es10/es10b-notifications.d.ts +30 -0
- package/dist/es10/es10b-notifications.d.ts.map +1 -0
- package/dist/es10/es10b-notifications.js +294 -0
- package/dist/es10/es10b-notifications.js.map +1 -0
- package/dist/es10/es10b.d.ts +34 -0
- package/dist/es10/es10b.d.ts.map +1 -0
- package/dist/es10/es10b.js +108 -0
- package/dist/es10/es10b.js.map +1 -0
- package/dist/es10/es10c.d.ts +12 -0
- package/dist/es10/es10c.d.ts.map +1 -0
- package/dist/es10/es10c.js +133 -0
- package/dist/es10/es10c.js.map +1 -0
- package/dist/es10/iccid.d.ts +9 -0
- package/dist/es10/iccid.d.ts.map +1 -0
- package/dist/es10/iccid.js +31 -0
- package/dist/es10/iccid.js.map +1 -0
- package/dist/es10/index.d.ts +5 -0
- package/dist/es10/index.d.ts.map +1 -0
- package/dist/es10/index.js +4 -0
- package/dist/es10/index.js.map +1 -0
- package/dist/es10/tags.d.ts +55 -0
- package/dist/es10/tags.d.ts.map +1 -0
- package/dist/es10/tags.js +63 -0
- package/dist/es10/tags.js.map +1 -0
- package/dist/es9plus.d.ts +52 -0
- package/dist/es9plus.d.ts.map +1 -0
- package/dist/es9plus.js +227 -0
- package/dist/es9plus.js.map +1 -0
- package/dist/gsma-rsp2-root-ci1.d.ts +38 -0
- package/dist/gsma-rsp2-root-ci1.d.ts.map +1 -0
- package/dist/gsma-rsp2-root-ci1.js +52 -0
- package/dist/gsma-rsp2-root-ci1.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lpa.d.ts +15 -0
- package/dist/lpa.d.ts.map +1 -0
- package/dist/lpa.js +283 -0
- package/dist/lpa.js.map +1 -0
- package/dist/tlv.d.ts +14 -0
- package/dist/tlv.d.ts.map +1 -0
- package/dist/tlv.js +132 -0
- package/dist/tlv.js.map +1 -0
- package/dist/types.d.ts +230 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +108 -0
- package/dist/types.js.map +1 -0
- package/package.json +50 -0
- package/src/activation-code.ts +64 -0
- package/src/apdu.ts +419 -0
- package/src/errors.ts +69 -0
- package/src/es10/es10b-notifications.ts +331 -0
- package/src/es10/es10b.ts +163 -0
- package/src/es10/es10c.ts +168 -0
- package/src/es10/iccid.ts +32 -0
- package/src/es10/index.ts +42 -0
- package/src/es10/tags.ts +69 -0
- package/src/es9plus.ts +331 -0
- package/src/gsma-rsp2-root-ci1.ts +53 -0
- package/src/index.ts +43 -0
- package/src/lpa.ts +346 -0
- package/src/tlv.ts +137 -0
- package/src/types.ts +264 -0
package/src/lpa.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { Es10Error, Es9PlusError, EsimError } from './errors.js';
|
|
2
|
+
import {
|
|
3
|
+
EnableProfileResult,
|
|
4
|
+
DisableProfileResult,
|
|
5
|
+
DeleteProfileResult,
|
|
6
|
+
CancelSessionReason,
|
|
7
|
+
BppInstallErrorCode,
|
|
8
|
+
type ServerAdapter,
|
|
9
|
+
type Profile,
|
|
10
|
+
type Notification,
|
|
11
|
+
type NotificationWithResult,
|
|
12
|
+
type SendResult,
|
|
13
|
+
type ProcessNotificationsOptions,
|
|
14
|
+
type ProcessNotificationsResult,
|
|
15
|
+
type DownloadResult,
|
|
16
|
+
type DownloadProgress,
|
|
17
|
+
type InstallProfileOptions,
|
|
18
|
+
type EsimLpaOptions,
|
|
19
|
+
} from './types.js';
|
|
20
|
+
import { ApduTransport } from './apdu.js';
|
|
21
|
+
import {
|
|
22
|
+
encodeGetEidRequest,
|
|
23
|
+
decodeGetEidResponse,
|
|
24
|
+
encodeGetProfilesInfoRequest,
|
|
25
|
+
decodeGetProfilesInfoResponse,
|
|
26
|
+
encodeEnableProfileRequest,
|
|
27
|
+
decodeEnableProfileResponse,
|
|
28
|
+
encodeDisableProfileRequest,
|
|
29
|
+
decodeDisableProfileResponse,
|
|
30
|
+
encodeDeleteProfileRequest,
|
|
31
|
+
decodeDeleteProfileResponse,
|
|
32
|
+
} from './es10/es10c.js';
|
|
33
|
+
import {
|
|
34
|
+
encodeGetEuiccChallengeRequest,
|
|
35
|
+
decodeGetEuiccChallengeResponse,
|
|
36
|
+
encodeGetEuiccInfo1Request,
|
|
37
|
+
encodeAuthenticateServerRequest,
|
|
38
|
+
decodeAuthenticateServerResponse,
|
|
39
|
+
encodePrepareDownloadRequest,
|
|
40
|
+
decodePrepareDownloadResponse,
|
|
41
|
+
hashConfirmationCode,
|
|
42
|
+
encodeCancelSessionRequest,
|
|
43
|
+
} from './es10/es10b.js';
|
|
44
|
+
import {
|
|
45
|
+
encodeRetrieveNotificationsListRequest,
|
|
46
|
+
decodeRetrieveNotificationsListFull,
|
|
47
|
+
decodeBppInstallationResult,
|
|
48
|
+
encodeRemoveNotificationRequest,
|
|
49
|
+
} from './es10/es10b-notifications.js';
|
|
50
|
+
import { parseActivationCode } from './activation-code.js';
|
|
51
|
+
import { Es9PlusClient } from './es9plus.js';
|
|
52
|
+
|
|
53
|
+
export class EsimLpa {
|
|
54
|
+
private apdu: ApduTransport;
|
|
55
|
+
private es9plus: Es9PlusClient;
|
|
56
|
+
|
|
57
|
+
constructor(options: EsimLpaOptions) {
|
|
58
|
+
this.apdu = new ApduTransport(options.device);
|
|
59
|
+
this.es9plus = new Es9PlusClient(options.server);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getEid(): Promise<string> {
|
|
63
|
+
try {
|
|
64
|
+
const request = encodeGetEidRequest();
|
|
65
|
+
const response = await this.apdu.sendCommand(request);
|
|
66
|
+
return decodeGetEidResponse(response);
|
|
67
|
+
} finally {
|
|
68
|
+
await this.apdu.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async listProfiles(): Promise<Profile[]> {
|
|
73
|
+
try {
|
|
74
|
+
const request = encodeGetProfilesInfoRequest();
|
|
75
|
+
const response = await this.apdu.sendCommand(request);
|
|
76
|
+
return decodeGetProfilesInfoResponse(response);
|
|
77
|
+
} finally {
|
|
78
|
+
await this.apdu.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async enableProfile(iccid: string, refreshFlag = false): Promise<void> {
|
|
83
|
+
try {
|
|
84
|
+
const request = encodeEnableProfileRequest(iccid, refreshFlag);
|
|
85
|
+
const response = await this.apdu.sendCommand(request);
|
|
86
|
+
const result = decodeEnableProfileResponse(response);
|
|
87
|
+
if (result !== EnableProfileResult.Ok) {
|
|
88
|
+
throw new Es10Error(result, `enableProfile failed: ${EnableProfileResult[result] ?? result}`);
|
|
89
|
+
}
|
|
90
|
+
} finally {
|
|
91
|
+
await this.apdu.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async disableProfile(iccid: string, refreshFlag = false): Promise<void> {
|
|
96
|
+
try {
|
|
97
|
+
const request = encodeDisableProfileRequest(iccid, refreshFlag);
|
|
98
|
+
const response = await this.apdu.sendCommand(request);
|
|
99
|
+
const result = decodeDisableProfileResponse(response);
|
|
100
|
+
if (result !== DisableProfileResult.Ok) {
|
|
101
|
+
throw new Es10Error(result, `disableProfile failed: ${DisableProfileResult[result] ?? result}`);
|
|
102
|
+
}
|
|
103
|
+
} finally {
|
|
104
|
+
await this.apdu.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async deleteProfile(iccid: string): Promise<void> {
|
|
109
|
+
try {
|
|
110
|
+
const request = encodeDeleteProfileRequest(iccid);
|
|
111
|
+
const response = await this.apdu.sendCommand(request);
|
|
112
|
+
const result = decodeDeleteProfileResponse(response);
|
|
113
|
+
if (result !== DeleteProfileResult.Ok) {
|
|
114
|
+
throw new Es10Error(result, `deleteProfile failed: ${DeleteProfileResult[result] ?? result}`);
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
await this.apdu.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async installProfile(options: InstallProfileOptions): Promise<DownloadResult> {
|
|
122
|
+
const progress = (step: DownloadProgress['step'], detail?: string) => {
|
|
123
|
+
options.onProgress?.({ step, detail });
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
let transactionId: string | undefined;
|
|
127
|
+
let smdpAddress: string | undefined;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Step 1: Parse activation code
|
|
131
|
+
progress('parseActivationCode');
|
|
132
|
+
const ac = parseActivationCode(options.activationCode);
|
|
133
|
+
smdpAddress = ac.smdpAddress;
|
|
134
|
+
|
|
135
|
+
// Step 2: Get eUICC challenge
|
|
136
|
+
progress('getEuiccChallenge');
|
|
137
|
+
const challengeReq = encodeGetEuiccChallengeRequest();
|
|
138
|
+
const challengeResp = await this.apdu.sendCommand(challengeReq);
|
|
139
|
+
const euiccChallenge = decodeGetEuiccChallengeResponse(challengeResp);
|
|
140
|
+
|
|
141
|
+
// Step 3: Get eUICC info
|
|
142
|
+
progress('getEuiccInfo');
|
|
143
|
+
const info1Req = encodeGetEuiccInfo1Request();
|
|
144
|
+
const euiccInfo1 = await this.apdu.sendCommand(info1Req);
|
|
145
|
+
|
|
146
|
+
// Step 4: Initiate authentication with SM-DP+
|
|
147
|
+
progress('initiateAuth');
|
|
148
|
+
const initAuthResp = await this.es9plus.initiateAuthentication(smdpAddress, {
|
|
149
|
+
euiccChallenge,
|
|
150
|
+
euiccInfo1,
|
|
151
|
+
});
|
|
152
|
+
transactionId = initAuthResp.transactionId;
|
|
153
|
+
|
|
154
|
+
// Step 5: Authenticate server on eUICC
|
|
155
|
+
progress('authenticateServer');
|
|
156
|
+
const authServerReq = encodeAuthenticateServerRequest({
|
|
157
|
+
serverSigned1: initAuthResp.serverSigned1,
|
|
158
|
+
serverSignature1: initAuthResp.serverSignature1,
|
|
159
|
+
euiccCiPKIdToBeUsed: initAuthResp.euiccCiPKIdToBeUsed,
|
|
160
|
+
serverCertificate: initAuthResp.serverCertificate,
|
|
161
|
+
matchingId: ac.matchingId || undefined,
|
|
162
|
+
});
|
|
163
|
+
const authServerResp = await this.apdu.sendCommand(authServerReq);
|
|
164
|
+
const authServerDecoded = decodeAuthenticateServerResponse(authServerResp);
|
|
165
|
+
|
|
166
|
+
// Step 6: Authenticate client on SM-DP+
|
|
167
|
+
progress('authenticateClient');
|
|
168
|
+
const authClientResp = await this.es9plus.authenticateClient(smdpAddress, {
|
|
169
|
+
transactionId,
|
|
170
|
+
authenticateServerResponse: authServerDecoded.raw,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Step 7: Prepare download on eUICC
|
|
174
|
+
progress('prepareDownload');
|
|
175
|
+
let hashCc: Uint8Array | undefined;
|
|
176
|
+
if (options.confirmationCode) {
|
|
177
|
+
hashCc = await hashConfirmationCode(options.confirmationCode, transactionId);
|
|
178
|
+
}
|
|
179
|
+
const prepDlReq = encodePrepareDownloadRequest({
|
|
180
|
+
smdpSigned2: authClientResp.smdpSigned2,
|
|
181
|
+
smdpSignature2: authClientResp.smdpSignature2,
|
|
182
|
+
smdpCertificate: authClientResp.smdpCertificate,
|
|
183
|
+
hashCc,
|
|
184
|
+
});
|
|
185
|
+
const prepDlResp = await this.apdu.sendCommand(prepDlReq);
|
|
186
|
+
const prepDlDecoded = decodePrepareDownloadResponse(prepDlResp);
|
|
187
|
+
|
|
188
|
+
// Step 8: Get bound profile package from SM-DP+
|
|
189
|
+
progress('getBoundProfilePackage');
|
|
190
|
+
const bppResp = await this.es9plus.getBoundProfilePackage(smdpAddress, {
|
|
191
|
+
transactionId,
|
|
192
|
+
prepareDownloadResponse: prepDlDecoded.raw,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Step 9: Load bound profile package on eUICC
|
|
196
|
+
progress('loadProfile');
|
|
197
|
+
const bppResultRaw = await this.apdu.loadBoundProfilePackage(bppResp.boundProfilePackage);
|
|
198
|
+
|
|
199
|
+
// Parse ProfileInstallationResult from eUICC (if returned)
|
|
200
|
+
let installResult;
|
|
201
|
+
if (bppResultRaw && bppResultRaw.length > 0) {
|
|
202
|
+
try {
|
|
203
|
+
installResult = decodeBppInstallationResult(bppResultRaw);
|
|
204
|
+
if (!installResult.success) {
|
|
205
|
+
// Return structured failure instead of throwing
|
|
206
|
+
const errorName = BppInstallErrorCode[installResult.errorReasonCode ?? -1] ?? 'Unknown';
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
error: `Profile installation failed: ${errorName}`,
|
|
210
|
+
errorSubjectCode: installResult.errorSubjectCode,
|
|
211
|
+
errorReasonCode: installResult.errorReasonCode,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// If parsing fails but no error was thrown during BPP loading, continue
|
|
216
|
+
// The eUICC may not return a result in all cases
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Step 10: Complete
|
|
221
|
+
progress('complete');
|
|
222
|
+
// Prefer ICCID from eUICC result (confirms actual installation), fallback to ES9+ metadata
|
|
223
|
+
const iccid = installResult?.iccid ?? authClientResp.profileMetadata?.iccid;
|
|
224
|
+
return {
|
|
225
|
+
success: true,
|
|
226
|
+
iccid,
|
|
227
|
+
profileMetadata: authClientResp.profileMetadata
|
|
228
|
+
? {
|
|
229
|
+
serviceProviderName: authClientResp.profileMetadata.serviceProviderName,
|
|
230
|
+
profileName: authClientResp.profileMetadata.profileName,
|
|
231
|
+
}
|
|
232
|
+
: undefined,
|
|
233
|
+
};
|
|
234
|
+
} catch (error) {
|
|
235
|
+
// Attempt cancel session on failure (best-effort)
|
|
236
|
+
if (transactionId && smdpAddress) {
|
|
237
|
+
await this.cancelSessionBestEffort(smdpAddress, transactionId, error);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
success: false,
|
|
242
|
+
error: error instanceof Error ? error.message : String(error),
|
|
243
|
+
};
|
|
244
|
+
} finally {
|
|
245
|
+
await this.apdu.close();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async processNotifications(options?: ProcessNotificationsOptions): Promise<ProcessNotificationsResult> {
|
|
250
|
+
const listOnly = options?.listOnly ?? false;
|
|
251
|
+
const forceRemove = options?.forceRemove ?? false;
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
// Single eUICC call: retrieve all notifications with metadata and raw bytes
|
|
255
|
+
const request = encodeRetrieveNotificationsListRequest();
|
|
256
|
+
const response = await this.apdu.sendCommand(request);
|
|
257
|
+
const notifications = decodeRetrieveNotificationsListFull(response);
|
|
258
|
+
|
|
259
|
+
// Sort by seqNumber (eUICC may return in arbitrary order)
|
|
260
|
+
notifications.sort((a, b) => a.seqNumber - b.seqNumber);
|
|
261
|
+
|
|
262
|
+
// If listOnly, just return notifications with sendResult: null
|
|
263
|
+
if (listOnly) {
|
|
264
|
+
const results: NotificationWithResult[] = notifications.map((n) => ({
|
|
265
|
+
seqNumber: n.seqNumber,
|
|
266
|
+
operation: n.operation,
|
|
267
|
+
notificationAddress: n.notificationAddress,
|
|
268
|
+
iccid: n.iccid,
|
|
269
|
+
success: n.success,
|
|
270
|
+
errorSubjectCode: n.errorSubjectCode,
|
|
271
|
+
errorReasonCode: n.errorReasonCode,
|
|
272
|
+
sendResult: null,
|
|
273
|
+
}));
|
|
274
|
+
return { notifications: results };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const results: NotificationWithResult[] = [];
|
|
278
|
+
let sent = 0;
|
|
279
|
+
let failed = 0;
|
|
280
|
+
|
|
281
|
+
// Process each notification - raw bytes already available from initial call
|
|
282
|
+
for (const notification of notifications) {
|
|
283
|
+
const sendResult: SendResult = {
|
|
284
|
+
sent: false,
|
|
285
|
+
removed: false,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
// Send raw PendingNotification bytes to SM-DP+
|
|
290
|
+
await this.es9plus.handleNotification(notification.notificationAddress, notification.raw);
|
|
291
|
+
sendResult.sent = true;
|
|
292
|
+
sent++;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
sendResult.error = error instanceof Error ? error.message : String(error);
|
|
295
|
+
failed++;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Remove from eUICC if successfully sent, or if forceRemove is enabled
|
|
299
|
+
if (sendResult.sent || forceRemove) {
|
|
300
|
+
try {
|
|
301
|
+
const removeReq = encodeRemoveNotificationRequest(notification.seqNumber);
|
|
302
|
+
await this.apdu.sendCommand(removeReq);
|
|
303
|
+
sendResult.removed = true;
|
|
304
|
+
} catch {
|
|
305
|
+
// Best-effort removal
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
results.push({
|
|
310
|
+
seqNumber: notification.seqNumber,
|
|
311
|
+
operation: notification.operation,
|
|
312
|
+
notificationAddress: notification.notificationAddress,
|
|
313
|
+
iccid: notification.iccid,
|
|
314
|
+
success: notification.success,
|
|
315
|
+
errorSubjectCode: notification.errorSubjectCode,
|
|
316
|
+
errorReasonCode: notification.errorReasonCode,
|
|
317
|
+
sendResult,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return { notifications: results, sent, failed };
|
|
322
|
+
} finally {
|
|
323
|
+
await this.apdu.close();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private async cancelSessionBestEffort(smdpAddress: string, transactionId: string, _error: unknown): Promise<void> {
|
|
328
|
+
try {
|
|
329
|
+
const reason = CancelSessionReason.LoadBppExecutionError;
|
|
330
|
+
const txIdBytes = hexToBytes(transactionId);
|
|
331
|
+
const cancelReq = encodeCancelSessionRequest(txIdBytes, reason);
|
|
332
|
+
const cancelResp = await this.apdu.sendCommand(cancelReq);
|
|
333
|
+
await this.es9plus.cancelSession(smdpAddress, transactionId, cancelResp);
|
|
334
|
+
} catch {
|
|
335
|
+
// Best-effort — ignore cancel failures
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
341
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
342
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
343
|
+
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
344
|
+
}
|
|
345
|
+
return bytes;
|
|
346
|
+
}
|
package/src/tlv.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { TlvError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
export interface TlvNode {
|
|
4
|
+
tag: number;
|
|
5
|
+
value: Uint8Array;
|
|
6
|
+
children?: TlvNode[];
|
|
7
|
+
raw?: Uint8Array;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Tags known to be constructed (have children)
|
|
11
|
+
const CONSTRUCTED_TAGS = new Set([
|
|
12
|
+
0xa0, 0xa1, 0xa2, 0xa3, 0xe3,
|
|
13
|
+
0xbf20, 0xbf21, 0xbf22, 0xbf28, 0xbf29, 0xbf2b, 0xbf2d, 0xbf2e, 0xbf2f,
|
|
14
|
+
0xbf30, 0xbf31, 0xbf32, 0xbf33, 0xbf34, 0xbf36, 0xbf38, 0xbf3e, 0xbf41, 0xbf43,
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function isConstructed(tag: number): boolean {
|
|
18
|
+
if (CONSTRUCTED_TAGS.has(tag)) return true;
|
|
19
|
+
// Check constructed bit (bit 5 of the first byte)
|
|
20
|
+
const firstByte = tag > 0xff ? (tag >> 8) & 0xff : tag;
|
|
21
|
+
return (firstByte & 0x20) !== 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readTag(data: Uint8Array, offset: number): [number, number] {
|
|
25
|
+
if (offset >= data.length) throw new TlvError('Unexpected end of data reading tag');
|
|
26
|
+
let tag = data[offset++];
|
|
27
|
+
if ((tag & 0x1f) === 0x1f) {
|
|
28
|
+
// Multi-byte tag
|
|
29
|
+
if (offset >= data.length) throw new TlvError('Unexpected end of data reading multi-byte tag');
|
|
30
|
+
let next = data[offset++];
|
|
31
|
+
tag = (tag << 8) | next;
|
|
32
|
+
if (next & 0x80) {
|
|
33
|
+
// Three-byte tag
|
|
34
|
+
if (offset >= data.length) throw new TlvError('Unexpected end of data reading 3-byte tag');
|
|
35
|
+
next = data[offset++];
|
|
36
|
+
tag = (tag << 8) | next;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return [tag, offset];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readLength(data: Uint8Array, offset: number): [number, number] {
|
|
43
|
+
if (offset >= data.length) throw new TlvError('Unexpected end of data reading length');
|
|
44
|
+
const first = data[offset++];
|
|
45
|
+
if (first < 0x80) return [first, offset];
|
|
46
|
+
const numBytes = first & 0x7f;
|
|
47
|
+
if (numBytes === 0) throw new TlvError('Indefinite length not supported');
|
|
48
|
+
if (numBytes > 3) throw new TlvError(`Length field too large: ${numBytes} bytes`);
|
|
49
|
+
if (offset + numBytes > data.length) throw new TlvError('Unexpected end of data reading length');
|
|
50
|
+
let length = 0;
|
|
51
|
+
for (let i = 0; i < numBytes; i++) {
|
|
52
|
+
length = (length << 8) | data[offset++];
|
|
53
|
+
}
|
|
54
|
+
return [length, offset];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function encodeTag(tag: number): Uint8Array {
|
|
58
|
+
if (tag <= 0xff) return new Uint8Array([tag]);
|
|
59
|
+
if (tag <= 0xffff) return new Uint8Array([(tag >> 8) & 0xff, tag & 0xff]);
|
|
60
|
+
return new Uint8Array([(tag >> 16) & 0xff, (tag >> 8) & 0xff, tag & 0xff]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function encodeLength(length: number): Uint8Array {
|
|
64
|
+
if (length < 0x80) return new Uint8Array([length]);
|
|
65
|
+
if (length <= 0xff) return new Uint8Array([0x81, length]);
|
|
66
|
+
if (length <= 0xffff) return new Uint8Array([0x82, (length >> 8) & 0xff, length & 0xff]);
|
|
67
|
+
return new Uint8Array([0x83, (length >> 16) & 0xff, (length >> 8) & 0xff, length & 0xff]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function tlvEncode(tag: number, value: Uint8Array): Uint8Array {
|
|
71
|
+
const tagBytes = encodeTag(tag);
|
|
72
|
+
const lengthBytes = encodeLength(value.length);
|
|
73
|
+
return tlvConcat(tagBytes, lengthBytes, value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function tlvDecode(data: Uint8Array, offset = 0): TlvNode {
|
|
77
|
+
const rawStart = offset;
|
|
78
|
+
const [tag, afterTag] = readTag(data, offset);
|
|
79
|
+
const [length, afterLength] = readLength(data, afterTag);
|
|
80
|
+
if (afterLength + length > data.length) {
|
|
81
|
+
throw new TlvError(`TLV length ${length} exceeds available data (${data.length - afterLength} bytes)`);
|
|
82
|
+
}
|
|
83
|
+
const value = data.slice(afterLength, afterLength + length);
|
|
84
|
+
const raw = data.slice(rawStart, afterLength + length);
|
|
85
|
+
|
|
86
|
+
let children: TlvNode[] | undefined;
|
|
87
|
+
if (isConstructed(tag) && length > 0) {
|
|
88
|
+
children = [];
|
|
89
|
+
let childOffset = 0;
|
|
90
|
+
while (childOffset < value.length) {
|
|
91
|
+
const child = tlvDecode(value, childOffset);
|
|
92
|
+
children.push(child);
|
|
93
|
+
const childRawLen = child.raw!.length;
|
|
94
|
+
childOffset += childRawLen;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { tag, value, children, raw };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function tlvDecodeAll(data: Uint8Array): TlvNode[] {
|
|
102
|
+
const nodes: TlvNode[] = [];
|
|
103
|
+
let offset = 0;
|
|
104
|
+
while (offset < data.length) {
|
|
105
|
+
const node = tlvDecode(data, offset);
|
|
106
|
+
nodes.push(node);
|
|
107
|
+
offset += node.raw!.length;
|
|
108
|
+
}
|
|
109
|
+
return nodes;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function tlvFind(node: TlvNode, tag: number): TlvNode | undefined {
|
|
113
|
+
if (!node.children) return undefined;
|
|
114
|
+
return node.children.find((c) => c.tag === tag);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function tlvFindAll(node: TlvNode, tag: number): TlvNode[] {
|
|
118
|
+
if (!node.children) return [];
|
|
119
|
+
return node.children.filter((c) => c.tag === tag);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function tlvConcat(...parts: Uint8Array[]): Uint8Array {
|
|
123
|
+
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
124
|
+
const result = new Uint8Array(totalLength);
|
|
125
|
+
let offset = 0;
|
|
126
|
+
for (const part of parts) {
|
|
127
|
+
result.set(part, offset);
|
|
128
|
+
offset += part.length;
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function tlvBuildTagList(tags: number[]): Uint8Array {
|
|
134
|
+
const tagBytes: Uint8Array[] = tags.map(encodeTag);
|
|
135
|
+
const value = tlvConcat(...tagBytes);
|
|
136
|
+
return tlvEncode(0x5c, value);
|
|
137
|
+
}
|