@ledgerhq/hw-app-str 6.24.1 → 6.25.1-alpha.3

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.
@@ -0,0 +1,316 @@
1
+ /********************************************************************************
2
+ * Ledger Node JS API
3
+ * (c) 2017-2018 Ledger
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ ********************************************************************************/
17
+ //@flow
18
+
19
+ import type Transport from "@ledgerhq/hw-transport";
20
+ import {
21
+ splitPath,
22
+ foreach,
23
+ encodeEd25519PublicKey,
24
+ verifyEd25519Signature,
25
+ checkStellarBip32Path,
26
+ hash,
27
+ } from "./utils";
28
+
29
+ const CLA = 0xe0;
30
+ const INS_GET_PK = 0x02;
31
+ const INS_SIGN_TX = 0x04;
32
+ const INS_GET_CONF = 0x06;
33
+ const INS_SIGN_TX_HASH = 0x08;
34
+ const INS_KEEP_ALIVE = 0x10;
35
+
36
+ const APDU_MAX_SIZE = 150;
37
+ const P1_FIRST_APDU = 0x00;
38
+ const P1_MORE_APDU = 0x80;
39
+ const P2_LAST_APDU = 0x00;
40
+ const P2_MORE_APDU = 0x80;
41
+
42
+ const SW_OK = 0x9000;
43
+ const SW_CANCEL = 0x6985;
44
+ const SW_UNKNOWN_OP = 0x6c24;
45
+ const SW_MULTI_OP = 0x6c25;
46
+ const SW_NOT_ALLOWED = 0x6c66;
47
+ const SW_UNSUPPORTED = 0x6d00;
48
+ const SW_KEEP_ALIVE = 0x6e02;
49
+
50
+ const TX_MAX_SIZE = 1540;
51
+
52
+ /**
53
+ * Stellar API
54
+ *
55
+ * @example
56
+ * import Str from "@ledgerhq/hw-app-str";
57
+ * const str = new Str(transport)
58
+ */
59
+ export default class Str {
60
+ transport: Transport<*>;
61
+
62
+ constructor(transport: Transport<*>, scrambleKey: string = "l0v") {
63
+ this.transport = transport;
64
+ transport.decorateAppAPIMethods(
65
+ this,
66
+ ["getAppConfiguration", "getPublicKey", "signTransaction", "signHash"],
67
+ scrambleKey
68
+ );
69
+ }
70
+
71
+ getAppConfiguration(): Promise<{
72
+ version: string,
73
+ }> {
74
+ return this.transport
75
+ .send(CLA, INS_GET_CONF, 0x00, 0x00)
76
+ .then((response) => {
77
+ let multiOpsEnabled = response[0] === 0x01 || response[1] < 0x02;
78
+ let version = "" + response[1] + "." + response[2] + "." + response[3];
79
+ return {
80
+ version: version,
81
+ multiOpsEnabled: multiOpsEnabled,
82
+ };
83
+ });
84
+ }
85
+
86
+ /**
87
+ * get Stellar public key for a given BIP 32 path.
88
+ * @param path a path in BIP 32 format
89
+ * @option boolValidate optionally enable key pair validation
90
+ * @option boolDisplay optionally enable or not the display
91
+ * @return an object with the publicKey (using XLM public key format) and
92
+ * the raw ed25519 public key.
93
+ * @example
94
+ * str.getPublicKey("44'/148'/0'").then(o => o.publicKey)
95
+ */
96
+ getPublicKey(
97
+ path: string,
98
+ boolValidate?: boolean,
99
+ boolDisplay?: boolean
100
+ ): Promise<{ publicKey: string, raw: Buffer }> {
101
+ checkStellarBip32Path(path);
102
+
103
+ let apdus = [];
104
+ let response;
105
+
106
+ let pathElts = splitPath(path);
107
+ let buffer = Buffer.alloc(1 + pathElts.length * 4);
108
+ buffer[0] = pathElts.length;
109
+ pathElts.forEach((element, index) => {
110
+ buffer.writeUInt32BE(element, 1 + 4 * index);
111
+ });
112
+ let verifyMsg = Buffer.from("via lumina", "ascii");
113
+ apdus.push(Buffer.concat([buffer, verifyMsg]));
114
+ let keepAlive = false;
115
+ return foreach(apdus, (data) =>
116
+ this.transport
117
+ .send(
118
+ CLA,
119
+ keepAlive ? INS_KEEP_ALIVE : INS_GET_PK,
120
+ boolValidate ? 0x01 : 0x00,
121
+ boolDisplay ? 0x01 : 0x00,
122
+ data,
123
+ [SW_OK, SW_KEEP_ALIVE]
124
+ )
125
+ .then((apduResponse) => {
126
+ let status = Buffer.from(
127
+ apduResponse.slice(apduResponse.length - 2)
128
+ ).readUInt16BE(0);
129
+ if (status === SW_KEEP_ALIVE) {
130
+ keepAlive = true;
131
+ apdus.push(Buffer.alloc(0));
132
+ }
133
+ response = apduResponse;
134
+ })
135
+ ).then(() => {
136
+ // response = Buffer.from(response, 'hex');
137
+ let offset = 0;
138
+ let rawPublicKey = response.slice(offset, offset + 32);
139
+ offset += 32;
140
+ let publicKey = encodeEd25519PublicKey(rawPublicKey);
141
+ if (boolValidate) {
142
+ let signature = response.slice(offset, offset + 64);
143
+ if (!verifyEd25519Signature(verifyMsg, signature, rawPublicKey)) {
144
+ throw new Error(
145
+ "Bad signature. Keypair is invalid. Please report this."
146
+ );
147
+ }
148
+ }
149
+ return {
150
+ publicKey: publicKey,
151
+ raw: rawPublicKey,
152
+ };
153
+ });
154
+ }
155
+
156
+ /**
157
+ * sign a Stellar transaction.
158
+ * @param path a path in BIP 32 format
159
+ * @param transaction signature base of the transaction to sign
160
+ * @return an object with the signature and the status
161
+ * @example
162
+ * str.signTransaction("44'/148'/0'", signatureBase).then(o => o.signature)
163
+ */
164
+ signTransaction(
165
+ path: string,
166
+ transaction: Buffer
167
+ ): Promise<{ signature: Buffer }> {
168
+ checkStellarBip32Path(path);
169
+
170
+ if (transaction.length > TX_MAX_SIZE) {
171
+ throw new Error(
172
+ "Transaction too large: max = " +
173
+ TX_MAX_SIZE +
174
+ "; actual = " +
175
+ transaction.length
176
+ );
177
+ }
178
+
179
+ let apdus = [];
180
+ let response;
181
+
182
+ let pathElts = splitPath(path);
183
+ let bufferSize = 1 + pathElts.length * 4;
184
+ let buffer = Buffer.alloc(bufferSize);
185
+ buffer[0] = pathElts.length;
186
+ pathElts.forEach(function (element, index) {
187
+ buffer.writeUInt32BE(element, 1 + 4 * index);
188
+ });
189
+ let chunkSize = APDU_MAX_SIZE - bufferSize;
190
+ if (transaction.length <= chunkSize) {
191
+ // it fits in a single apdu
192
+ apdus.push(Buffer.concat([buffer, transaction]));
193
+ } else {
194
+ // we need to send multiple apdus to transmit the entire transaction
195
+ let chunk = Buffer.alloc(chunkSize);
196
+ let offset = 0;
197
+ transaction.copy(chunk, 0, offset, chunkSize);
198
+ apdus.push(Buffer.concat([buffer, chunk]));
199
+ offset += chunkSize;
200
+ while (offset < transaction.length) {
201
+ let remaining = transaction.length - offset;
202
+ chunkSize = remaining < APDU_MAX_SIZE ? remaining : APDU_MAX_SIZE;
203
+ chunk = Buffer.alloc(chunkSize);
204
+ transaction.copy(chunk, 0, offset, offset + chunkSize);
205
+ offset += chunkSize;
206
+ apdus.push(chunk);
207
+ }
208
+ }
209
+ let keepAlive = false;
210
+ return foreach(apdus, (data, i) =>
211
+ this.transport
212
+ .send(
213
+ CLA,
214
+ keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX,
215
+ i === 0 ? P1_FIRST_APDU : P1_MORE_APDU,
216
+ i === apdus.length - 1 ? P2_LAST_APDU : P2_MORE_APDU,
217
+ data,
218
+ [SW_OK, SW_CANCEL, SW_UNKNOWN_OP, SW_MULTI_OP, SW_KEEP_ALIVE]
219
+ )
220
+ .then((apduResponse) => {
221
+ let status = Buffer.from(
222
+ apduResponse.slice(apduResponse.length - 2)
223
+ ).readUInt16BE(0);
224
+ if (status === SW_KEEP_ALIVE) {
225
+ keepAlive = true;
226
+ apdus.push(Buffer.alloc(0));
227
+ }
228
+ response = apduResponse;
229
+ })
230
+ ).then(() => {
231
+ let status = Buffer.from(
232
+ response.slice(response.length - 2)
233
+ ).readUInt16BE(0);
234
+ if (status === SW_OK) {
235
+ let signature = Buffer.from(response.slice(0, response.length - 2));
236
+ return {
237
+ signature: signature,
238
+ };
239
+ } else if (status === SW_UNKNOWN_OP) {
240
+ // pre-v2 app version: fall back on hash signing
241
+ return this.signHash_private(path, hash(transaction));
242
+ } else if (status === SW_MULTI_OP) {
243
+ // multi-operation transaction: attempt hash signing
244
+ return this.signHash_private(path, hash(transaction));
245
+ } else {
246
+ throw new Error("Transaction approval request was rejected");
247
+ }
248
+ });
249
+ }
250
+
251
+ /**
252
+ * sign a Stellar transaction hash.
253
+ * @param path a path in BIP 32 format
254
+ * @param hash hash of the transaction to sign
255
+ * @return an object with the signature
256
+ * @example
257
+ * str.signHash("44'/148'/0'", hash).then(o => o.signature)
258
+ */
259
+ signHash(path: string, hash: Buffer): Promise<{ signature: Buffer }> {
260
+ checkStellarBip32Path(path);
261
+ return this.signHash_private(path, hash);
262
+ }
263
+
264
+ signHash_private(path: string, hash: Buffer): Promise<{ signature: Buffer }> {
265
+ let apdus = [];
266
+ let response;
267
+
268
+ let pathElts = splitPath(path);
269
+ let buffer = Buffer.alloc(1 + pathElts.length * 4);
270
+ buffer[0] = pathElts.length;
271
+ pathElts.forEach(function (element, index) {
272
+ buffer.writeUInt32BE(element, 1 + 4 * index);
273
+ });
274
+ apdus.push(Buffer.concat([buffer, hash]));
275
+ let keepAlive = false;
276
+ return foreach(apdus, (data) =>
277
+ this.transport
278
+ .send(
279
+ CLA,
280
+ keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX_HASH,
281
+ 0x00,
282
+ 0x00,
283
+ data,
284
+ [SW_OK, SW_CANCEL, SW_NOT_ALLOWED, SW_UNSUPPORTED, SW_KEEP_ALIVE]
285
+ )
286
+ .then((apduResponse) => {
287
+ let status = Buffer.from(
288
+ apduResponse.slice(apduResponse.length - 2)
289
+ ).readUInt16BE(0);
290
+ if (status === SW_KEEP_ALIVE) {
291
+ keepAlive = true;
292
+ apdus.push(Buffer.alloc(0));
293
+ }
294
+ response = apduResponse;
295
+ })
296
+ ).then(() => {
297
+ let status = Buffer.from(
298
+ response.slice(response.length - 2)
299
+ ).readUInt16BE(0);
300
+ if (status === SW_OK) {
301
+ let signature = Buffer.from(response.slice(0, response.length - 2));
302
+ return {
303
+ signature: signature,
304
+ };
305
+ } else if (status === SW_CANCEL) {
306
+ throw new Error("Transaction approval request was rejected");
307
+ } else if (status === SW_UNSUPPORTED) {
308
+ throw new Error("Hash signing is not supported");
309
+ } else {
310
+ throw new Error(
311
+ "Hash signing not allowed. Have you enabled it in the app settings?"
312
+ );
313
+ }
314
+ });
315
+ }
316
+ }
@@ -0,0 +1,116 @@
1
+ /********************************************************************************
2
+ * Ledger Node JS API
3
+ * (c) 2017-2018 Ledger
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ ********************************************************************************/
17
+ //@flow
18
+
19
+ import base32 from "base32.js";
20
+ import nacl from "tweetnacl";
21
+ import { sha256 } from "sha.js";
22
+
23
+ // TODO use bip32-path library
24
+ export function splitPath(path: string): number[] {
25
+ let result = [];
26
+ let components = path.split("/");
27
+ components.forEach((element) => {
28
+ let number = parseInt(element, 10);
29
+ if (isNaN(number)) {
30
+ return; // FIXME shouldn't it throws instead?
31
+ }
32
+ if (element.length > 1 && element[element.length - 1] === "'") {
33
+ number += 0x80000000;
34
+ }
35
+ result.push(number);
36
+ });
37
+ return result;
38
+ }
39
+
40
+ export function foreach<T, A>(
41
+ arr: T[],
42
+ callback: (T, number) => Promise<A>
43
+ ): Promise<A[]> {
44
+ function iterate(index, array, result) {
45
+ if (index >= array.length) {
46
+ return result;
47
+ } else {
48
+ return callback(array[index], index).then(function (res) {
49
+ result.push(res);
50
+ return iterate(index + 1, array, result);
51
+ });
52
+ }
53
+ }
54
+ return Promise.resolve().then(() => iterate(0, arr, []));
55
+ }
56
+
57
+ export function crc16xmodem(buf: Buffer, previous?: number): number {
58
+ let crc = typeof previous !== "undefined" ? ~~previous : 0x0;
59
+
60
+ for (var index = 0; index < buf.length; index++) {
61
+ const byte = buf[index];
62
+ let code = (crc >>> 8) & 0xff;
63
+
64
+ code ^= byte & 0xff;
65
+ code ^= code >>> 4;
66
+ crc = (crc << 8) & 0xffff;
67
+ crc ^= code;
68
+ code = (code << 5) & 0xffff;
69
+ crc ^= code;
70
+ code = (code << 7) & 0xffff;
71
+ crc ^= code;
72
+ }
73
+
74
+ return crc;
75
+ }
76
+
77
+ export function encodeEd25519PublicKey(rawPublicKey: Buffer): string {
78
+ let versionByte = 6 << 3; // 'G'
79
+ let data = Buffer.from(rawPublicKey);
80
+ let versionBuffer = Buffer.from([versionByte]);
81
+ let payload = Buffer.concat([versionBuffer, data]);
82
+ let checksum = Buffer.alloc(2);
83
+ checksum.writeUInt16LE(crc16xmodem(payload), 0);
84
+ let unencoded = Buffer.concat([payload, checksum]);
85
+ return base32.encode(unencoded);
86
+ }
87
+
88
+ export function verifyEd25519Signature(
89
+ data: Buffer,
90
+ signature: Buffer,
91
+ publicKey: Buffer
92
+ ): boolean {
93
+ return nacl.sign.detached.verify(
94
+ new Uint8Array(data.toJSON().data),
95
+ new Uint8Array(signature.toJSON().data),
96
+ new Uint8Array(publicKey.toJSON().data)
97
+ );
98
+ }
99
+
100
+ export function hash(data: Buffer) {
101
+ let hasher = new sha256();
102
+ hasher.update(data, "utf8");
103
+ return hasher.digest();
104
+ }
105
+
106
+ export function checkStellarBip32Path(path: string): void {
107
+ path.split("/").forEach(function (element) {
108
+ if (!element.toString().endsWith("'")) {
109
+ throw new Error(
110
+ "Detected a non-hardened path element in requested BIP32 path." +
111
+ " Non-hardended paths are not supported at this time. Please use an all-hardened path." +
112
+ " Example: 44'/148'/0'"
113
+ );
114
+ }
115
+ });
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/hw-app-str",
3
- "version": "6.24.1",
3
+ "version": "6.25.1-alpha.3+eb669e17",
4
4
  "description": "Ledger Hardware Wallet Stellar Application API",
5
5
  "keywords": [
6
6
  "Ledger",
@@ -27,7 +27,7 @@
27
27
  "types": "lib/Str.d.ts",
28
28
  "license": "Apache-2.0",
29
29
  "dependencies": {
30
- "@ledgerhq/hw-transport": "^6.24.1",
30
+ "@ledgerhq/hw-transport": "^6.25.1-alpha.3+eb669e17",
31
31
  "base32.js": "^0.1.0",
32
32
  "sha.js": "^2.3.6",
33
33
  "tweetnacl": "^1.0.3"
@@ -37,5 +37,5 @@
37
37
  "build": "bash ../../script/build.sh",
38
38
  "watch": "bash ../../script/watch.sh"
39
39
  },
40
- "gitHead": "159269dafc5f177c7af5b20761ab0ef3550e3faf"
40
+ "gitHead": "eb669e17dd87d3ab568beab1f9a5ddb1a2536e83"
41
41
  }