@ledgerhq/hw-app-str 6.29.0 → 7.0.0-next.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/src/Str.ts CHANGED
@@ -15,301 +15,235 @@
15
15
  * limitations under the License.
16
16
  ********************************************************************************/
17
17
  import type Transport from "@ledgerhq/hw-transport";
18
+ import BIPPath from "bip32-path";
18
19
  import {
19
- splitPath,
20
- foreach,
21
- encodeEd25519PublicKey,
22
- verifyEd25519Signature,
23
- checkStellarBip32Path,
24
- hash,
25
- } from "./utils";
20
+ StellarHashSigningNotEnabledError,
21
+ StellarDataParsingFailedError,
22
+ StellarUserRefusedError,
23
+ StellarDataTooLargeError,
24
+ } from "./errors";
25
+
26
26
  const CLA = 0xe0;
27
+ const P1_FIRST = 0x00;
28
+ const P1_MORE = 0x80;
29
+ const P2_LAST = 0x00;
30
+ const P2_MORE = 0x80;
31
+ const P2_NON_CONFIRM = 0x00; // for getPublicKey
32
+ const P2_CONFIRM = 0x01; // for getPublicKey
33
+
27
34
  const INS_GET_PK = 0x02;
28
35
  const INS_SIGN_TX = 0x04;
29
36
  const INS_GET_CONF = 0x06;
30
- const INS_SIGN_TX_HASH = 0x08;
31
- const INS_KEEP_ALIVE = 0x10;
32
- const APDU_MAX_SIZE = 150;
33
- const P1_FIRST_APDU = 0x00;
34
- const P1_MORE_APDU = 0x80;
35
- const P2_LAST_APDU = 0x00;
36
- const P2_MORE_APDU = 0x80;
37
- const SW_OK = 0x9000;
38
- const SW_CANCEL = 0x6985;
39
- const SW_UNKNOWN_OP = 0x6c24;
40
- const SW_MULTI_OP = 0x6c25;
41
- const SW_NOT_ALLOWED = 0x6c66;
42
- const SW_UNSUPPORTED = 0x6d00;
43
- const SW_KEEP_ALIVE = 0x6e02;
44
- const TX_MAX_SIZE = 1540;
37
+ const INS_SIGN_HASH = 0x08;
38
+ const INS_SIGN_SOROBAN_AUTHORIZATION = 0x0a;
39
+
40
+ const APDU_MAX_PAYLOAD = 255;
41
+
42
+ const SW_DENY = 0x6985;
43
+ const SW_HASH_SIGNING_MODE_NOT_ENABLED = 0x6c66;
44
+ const SW_DATA_TOO_LARGE = 0xb004;
45
+ const SW_DATA_PARSING_FAIL = 0xb005;
46
+
45
47
  /**
46
48
  * Stellar API
47
49
  *
50
+ * @param transport a transport for sending commands to a device
51
+ * @param scrambleKey a scramble key
52
+ *
48
53
  * @example
49
54
  * import Str from "@ledgerhq/hw-app-str";
50
55
  * const str = new Str(transport)
51
56
  */
