@ledgerhq/hw-app-str
Advanced tools
Comparing version 6.29.0 to 7.0.0-next.0
# @ledgerhq/hw-app-str | ||
## 7.0.0-next.0 | ||
### Major Changes | ||
- [#6923](https://github.com/LedgerHQ/ledger-live/pull/6923) [`782d637`](https://github.com/LedgerHQ/ledger-live/commit/782d637b5fba8c9c9d37609b6ad492f45a4b3737) Thanks [@overcat](https://github.com/overcat)! - Refactor `hw-app-str` and add `signSorobanAuthorization`. Please check the changelog and documentation of "@ledgerhq/hw-app-str" for more information. | ||
- `Str.getPublicKey`'s function signature has changed. Previously, it was `getPublicKey(path: string, boolValidate?: boolean, boolDisplay?: boolean): Promise<{ publicKey: string; raw: Buffer; }>` and now it is `async getPublicKey(path: string, display = false): Promise<{ rawPublicKey: Buffer }>` | ||
- `Str.signTransaction` will no longer automatically fallback to `Str.signHash`. If you want to sign a hash, you have to call `Str.signHash` directly. | ||
- Removed the fixed limit on the maximum length of the transaction in `Str.signTransaction`. Currently, if the transaction is too large for the device to handle, `StellarUserRefusedError` will be thrown. | ||
- Add `Str.signSorobanAuthorization` method to sign Stellar Soroban authorization. | ||
- `Str.getAppConfiguration` now returns `maxDataSize`, it represents the maximum size of the data that the device can processed. | ||
- Add error classes for better error handling, check the documentation for more information: | ||
- `StellarUserRefusedError` | ||
- `StellarHashSigningNotEnabledError` | ||
- `StellarDataTooLargeError` | ||
- `StellarDataParsingFailedError` | ||
## 6.29.0 | ||
@@ -4,0 +21,0 @@ |
@@ -22,2 +22,5 @@ /// <reference types="node" /> | ||
* | ||
* @param transport a transport for sending commands to a device | ||
* @param scrambleKey a scramble key | ||
* | ||
* @example | ||
@@ -28,26 +31,36 @@ * import Str from "@ledgerhq/hw-app-str"; | ||
export default class Str { | ||
transport: Transport; | ||
private transport; | ||
constructor(transport: Transport, scrambleKey?: string); | ||
/** | ||
* Get Stellar application configuration. | ||
* | ||
* @returns an object with the application configuration, including the version, | ||
* whether hash signing is enabled, and the maximum data size in bytes that the device can sign. | ||
* @example | ||
* str.getAppConfiguration().then(o => o.version) | ||
*/ | ||
getAppConfiguration(): Promise<{ | ||
version: string; | ||
hashSigningEnabled: boolean; | ||
maxDataSize?: number; | ||
}>; | ||
/** | ||
* get Stellar public key for a given BIP 32 path. | ||
* Get Stellar raw public key for a given BIP 32 path. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @option boolValidate optionally enable key pair validation | ||
* @option boolDisplay optionally enable or not the display | ||
* @return an object with the publicKey (using XLM public key format) and | ||
* the raw ed25519 public key. | ||
* @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 | ||
* @return an object with the raw ed25519 public key. | ||
* If you want to convert it to string, you can use {@link https://stellar.github.io/js-stellar-base/StrKey.html#.encodeEd25519PublicKey StrKey.encodeEd25519PublicKey} | ||
* @example | ||
* str.getPublicKey("44'/148'/0'").then(o => o.publicKey) | ||
* str.getPublicKey("44'/148'/0'").then(o => o.rawPublicKey) | ||
*/ | ||
getPublicKey(path: string, boolValidate?: boolean, boolDisplay?: boolean): Promise<{ | ||
publicKey: string; | ||
raw: Buffer; | ||
getPublicKey(path: string, display?: boolean): Promise<{ | ||
rawPublicKey: Buffer; | ||
}>; | ||
/** | ||
* sign a Stellar transaction. | ||
* Sign a Stellar transaction. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param transaction signature base of the transaction to sign | ||
* @return an object with the signature and the status | ||
* @param transaction {@link https://stellar.github.io/js-stellar-base/Transaction.html#signatureBase signature base} of the transaction to sign | ||
* @return an object with the signature | ||
* @example | ||
@@ -60,7 +73,20 @@ * str.signTransaction("44'/148'/0'", signatureBase).then(o => o.signature) | ||
/** | ||
* sign a Stellar transaction hash. | ||
* Sign a Stellar Soroban authorization. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param hash hash of the transaction to sign | ||
* @param hashIdPreimage the {@link https://github.com/stellar/stellar-xdr/blob/1a04392432dacc0092caaeae22a600ea1af3c6a5/Stellar-transaction.x#L702-L709 Soroban authorization hashIdPreimage} to sign | ||
* @return an object with the signature | ||
* @example | ||
* str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage).then(o => o.signature) | ||
*/ | ||
signSorobanAuthorization(path: string, hashIdPreimage: Buffer): Promise<{ | ||
signature: Buffer; | ||
}>; | ||
/** | ||
* Sign a hash. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param hash the hash to sign | ||
* @return an object with the signature | ||
* @example | ||
* str.signHash("44'/148'/0'", hash).then(o => o.signature) | ||
@@ -71,6 +97,5 @@ */ | ||
}>; | ||
signHash_private(path: string, hash: Buffer): Promise<{ | ||
signature: Buffer; | ||
}>; | ||
private sendToDevice; | ||
} | ||
export * from "./errors"; | ||
//# sourceMappingURL=Str.d.ts.map |
@@ -1,24 +0,35 @@ | ||
import { splitPath, foreach, encodeEd25519PublicKey, verifyEd25519Signature, checkStellarBip32Path, hash, } from "./utils"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
import BIPPath from "bip32-path"; | ||
import { StellarHashSigningNotEnabledError, StellarDataParsingFailedError, StellarUserRefusedError, StellarDataTooLargeError, } from "./errors"; | ||
const CLA = 0xe0; | ||
const P1_FIRST = 0x00; | ||
const P1_MORE = 0x80; | ||
const P2_LAST = 0x00; | ||
const P2_MORE = 0x80; | ||
const P2_NON_CONFIRM = 0x00; // for getPublicKey | ||
const P2_CONFIRM = 0x01; // for getPublicKey | ||
const INS_GET_PK = 0x02; | ||
const INS_SIGN_TX = 0x04; | ||
const INS_GET_CONF = 0x06; | ||
const INS_SIGN_TX_HASH = 0x08; | ||
const INS_KEEP_ALIVE = 0x10; | ||
const APDU_MAX_SIZE = 150; | ||
const P1_FIRST_APDU = 0x00; | ||
const P1_MORE_APDU = 0x80; | ||
const P2_LAST_APDU = 0x00; | ||
const P2_MORE_APDU = 0x80; | ||
const SW_OK = 0x9000; | ||
const SW_CANCEL = 0x6985; | ||
const SW_UNKNOWN_OP = 0x6c24; | ||
const SW_MULTI_OP = 0x6c25; | ||
const SW_NOT_ALLOWED = 0x6c66; | ||
const SW_UNSUPPORTED = 0x6d00; | ||
const SW_KEEP_ALIVE = 0x6e02; | ||
const TX_MAX_SIZE = 1540; | ||
const INS_SIGN_HASH = 0x08; | ||
const INS_SIGN_SOROBAN_AUTHORIZATION = 0x0a; | ||
const APDU_MAX_PAYLOAD = 255; | ||
const SW_DENY = 0x6985; | ||
const SW_HASH_SIGNING_MODE_NOT_ENABLED = 0x6c66; | ||
const SW_DATA_TOO_LARGE = 0xb004; | ||
const SW_DATA_PARSING_FAIL = 0xb005; | ||
/** | ||
* Stellar API | ||
* | ||
* @param transport a transport for sending commands to a device | ||
* @param scrambleKey a scramble key | ||
* | ||
* @example | ||
@@ -31,11 +42,26 @@ * import Str from "@ledgerhq/hw-app-str"; | ||
this.transport = transport; | ||
transport.decorateAppAPIMethods(this, ["getAppConfiguration", "getPublicKey", "signTransaction", "signHash"], scrambleKey); | ||
transport.decorateAppAPIMethods(this, [ | ||
"getAppConfiguration", | ||
"getPublicKey", | ||
"signTransaction", | ||
"signSorobanAuthorization", | ||
"signHash", | ||
], scrambleKey); | ||
} | ||
/** | ||
* Get Stellar application configuration. | ||
* | ||
* @returns an object with the application configuration, including the version, | ||
* whether hash signing is enabled, and the maximum data size in bytes that the device can sign. | ||
* @example | ||
* str.getAppConfiguration().then(o => o.version) | ||
*/ | ||
getAppConfiguration() { | ||
return this.transport.send(CLA, INS_GET_CONF, 0x00, 0x00).then(response => { | ||
const multiOpsEnabled = response[0] === 0x01 || response[1] < 0x02; | ||
const version = "" + response[1] + "." + response[2] + "." + response[3]; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const resp = yield this.sendToDevice(INS_GET_CONF, Buffer.alloc(0)); | ||
const [hashSigningEnabled, major, minor, patch, maxDataSizeHi, maxDataSizeLo] = resp; | ||
return { | ||
version: version, | ||
multiOpsEnabled: multiOpsEnabled, | ||
hashSigningEnabled: hashSigningEnabled === 0x01, | ||
version: `${major}.${minor}.${patch}`, | ||
maxDataSize: resp.length > 4 ? (maxDataSizeHi << 8) | maxDataSizeLo : undefined, // For compatibility with older app, let's remove this in the future | ||
}; | ||
@@ -45,56 +71,30 @@ }); | ||
/** | ||
* get Stellar public key for a given BIP 32 path. | ||
* Get Stellar raw public key for a given BIP 32 path. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @option boolValidate optionally enable key pair validation | ||
* @option boolDisplay optionally enable or not the display | ||
* @return an object with the publicKey (using XLM public key format) and | ||
* the raw ed25519 public key. | ||
* @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 | ||
* @return an object with the raw ed25519 public key. | ||
* If you want to convert it to string, you can use {@link https://stellar.github.io/js-stellar-base/StrKey.html#.encodeEd25519PublicKey StrKey.encodeEd25519PublicKey} | ||
* @example | ||
* str.getPublicKey("44'/148'/0'").then(o => o.publicKey) | ||
* str.getPublicKey("44'/148'/0'").then(o => o.rawPublicKey) | ||
*/ | ||
getPublicKey(path, boolValidate, boolDisplay) { | ||
checkStellarBip32Path(path); | ||
const apdus = []; | ||
let response; | ||
const pathElts = splitPath(path); | ||
const buffer = Buffer.alloc(1 + pathElts.length * 4); | ||
buffer[0] = pathElts.length; | ||
pathElts.forEach((element, index) => { | ||
buffer.writeUInt32BE(element, 1 + 4 * index); | ||
}); | ||
const verifyMsg = Buffer.from("via lumina", "ascii"); | ||
apdus.push(Buffer.concat([buffer, verifyMsg])); | ||
let keepAlive = false; | ||
return foreach(apdus, data => this.transport | ||
.send(CLA, keepAlive ? INS_KEEP_ALIVE : INS_GET_PK, boolValidate ? 0x01 : 0x00, boolDisplay ? 0x01 : 0x00, data, [SW_OK, SW_KEEP_ALIVE]) | ||
.then(apduResponse => { | ||
const status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0); | ||
if (status === SW_KEEP_ALIVE) { | ||
keepAlive = true; | ||
apdus.push(Buffer.alloc(0)); | ||
getPublicKey(path_1) { | ||
return __awaiter(this, arguments, void 0, function* (path, display = false) { | ||
const pathBuffer = pathToBuffer(path); | ||
const p2 = display ? P2_CONFIRM : P2_NON_CONFIRM; | ||
try { | ||
const data = yield this.transport.send(CLA, INS_GET_PK, P1_FIRST, p2, pathBuffer); | ||
return { rawPublicKey: data.slice(0, -2) }; | ||
} | ||
response = apduResponse; | ||
})).then(() => { | ||
// response = Buffer.from(response, 'hex'); | ||
let offset = 0; | ||
const rawPublicKey = response.slice(offset, offset + 32); | ||
offset += 32; | ||
const publicKey = encodeEd25519PublicKey(rawPublicKey); | ||
if (boolValidate) { | ||
const signature = response.slice(offset, offset + 64); | ||
if (!verifyEd25519Signature(verifyMsg, signature, rawPublicKey)) { | ||
throw new Error("Bad signature. Keypair is invalid. Please report this."); | ||
} | ||
catch (e) { | ||
throw remapErrors(e); | ||
} | ||
return { | ||
publicKey: publicKey, | ||
raw: rawPublicKey, | ||
}; | ||
}); | ||
} | ||
/** | ||
* sign a Stellar transaction. | ||
* Sign a Stellar transaction. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param transaction signature base of the transaction to sign | ||
* @return an object with the signature and the status | ||
* @param transaction {@link https://stellar.github.io/js-stellar-base/Transaction.html#signatureBase signature base} of the transaction to sign | ||
* @return an object with the signature | ||
* @example | ||
@@ -104,71 +104,31 @@ * str.signTransaction("44'/148'/0'", signatureBase).then(o => o.signature) | ||
signTransaction(path, transaction) { | ||
checkStellarBip32Path(path); | ||
if (transaction.length > TX_MAX_SIZE) { | ||
throw new Error("Transaction too large: max = " + TX_MAX_SIZE + "; actual = " + transaction.length); | ||
} | ||
const apdus = []; | ||
let response; | ||
const pathElts = splitPath(path); | ||
const bufferSize = 1 + pathElts.length * 4; | ||
const buffer = Buffer.alloc(bufferSize); | ||
buffer[0] = pathElts.length; | ||
pathElts.forEach(function (element, index) { | ||
buffer.writeUInt32BE(element, 1 + 4 * index); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const pathBuffer = pathToBuffer(path); | ||
const payload = Buffer.concat([pathBuffer, transaction]); | ||
const resp = yield this.sendToDevice(INS_SIGN_TX, payload); | ||
return { signature: resp }; | ||
}); | ||
let chunkSize = APDU_MAX_SIZE - bufferSize; | ||
if (transaction.length <= chunkSize) { | ||
// it fits in a single apdu | ||
apdus.push(Buffer.concat([buffer, transaction])); | ||
} | ||
else { | ||
// we need to send multiple apdus to transmit the entire transaction | ||
let chunk = Buffer.alloc(chunkSize); | ||
let offset = 0; | ||
transaction.copy(chunk, 0, offset, chunkSize); | ||
apdus.push(Buffer.concat([buffer, chunk])); | ||
offset += chunkSize; | ||
while (offset < transaction.length) { | ||
const remaining = transaction.length - offset; | ||
chunkSize = remaining < APDU_MAX_SIZE ? remaining : APDU_MAX_SIZE; | ||
chunk = Buffer.alloc(chunkSize); | ||
transaction.copy(chunk, 0, offset, offset + chunkSize); | ||
offset += chunkSize; | ||
apdus.push(chunk); | ||
} | ||
} | ||
let keepAlive = false; | ||
return foreach(apdus, (data, i) => this.transport | ||
.send(CLA, keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX, i === 0 ? P1_FIRST_APDU : P1_MORE_APDU, i === apdus.length - 1 ? P2_LAST_APDU : P2_MORE_APDU, data, [SW_OK, SW_CANCEL, SW_UNKNOWN_OP, SW_MULTI_OP, SW_KEEP_ALIVE]) | ||
.then(apduResponse => { | ||
const status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0); | ||
if (status === SW_KEEP_ALIVE) { | ||
keepAlive = true; | ||
apdus.push(Buffer.alloc(0)); | ||
} | ||
response = apduResponse; | ||
})).then(() => { | ||
const status = Buffer.from(response.slice(response.length - 2)).readUInt16BE(0); | ||
if (status === SW_OK) { | ||
const signature = Buffer.from(response.slice(0, response.length - 2)); | ||
return { | ||
signature: signature, | ||
}; | ||
} | ||
else if (status === SW_UNKNOWN_OP) { | ||
// pre-v2 app version: fall back on hash signing | ||
return this.signHash_private(path, hash(transaction)); | ||
} | ||
else if (status === SW_MULTI_OP) { | ||
// multi-operation transaction: attempt hash signing | ||
return this.signHash_private(path, hash(transaction)); | ||
} | ||
else { | ||
throw new Error("Transaction approval request was rejected"); | ||
} | ||
} | ||
/** | ||
* Sign a Stellar Soroban authorization. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param hashIdPreimage the {@link https://github.com/stellar/stellar-xdr/blob/1a04392432dacc0092caaeae22a600ea1af3c6a5/Stellar-transaction.x#L702-L709 Soroban authorization hashIdPreimage} to sign | ||
* @return an object with the signature | ||
* @example | ||
* str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage).then(o => o.signature) | ||
*/ | ||
signSorobanAuthorization(path, hashIdPreimage) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const pathBuffer = pathToBuffer(path); | ||
const payload = Buffer.concat([pathBuffer, hashIdPreimage]); | ||
const resp = yield this.sendToDevice(INS_SIGN_SOROBAN_AUTHORIZATION, payload); | ||
return { signature: resp }; | ||
}); | ||
} | ||
/** | ||
* sign a Stellar transaction hash. | ||
* Sign a hash. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param hash hash of the transaction to sign | ||
* @param hash the hash to sign | ||
* @return an object with the signature | ||
@@ -179,51 +139,65 @@ * @example | ||
signHash(path, hash) { | ||
checkStellarBip32Path(path); | ||
return this.signHash_private(path, hash); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const pathBuffer = pathToBuffer(path); | ||
const payload = Buffer.concat([pathBuffer, hash]); | ||
const resp = yield this.sendToDevice(INS_SIGN_HASH, payload); | ||
return { signature: resp }; | ||
}); | ||
} | ||
signHash_private(path, hash) { | ||
const apdus = []; | ||
let response; | ||
const pathElts = splitPath(path); | ||
const buffer = Buffer.alloc(1 + pathElts.length * 4); | ||
buffer[0] = pathElts.length; | ||
pathElts.forEach(function (element, index) { | ||
buffer.writeUInt32BE(element, 1 + 4 * index); | ||
}); | ||
apdus.push(Buffer.concat([buffer, hash])); | ||
let keepAlive = false; | ||
return foreach(apdus, data => this.transport | ||
.send(CLA, keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX_HASH, 0x00, 0x00, data, [ | ||
SW_OK, | ||
SW_CANCEL, | ||
SW_NOT_ALLOWED, | ||
SW_UNSUPPORTED, | ||
SW_KEEP_ALIVE, | ||
]) | ||
.then(apduResponse => { | ||
const status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0); | ||
if (status === SW_KEEP_ALIVE) { | ||
keepAlive = true; | ||
apdus.push(Buffer.alloc(0)); | ||
sendToDevice(instruction, payload) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
let response = Buffer.alloc(0); | ||
let remaining = payload.length; | ||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
const chunkSize = remaining > APDU_MAX_PAYLOAD ? APDU_MAX_PAYLOAD : remaining; | ||
const p1 = remaining === payload.length ? P1_FIRST : P1_MORE; | ||
const p2 = remaining - chunkSize === 0 ? P2_LAST : P2_MORE; | ||
const chunk = payload.slice(payload.length - remaining, payload.length - remaining + chunkSize); | ||
response = yield this.transport.send(CLA, instruction, p1, p2, chunk).catch(e => { | ||
throw remapErrors(e); | ||
}); | ||
remaining -= chunkSize; | ||
if (remaining === 0) { | ||
break; | ||
} | ||
} | ||
response = apduResponse; | ||
})).then(() => { | ||
const status = Buffer.from(response.slice(response.length - 2)).readUInt16BE(0); | ||
if (status === SW_OK) { | ||
const signature = Buffer.from(response.slice(0, response.length - 2)); | ||
return { | ||
signature: signature, | ||
}; | ||
} | ||
else if (status === SW_CANCEL) { | ||
throw new Error("Transaction approval request was rejected"); | ||
} | ||
else if (status === SW_UNSUPPORTED) { | ||
throw new Error("Hash signing is not supported"); | ||
} | ||
else { | ||
throw new Error("Hash signing not allowed. Have you enabled it in the app settings?"); | ||
} | ||
return response.slice(0, -2); | ||
}); | ||
} | ||
} | ||
const remapErrors = e => { | ||
if (e) { | ||
switch (e.statusCode) { | ||
case SW_DENY: | ||
return new StellarUserRefusedError("User refused the request", undefined, { cause: e }); | ||
case SW_DATA_PARSING_FAIL: | ||
return new StellarDataParsingFailedError("Unable to parse the provided data", undefined, { | ||
cause: e, | ||
}); | ||
case SW_HASH_SIGNING_MODE_NOT_ENABLED: | ||
return new StellarHashSigningNotEnabledError("Hash signing not allowed. Have you enabled it in the app settings?", undefined, { cause: e }); | ||
case SW_DATA_TOO_LARGE: | ||
return new StellarDataTooLargeError("The provided data is too large for the device to process", undefined, { cause: e }); | ||
} | ||
} | ||
return e; | ||
}; | ||
const pathToBuffer = (originalPath) => { | ||
const path = originalPath | ||
.split("/") | ||
.map(value => (value.endsWith("'") || value.endsWith("h") ? value : `${value}'`)) | ||
.join("/"); | ||
const pathNums = BIPPath.fromString(path).toPathArray(); | ||
return serializePath(pathNums); | ||
}; | ||
const serializePath = (path) => { | ||
const buf = Buffer.alloc(1 + path.length * 4); | ||
buf.writeUInt8(path.length, 0); | ||
for (const [i, num] of path.entries()) { | ||
buf.writeUInt32BE(num, 1 + i * 4); | ||
} | ||
return buf; | ||
}; | ||
export * from "./errors"; | ||
//# sourceMappingURL=Str.js.map |
@@ -22,2 +22,5 @@ /// <reference types="node" /> | ||
* | ||
* @param transport a transport for sending commands to a device | ||
* @param scrambleKey a scramble key | ||
* | ||
* @example | ||
@@ -28,26 +31,36 @@ * import Str from "@ledgerhq/hw-app-str"; | ||
export default class Str { | ||
transport: Transport; | ||
private transport; | ||
constructor(transport: Transport, scrambleKey?: string); | ||
/** | ||
* Get Stellar application configuration. | ||
* | ||
* @returns an object with the application configuration, including the version, | ||
* whether hash signing is enabled, and the maximum data size in bytes that the device can sign. | ||
* @example | ||
* str.getAppConfiguration().then(o => o.version) | ||
*/ | ||
getAppConfiguration(): Promise<{ | ||
version: string; | ||
hashSigningEnabled: boolean; | ||
maxDataSize?: number; | ||
}>; | ||
/** | ||
* get Stellar public key for a given BIP 32 path. | ||
* Get Stellar raw public key for a given BIP 32 path. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @option boolValidate optionally enable key pair validation | ||
* @option boolDisplay optionally enable or not the display | ||
* @return an object with the publicKey (using XLM public key format) and | ||
* the raw ed25519 public key. | ||
* @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 | ||
* @return an object with the raw ed25519 public key. | ||
* If you want to convert it to string, you can use {@link https://stellar.github.io/js-stellar-base/StrKey.html#.encodeEd25519PublicKey StrKey.encodeEd25519PublicKey} | ||
* @example | ||
* str.getPublicKey("44'/148'/0'").then(o => o.publicKey) | ||
* str.getPublicKey("44'/148'/0'").then(o => o.rawPublicKey) | ||
*/ | ||
getPublicKey(path: string, boolValidate?: boolean, boolDisplay?: boolean): Promise<{ | ||
publicKey: string; | ||
raw: Buffer; | ||
getPublicKey(path: string, display?: boolean): Promise<{ | ||
rawPublicKey: Buffer; | ||
}>; | ||
/** | ||
* sign a Stellar transaction. | ||
* Sign a Stellar transaction. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param transaction signature base of the transaction to sign | ||
* @return an object with the signature and the status | ||
* @param transaction {@link https://stellar.github.io/js-stellar-base/Transaction.html#signatureBase signature base} of the transaction to sign | ||
* @return an object with the signature | ||
* @example | ||
@@ -60,7 +73,20 @@ * str.signTransaction("44'/148'/0'", signatureBase).then(o => o.signature) | ||
/** | ||
* sign a Stellar transaction hash. | ||
* Sign a Stellar Soroban authorization. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param hash hash of the transaction to sign | ||
* @param hashIdPreimage the {@link https://github.com/stellar/stellar-xdr/blob/1a04392432dacc0092caaeae22a600ea1af3c6a5/Stellar-transaction.x#L702-L709 Soroban authorization hashIdPreimage} to sign | ||
* @return an object with the signature | ||
* @example | ||
* str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage).then(o => o.signature) | ||
*/ | ||
signSorobanAuthorization(path: string, hashIdPreimage: Buffer): Promise<{ | ||
signature: Buffer; | ||
}>; | ||
/** | ||
* Sign a hash. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param hash the hash to sign | ||
* @return an object with the signature | ||
* @example | ||
* str.signHash("44'/148'/0'", hash).then(o => o.signature) | ||
@@ -71,6 +97,5 @@ */ | ||
}>; | ||
signHash_private(path: string, hash: Buffer): Promise<{ | ||
signature: Buffer; | ||
}>; | ||
private sendToDevice; | ||
} | ||
export * from "./errors"; | ||
//# sourceMappingURL=Str.d.ts.map |
341
lib/Str.js
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
} | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const utils_1 = require("./utils"); | ||
const bip32_path_1 = __importDefault(require("bip32-path")); | ||
const errors_1 = require("./errors"); | ||
const CLA = 0xe0; | ||
const P1_FIRST = 0x00; | ||
const P1_MORE = 0x80; | ||
const P2_LAST = 0x00; | ||
const P2_MORE = 0x80; | ||
const P2_NON_CONFIRM = 0x00; // for getPublicKey | ||
const P2_CONFIRM = 0x01; // for getPublicKey | ||
const INS_GET_PK = 0x02; | ||
const INS_SIGN_TX = 0x04; | ||
const INS_GET_CONF = 0x06; | ||
const INS_SIGN_TX_HASH = 0x08; | ||
const INS_KEEP_ALIVE = 0x10; | ||
const APDU_MAX_SIZE = 150; | ||
const P1_FIRST_APDU = 0x00; | ||
const P1_MORE_APDU = 0x80; | ||
const P2_LAST_APDU = 0x00; | ||
const P2_MORE_APDU = 0x80; | ||
const SW_OK = 0x9000; | ||
const SW_CANCEL = 0x6985; | ||
const SW_UNKNOWN_OP = 0x6c24; | ||
const SW_MULTI_OP = 0x6c25; | ||
const SW_NOT_ALLOWED = 0x6c66; | ||
const SW_UNSUPPORTED = 0x6d00; | ||
const SW_KEEP_ALIVE = 0x6e02; | ||
const TX_MAX_SIZE = 1540; | ||
const INS_SIGN_HASH = 0x08; | ||
const INS_SIGN_SOROBAN_AUTHORIZATION = 0x0a; | ||
const APDU_MAX_PAYLOAD = 255; | ||
const SW_DENY = 0x6985; | ||
const SW_HASH_SIGNING_MODE_NOT_ENABLED = 0x6c66; | ||
const SW_DATA_TOO_LARGE = 0xb004; | ||
const SW_DATA_PARSING_FAIL = 0xb005; | ||
/** | ||
* Stellar API | ||
* | ||
* @param transport a transport for sending commands to a device | ||
* @param scrambleKey a scramble key | ||
* | ||
* @example | ||
@@ -33,11 +61,26 @@ * import Str from "@ledgerhq/hw-app-str"; | ||
this.transport = transport; | ||
transport.decorateAppAPIMethods(this, ["getAppConfiguration", "getPublicKey", "signTransaction", "signHash"], scrambleKey); | ||
transport.decorateAppAPIMethods(this, [ | ||
"getAppConfiguration", | ||
"getPublicKey", | ||
"signTransaction", | ||
"signSorobanAuthorization", | ||
"signHash", | ||
], scrambleKey); | ||
} | ||
/** | ||
* Get Stellar application configuration. | ||
* | ||
* @returns an object with the application configuration, including the version, | ||
* whether hash signing is enabled, and the maximum data size in bytes that the device can sign. | ||
* @example | ||
* str.getAppConfiguration().then(o => o.version) | ||
*/ | ||
getAppConfiguration() { | ||
return this.transport.send(CLA, INS_GET_CONF, 0x00, 0x00).then(response => { | ||
const multiOpsEnabled = response[0] === 0x01 || response[1] < 0x02; | ||
const version = "" + response[1] + "." + response[2] + "." + response[3]; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const resp = yield this.sendToDevice(INS_GET_CONF, Buffer.alloc(0)); | ||
const [hashSigningEnabled, major, minor, patch, maxDataSizeHi, maxDataSizeLo] = resp; | ||
return { | ||
version: version, | ||
multiOpsEnabled: multiOpsEnabled, | ||
hashSigningEnabled: hashSigningEnabled === 0x01, | ||
version: `${major}.${minor}.${patch}`, | ||
maxDataSize: resp.length > 4 ? (maxDataSizeHi << 8) | maxDataSizeLo : undefined, // For compatibility with older app, let's remove this in the future | ||
}; | ||
@@ -47,56 +90,30 @@ }); | ||
/** | ||
* get Stellar public key for a given BIP 32 path. | ||
* Get Stellar raw public key for a given BIP 32 path. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @option boolValidate optionally enable key pair validation | ||
* @option boolDisplay optionally enable or not the display | ||
* @return an object with the publicKey (using XLM public key format) and | ||
* the raw ed25519 public key. | ||
* @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 | ||
* @return an object with the raw ed25519 public key. | ||
* If you want to convert it to string, you can use {@link https://stellar.github.io/js-stellar-base/StrKey.html#.encodeEd25519PublicKey StrKey.encodeEd25519PublicKey} | ||
* @example | ||
* str.getPublicKey("44'/148'/0'").then(o => o.publicKey) | ||
* str.getPublicKey("44'/148'/0'").then(o => o.rawPublicKey) | ||
*/ | ||
getPublicKey(path, boolValidate, boolDisplay) { | ||
(0, utils_1.checkStellarBip32Path)(path); | ||
const apdus = []; | ||
let response; | ||
const pathElts = (0, utils_1.splitPath)(path); | ||
const buffer = Buffer.alloc(1 + pathElts.length * 4); | ||
buffer[0] = pathElts.length; | ||
pathElts.forEach((element, index) => { | ||
buffer.writeUInt32BE(element, 1 + 4 * index); | ||
}); | ||
const verifyMsg = Buffer.from("via lumina", "ascii"); | ||
apdus.push(Buffer.concat([buffer, verifyMsg])); | ||
let keepAlive = false; | ||
return (0, utils_1.foreach)(apdus, data => this.transport | ||
.send(CLA, keepAlive ? INS_KEEP_ALIVE : INS_GET_PK, boolValidate ? 0x01 : 0x00, boolDisplay ? 0x01 : 0x00, data, [SW_OK, SW_KEEP_ALIVE]) | ||
.then(apduResponse => { | ||
const status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0); | ||
if (status === SW_KEEP_ALIVE) { | ||
keepAlive = true; | ||
apdus.push(Buffer.alloc(0)); | ||
getPublicKey(path_1) { | ||
return __awaiter(this, arguments, void 0, function* (path, display = false) { | ||
const pathBuffer = pathToBuffer(path); | ||
const p2 = display ? P2_CONFIRM : P2_NON_CONFIRM; | ||
try { | ||
const data = yield this.transport.send(CLA, INS_GET_PK, P1_FIRST, p2, pathBuffer); | ||
return { rawPublicKey: data.slice(0, -2) }; | ||
} | ||
response = apduResponse; | ||
})).then(() => { | ||
// response = Buffer.from(response, 'hex'); | ||
let offset = 0; | ||
const rawPublicKey = response.slice(offset, offset + 32); | ||
offset += 32; | ||
const publicKey = (0, utils_1.encodeEd25519PublicKey)(rawPublicKey); | ||
if (boolValidate) { | ||
const signature = response.slice(offset, offset + 64); | ||
if (!(0, utils_1.verifyEd25519Signature)(verifyMsg, signature, rawPublicKey)) { | ||
throw new Error("Bad signature. Keypair is invalid. Please report this."); | ||
} | ||
catch (e) { | ||
throw remapErrors(e); | ||
} | ||
return { | ||
publicKey: publicKey, | ||
raw: rawPublicKey, | ||
}; | ||
}); | ||
} | ||
/** | ||
* sign a Stellar transaction. | ||
* Sign a Stellar transaction. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param transaction signature base of the transaction to sign | ||
* @return an object with the signature and the status | ||
* @param transaction {@link https://stellar.github.io/js-stellar-base/Transaction.html#signatureBase signature base} of the transaction to sign | ||
* @return an object with the signature | ||
* @example | ||
@@ -106,71 +123,31 @@ * str.signTransaction("44'/148'/0'", signatureBase).then(o => o.signature) | ||
signTransaction(path, transaction) { | ||
(0, utils_1.checkStellarBip32Path)(path); | ||
if (transaction.length > TX_MAX_SIZE) { | ||
throw new Error("Transaction too large: max = " + TX_MAX_SIZE + "; actual = " + transaction.length); | ||
} | ||
const apdus = []; | ||
let response; | ||
const pathElts = (0, utils_1.splitPath)(path); | ||
const bufferSize = 1 + pathElts.length * 4; | ||
const buffer = Buffer.alloc(bufferSize); | ||
buffer[0] = pathElts.length; | ||
pathElts.forEach(function (element, index) { | ||
buffer.writeUInt32BE(element, 1 + 4 * index); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const pathBuffer = pathToBuffer(path); | ||
const payload = Buffer.concat([pathBuffer, transaction]); | ||
const resp = yield this.sendToDevice(INS_SIGN_TX, payload); | ||
return { signature: resp }; | ||
}); | ||
let chunkSize = APDU_MAX_SIZE - bufferSize; | ||
if (transaction.length <= chunkSize) { | ||
// it fits in a single apdu | ||
apdus.push(Buffer.concat([buffer, transaction])); | ||
} | ||
else { | ||
// we need to send multiple apdus to transmit the entire transaction | ||
let chunk = Buffer.alloc(chunkSize); | ||
let offset = 0; | ||
transaction.copy(chunk, 0, offset, chunkSize); | ||
apdus.push(Buffer.concat([buffer, chunk])); | ||
offset += chunkSize; | ||
while (offset < transaction.length) { | ||
const remaining = transaction.length - offset; | ||
chunkSize = remaining < APDU_MAX_SIZE ? remaining : APDU_MAX_SIZE; | ||
chunk = Buffer.alloc(chunkSize); | ||
transaction.copy(chunk, 0, offset, offset + chunkSize); | ||
offset += chunkSize; | ||
apdus.push(chunk); | ||
} | ||
} | ||
let keepAlive = false; | ||
return (0, utils_1.foreach)(apdus, (data, i) => this.transport | ||
.send(CLA, keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX, i === 0 ? P1_FIRST_APDU : P1_MORE_APDU, i === apdus.length - 1 ? P2_LAST_APDU : P2_MORE_APDU, data, [SW_OK, SW_CANCEL, SW_UNKNOWN_OP, SW_MULTI_OP, SW_KEEP_ALIVE]) | ||
.then(apduResponse => { | ||
const status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0); | ||
if (status === SW_KEEP_ALIVE) { | ||
keepAlive = true; | ||
apdus.push(Buffer.alloc(0)); | ||
} | ||
response = apduResponse; | ||
})).then(() => { | ||
const status = Buffer.from(response.slice(response.length - 2)).readUInt16BE(0); | ||
if (status === SW_OK) { | ||
const signature = Buffer.from(response.slice(0, response.length - 2)); | ||
return { | ||
signature: signature, | ||
}; | ||
} | ||
else if (status === SW_UNKNOWN_OP) { | ||
// pre-v2 app version: fall back on hash signing | ||
return this.signHash_private(path, (0, utils_1.hash)(transaction)); | ||
} | ||
else if (status === SW_MULTI_OP) { | ||
// multi-operation transaction: attempt hash signing | ||
return this.signHash_private(path, (0, utils_1.hash)(transaction)); | ||
} | ||
else { | ||
throw new Error("Transaction approval request was rejected"); | ||
} | ||
} | ||
/** | ||
* Sign a Stellar Soroban authorization. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param hashIdPreimage the {@link https://github.com/stellar/stellar-xdr/blob/1a04392432dacc0092caaeae22a600ea1af3c6a5/Stellar-transaction.x#L702-L709 Soroban authorization hashIdPreimage} to sign | ||
* @return an object with the signature | ||
* @example | ||
* str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage).then(o => o.signature) | ||
*/ | ||
signSorobanAuthorization(path, hashIdPreimage) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const pathBuffer = pathToBuffer(path); | ||
const payload = Buffer.concat([pathBuffer, hashIdPreimage]); | ||
const resp = yield this.sendToDevice(INS_SIGN_SOROBAN_AUTHORIZATION, payload); | ||
return { signature: resp }; | ||
}); | ||
} | ||
/** | ||
* sign a Stellar transaction hash. | ||
* Sign a hash. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param hash hash of the transaction to sign | ||
* @param hash the hash to sign | ||
* @return an object with the signature | ||
@@ -181,48 +158,28 @@ * @example | ||
signHash(path, hash) { | ||
(0, utils_1.checkStellarBip32Path)(path); | ||
return this.signHash_private(path, hash); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const pathBuffer = pathToBuffer(path); | ||
const payload = Buffer.concat([pathBuffer, hash]); | ||
const resp = yield this.sendToDevice(INS_SIGN_HASH, payload); | ||
return { signature: resp }; | ||
}); | ||
} | ||
signHash_private(path, hash) { | ||
const apdus = []; | ||
let response; | ||
const pathElts = (0, utils_1.splitPath)(path); | ||
const buffer = Buffer.alloc(1 + pathElts.length * 4); | ||
buffer[0] = pathElts.length; | ||
pathElts.forEach(function (element, index) { | ||
buffer.writeUInt32BE(element, 1 + 4 * index); | ||
}); | ||
apdus.push(Buffer.concat([buffer, hash])); | ||
let keepAlive = false; | ||
return (0, utils_1.foreach)(apdus, data => this.transport | ||
.send(CLA, keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX_HASH, 0x00, 0x00, data, [ | ||
SW_OK, | ||
SW_CANCEL, | ||
SW_NOT_ALLOWED, | ||
SW_UNSUPPORTED, | ||
SW_KEEP_ALIVE, | ||
]) | ||
.then(apduResponse => { | ||
const status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0); | ||
if (status === SW_KEEP_ALIVE) { | ||
keepAlive = true; | ||
apdus.push(Buffer.alloc(0)); | ||
sendToDevice(instruction, payload) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
let response = Buffer.alloc(0); | ||
let remaining = payload.length; | ||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
const chunkSize = remaining > APDU_MAX_PAYLOAD ? APDU_MAX_PAYLOAD : remaining; | ||
const p1 = remaining === payload.length ? P1_FIRST : P1_MORE; | ||
const p2 = remaining - chunkSize === 0 ? P2_LAST : P2_MORE; | ||
const chunk = payload.slice(payload.length - remaining, payload.length - remaining + chunkSize); | ||
response = yield this.transport.send(CLA, instruction, p1, p2, chunk).catch(e => { | ||
throw remapErrors(e); | ||
}); | ||
remaining -= chunkSize; | ||
if (remaining === 0) { | ||
break; | ||
} | ||
} | ||
response = apduResponse; | ||
})).then(() => { | ||
const status = Buffer.from(response.slice(response.length - 2)).readUInt16BE(0); | ||
if (status === SW_OK) { | ||
const signature = Buffer.from(response.slice(0, response.length - 2)); | ||
return { | ||
signature: signature, | ||
}; | ||
} | ||
else if (status === SW_CANCEL) { | ||
throw new Error("Transaction approval request was rejected"); | ||
} | ||
else if (status === SW_UNSUPPORTED) { | ||
throw new Error("Hash signing is not supported"); | ||
} | ||
else { | ||
throw new Error("Hash signing not allowed. Have you enabled it in the app settings?"); | ||
} | ||
return response.slice(0, -2); | ||
}); | ||
@@ -232,2 +189,36 @@ } | ||
exports.default = Str; | ||
const remapErrors = e => { | ||
if (e) { | ||
switch (e.statusCode) { | ||
case SW_DENY: | ||
return new errors_1.StellarUserRefusedError("User refused the request", undefined, { cause: e }); | ||
case SW_DATA_PARSING_FAIL: | ||
return new errors_1.StellarDataParsingFailedError("Unable to parse the provided data", undefined, { | ||
cause: e, | ||
}); | ||
case SW_HASH_SIGNING_MODE_NOT_ENABLED: | ||
return new errors_1.StellarHashSigningNotEnabledError("Hash signing not allowed. Have you enabled it in the app settings?", undefined, { cause: e }); | ||
case SW_DATA_TOO_LARGE: | ||
return new errors_1.StellarDataTooLargeError("The provided data is too large for the device to process", undefined, { cause: e }); | ||
} | ||
} | ||
return e; | ||
}; | ||
const pathToBuffer = (originalPath) => { | ||
const path = originalPath | ||
.split("/") | ||
.map(value => (value.endsWith("'") || value.endsWith("h") ? value : `${value}'`)) | ||
.join("/"); | ||
const pathNums = bip32_path_1.default.fromString(path).toPathArray(); | ||
return serializePath(pathNums); | ||
}; | ||
const serializePath = (path) => { | ||
const buf = Buffer.alloc(1 + path.length * 4); | ||
buf.writeUInt8(path.length, 0); | ||
for (const [i, num] of path.entries()) { | ||
buf.writeUInt32BE(num, 1 + i * 4); | ||
} | ||
return buf; | ||
}; | ||
__exportStar(require("./errors"), exports); | ||
//# sourceMappingURL=Str.js.map |
{ | ||
"name": "@ledgerhq/hw-app-str", | ||
"version": "6.29.0", | ||
"version": "7.0.0-next.0", | ||
"description": "Ledger Hardware Wallet Stellar Application API", | ||
@@ -30,6 +30,5 @@ "keywords": [ | ||
"dependencies": { | ||
"base32.js": "^0.1.0", | ||
"sha.js": "^2.3.6", | ||
"tweetnacl": "^1.0.3", | ||
"@ledgerhq/hw-transport": "^6.31.0" | ||
"bip32-path": "^0.4.2", | ||
"@ledgerhq/hw-transport": "^6.31.0", | ||
"@ledgerhq/errors": "^6.17.0" | ||
}, | ||
@@ -53,2 +52,3 @@ "devDependencies": { | ||
"watch": "tsc --watch", | ||
"doc": "documentation readme src/** --section=API --pe ts --re ts --re d.ts", | ||
"lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", | ||
@@ -55,0 +55,0 @@ "lint:fix": "pnpm lint --fix", |
212
README.md
<img src="https://user-images.githubusercontent.com/4631227/191834116-59cf590e-25cc-4956-ae5c-812ea464f324.png" height="100" /> | ||
## Ledger Stellar app API | ||
[GitHub](https://github.com/LedgerHQ/ledger-live/), | ||
[Ledger Devs Discord](https://developers.ledger.com/discord-pro), | ||
[Developer Portal](https://developers.ledger.com/) | ||
## Usage | ||
## @ledgerhq/hw-app-str | ||
Ledger Hardware Wallet Stellar JavaScript bindings. | ||
```js | ||
// when using "@ledgerhq/hw-transport-node-hid" library you need to go to | ||
// Settings -> Browser support in ledger stellar app and set this setting to 'No' | ||
import Transport from "@ledgerhq/hw-transport-node-hid"; | ||
// import Transport from "@ledgerhq/hw-transport-u2f"; // for browser | ||
*** | ||
## Are you adding Ledger support to your software wallet? | ||
You may be using this package to communicate with the Stellar Nano App. | ||
For a smooth and quick integration: | ||
* See the developers’ documentation on the [Developer Portal](https://developers.ledger.com/docs/transport/overview/) and | ||
* Go on [Discord](https://developers.ledger.com/discord-pro/) to chat with developer support and the developer community. | ||
*** | ||
## Errors handling | ||
All functions may throw an error, it's important to handle the errors properly. | ||
We have written corresponding classes for exceptions that developers should actively handle, you can find them in the [API](#api) section. | ||
*** | ||
## API | ||
<!-- Generated by documentation.js. Update this documentation by updating the source code. --> | ||
#### Table of Contents | ||
* [StellarHashSigningNotEnabledError](#stellarhashsigningnotenablederror) | ||
* [StellarDataParsingFailedError](#stellardataparsingfailederror) | ||
* [StellarUserRefusedError](#stellaruserrefusederror) | ||
* [StellarDataTooLargeError](#stellardatatoolargeerror) | ||
* [Str](#str) | ||
* [Parameters](#parameters) | ||
* [Examples](#examples) | ||
* [getAppConfiguration](#getappconfiguration) | ||
* [Examples](#examples-1) | ||
* [getPublicKey](#getpublickey) | ||
* [Parameters](#parameters-1) | ||
* [Examples](#examples-2) | ||
* [signTransaction](#signtransaction) | ||
* [Parameters](#parameters-2) | ||
* [Examples](#examples-3) | ||
* [signSorobanAuthorization](#signsorobanauthorization) | ||
* [Parameters](#parameters-3) | ||
* [Examples](#examples-4) | ||
* [signHash](#signhash) | ||
* [Parameters](#parameters-4) | ||
* [Examples](#examples-5) | ||
### StellarHashSigningNotEnabledError | ||
Error thrown when hash signing is not enabled on the device. | ||
### StellarDataParsingFailedError | ||
Error thrown when data parsing fails. | ||
For example, when parsing the transaction fails, this error is thrown. | ||
### StellarUserRefusedError | ||
Error thrown when the user refuses the request on the device. | ||
### StellarDataTooLargeError | ||
Error thrown when the data is too large to be processed by the device. | ||
### Str | ||
Stellar API | ||
#### Parameters | ||
* `transport` **Transport** a transport for sending commands to a device | ||
* `scrambleKey` a scramble key (optional, default `"l0v"`) | ||
#### Examples | ||
```javascript | ||
import Str from "@ledgerhq/hw-app-str"; | ||
import * as StellarSdk from "@stellar/stellar-sdk"; | ||
const str = new Str(transport) | ||
``` | ||
const getStrAppVersion = async () => { | ||
const transport = await Transport.create(); | ||
const str = new Str(transport); | ||
const result = await str.getAppConfiguration(); | ||
return result.version; | ||
} | ||
getStrAppVersion().then(v => console.log(v)); | ||
#### getAppConfiguration | ||
const getStrPublicKey = async () => { | ||
const transport = await Transport.create(); | ||
const str = new Str(transport); | ||
const result = await str.getPublicKey("44'/148'/0'"); | ||
return result.publicKey; | ||
}; | ||
let publicKey; | ||
getStrPublicKey().then(pk => { | ||
console.log(pk); | ||
publicKey = pk; | ||
}); | ||
Get Stellar application configuration. | ||
const signStrTransaction = async (publicKey) => { | ||
const transaction = new StellarSdk.TransactionBuilder({accountId: () => publicKey, sequenceNumber: () => '1234', incrementSequenceNumber: () => null}) | ||
.addOperation(StellarSdk.Operation.createAccount({ | ||
source: publicKey, | ||
destination: 'GBLYVYCCCRYTZTWTWGOMJYKEGQMTH2U3X4R4NUI7CUGIGEJEKYD5S5OJ', // SATIS5GR33FXKM7HVWZ2UQO33GM66TVORZUEF2HPUQ3J7K634CTOAWQ7 | ||
startingBalance: '11.331', | ||
})) | ||
.build(); | ||
const transport = await Transport.create(); | ||
const str = new Str(transport); | ||
const result = await str.signTransaction("44'/148'/0'", transaction.signatureBase()); | ||
// add signature to transaction | ||
const keyPair = StellarSdk.Keypair.fromPublicKey(publicKey); | ||
const hint = keyPair.signatureHint(); | ||
const decorated = new StellarSdk.xdr.DecoratedSignature({hint: hint, signature: result.signature}); | ||
transaction.signatures.push(decorated); | ||
return transaction; | ||
} | ||
signStrTransaction(publicKey).then(transaction => console.log(transaction.toEnvelope().toXDR().toString('base64'))); | ||
##### Examples | ||
```javascript | ||
str.getAppConfiguration().then(o => o.version) | ||
``` | ||
--- | ||
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{version: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), hashSigningEnabled: [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean), maxDataSize: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?}>** an object with the application configuration, including the version, | ||
whether hash signing is enabled, and the maximum data size in bytes that the device can sign. | ||
## Are you adding Ledger support to your software wallet? | ||
#### getPublicKey | ||
You may be using this package to communicate with the Stellar Nano App. | ||
Get Stellar raw public key for a given BIP 32 path. | ||
For a smooth and quick integration: | ||
##### Parameters | ||
- See the developers’ documentation on the [Developer Portal](https://developers.ledger.com/docs/transport/overview/) and | ||
- Go on [Discord](https://developers.ledger.com/discord-pro/) to chat with developer support and the developer community. | ||
* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a path in BIP 32 format | ||
* `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 (optional, default `false`) | ||
--- | ||
##### Examples | ||
```javascript | ||
str.getPublicKey("44'/148'/0'").then(o => o.rawPublicKey) | ||
``` | ||
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{rawPublicKey: [Buffer](https://nodejs.org/api/buffer.html)}>** an object with the raw ed25519 public key. | ||
If you want to convert it to string, you can use [StrKey.encodeEd25519PublicKey](https://stellar.github.io/js-stellar-base/StrKey.html#.encodeEd25519PublicKey) | ||
#### signTransaction | ||
Sign a Stellar transaction. | ||
##### Parameters | ||
* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a path in BIP 32 format | ||
* `transaction` **[Buffer](https://nodejs.org/api/buffer.html)** [signature base](https://stellar.github.io/js-stellar-base/Transaction.html#signatureBase) of the transaction to sign | ||
##### Examples | ||
```javascript | ||
str.signTransaction("44'/148'/0'", signatureBase).then(o => o.signature) | ||
``` | ||
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{signature: [Buffer](https://nodejs.org/api/buffer.html)}>** an object with the signature | ||
#### signSorobanAuthorization | ||
Sign a Stellar Soroban authorization. | ||
##### Parameters | ||
* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a path in BIP 32 format | ||
* `hashIdPreimage` **[Buffer](https://nodejs.org/api/buffer.html)** the [Soroban authorization hashIdPreimage](https://github.com/stellar/stellar-xdr/blob/1a04392432dacc0092caaeae22a600ea1af3c6a5/Stellar-transaction.x#L702-L709) to sign | ||
##### Examples | ||
```javascript | ||
str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage).then(o => o.signature) | ||
``` | ||
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{signature: [Buffer](https://nodejs.org/api/buffer.html)}>** an object with the signature | ||
#### signHash | ||
Sign a hash. | ||
##### Parameters | ||
* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a path in BIP 32 format | ||
* `hash` **[Buffer](https://nodejs.org/api/buffer.html)** the hash to sign | ||
##### Examples | ||
```javascript | ||
str.signHash("44'/148'/0'", hash).then(o => o.signature) | ||
``` | ||
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{signature: [Buffer](https://nodejs.org/api/buffer.html)}>** an object with the signature |
402
src/Str.ts
@@ -18,32 +18,37 @@ /******************************************************************************** | ||
import type Transport from "@ledgerhq/hw-transport"; | ||
import BIPPath from "bip32-path"; | ||
import { | ||
splitPath, | ||
foreach, | ||
encodeEd25519PublicKey, | ||
verifyEd25519Signature, | ||
checkStellarBip32Path, | ||
hash, | ||
} from "./utils"; | ||
StellarHashSigningNotEnabledError, | ||
StellarDataParsingFailedError, | ||
StellarUserRefusedError, | ||
StellarDataTooLargeError, | ||
} from "./errors"; | ||
const CLA = 0xe0; | ||
const P1_FIRST = 0x00; | ||
const P1_MORE = 0x80; | ||
const P2_LAST = 0x00; | ||
const P2_MORE = 0x80; | ||
const P2_NON_CONFIRM = 0x00; // for getPublicKey | ||
const P2_CONFIRM = 0x01; // for getPublicKey | ||
const INS_GET_PK = 0x02; | ||
const INS_SIGN_TX = 0x04; | ||
const INS_GET_CONF = 0x06; | ||
const INS_SIGN_TX_HASH = 0x08; | ||
const INS_KEEP_ALIVE = 0x10; | ||
const APDU_MAX_SIZE = 150; | ||
const P1_FIRST_APDU = 0x00; | ||
const P1_MORE_APDU = 0x80; | ||
const P2_LAST_APDU = 0x00; | ||
const P2_MORE_APDU = 0x80; | ||
const SW_OK = 0x9000; | ||
const SW_CANCEL = 0x6985; | ||
const SW_UNKNOWN_OP = 0x6c24; | ||
const SW_MULTI_OP = 0x6c25; | ||
const SW_NOT_ALLOWED = 0x6c66; | ||
const SW_UNSUPPORTED = 0x6d00; | ||
const SW_KEEP_ALIVE = 0x6e02; | ||
const TX_MAX_SIZE = 1540; | ||
const INS_SIGN_HASH = 0x08; | ||
const INS_SIGN_SOROBAN_AUTHORIZATION = 0x0a; | ||
const APDU_MAX_PAYLOAD = 255; | ||
const SW_DENY = 0x6985; | ||
const SW_HASH_SIGNING_MODE_NOT_ENABLED = 0x6c66; | ||
const SW_DATA_TOO_LARGE = 0xb004; | ||
const SW_DATA_PARSING_FAIL = 0xb005; | ||
/** | ||
* Stellar API | ||
* | ||
* @param transport a transport for sending commands to a device | ||
* @param scrambleKey a scramble key | ||
* | ||
* @example | ||
@@ -53,5 +58,4 @@ * import Str from "@ledgerhq/hw-app-str"; | ||
*/ | ||
export default class Str { | ||
transport: Transport; | ||
private transport: Transport; | ||
@@ -62,3 +66,9 @@ constructor(transport: Transport, scrambleKey = "l0v") { | ||
this, | ||
["getAppConfiguration", "getPublicKey", "signTransaction", "signHash"], | ||
[ | ||
"getAppConfiguration", | ||
"getPublicKey", | ||
"signTransaction", | ||
"signSorobanAuthorization", | ||
"signHash", | ||
], | ||
scrambleKey, | ||
@@ -68,96 +78,55 @@ ); | ||
getAppConfiguration(): Promise<{ | ||
/** | ||
* Get Stellar application configuration. | ||
* | ||
* @returns an object with the application configuration, including the version, | ||
* whether hash signing is enabled, and the maximum data size in bytes that the device can sign. | ||
* @example | ||
* str.getAppConfiguration().then(o => o.version) | ||
*/ | ||
async getAppConfiguration(): Promise<{ | ||
version: string; | ||
hashSigningEnabled: boolean; | ||
maxDataSize?: number; | ||
}> { | ||
return this.transport.send(CLA, INS_GET_CONF, 0x00, 0x00).then(response => { | ||
const multiOpsEnabled = response[0] === 0x01 || response[1] < 0x02; | ||
const version = "" + response[1] + "." + response[2] + "." + response[3]; | ||
return { | ||
version: version, | ||
multiOpsEnabled: multiOpsEnabled, | ||
}; | ||
}); | ||
const resp = await this.sendToDevice(INS_GET_CONF, Buffer.alloc(0)); | ||
const [hashSigningEnabled, major, minor, patch, maxDataSizeHi, maxDataSizeLo] = resp; | ||
return { | ||
hashSigningEnabled: hashSigningEnabled === 0x01, | ||
version: `${major}.${minor}.${patch}`, | ||
maxDataSize: resp.length > 4 ? (maxDataSizeHi << 8) | maxDataSizeLo : undefined, // For compatibility with older app, let's remove this in the future | ||
}; | ||
} | ||
/** | ||
* get Stellar public key for a given BIP 32 path. | ||
* Get Stellar raw public key for a given BIP 32 path. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @option boolValidate optionally enable key pair validation | ||
* @option boolDisplay optionally enable or not the display | ||
* @return an object with the publicKey (using XLM public key format) and | ||
* the raw ed25519 public key. | ||
* @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 | ||
* @return an object with the raw ed25519 public key. | ||
* If you want to convert it to string, you can use {@link https://stellar.github.io/js-stellar-base/StrKey.html#.encodeEd25519PublicKey StrKey.encodeEd25519PublicKey} | ||
* @example | ||
* str.getPublicKey("44'/148'/0'").then(o => o.publicKey) | ||
* str.getPublicKey("44'/148'/0'").then(o => o.rawPublicKey) | ||
*/ | ||
getPublicKey( | ||
path: string, | ||
boolValidate?: boolean, | ||
boolDisplay?: boolean, | ||
): Promise<{ | ||
publicKey: string; | ||
raw: Buffer; | ||
}> { | ||
checkStellarBip32Path(path); | ||
const apdus: Buffer[] = []; | ||
let response; | ||
const pathElts = splitPath(path); | ||
const buffer = Buffer.alloc(1 + pathElts.length * 4); | ||
buffer[0] = pathElts.length; | ||
pathElts.forEach((element, index) => { | ||
buffer.writeUInt32BE(element, 1 + 4 * index); | ||
}); | ||
const verifyMsg = Buffer.from("via lumina", "ascii"); | ||
apdus.push(Buffer.concat([buffer, verifyMsg])); | ||
let keepAlive = false; | ||
return foreach(apdus, data => | ||
this.transport | ||
.send( | ||
CLA, | ||
keepAlive ? INS_KEEP_ALIVE : INS_GET_PK, | ||
boolValidate ? 0x01 : 0x00, | ||
boolDisplay ? 0x01 : 0x00, | ||
data, | ||
[SW_OK, SW_KEEP_ALIVE], | ||
) | ||
.then(apduResponse => { | ||
const status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0); | ||
if (status === SW_KEEP_ALIVE) { | ||
keepAlive = true; | ||
apdus.push(Buffer.alloc(0)); | ||
} | ||
response = apduResponse; | ||
}), | ||
).then(() => { | ||
// response = Buffer.from(response, 'hex'); | ||
let offset = 0; | ||
const rawPublicKey = response.slice(offset, offset + 32); | ||
offset += 32; | ||
const publicKey = encodeEd25519PublicKey(rawPublicKey); | ||
if (boolValidate) { | ||
const signature = response.slice(offset, offset + 64); | ||
if (!verifyEd25519Signature(verifyMsg, signature, rawPublicKey)) { | ||
throw new Error("Bad signature. Keypair is invalid. Please report this."); | ||
} | ||
} | ||
return { | ||
publicKey: publicKey, | ||
raw: rawPublicKey, | ||
}; | ||
}); | ||
async getPublicKey(path: string, display = false): Promise<{ rawPublicKey: Buffer }> { | ||
const pathBuffer = pathToBuffer(path); | ||
const p2 = display ? P2_CONFIRM : P2_NON_CONFIRM; | ||
try { | ||
const data = await this.transport.send(CLA, INS_GET_PK, P1_FIRST, p2, pathBuffer); | ||
return { rawPublicKey: data.slice(0, -2) }; | ||
} catch (e) { | ||
throw remapErrors(e); | ||
} | ||
} | ||
/** | ||
* sign a Stellar transaction. | ||
* Sign a Stellar transaction. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param transaction signature base of the transaction to sign | ||
* @return an object with the signature and the status | ||
* @param transaction {@link https://stellar.github.io/js-stellar-base/Transaction.html#signatureBase signature base} of the transaction to sign | ||
* @return an object with the signature | ||
* @example | ||
* str.signTransaction("44'/148'/0'", signatureBase).then(o => o.signature) | ||
*/ | ||
signTransaction( | ||
async signTransaction( | ||
path: string, | ||
@@ -168,102 +137,39 @@ transaction: Buffer, | ||
}> { | ||
checkStellarBip32Path(path); | ||
if (transaction.length > TX_MAX_SIZE) { | ||
throw new Error( | ||
"Transaction too large: max = " + TX_MAX_SIZE + "; actual = " + transaction.length, | ||
); | ||
} | ||
const apdus: Buffer[] = []; | ||
let response; | ||
const pathElts = splitPath(path); | ||
const bufferSize = 1 + pathElts.length * 4; | ||
const buffer = Buffer.alloc(bufferSize); | ||
buffer[0] = pathElts.length; | ||
pathElts.forEach(function (element, index) { | ||
buffer.writeUInt32BE(element, 1 + 4 * index); | ||
}); | ||
let chunkSize = APDU_MAX_SIZE - bufferSize; | ||
if (transaction.length <= chunkSize) { | ||
// it fits in a single apdu | ||
apdus.push(Buffer.concat([buffer, transaction])); | ||
} else { | ||
// we need to send multiple apdus to transmit the entire transaction | ||
let chunk = Buffer.alloc(chunkSize); | ||
let offset = 0; | ||
transaction.copy(chunk, 0, offset, chunkSize); | ||
apdus.push(Buffer.concat([buffer, chunk])); | ||
offset += chunkSize; | ||
while (offset < transaction.length) { | ||
const remaining = transaction.length - offset; | ||
chunkSize = remaining < APDU_MAX_SIZE ? remaining : APDU_MAX_SIZE; | ||
chunk = Buffer.alloc(chunkSize); | ||
transaction.copy(chunk, 0, offset, offset + chunkSize); | ||
offset += chunkSize; | ||
apdus.push(chunk); | ||
} | ||
} | ||
let keepAlive = false; | ||
return foreach(apdus, (data, i) => | ||
this.transport | ||
.send( | ||
CLA, | ||
keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX, | ||
i === 0 ? P1_FIRST_APDU : P1_MORE_APDU, | ||
i === apdus.length - 1 ? P2_LAST_APDU : P2_MORE_APDU, | ||
data, | ||
[SW_OK, SW_CANCEL, SW_UNKNOWN_OP, SW_MULTI_OP, SW_KEEP_ALIVE], | ||
) | ||
.then(apduResponse => { | ||
const status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0); | ||
if (status === SW_KEEP_ALIVE) { | ||
keepAlive = true; | ||
apdus.push(Buffer.alloc(0)); | ||
} | ||
response = apduResponse; | ||
}), | ||
).then(() => { | ||
const status = Buffer.from(response.slice(response.length - 2)).readUInt16BE(0); | ||
if (status === SW_OK) { | ||
const signature = Buffer.from(response.slice(0, response.length - 2)); | ||
return { | ||
signature: signature, | ||
}; | ||
} else if (status === SW_UNKNOWN_OP) { | ||
// pre-v2 app version: fall back on hash signing | ||
return this.signHash_private(path, hash(transaction)); | ||
} else if (status === SW_MULTI_OP) { | ||
// multi-operation transaction: attempt hash signing | ||
return this.signHash_private(path, hash(transaction)); | ||
} else { | ||
throw new Error("Transaction approval request was rejected"); | ||
} | ||
}); | ||
const pathBuffer = pathToBuffer(path); | ||
const payload = Buffer.concat([pathBuffer, transaction]); | ||
const resp = await this.sendToDevice(INS_SIGN_TX, payload); | ||
return { signature: resp }; | ||
} | ||
/** | ||
* sign a Stellar transaction hash. | ||
* Sign a Stellar Soroban authorization. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param hash hash of the transaction to sign | ||
* @param hashIdPreimage the {@link https://github.com/stellar/stellar-xdr/blob/1a04392432dacc0092caaeae22a600ea1af3c6a5/Stellar-transaction.x#L702-L709 Soroban authorization hashIdPreimage} to sign | ||
* @return an object with the signature | ||
* @example | ||
* str.signHash("44'/148'/0'", hash).then(o => o.signature) | ||
* str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage).then(o => o.signature) | ||
*/ | ||
signHash( | ||
async signSorobanAuthorization( | ||
path: string, | ||
hash: Buffer, | ||
hashIdPreimage: Buffer, | ||
): Promise<{ | ||
signature: Buffer; | ||
}> { | ||
checkStellarBip32Path(path); | ||
return this.signHash_private(path, hash); | ||
const pathBuffer = pathToBuffer(path); | ||
const payload = Buffer.concat([pathBuffer, hashIdPreimage]); | ||
const resp = await this.sendToDevice(INS_SIGN_SOROBAN_AUTHORIZATION, payload); | ||
return { signature: resp }; | ||
} | ||
signHash_private( | ||
/** | ||
* Sign a hash. | ||
* | ||
* @param path a path in BIP 32 format | ||
* @param hash the hash to sign | ||
* @return an object with the signature | ||
* @example | ||
* str.signHash("44'/148'/0'", hash).then(o => o.signature) | ||
*/ | ||
async signHash( | ||
path: string, | ||
@@ -274,48 +180,76 @@ hash: Buffer, | ||
}> { | ||
const apdus: Buffer[] = []; | ||
let response; | ||
const pathElts = splitPath(path); | ||
const buffer = Buffer.alloc(1 + pathElts.length * 4); | ||
buffer[0] = pathElts.length; | ||
pathElts.forEach(function (element, index) { | ||
buffer.writeUInt32BE(element, 1 + 4 * index); | ||
}); | ||
apdus.push(Buffer.concat([buffer, hash])); | ||
let keepAlive = false; | ||
return foreach(apdus, data => | ||
this.transport | ||
.send(CLA, keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX_HASH, 0x00, 0x00, data, [ | ||
SW_OK, | ||
SW_CANCEL, | ||
SW_NOT_ALLOWED, | ||
SW_UNSUPPORTED, | ||
SW_KEEP_ALIVE, | ||
]) | ||
.then(apduResponse => { | ||
const status = Buffer.from(apduResponse.slice(apduResponse.length - 2)).readUInt16BE(0); | ||
const pathBuffer = pathToBuffer(path); | ||
const payload = Buffer.concat([pathBuffer, hash]); | ||
const resp = await this.sendToDevice(INS_SIGN_HASH, payload); | ||
return { signature: resp }; | ||
} | ||
if (status === SW_KEEP_ALIVE) { | ||
keepAlive = true; | ||
apdus.push(Buffer.alloc(0)); | ||
} | ||
private async sendToDevice(instruction: number, payload: Buffer) { | ||
let response: Buffer = Buffer.alloc(0); | ||
let remaining = payload.length; | ||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
const chunkSize = remaining > APDU_MAX_PAYLOAD ? APDU_MAX_PAYLOAD : remaining; | ||
const p1 = remaining === payload.length ? P1_FIRST : P1_MORE; | ||
const p2 = remaining - chunkSize === 0 ? P2_LAST : P2_MORE; | ||
const chunk = payload.slice( | ||
payload.length - remaining, | ||
payload.length - remaining + chunkSize, | ||
); | ||
response = await this.transport.send(CLA, instruction, p1, p2, chunk).catch(e => { | ||
throw remapErrors(e); | ||
}); | ||
remaining -= chunkSize; | ||
if (remaining === 0) { | ||
break; | ||
} | ||
} | ||
return response.slice(0, -2); | ||
} | ||
} | ||
response = apduResponse; | ||
}), | ||
).then(() => { | ||
const status = Buffer.from(response.slice(response.length - 2)).readUInt16BE(0); | ||
const remapErrors = e => { | ||
if (e) { | ||
switch (e.statusCode) { | ||
case SW_DENY: | ||
return new StellarUserRefusedError("User refused the request", undefined, { cause: e }); | ||
case SW_DATA_PARSING_FAIL: | ||
return new StellarDataParsingFailedError("Unable to parse the provided data", undefined, { | ||
cause: e, | ||
}); | ||
case SW_HASH_SIGNING_MODE_NOT_ENABLED: | ||
return new StellarHashSigningNotEnabledError( | ||
"Hash signing not allowed. Have you enabled it in the app settings?", | ||
undefined, | ||
{ cause: e }, | ||
); | ||
case SW_DATA_TOO_LARGE: | ||
return new StellarDataTooLargeError( | ||
"The provided data is too large for the device to process", | ||
undefined, | ||
{ cause: e }, | ||
); | ||
} | ||
} | ||
return e; | ||
}; | ||
if (status === SW_OK) { | ||
const signature = Buffer.from(response.slice(0, response.length - 2)); | ||
return { | ||
signature: signature, | ||
}; | ||
} else if (status === SW_CANCEL) { | ||
throw new Error("Transaction approval request was rejected"); | ||
} else if (status === SW_UNSUPPORTED) { | ||
throw new Error("Hash signing is not supported"); | ||
} else { | ||
throw new Error("Hash signing not allowed. Have you enabled it in the app settings?"); | ||
} | ||
}); | ||
const pathToBuffer = (originalPath: string) => { | ||
const path = originalPath | ||
.split("/") | ||
.map(value => (value.endsWith("'") || value.endsWith("h") ? value : `${value}'`)) | ||
.join("/"); | ||
const pathNums: number[] = BIPPath.fromString(path).toPathArray(); | ||
return serializePath(pathNums); | ||
}; | ||
const serializePath = (path: number[]) => { | ||
const buf = Buffer.alloc(1 + path.length * 4); | ||
buf.writeUInt8(path.length, 0); | ||
for (const [i, num] of path.entries()) { | ||
buf.writeUInt32BE(num, 1 + i * 4); | ||
} | ||
} | ||
return buf; | ||
}; | ||
export * from "./errors"; |
@@ -6,8 +6,10 @@ import { | ||
import Str from "../src/Str"; | ||
import { StellarHashSigningNotEnabledError, StellarDataParsingFailedError, StellarUserRefusedError, StellarDataTooLargeError } from "../src/errors"; | ||
import { TransportStatusError } from "@ledgerhq/errors"; | ||
test("getAppConfiguration", async () => { | ||
test("getAppConfiguration (hash signing disabled)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e006000000 | ||
<= 000300019000 | ||
<= 00050401061a9000 | ||
`) | ||
@@ -17,29 +19,119 @@ ); | ||
const result = await str.getAppConfiguration(); | ||
expect(result).toEqual({ multiOpsEnabled: false, version: "3.0.1" }); | ||
expect(result).toEqual({ hashSigningEnabled: false, maxDataSize: 1562, version: "5.4.1" }); | ||
}); | ||
test("getPublicKey", async () => { | ||
test("getAppConfiguration (hash signing enabled)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e002010117038000002c8000009480000000766961206c756d696e61 | ||
<= 7691d85048acc4ed085d9061ce0948bbdf7de6a92b790aaf241d31b7dcaa423881b9f7cb3bd2fad4f0fdab9da1407e8e85f702fa58584fba3104e4549b85ca8046d73a4010870bc4765eff7e0bafcfe91390c4475ba3fcc598750758ed770e0f9000 | ||
=> e006000000 | ||
<= 01050401061a9000 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const { publicKey, raw } = await str.getPublicKey("44'/148'/0'", true, true); | ||
expect(publicKey).toEqual( | ||
"GB3JDWCQJCWMJ3IILWIGDTQJJC5567PGVEVXSCVPEQOTDN64VJBDQBYX" | ||
const result = await str.getAppConfiguration(); | ||
expect(result).toEqual({ hashSigningEnabled: true, maxDataSize: 1562, version: "5.4.1" }); | ||
}); | ||
test("getAppConfiguration (old version app < 5.4.1)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e006000000 | ||
<= 000504019000 | ||
`) | ||
); | ||
expect(raw.toString("hex")).toEqual( | ||
"7691d85048acc4ed085d9061ce0948bbdf7de6a92b790aaf241d31b7dcaa4238" | ||
const str = new Str(transport); | ||
const result = await str.getAppConfiguration(); | ||
expect(result).toStrictEqual({ hashSigningEnabled: false, maxDataSize: undefined, version: "5.4.1" }); | ||
}); | ||
test("getPublicKey (without confirm)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00200000d038000002c8000009480000000 | ||
<= 9a222500cf47b03d05edec04ed3294cece1de727ccadb401f47d6b4b230e81a09000 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const { rawPublicKey } = await str.getPublicKey("44'/148'/0'", false); | ||
expect(rawPublicKey.toString("hex")).toEqual( | ||
"9a222500cf47b03d05edec04ed3294cece1de727ccadb401f47d6b4b230e81a0" | ||
); | ||
}); | ||
test("signTransaction", async () => { | ||
test("getPublicKey (with confirm)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e004008096038000002c80000094800000007ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a979000000020000000020da998b75e42b1f7f85d075c127f5b246df12ad96f010bcf7f76f72b16e57130000006400c5b4a5000000190000000000000000000000010000000000000001000000009541f02746240c1e9f3843d28e56f0a583ecd27502fb0f4a27d4d0922f | ||
=> e00200010d038000002c8000009480000000 | ||
<= 9a222500cf47b03d05edec04ed3294cece1de727ccadb401f47d6b4b230e81a09000 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const { rawPublicKey } = await str.getPublicKey("44'/148'/0'", true); | ||
expect(rawPublicKey.toString("hex")).toEqual( | ||
"9a222500cf47b03d05edec04ed3294cece1de727ccadb401f47d6b4b230e81a0" | ||
); | ||
}); | ||
test("getPublicKey (with confirm and rejected)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00200010d038000002c8000009480000000 | ||
<= 6985 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
await expect(str.getPublicKey("44'/148'/0'", true)).rejects.toThrow(StellarUserRefusedError); | ||
}); | ||
test("signHash (hash signing enabled)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00800002d038000002c8000009480000000a85c933d37847689825acbdfeb50e66d791814abf03b9be4bd6450abb6c99616 | ||
<= bd4ddd948dd1b27d92672d968eba50b8822eacebf67142f77eda5c5ee9c569c048c90b074a677470984bb203417e911d5b1d74270c2a82772cb80893fca1d70b9000 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const hash = Buffer.from("a85c933d37847689825acbdfeb50e66d791814abf03b9be4bd6450abb6c99616", "hex"); | ||
const { signature } = await str.signHash("44'/148'/0'", hash); | ||
const result = signature.toString("hex"); | ||
expect(result).toEqual( | ||
"bd4ddd948dd1b27d92672d968eba50b8822eacebf67142f77eda5c5ee9c569c048c90b074a677470984bb203417e911d5b1d74270c2a82772cb80893fca1d70b" | ||
); | ||
}) | ||
test("signHash (hash signing disabled)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00800002d038000002c8000009480000000a85c933d37847689825acbdfeb50e66d791814abf03b9be4bd6450abb6c99616 | ||
<= 6c66 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const hash = Buffer.from("a85c933d37847689825acbdfeb50e66d791814abf03b9be4bd6450abb6c99616", "hex"); | ||
await expect(str.signHash("44'/148'/0'", hash)).rejects.toThrow(StellarHashSigningNotEnabledError); | ||
}) | ||
test("signHash (rejected)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00800002d038000002c8000009480000000a85c933d37847689825acbdfeb50e66d791814abf03b9be4bd6450abb6c99616 | ||
<= 6985 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const hash = Buffer.from("a85c933d37847689825acbdfeb50e66d791814abf03b9be4bd6450abb6c99616", "hex"); | ||
await expect(str.signHash("44'/148'/0'", hash)).rejects.toThrow(StellarUserRefusedError); | ||
}) | ||
test("signTransaction (size exceeds APDU_MAX_PAYLOAD)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e0040080ff038000002c80000094800000007ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000200000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd000071d7002d9fed000000010000000000000000000000010000000100000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd00000018000000020000024c0061736d0100000001150460027e7e017e60037e7e7e017e6000017e600000021904016c01300000016c01310000016c015f0001016c013800000305040203030305030100100619037f01418080c0000b7f00418080c0000b7f00418080 | ||
<= 9000 | ||
=> e004800013e064a200000000000000000098968000000000 | ||
<= 79e6da561676d16f17e91ad0dbbe917e3da0fffe660aa9f277669385960b0aec8dcf002b7305b329cc02f2eabd2f20320dee4828b412ed2850b9771ffb23920d9000 | ||
=> e0048080ffc0000b073505066d656d6f7279020009696e6372656d656e740004015f00070a5f5f646174615f656e6403010b5f5f686561705f6261736503020aa70104920102017f017e41002100024002400240428ebad0af86d43942021080808080004201520d00428ebad0af86d4394202108180808000220142ff01834204520d012001422088a721000b200041016a2200450d01428ebad0af86d4392000ad422086420484220142021082808080001a4284808080a0064284808080c00c1083808080001a20010f0b00000b108580808000000b0900108680808000000b040000000b02000b00730e636f6e74726163747370656376300000000000000040496e | ||
<= 9000 | ||
=> e0048080ff6372656d656e7420696e6372656d656e747320616e20696e7465726e616c20636f756e7465722c20616e642072657475726e73207468652076616c75652e00000009696e6372656d656e74000000000000000000000100000004001e11636f6e7472616374656e766d657461763000000000000000140000003900730e636f6e74726163746d65746176300000000000000005727376657200000000000006312e37332e3000000000000000000008727373646b7665720000003332302e302e302d72633223303939323431336639623035653562666231663837326263653939653839643931323962326536310000000000000000010000000000000001 | ||
<= 9000 | ||
=> e00480003c0000000713e16858bde4ab50a006dbf07172288f3ec19d8640d1a853016c60c15c2511170000000000186636000002b000000000000000000000000c | ||
<= 83968afc83b45d0672f5b5259a7bab68ee030f6ffda0c2f3fd550e3b5dd40b3a6bee1f246e760523ab6bd434f9e11f237ffd16bc8f18bb58a59eb94abe1a5a0c9000 | ||
`) | ||
@@ -49,3 +141,3 @@ ); | ||
const transaction = Buffer.from( | ||
"7ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a979000000020000000020da998b75e42b1f7f85d075c127f5b246df12ad96f010bcf7f76f72b16e57130000006400c5b4a5000000190000000000000000000000010000000000000001000000009541f02746240c1e9f3843d28e56f0a583ecd27502fb0f4a27d4d0922fe064a200000000000000000098968000000000", | ||
"7ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000200000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd000071d7002d9fed000000010000000000000000000000010000000100000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd00000018000000020000024c0061736d0100000001150460027e7e017e60037e7e7e017e6000017e600000021904016c01300000016c01310000016c015f0001016c013800000305040203030305030100100619037f01418080c0000b7f00418080c0000b7f00418080c0000b073505066d656d6f7279020009696e6372656d656e740004015f00070a5f5f646174615f656e6403010b5f5f686561705f6261736503020aa70104920102017f017e41002100024002400240428ebad0af86d43942021080808080004201520d00428ebad0af86d4394202108180808000220142ff01834204520d012001422088a721000b200041016a2200450d01428ebad0af86d4392000ad422086420484220142021082808080001a4284808080a0064284808080c00c1083808080001a20010f0b00000b108580808000000b0900108680808000000b040000000b02000b00730e636f6e74726163747370656376300000000000000040496e6372656d656e7420696e6372656d656e747320616e20696e7465726e616c20636f756e7465722c20616e642072657475726e73207468652076616c75652e00000009696e6372656d656e74000000000000000000000100000004001e11636f6e7472616374656e766d657461763000000000000000140000003900730e636f6e74726163746d65746176300000000000000005727376657200000000000006312e37332e3000000000000000000008727373646b7665720000003332302e302e302d726332233039393234313366396230356535626662316638373262636539396538396439313239623265363100000000000000000100000000000000010000000713e16858bde4ab50a006dbf07172288f3ec19d8640d1a853016c60c15c2511170000000000186636000002b000000000000000000000000c", | ||
"hex" | ||
@@ -56,4 +148,217 @@ ); | ||
expect(result).toEqual( | ||
"79e6da561676d16f17e91ad0dbbe917e3da0fffe660aa9f277669385960b0aec8dcf002b7305b329cc02f2eabd2f20320dee4828b412ed2850b9771ffb23920d" | ||
"83968afc83b45d0672f5b5259a7bab68ee030f6ffda0c2f3fd550e3b5dd40b3a6bee1f246e760523ab6bd434f9e11f237ffd16bc8f18bb58a59eb94abe1a5a0c" | ||
); | ||
}); | ||
test("signTransaction (size does not exceed APDU_MAX_PAYLOAD)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e0040000ff038000002c80000094800000007ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000200000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd000071d7002d9fed000000010000000000000000000000010000000100000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd00000018000000020000024c0061736d0100000001150460027e7e017e60037e7e7e017e6000017e600000021904016c01300000016c01310000016c015f0001016c013800000305040203030305030100100619037f01418080c0000b7f00418080c0000b7f00418080 | ||
<= f30f5d707a6dbde681f9604832391295719130790578ecaca0b1d37a68923b15714b36f4507e56aff2d0e3256ed5af496a6b021896e13dca00a8283e426860089000 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const transaction = Buffer.from( | ||
"7ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000200000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd000071d7002d9fed000000010000000000000000000000010000000100000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd00000018000000020000024c0061736d0100000001150460027e7e017e60037e7e7e017e6000017e600000021904016c01300000016c01310000016c015f0001016c013800000305040203030305030100100619037f01418080c0000b7f00418080c0000b7f00418080", | ||
"hex" | ||
); | ||
const { signature } = await str.signTransaction("44'/148'/0'", transaction); | ||
const result = signature.toString("hex"); | ||
expect(result).toEqual( | ||
"f30f5d707a6dbde681f9604832391295719130790578ecaca0b1d37a68923b15714b36f4507e56aff2d0e3256ed5af496a6b021896e13dca00a8283e42686008" | ||
); | ||
}); | ||
test("signTransaction (size too long)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e0040080ff038000002c80000094800000007ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000200000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd000071d7002d9fed000000010000000000000000000000010000000100000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd00000018000000020000024c0061736d0100000001150460027e7e017e60037e7e7e017e6000017e600000021904016c01300000016c01310000016c015f0001016c013800000305040203030305030100100619037f01418080c0000b7f00418080c0000b7f00418080 | ||
<= 9000 | ||
=> e0048080ffc0000b073505066d656d6f7279020009696e6372656d656e740004015f00070a5f5f646174615f656e6403010b5f5f686561705f6261736503020aa70104920102017f017e41002100024002400240428ebad0af86d43942021080808080004201520d00428ebad0af86d4394202108180808000220142ff01834204520d012001422088a721000b200041016a2200450d01428ebad0af86d4392000ad422086420484220142021082808080001a4284808080a0064284808080c00c1083808080001a20010f0b00000b108580808000000b0900108680808000000b040000000b02000b00730e636f6e74726163747370656376300000000000000040496e | ||
<= 9000 | ||
=> e0048080ff6372656d656e7420696e6372656d656e747320616e20696e7465726e616c20636f756e7465722c20616e642072657475726e73207468652076616c75652e00000009696e6372656d656e74000000000000000000000100000004001e11636f6e7472616374656e766d657461763000000000000000140000003900730e636f6e74726163746d65746176300000000000000005727376657200000000000006312e37332e3000000000000000000008727373646b7665720000003332302e302e302d72633223303939323431336639623035653562666231663837326263653939653839643931323962326536310000000000000000010000000000000001 | ||
<= b004 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const transaction = Buffer.from( | ||
"7ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000200000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd000071d7002d9fed000000010000000000000000000000010000000100000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd00000018000000020000024c0061736d0100000001150460027e7e017e60037e7e7e017e6000017e600000021904016c01300000016c01310000016c015f0001016c013800000305040203030305030100100619037f01418080c0000b7f00418080c0000b7f00418080c0000b073505066d656d6f7279020009696e6372656d656e740004015f00070a5f5f646174615f656e6403010b5f5f686561705f6261736503020aa70104920102017f017e41002100024002400240428ebad0af86d43942021080808080004201520d00428ebad0af86d4394202108180808000220142ff01834204520d012001422088a721000b200041016a2200450d01428ebad0af86d4392000ad422086420484220142021082808080001a4284808080a0064284808080c00c1083808080001a20010f0b00000b108580808000000b0900108680808000000b040000000b02000b00730e636f6e74726163747370656376300000000000000040496e6372656d656e7420696e6372656d656e747320616e20696e7465726e616c20636f756e7465722c20616e642072657475726e73207468652076616c75652e00000009696e6372656d656e74000000000000000000000100000004001e11636f6e7472616374656e766d657461763000000000000000140000003900730e636f6e74726163746d65746176300000000000000005727376657200000000000006312e37332e3000000000000000000008727373646b7665720000003332302e302e302d726332233039393234313366396230356535626662316638373262636539396538396439313239623265363100000000000000000100000000000000010000000713e16858bde4ab50a006dbf07172288f3ec19d8640d1a853016c60c15c2511170000000000186636000002b000000000000000000000000c", | ||
"hex" | ||
); | ||
await expect(str.signTransaction("44'/148'/0'", transaction)).rejects.toThrow(StellarDataTooLargeError); | ||
}); | ||
test("signTransaction (rejected)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e0040080ff038000002c80000094800000007ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000200000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd000071d7002d9fed000000010000000000000000000000010000000100000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd00000018000000020000024c0061736d0100000001150460027e7e017e60037e7e7e017e6000017e600000021904016c01300000016c01310000016c015f0001016c013800000305040203030305030100100619037f01418080c0000b7f00418080c0000b7f00418080 | ||
<= 9000 | ||
=> e0048080ffc0000b073505066d656d6f7279020009696e6372656d656e740004015f00070a5f5f646174615f656e6403010b5f5f686561705f6261736503020aa70104920102017f017e41002100024002400240428ebad0af86d43942021080808080004201520d00428ebad0af86d4394202108180808000220142ff01834204520d012001422088a721000b200041016a2200450d01428ebad0af86d4392000ad422086420484220142021082808080001a4284808080a0064284808080c00c1083808080001a20010f0b00000b108580808000000b0900108680808000000b040000000b02000b00730e636f6e74726163747370656376300000000000000040496e | ||
<= 9000 | ||
=> e0048080ff6372656d656e7420696e6372656d656e747320616e20696e7465726e616c20636f756e7465722c20616e642072657475726e73207468652076616c75652e00000009696e6372656d656e74000000000000000000000100000004001e11636f6e7472616374656e766d657461763000000000000000140000003900730e636f6e74726163746d65746176300000000000000005727376657200000000000006312e37332e3000000000000000000008727373646b7665720000003332302e302e302d72633223303939323431336639623035653562666231663837326263653939653839643931323962326536310000000000000000010000000000000001 | ||
<= 9000 | ||
=> e00480003c0000000713e16858bde4ab50a006dbf07172288f3ec19d8640d1a853016c60c15c2511170000000000186636000002b000000000000000000000000c | ||
<= 6985 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const transaction = Buffer.from( | ||
"7ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000200000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd000071d7002d9fed000000010000000000000000000000010000000100000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd00000018000000020000024c0061736d0100000001150460027e7e017e60037e7e7e017e6000017e600000021904016c01300000016c01310000016c015f0001016c013800000305040203030305030100100619037f01418080c0000b7f00418080c0000b7f00418080c0000b073505066d656d6f7279020009696e6372656d656e740004015f00070a5f5f646174615f656e6403010b5f5f686561705f6261736503020aa70104920102017f017e41002100024002400240428ebad0af86d43942021080808080004201520d00428ebad0af86d4394202108180808000220142ff01834204520d012001422088a721000b200041016a2200450d01428ebad0af86d4392000ad422086420484220142021082808080001a4284808080a0064284808080c00c1083808080001a20010f0b00000b108580808000000b0900108680808000000b040000000b02000b00730e636f6e74726163747370656376300000000000000040496e6372656d656e7420696e6372656d656e747320616e20696e7465726e616c20636f756e7465722c20616e642072657475726e73207468652076616c75652e00000009696e6372656d656e74000000000000000000000100000004001e11636f6e7472616374656e766d657461763000000000000000140000003900730e636f6e74726163746d65746176300000000000000005727376657200000000000006312e37332e3000000000000000000008727373646b7665720000003332302e302e302d726332233039393234313366396230356535626662316638373262636539396538396439313239623265363100000000000000000100000000000000010000000713e16858bde4ab50a006dbf07172288f3ec19d8640d1a853016c60c15c2511170000000000186636000002b000000000000000000000000c", | ||
"hex" | ||
); | ||
await expect(str.signTransaction("44'/148'/0'", transaction)).rejects.toThrow(StellarUserRefusedError); | ||
}); | ||
test("signTransaction (parse data failed)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e0040000ff038000002c80000094800000007ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000200000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd000071d7002d9fed000000010000000000000000000000010000000100000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd00000018000000020000024c0061736d0100000001150460027e7e017e60037e7e7e017e6000017e600000021904016c01300000016c01310000016c015f0001016c013800000305040203030305030100100619037f01418080c0000b7f00418080c0000b7f00418080 | ||
<= b005 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const transaction = Buffer.from( | ||
"7ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000200000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd000071d7002d9fed000000010000000000000000000000010000000100000000e93388bbfd2fbd11806dd0bd59cea9079e7cc70ce7b1e154f114cdfe4e466ecd00000018000000020000024c0061736d0100000001150460027e7e017e60037e7e7e017e6000017e600000021904016c01300000016c01310000016c015f0001016c013800000305040203030305030100100619037f01418080c0000b7f00418080c0000b7f00418080", | ||
"hex" | ||
); | ||
await expect(str.signTransaction("44'/148'/0'", transaction)).rejects.toThrow(StellarDataParsingFailedError); | ||
}); | ||
test("signSorobanAuthorization (size exceeds APDU_MAX_PAYLOAD)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00a0080ff038000002c8000009480000000000000097ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000049756d450210c89f0000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000020000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55 | ||
<= 9000 | ||
=> e00a8080ffa8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000000000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079 | ||
<= 9000 | ||
=> e00a800037a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a0000000000000000000000026944050000000000 | ||
<= 2e45c6d44fe613c3d7b38c9f7147bd70dc58cf7e1badb80d33632dcdafb90944674096da05dfe0f185da86535a6ca1a1fcef852a19ae99b22e4438ca5c43b9089000 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const hashIdPreimage = Buffer.from( | ||
"000000097ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000049756d450210c89f0000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000020000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000000000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a0000000000000000000000026944050000000000", | ||
"hex" | ||
); | ||
const { signature } = await str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage); | ||
const result = signature.toString("hex"); | ||
expect(result).toEqual( | ||
"2e45c6d44fe613c3d7b38c9f7147bd70dc58cf7e1badb80d33632dcdafb90944674096da05dfe0f185da86535a6ca1a1fcef852a19ae99b22e4438ca5c43b908" | ||
); | ||
}); | ||
test("signSorobanAuthorization (size does not exceed APDU_MAX_PAYLOAD)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00a0000fd038000002c8000009480000000000000097ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000049756d450210c89f0000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000020000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d50 | ||
<= 2e45c6d44fe613c3d7b38c9f7147bd70dc58cf7e1badb80d33632dcdafb90944674096da05dfe0f185da86535a6ca1a1fcef852a19ae99b22e4438ca5c43b9089000 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const hashIdPreimage = Buffer.from( | ||
"000000097ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000049756d450210c89f0000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000020000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d50", | ||
"hex" | ||
); | ||
const { signature } = await str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage); | ||
const result = signature.toString("hex"); | ||
expect(result).toEqual( | ||
"2e45c6d44fe613c3d7b38c9f7147bd70dc58cf7e1badb80d33632dcdafb90944674096da05dfe0f185da86535a6ca1a1fcef852a19ae99b22e4438ca5c43b908" | ||
); | ||
}); | ||
test("signSorobanAuthorization (size too long)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00a0080ff038000002c8000009480000000000000097ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000049756d450210c89f0000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000020000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55 | ||
<= 9000 | ||
=> e00a8080ffa8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000000000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079 | ||
<= b004 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const hashIdPreimage = Buffer.from( | ||
"000000097ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000049756d450210c89f0000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000020000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000000000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a0000000000000000000000026944050000000000", | ||
"hex" | ||
); | ||
await expect(str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage)).rejects.toThrow(StellarDataTooLargeError); | ||
}); | ||
test("signSorobanAuthorization (rejected)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00a0000fd038000002c8000009480000000000000097ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000049756d450210c89f0000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000020000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d50 | ||
<= 6985 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const hashIdPreimage = Buffer.from( | ||
"000000097ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000049756d450210c89f0000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000020000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d50", | ||
"hex" | ||
); | ||
await expect(str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage)).rejects.toThrow(StellarUserRefusedError); | ||
}); | ||
test("signSorobanAuthorization (parse data failed)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00a0080ff038000002c8000009480000000000000097ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000049756d450210c89f0000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000020000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55 | ||
<= 9000 | ||
=> e00a8080ffa8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000000000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079 | ||
<= 9000 | ||
=> e00a800037a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a0000000000000000000000026944050000000000 | ||
<= b005 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const hashIdPreimage = Buffer.from( | ||
"000000097ac33997544e3175d266bd022439b22cdb16508c01163f26e5cb2a3e1045a9790000000049756d450210c89f0000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000020000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a00000000000000000000000269440500000000000000000000000001d7928b72c2703ccfeaf7eb9ff4ef4d504a55a8b979fc9b450ea2c842b4d1ce61000000087472616e7366657200000003000000120000000000000000acd0adc778238000bdb6bad22e919923897f7a87e310cef1505e6c0b0b71542500000012000000000000000079a5a13baf5571637a2157c6affb7491d7c4454eecd2b0b7e40e67023f096fac0000000a0000000000000000000000026944050000000000", | ||
"hex" | ||
); | ||
await expect(str.signSorobanAuthorization("44'/148'/0'", hashIdPreimage)).rejects.toThrow(StellarDataParsingFailedError); | ||
}); | ||
test("pathToBuffer (44'/148'/19773')", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00200000d038000002c8000009480004d3d | ||
<= 04becf1537ae24f24d326368a2ad8a09b06cb0671e96e6878d4e623aab4bf6e79000 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const { rawPublicKey } = await str.getPublicKey("44'/148'/19773'", false); | ||
expect(rawPublicKey.toString("hex")).toEqual( | ||
"04becf1537ae24f24d326368a2ad8a09b06cb0671e96e6878d4e623aab4bf6e7" | ||
); | ||
}); | ||
test("pathToBuffer (44'/148'/19773)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00200000d038000002c8000009480004d3d | ||
<= 04becf1537ae24f24d326368a2ad8a09b06cb0671e96e6878d4e623aab4bf6e79000 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
const { rawPublicKey } = await str.getPublicKey("44'/148'/19773", false); | ||
expect(rawPublicKey.toString("hex")).toEqual( | ||
"04becf1537ae24f24d326368a2ad8a09b06cb0671e96e6878d4e623aab4bf6e7" | ||
); | ||
}); | ||
test("remapErrors (unexpected)", async () => { | ||
const transport = await openTransportReplayer( | ||
RecordStore.fromString(` | ||
=> e00200000d038000002c8000009480004d3d | ||
<= b001 | ||
`) | ||
); | ||
const str = new Str(transport); | ||
try { | ||
await str.getPublicKey("44'/148'/19773", false); | ||
} catch (error) { | ||
expect(error).toBeInstanceOf(TransportStatusError); | ||
if (error instanceof TransportStatusError) { | ||
expect(error.statusCode).toBe(0xb001); | ||
} | ||
} | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
105078
3
1381
175
1
1
+ Added@ledgerhq/errors@^6.17.0
+ Addedbip32-path@^0.4.2
+ Addedbip32-path@0.4.2(transitive)
- Removedbase32.js@^0.1.0
- Removedsha.js@^2.3.6
- Removedtweetnacl@^1.0.3
- Removedbase32.js@0.1.0(transitive)
- Removedinherits@2.0.4(transitive)
- Removedsafe-buffer@5.2.1(transitive)
- Removedsha.js@2.4.11(transitive)
- Removedtweetnacl@1.0.3(transitive)