@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/apdu.ts
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import type { DeviceAdapter } from './types.js';
|
|
2
|
+
import { DeviceError } from './errors.js';
|
|
3
|
+
import { tlvDecode, tlvFind, tlvConcat, type TlvNode } from './tlv.js';
|
|
4
|
+
|
|
5
|
+
// ISD-R AID per SGP.22
|
|
6
|
+
const ISD_R_AID = new Uint8Array([
|
|
7
|
+
0xa0, 0x00, 0x00, 0x05, 0x59, 0x10, 0x10,
|
|
8
|
+
0xff, 0xff, 0xff, 0xff, 0x89, 0x00, 0x00, 0x01, 0x00,
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const STORE_DATA_MSS = 255; // max APDU data size per SGP.22
|
|
12
|
+
|
|
13
|
+
// BPP segment tags (children of BF36 BoundProfilePackage)
|
|
14
|
+
const TAG_BPP_INIT_SECURE_CHANNEL = 0xbf23;
|
|
15
|
+
const TAG_BPP_FIRST_SEQ_OF_87 = 0xa0;
|
|
16
|
+
const TAG_BPP_SEQ_OF_88 = 0xa1;
|
|
17
|
+
const TAG_BPP_SECOND_SEQ_OF_87 = 0xa2;
|
|
18
|
+
const TAG_BPP_SEQ_OF_86 = 0xa3;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* APDU transport layer — handles logical channel management, STORE DATA
|
|
22
|
+
* chunking, GET RESPONSE chaining, and BPP segmentation over a DeviceAdapter
|
|
23
|
+
* that sends individual APDUs.
|
|
24
|
+
*/
|
|
25
|
+
export class ApduTransport {
|
|
26
|
+
private device: DeviceAdapter;
|
|
27
|
+
private channel = -1;
|
|
28
|
+
|
|
29
|
+
constructor(device: DeviceAdapter) {
|
|
30
|
+
this.device = device;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Send a complete SGP.22 TLV payload to the eUICC and return the complete
|
|
35
|
+
* TLV response. Handles STORE DATA chunking and GET RESPONSE chaining.
|
|
36
|
+
*/
|
|
37
|
+
async sendCommand(data: Uint8Array): Promise<Uint8Array> {
|
|
38
|
+
await this.ensureChannel();
|
|
39
|
+
return this.storeDataAndGetResponse(data);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load BPP per SGP.22 Section 2.5.5 Segmented BPP loading.
|
|
44
|
+
*
|
|
45
|
+
* Segmentation order:
|
|
46
|
+
* 1. BF36 header + BF23 (InitialiseSecureChannel)
|
|
47
|
+
* 2. A0 complete (firstSequenceOf87)
|
|
48
|
+
* 3. A1 header only (sequenceOf88)
|
|
49
|
+
* 4. Each 88 TLV individually
|
|
50
|
+
* 5. A2 complete if present (secondSequenceOf87)
|
|
51
|
+
* 6. A3 header only (sequenceOf86)
|
|
52
|
+
* 7. Each 86 TLV individually
|
|
53
|
+
*
|
|
54
|
+
* Critical: P1=0x91 only on the VERY LAST APDU of entire BPP.
|
|
55
|
+
* Block number (P2) resets at each segment boundary.
|
|
56
|
+
*/
|
|
57
|
+
async loadBoundProfilePackage(bpp: Uint8Array): Promise<Uint8Array | undefined> {
|
|
58
|
+
await this.ensureChannel();
|
|
59
|
+
|
|
60
|
+
const bppNode = tlvDecode(bpp);
|
|
61
|
+
if (bppNode.tag !== 0xbf36) {
|
|
62
|
+
throw new DeviceError(-1, `Expected BPP tag BF36, got ${bppNode.tag.toString(16)}`);
|
|
63
|
+
}
|
|
64
|
+
if (!bppNode.children || bppNode.children.length === 0) {
|
|
65
|
+
throw new DeviceError(-1, 'BPP has no children');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Extract required children
|
|
69
|
+
const initSecureChannel = tlvFind(bppNode, TAG_BPP_INIT_SECURE_CHANNEL);
|
|
70
|
+
const firstSeq87 = tlvFind(bppNode, TAG_BPP_FIRST_SEQ_OF_87);
|
|
71
|
+
const seq88 = tlvFind(bppNode, TAG_BPP_SEQ_OF_88);
|
|
72
|
+
const secondSeq87 = tlvFind(bppNode, TAG_BPP_SECOND_SEQ_OF_87);
|
|
73
|
+
const seq86 = tlvFind(bppNode, TAG_BPP_SEQ_OF_86);
|
|
74
|
+
|
|
75
|
+
if (!initSecureChannel || !firstSeq87 || !seq88 || !seq86) {
|
|
76
|
+
throw new DeviceError(-1, 'Invalid BPP structure: missing required segments');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Build list of segments per SGP.22 Section 2.5.5
|
|
80
|
+
const segments: Uint8Array[] = [];
|
|
81
|
+
|
|
82
|
+
// Segment 1: BF36 header + BF23 (InitialiseSecureChannel)
|
|
83
|
+
segments.push(this.buildBppSegment1(bppNode, initSecureChannel));
|
|
84
|
+
|
|
85
|
+
// Segment 2: A0 complete (firstSequenceOf87)
|
|
86
|
+
if (firstSeq87.raw) {
|
|
87
|
+
segments.push(firstSeq87.raw);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Segment 3: A1 header only (sequenceOf88)
|
|
91
|
+
segments.push(this.buildSequenceHeader(seq88));
|
|
92
|
+
|
|
93
|
+
// Segment 4-N: Each 88 TLV individually
|
|
94
|
+
if (seq88.children) {
|
|
95
|
+
for (const child of seq88.children) {
|
|
96
|
+
if (child.raw) {
|
|
97
|
+
segments.push(child.raw);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Segment N+1: A2 complete if present (secondSequenceOf87)
|
|
103
|
+
if (secondSeq87?.raw) {
|
|
104
|
+
segments.push(secondSeq87.raw);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Segment N+2: A3 header only (sequenceOf86)
|
|
108
|
+
segments.push(this.buildSequenceHeader(seq86));
|
|
109
|
+
|
|
110
|
+
// Remaining segments: Each 86 TLV individually
|
|
111
|
+
if (seq86.children) {
|
|
112
|
+
for (const child of seq86.children) {
|
|
113
|
+
if (child.raw) {
|
|
114
|
+
segments.push(child.raw);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Send all segments with proper P1 handling
|
|
120
|
+
// Returns ProfileInstallationResult (BF37) from the last APDU
|
|
121
|
+
return await this.sendBppSegments(segments);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build Segment 1: BF36 header (tag+length) + complete BF23
|
|
126
|
+
*/
|
|
127
|
+
private buildBppSegment1(bppNode: TlvNode, initSecureChannel: TlvNode): Uint8Array {
|
|
128
|
+
if (!bppNode.raw || !initSecureChannel.raw) {
|
|
129
|
+
throw new DeviceError(-1, 'BPP or InitialiseSecureChannel missing raw data');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// BF36 header = raw minus value
|
|
133
|
+
const headerSize = bppNode.raw.length - bppNode.value.length;
|
|
134
|
+
const bppHeader = bppNode.raw.slice(0, headerSize);
|
|
135
|
+
|
|
136
|
+
return tlvConcat(bppHeader, initSecureChannel.raw);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Build sequence header (tag+length only, no children)
|
|
141
|
+
*/
|
|
142
|
+
private buildSequenceHeader(node: TlvNode): Uint8Array {
|
|
143
|
+
if (!node.raw) {
|
|
144
|
+
throw new DeviceError(-1, 'Node missing raw data');
|
|
145
|
+
}
|
|
146
|
+
const headerSize = node.raw.length - node.value.length;
|
|
147
|
+
return node.raw.slice(0, headerSize);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Send BPP segments with correct P1 handling:
|
|
152
|
+
* - P1=0x11 for all APDUs except the very last
|
|
153
|
+
* - P1=0x91 only on the VERY LAST APDU of the entire BPP
|
|
154
|
+
* - P2 resets at each segment boundary
|
|
155
|
+
*
|
|
156
|
+
* Returns the ProfileInstallationResult (BF37) from the last APDU response.
|
|
157
|
+
*/
|
|
158
|
+
private async sendBppSegments(segments: Uint8Array[]): Promise<Uint8Array | undefined> {
|
|
159
|
+
const cla = this.gpCla();
|
|
160
|
+
let installationResult: Uint8Array | undefined;
|
|
161
|
+
|
|
162
|
+
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
163
|
+
const segment = segments[segIdx];
|
|
164
|
+
const isLastSegment = segIdx === segments.length - 1;
|
|
165
|
+
const numChunks = Math.max(1, Math.ceil(segment.length / STORE_DATA_MSS));
|
|
166
|
+
|
|
167
|
+
for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
|
|
168
|
+
const offset = chunkIdx * STORE_DATA_MSS;
|
|
169
|
+
const chunkLen = Math.min(STORE_DATA_MSS, segment.length - offset);
|
|
170
|
+
const isLastChunk = chunkIdx === numChunks - 1;
|
|
171
|
+
const isVeryLastApdu = isLastSegment && isLastChunk;
|
|
172
|
+
|
|
173
|
+
const apdu = new Uint8Array(5 + chunkLen);
|
|
174
|
+
apdu[0] = cla;
|
|
175
|
+
apdu[1] = 0xe2; // STORE DATA
|
|
176
|
+
apdu[2] = isVeryLastApdu ? 0x91 : 0x11; // P1: only 0x91 on very last APDU
|
|
177
|
+
apdu[3] = chunkIdx & 0xff; // P2: resets at each segment
|
|
178
|
+
apdu[4] = chunkLen;
|
|
179
|
+
apdu.set(segment.subarray(offset, offset + chunkLen), 5);
|
|
180
|
+
|
|
181
|
+
const resp = await this.device.sendApdu(apdu);
|
|
182
|
+
if (resp.length < 2) {
|
|
183
|
+
throw new DeviceError(-1, `BPP STORE DATA: response too short (segment ${segIdx}, chunk ${chunkIdx})`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let sw1 = resp[resp.length - 2];
|
|
187
|
+
let sw2 = resp[resp.length - 1];
|
|
188
|
+
|
|
189
|
+
// Collect response data for ProfileInstallationResult
|
|
190
|
+
const responseChunks: Uint8Array[] = [];
|
|
191
|
+
const initialData = resp.subarray(0, resp.length - 2);
|
|
192
|
+
if (initialData.length > 0) {
|
|
193
|
+
responseChunks.push(initialData);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Handle SW=61XX (more data available) by draining with GET RESPONSE
|
|
197
|
+
while (sw1 === 0x61) {
|
|
198
|
+
const getResp = await this.device.sendApdu(new Uint8Array([cla, 0xc0, 0x00, 0x00, sw2]));
|
|
199
|
+
if (getResp.length < 2) {
|
|
200
|
+
throw new DeviceError(-1, `BPP GET RESPONSE too short (segment ${segIdx}, chunk ${chunkIdx})`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const respData = getResp.subarray(0, getResp.length - 2);
|
|
204
|
+
if (respData.length > 0) {
|
|
205
|
+
responseChunks.push(respData);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
sw1 = getResp[getResp.length - 2];
|
|
209
|
+
sw2 = getResp[getResp.length - 1];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Per SGP.22: accept SW 90XX or 91XX
|
|
213
|
+
if (sw1 !== 0x90 && sw1 !== 0x91) {
|
|
214
|
+
throw new DeviceError(-1, `BPP segment ${segIdx} chunk ${chunkIdx} failed: SW=${sw1.toString(16)}${sw2.toString(16)}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Collect response data if present
|
|
218
|
+
if (responseChunks.length > 0) {
|
|
219
|
+
const totalLen = responseChunks.reduce((sum, c) => sum + c.length, 0);
|
|
220
|
+
installationResult = new Uint8Array(totalLen);
|
|
221
|
+
let pos = 0;
|
|
222
|
+
for (const chunk of responseChunks) {
|
|
223
|
+
installationResult.set(chunk, pos);
|
|
224
|
+
pos += chunk.length;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// If we got a ProfileInstallationResult (BF37), stop sending - eUICC has responded
|
|
228
|
+
// This handles both early errors and final success/failure
|
|
229
|
+
if (this.isBppResult(installationResult)) {
|
|
230
|
+
return installationResult;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return installationResult;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if BPP response data contains a ProfileInstallationResult (BF37).
|
|
241
|
+
* Returns true if BF37 is detected (caller should stop sending and return the data).
|
|
242
|
+
*/
|
|
243
|
+
private isBppResult(data: Uint8Array): boolean {
|
|
244
|
+
// Look for BF37 tag (ProfileInstallationResult)
|
|
245
|
+
// BF37 = 0xBF 0x37
|
|
246
|
+
if (data.length < 4) return false;
|
|
247
|
+
return data[0] === 0xbf && data[1] === 0x37;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Close the logical channel if open.
|
|
252
|
+
*/
|
|
253
|
+
async close(): Promise<void> {
|
|
254
|
+
if (this.channel >= 0) {
|
|
255
|
+
await this.closeChannel(this.channel);
|
|
256
|
+
this.channel = -1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Logical channel management
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
private async ensureChannel(): Promise<void> {
|
|
265
|
+
if (this.channel >= 0) return;
|
|
266
|
+
|
|
267
|
+
let ch = await this.openChannel();
|
|
268
|
+
if (ch < 0) {
|
|
269
|
+
// Stale channels from a previous run — close all and retry
|
|
270
|
+
await this.closeAllChannels();
|
|
271
|
+
ch = await this.openChannel();
|
|
272
|
+
if (ch < 0) {
|
|
273
|
+
throw new DeviceError(-1, 'Failed to open logical channel');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await this.selectIsdR(ch);
|
|
278
|
+
this.channel = ch;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private async openChannel(): Promise<number> {
|
|
282
|
+
// MANAGE CHANNEL OPEN: 00 70 00 00 01
|
|
283
|
+
const resp = await this.device.sendApdu(new Uint8Array([0x00, 0x70, 0x00, 0x00, 0x01]));
|
|
284
|
+
if (resp.length < 3) return -1;
|
|
285
|
+
|
|
286
|
+
const sw1 = resp[resp.length - 2];
|
|
287
|
+
const sw2 = resp[resp.length - 1];
|
|
288
|
+
if (sw1 !== 0x90 || sw2 !== 0x00) return -1;
|
|
289
|
+
|
|
290
|
+
return resp[0];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async selectIsdR(channel: number): Promise<void> {
|
|
294
|
+
// SELECT: CLA=channel, INS=A4, P1=04, P2=00, Lc=AID_len, Data=AID, Le=00
|
|
295
|
+
const cmd = new Uint8Array(5 + ISD_R_AID.length + 1);
|
|
296
|
+
cmd[0] = channel <= 3 ? (0x00 | channel) : (0x40 | (channel - 4));
|
|
297
|
+
cmd[1] = 0xa4;
|
|
298
|
+
cmd[2] = 0x04;
|
|
299
|
+
cmd[3] = 0x00;
|
|
300
|
+
cmd[4] = ISD_R_AID.length;
|
|
301
|
+
cmd.set(ISD_R_AID, 5);
|
|
302
|
+
cmd[5 + ISD_R_AID.length] = 0x00; // Le
|
|
303
|
+
|
|
304
|
+
const resp = await this.device.sendApdu(cmd);
|
|
305
|
+
if (resp.length < 2) {
|
|
306
|
+
throw new DeviceError(-1, 'SELECT ISD-R: response too short');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const sw1 = resp[resp.length - 2];
|
|
310
|
+
if (sw1 !== 0x90 && sw1 !== 0x61) {
|
|
311
|
+
const sw2 = resp[resp.length - 1];
|
|
312
|
+
throw new DeviceError(-1, `SELECT ISD-R failed: SW=${sw1.toString(16)}${sw2.toString(16)}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private async closeChannel(channel: number): Promise<void> {
|
|
317
|
+
// MANAGE CHANNEL CLOSE
|
|
318
|
+
await this.device.sendApdu(new Uint8Array([0x00, 0x70, 0x80, channel, 0x00]));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private async closeAllChannels(): Promise<void> {
|
|
322
|
+
for (let ch = 1; ch <= 3; ch++) {
|
|
323
|
+
try {
|
|
324
|
+
await this.closeChannel(ch);
|
|
325
|
+
} catch {
|
|
326
|
+
// Ignore errors — channel may not be open
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// STORE DATA + GET RESPONSE
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* GlobalPlatform CLA byte for the current logical channel.
|
|
337
|
+
*/
|
|
338
|
+
private gpCla(): number {
|
|
339
|
+
return this.channel <= 3 ? (0x80 | this.channel) : (0xc0 | (this.channel - 4));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Send data via STORE DATA chunking and collect the response via
|
|
344
|
+
* GET RESPONSE chaining.
|
|
345
|
+
*/
|
|
346
|
+
private async storeDataAndGetResponse(data: Uint8Array): Promise<Uint8Array> {
|
|
347
|
+
const cla = this.gpCla();
|
|
348
|
+
const numChunks = Math.max(1, Math.ceil(data.length / STORE_DATA_MSS));
|
|
349
|
+
|
|
350
|
+
let lastResp: Uint8Array = new Uint8Array(0);
|
|
351
|
+
|
|
352
|
+
for (let i = 0; i < numChunks; i++) {
|
|
353
|
+
const offset = i * STORE_DATA_MSS;
|
|
354
|
+
const chunkLen = Math.min(STORE_DATA_MSS, data.length - offset);
|
|
355
|
+
const isLast = i === numChunks - 1;
|
|
356
|
+
|
|
357
|
+
const apdu = new Uint8Array(5 + chunkLen);
|
|
358
|
+
apdu[0] = cla;
|
|
359
|
+
apdu[1] = 0xe2; // STORE DATA
|
|
360
|
+
apdu[2] = isLast ? 0x91 : 0x11; // P1: last or continue
|
|
361
|
+
apdu[3] = i & 0xff; // P2: sequence number
|
|
362
|
+
apdu[4] = chunkLen;
|
|
363
|
+
apdu.set(data.subarray(offset, offset + chunkLen), 5);
|
|
364
|
+
|
|
365
|
+
lastResp = await this.device.sendApdu(apdu);
|
|
366
|
+
if (lastResp.length < 2) {
|
|
367
|
+
throw new DeviceError(-1, 'STORE DATA: response too short');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!isLast) {
|
|
371
|
+
const sw1 = lastResp[lastResp.length - 2];
|
|
372
|
+
const sw2 = lastResp[lastResp.length - 1];
|
|
373
|
+
if (sw1 !== 0x90 || sw2 !== 0x00) {
|
|
374
|
+
throw new DeviceError(-1, `STORE DATA chunk ${i} failed: SW=${sw1.toString(16)}${sw2.toString(16)}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Collect response data from last STORE DATA and GET RESPONSE chains
|
|
380
|
+
const parts: Uint8Array[] = [];
|
|
381
|
+
|
|
382
|
+
// Data from last STORE DATA (excluding SW)
|
|
383
|
+
if (lastResp.length > 2) {
|
|
384
|
+
parts.push(lastResp.subarray(0, lastResp.length - 2));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let sw1 = lastResp[lastResp.length - 2];
|
|
388
|
+
let sw2 = lastResp[lastResp.length - 1];
|
|
389
|
+
|
|
390
|
+
// GET RESPONSE chaining
|
|
391
|
+
while (sw1 === 0x61) {
|
|
392
|
+
const getResp = await this.device.sendApdu(new Uint8Array([cla, 0xc0, 0x00, 0x00, sw2]));
|
|
393
|
+
if (getResp.length < 2) {
|
|
394
|
+
throw new DeviceError(-1, 'GET RESPONSE: response too short');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (getResp.length > 2) {
|
|
398
|
+
parts.push(getResp.subarray(0, getResp.length - 2));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
sw1 = getResp[getResp.length - 2];
|
|
402
|
+
sw2 = getResp[getResp.length - 1];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (sw1 !== 0x90) {
|
|
406
|
+
throw new DeviceError(-1, `Final SW=${sw1.toString(16)}${sw2.toString(16)}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Concatenate all response parts
|
|
410
|
+
const totalLen = parts.reduce((sum, p) => sum + p.length, 0);
|
|
411
|
+
const result = new Uint8Array(totalLen);
|
|
412
|
+
let pos = 0;
|
|
413
|
+
for (const part of parts) {
|
|
414
|
+
result.set(part, pos);
|
|
415
|
+
pos += part.length;
|
|
416
|
+
}
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export class EsimError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
|
|
4
|
+
constructor(code: string, message: string) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'EsimError';
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class DeviceError extends EsimError {
|
|
12
|
+
readonly result: number;
|
|
13
|
+
|
|
14
|
+
constructor(result: number, message?: string) {
|
|
15
|
+
super('DEVICE_ERROR', message ?? `Device error: ${result}`);
|
|
16
|
+
this.name = 'DeviceError';
|
|
17
|
+
this.result = result;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class Es10Error extends EsimError {
|
|
22
|
+
readonly resultCode: number;
|
|
23
|
+
|
|
24
|
+
constructor(resultCode: number, message?: string) {
|
|
25
|
+
super('ES10_ERROR', message ?? `eUICC error: ${resultCode}`);
|
|
26
|
+
this.name = 'Es10Error';
|
|
27
|
+
this.resultCode = resultCode;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class Es9PlusError extends EsimError {
|
|
32
|
+
readonly statusCode: number;
|
|
33
|
+
readonly serverStatus?: string;
|
|
34
|
+
readonly subjectCode?: string;
|
|
35
|
+
readonly reasonCode?: string;
|
|
36
|
+
|
|
37
|
+
constructor(params: {
|
|
38
|
+
statusCode: number;
|
|
39
|
+
serverStatus?: string;
|
|
40
|
+
subjectCode?: string;
|
|
41
|
+
reasonCode?: string;
|
|
42
|
+
message?: string;
|
|
43
|
+
}) {
|
|
44
|
+
super(
|
|
45
|
+
'ES9PLUS_ERROR',
|
|
46
|
+
params.message ??
|
|
47
|
+
`SM-DP+ error: HTTP ${params.statusCode}${params.serverStatus ? ` (${params.serverStatus})` : ''}`,
|
|
48
|
+
);
|
|
49
|
+
this.name = 'Es9PlusError';
|
|
50
|
+
this.statusCode = params.statusCode;
|
|
51
|
+
this.serverStatus = params.serverStatus;
|
|
52
|
+
this.subjectCode = params.subjectCode;
|
|
53
|
+
this.reasonCode = params.reasonCode;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class TlvError extends EsimError {
|
|
58
|
+
constructor(message: string) {
|
|
59
|
+
super('TLV_ERROR', message);
|
|
60
|
+
this.name = 'TlvError';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class ActivationCodeError extends EsimError {
|
|
65
|
+
constructor(message: string) {
|
|
66
|
+
super('ACTIVATION_CODE_ERROR', message);
|
|
67
|
+
this.name = 'ActivationCodeError';
|
|
68
|
+
}
|
|
69
|
+
}
|