52
-
53
57
  export default class Str {
54
- transport: Transport;
58
+ private transport: Transport;
55
59
 
56
60
  constructor(transport: Transport, scrambleKey = "l0v") {
57
61
  this.transport = transport;
58
62
  transport.decorateAppAPIMethods(
59
63
  this,
60
- ["getAppConfiguration", "getPublicKey", "signTransaction", "signHash"],
64
+ [
65
+ "getAppConfiguration",
66
+ "getPublicKey",
67
+ "signTransaction",
68
+ "signSorobanAuthorization",
69
+ "signHash",
70
+ ],
61
71
  scrambleKey,
62
72
  );
63
73
  }
64
74
 
65
- getAppConfiguration(): Promise<{
75
+ /**
76
+ * Get Stellar application configuration.
77
+ *
78
+ * @returns an object with the application configuration, including the version,
79
+ * whether hash signing is enabled, and the maximum data size in bytes that the device can sign.
80
+ * @example
81
+ * str.getAppConfiguration().then(o => o.version)
82
+ */
83
+ async getAppConfiguration(): Promise<{
66
84
  version: string;
85
+ hashSigningEnabled: boolean;
86
+ maxDataSize?: number;
67
87
  }> {
68
- return this.transport.send(CLA, INS_GET_CONF, 0x00, 0x00).then(response => {
69
- const multiOpsEnabled = response[0] === 0x01 || response[1] < 0x02;
70
- const version = "" + response[1] + "." + response[2] + "." + response[3];
71
- return {
72
- version: version,
73
- multiOpsEnabled: multiOpsEnabled,
74
- };
75
- });
88
+ const resp = await this.sendToDevice(INS_GET_CONF, Buffer.alloc(0));
89
+ const [hashSigningEnabled, major, minor, patch, maxDataSizeHi, maxDataSizeLo] = resp;
90
+ return {
91
+ hashSigningEnabled: hashSigningEnabled === 0x01,
92
+ version: `${major}.${minor}.${patch}`,
93
+ maxDataSize: resp.length > 4 ? (maxDataSizeHi << 8) | maxDataSizeLo : undefined, // For compatibility with older app, let's remove this in the future
94
+ };
76
95
  }
77
96
 
78
97
  /**
79
- * get Stellar public key for a given BIP 32 path.
98
+ * Get Stellar raw public key for a given BIP 32 path.
99
+ *
80
100
  * @param path a path in BIP 32 format
81
- * @option boolValidate optionally enable key pair validation
82
- * @option boolDisplay optionally enable or not the display
83
- * @return an object with the publicKey (using XLM public key format) and
84
- * the raw ed25519 public key.
101
+ * @param display if true, the device will ask the user to confirm the address on the device, if false, it will return the raw public key directly
102
+ * @return an object with the raw ed25519 public key.
103
+ * If you want to convert it to string, you can use {@link https://stellar.github.io/js-stellar-base/StrKey.html#.encodeEd25519PublicKey StrKey.encodeEd25519PublicKey}
85
104
  * @example
86
- * str.getPublicKey("44'/148'/0'").then(o => o.publicKey)
105
+ * str.getPublicKey("44'/148'/0'").then(o => o.rawPublicKey)
87
106
  */
88
- getPublicKey(
89
- path: string,
90
- boolValidate?: boolean,
91
- boolDisplay?: boolean,
92
- ): Promise<{
93
- publicKey: string;
94
- raw: Buffer;
95
- }> {
96
- checkStellarBip32Path(path);
97
- const apdus: Buffer[] = [];
98
- let response;
99
- const pathElts = splitPath(path);
100
- const buffer = Buffer.alloc(1 + pathElts.length * 4);
101
- buffer[0] = pathElts.length;
102
- pathElts.forEach((element, index) => {
103
- buffer.writeUInt32BE(element, 1 + 4 * index);
104
- });
105
- const verifyMsg = Buffer.from("via lumina", "ascii");
106
- apdus.push(Buffer.concat([buffer, verifyMsg]));
107
- let keepAlive = false;
108
- return foreach(apdus, data =>
109
- this.transport
110
- .send(
111
- CLA,
112
- keepAlive ? INS_KEEP_ALIVE : INS_GET_PK,
113
- boolValidate ? 0x01 : 0x00,
114
- boolDisplay ? 0x01 : 0x00,
115
- data,
116
- [SW_OK, SW_KEEP_ALIVE],
117
- )
118
- .then(apduResponse => {
119
- const status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0);
120
-
121
- if (status === SW_KEEP_ALIVE) {
122
- keepAlive = true;
123
- apdus.push(Buffer.alloc(0));
124
- }
125
-
126
- response = apduResponse;
127
- }),
128
- ).then(() => {
129
- // response = Buffer.from(response, 'hex');
130
- let offset = 0;
131
- const rawPublicKey = response.slice(offset, offset + 32);
132
- offset += 32;
133
- const publicKey = encodeEd25519PublicKey(rawPublicKey);
134
-
135
- if (boolValidate) {
136
- const signature = response.slice(offset, offset + 64);
137
-
138
- if (!verifyEd25519Signature(verifyMsg, signature, rawPublicKey)) {
139
- throw new Error("Bad signature. Keypair is invalid. Please report this.");
140
- }
141
- }
142
-
143
- return {
144
- publicKey: publicKey,
145
- raw: rawPublicKey,
146
- };
147
- });
107
+ async getPublicKey(path: string, display = false): Promise<{ rawPublicKey: Buffer }> {
108
+ const pathBuffer = pathToBuffer(path);
109
+ const p2 = display ? P2_CONFIRM : P2_NON_CONFIRM;
110
+ try {
111
+ const data = await this.transport.send(CLA, INS_GET_PK, P1_FIRST, p2, pathBuffer);
112
+ return { rawPublicKey: data.slice(0, -2) };
113
+ } catch (e) {
114
+ throw remapErrors(e);
115
+ }
148
116
  }
149
117
 
150
118
  /**
151
- * sign a Stellar transaction.
119
+ * Sign a Stellar transaction.
120
+ *
152
121
  * @param path a path in BIP 32 format
153
- * @param transaction signature base of the transaction to sign
154
- * @return an object with the signature and the status
122
+ * @param transaction {@link https://stellar.github.io/js-stellar-base/Transaction.html#signatureBase signature base} of the transaction to sign
123
+ * @return an object with the signature
155
124
  * @example
156
125
  * str.signTransaction("44'/148'/0'", signatureBase).then(o => o.signature)
157
126
  */
158
- signTransaction(
127
+ async signTransaction(
159
128
  path: string,
160
129
  transaction: Buffer,
161
130
  ): Promise<{
162
131
  signature: Buffer;
163
132
  }> {
164
- checkStellarBip32Path(path);
165
-
166
- if (transaction.length > TX_MAX_SIZE) {
167
- throw new Error(
168
- "Transaction too large: max = " + TX_MAX_SIZE + "; actual = " + transaction.length,
169
- );
170
- }
171
-
172
- const apdus: Buffer[] = [];
173
- let response;
174
- const pathElts = splitPath(path);
175
- const bufferSize = 1 + pathElts.length * 4;
176
- const buffer = Buffer.alloc(bufferSize);
177
- buffer[0] = pathElts.length;
178
- pathElts.forEach(function (element, index) {
179
- buffer.writeUInt32BE(element, 1 + 4 * index);
180
- });
181
- let chunkSize = APDU_MAX_SIZE - bufferSize;
182
-
183
- if (transaction.length <= chunkSize) {
184
- // it fits in a single apdu
185
- apdus.push(Buffer.concat([buffer, transaction]));
186
- } else {
187
- // we need to send multiple apdus to transmit the entire transaction
188
- let chunk = Buffer.alloc(chunkSize);
189
- let offset = 0;
190
- transaction.copy(chunk, 0, offset, chunkSize);
191
- apdus.push(Buffer.concat([buffer, chunk]));
192
- offset += chunkSize;
193
-
194
- while (offset < transaction.length) {
195
- const remaining = transaction.length - offset;
196
- chunkSize = remaining < APDU_MAX_SIZE ? remaining : APDU_MAX_SIZE;
197
- chunk = Buffer.alloc(chunkSize);
198
- transaction.copy(chunk, 0, offset, offset + chunkSize);
199
- offset += chunkSize;
200
- apdus.push(chunk);
201
- }
202
- }
203
-
204
- let keepAlive = false;
205
- return foreach(apdus, (data, i) =>
206
- this.transport
207
- .send(
208
- CLA,
209
- keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX,
210
- i === 0 ? P1_FIRST_APDU : P1_MORE_APDU,
211
- i === apdus.length - 1 ? P2_LAST_APDU : P2_MORE_APDU,
212
- data,
213
- [SW_OK, SW_CANCEL, SW_UNKNOWN_OP, SW_MULTI_OP, SW_KEEP_ALIVE],
214
- )
215
- .then(apduResponse => {
216
- const status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0);
217
-
218
- if (status === SW_KEEP_ALIVE) {
219
- keepAlive = true;
220
- apdus.push(Buffer.alloc(0));
221
- }
222
-
223
- response = apduResponse;
224
- }),
225
- ).then(() => {
226
- const status = Buffer.from(response.slice(response.length - 2)).readUInt16BE(0);
227
-
228
- if (status === SW_OK) {
229
- const signature = Buffer.from(response.slice(0, response.length - 2));
230
- return {
231
- signature: signature,
232
- };
233
- } else if (status === SW_UNKNOWN_OP) {
234
- // pre-v2 app version: fall back on hash signing
235
- return this.signHash_private(path, hash(transaction));
236
- } else if (status === SW_MULTI_OP) {
237
- // multi-operation transaction: attempt hash signing
238
- return this.signHash_private(path, hash(transaction));
239
- } else {
240
- throw new Error("Transaction approval request was rejected");
241
- }
242
- });
133
+ const pathBuffer = pathToBuffer(path);
134
+ const payload = Buffer.concat([pathBuffer, transaction]);
135
+ const resp = await this.sendToDevice(INS_SIGN_TX, payload);
136
+ return { signature: resp };
243
137
  }
244
138
 
245
139
  /**
246
- * sign a Stellar transaction hash.
140
+ * Sign a Stellar Soroban authorization.
141
+ *
247
142
  * @param path a path in BIP 32 format
248
- * @param hash hash of the transaction to sign
143
+ * @param hashIdPreimage the {@link https://github.com/stellar/stellar-xdr/blob/1a04392432dacc0092caaeae22a600ea1af3c6a5/Stellar-transaction.x#L702-L709 Soroban authorization hashIdPreimage} to sign
249
144
  * @return an object with the signature
250
145
  * @example
251
- * str.signHash("44'/148'/0'", hash).then(o => o.signature)
146
+ * str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage).then(o => o.signature)
252
147
  */
253
- signHash(
148
+ async signSorobanAuthorization(
254
149
  path: string,
255
- hash: Buffer,
150
+ hashIdPreimage: Buffer,
256
151
  ): Promise<{
257
152
  signature: Buffer;
258
153
  }> {
259
- checkStellarBip32Path(path);
260
- return this.signHash_private(path, hash);
154
+ const pathBuffer = pathToBuffer(path);
155
+ const payload = Buffer.concat([pathBuffer, hashIdPreimage]);
156
+ const resp = await this.sendToDevice(INS_SIGN_SOROBAN_AUTHORIZATION, payload);
157
+ return { signature: resp };
261
158
  }
262
159
 
263
- signHash_private(
160
+ /**
161
+ * Sign a hash.
162
+ *
163
+ * @param path a path in BIP 32 format
164
+ * @param hash the hash to sign
165
+ * @return an object with the signature
166
+ * @example
167
+ * str.signHash("44'/148'/0'", hash).then(o => o.signature)
168
+ */
169
+ async signHash(
264
170
  path: string,
265
171
  hash: Buffer,
266
172
  ): Promise<{
267
173
  signature: Buffer;
268
174
  }> {
269
- const apdus: Buffer[] = [];
270
- let response;
271
- const pathElts = splitPath(path);
272
- const buffer = Buffer.alloc(1 + pathElts.length * 4);
273
- buffer[0] = pathElts.length;
274
- pathElts.forEach(function (element, index) {
275
- buffer.writeUInt32BE(element, 1 + 4 * index);
276
- });
277
- apdus.push(Buffer.concat([buffer, hash]));
278
- let keepAlive = false;
279
- return foreach(apdus, data =>
280
- this.transport
281
- .send(CLA, keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX_HASH, 0x00, 0x00, data, [
282
- SW_OK,
283
- SW_CANCEL,
284
- SW_NOT_ALLOWED,
285
- SW_UNSUPPORTED,
286
- SW_KEEP_ALIVE,
287
- ])
288
- .then(apduResponse => {
289
- const status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0);
290
-
291
- if (status === SW_KEEP_ALIVE) {
292
- keepAlive = true;
293
- apdus.push(Buffer.alloc(0));
294
- }
295
-
296
- response = apduResponse;
297
- }),
298
- ).then(() => {
299
- const status = Buffer.from(response.slice(response.length - 2)).readUInt16BE(0);
175
+ const pathBuffer = pathToBuffer(path);
176
+ const payload = Buffer.concat([pathBuffer, hash]);
177
+ const resp = await this.sendToDevice(INS_SIGN_HASH, payload);
178
+ return { signature: resp };
179
+ }
300
180
 
301
- if (status === SW_OK) {
302
- const signature = Buffer.from(response.slice(0, response.length - 2));
303
- return {
304
- signature: signature,
305
- };
306
- } else if (status === SW_CANCEL) {
307
- throw new Error("Transaction approval request was rejected");
308
- } else if (status === SW_UNSUPPORTED) {
309
- throw new Error("Hash signing is not supported");
310
- } else {
311
- throw new Error("Hash signing not allowed. Have you enabled it in the app settings?");
181
+ private async sendToDevice(instruction: number, payload: Buffer) {
182
+ let response: Buffer = Buffer.alloc(0);
183
+ let remaining = payload.length;
184
+ // eslint-disable-next-line no-constant-condition
185
+ while (true) {
186
+ const chunkSize = remaining > APDU_MAX_PAYLOAD ? APDU_MAX_PAYLOAD : remaining;
187
+ const p1 = remaining === payload.length ? P1_FIRST : P1_MORE;
188
+ const p2 = remaining - chunkSize === 0 ? P2_LAST : P2_MORE;
189
+ const chunk = payload.slice(
190
+ payload.length - remaining,
191
+ payload.length - remaining + chunkSize,
192
+ );
193
+ response = await this.transport.send(CLA, instruction, p1, p2, chunk).catch(e => {
194
+ throw remapErrors(e);
195
+ });
196
+ remaining -= chunkSize;
197
+ if (remaining === 0) {
198
+ break;
312
199
  }
313
- });
200
+ }
201
+ return response.slice(0, -2);
314
202
  }
315
203
  }
204
+
205
+ const remapErrors = e => {
206
+ if (e) {
207
+ switch (e.statusCode) {
208
+ case SW_DENY:
209
+ return new StellarUserRefusedError("User refused the request", undefined, { cause: e });
210
+ case SW_DATA_PARSING_FAIL:
211
+ return new StellarDataParsingFailedError("Unable to parse the provided data", undefined, {
212
+ cause: e,
213
+ });
214
+ case SW_HASH_SIGNING_MODE_NOT_ENABLED:
215
+ return new StellarHashSigningNotEnabledError(
216
+ "Hash signing not allowed. Have you enabled it in the app settings?",
217
+ undefined,
218
+ { cause: e },
219
+ );
220
+ case SW_DATA_TOO_LARGE:
221
+ return new StellarDataTooLargeError(
222
+ "The provided data is too large for the device to process",
223
+ undefined,
224
+ { cause: e },
225
+ );
226
+ }
227
+ }
228
+ return e;
229
+ };
230
+
231
+ const pathToBuffer = (originalPath: string) => {
232
+ const path = originalPath
233
+ .split("/")
234
+ .map(value => (value.endsWith("'") || value.endsWith("h") ? value : `${value}'`))
235
+ .join("/");
236
+ const pathNums: number[] = BIPPath.fromString(path).toPathArray();
237
+ return serializePath(pathNums);
238
+ };
239
+
240
+ const serializePath = (path: number[]) => {
241
+ const buf = Buffer.alloc(1 + path.length * 4);
242
+ buf.writeUInt8(path.length, 0);
243
+ for (const [i, num] of path.entries()) {
244
+ buf.writeUInt32BE(num, 1 + i * 4);
245
+ }
246
+ return buf;
247
+ };
248
+
249
+ export * from "./errors";
package/src/errors.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { createCustomErrorClass } from "@ledgerhq/errors";
2
+
3
+ /**
4
+ * Error thrown when hash signing is not enabled on the device.
5
+ */
6
+ export const StellarHashSigningNotEnabledError = createCustomErrorClass(
7
+ "StellarHashSigningNotEnabledError",
8
+ );
9
+
10
+ /**
11
+ * Error thrown when data parsing fails.
12
+ *
13
+ * For example, when parsing the transaction fails, this error is thrown.
14
+ */
15
+ export const StellarDataParsingFailedError = createCustomErrorClass(
16
+ "StellarDataParsingFailedError",
17
+ );
18
+
19
+ /**
20
+ * Error thrown when the user refuses the request on the device.
21
+ */
22
+ export const StellarUserRefusedError = createCustomErrorClass("StellarUserRefusedError");
23
+
24
+ /**
25
+ * Error thrown when the data is too large to be processed by the device.
26
+ */
27
+ export const StellarDataTooLargeError = createCustomErrorClass("StellarDataTooLargeError");