Socket
Socket
Sign inDemoInstall

@ledgerhq/devices

Package Overview
Dependencies
Maintainers
21
Versions
261
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ledgerhq/devices - npm Package Compare versions

Comparing version 8.1.0 to 8.2.0-next.0

lib-es/ble/receiveAPDU.test.d.ts

16

CHANGELOG.md
# @ledgerhq/devices
## 8.2.0-next.0
### Minor Changes
- [#5171](https://github.com/LedgerHQ/ledger-live/pull/5171) [`52a3732`](https://github.com/LedgerHQ/ledger-live/commit/52a373273dee3b2cb5a3e8d2d4b05f90616d71a2) Thanks [@alexandremgo](https://github.com/alexandremgo)! - Feat: cleaner refactoring of BLE and USB HID frames encoding/decoding
- Cleans up + documentation + tracing/logs + unit tests of BLE frame encoding and decoding:
`receiveAPDU` and `sendAPDU`
- Cleans up + documentation + tracing/logs + unit tests of HID USB frame encoding and decoding:
`hid-framing`
### Patch Changes
- Updated dependencies [[`52a3732`](https://github.com/LedgerHQ/ledger-live/commit/52a373273dee3b2cb5a3e8d2d4b05f90616d71a2), [`4d1aade`](https://github.com/LedgerHQ/ledger-live/commit/4d1aade53cd33f8e7548ce340f54fbb834bdcdcb)]:
- @ledgerhq/errors@6.16.1-next.0
## 8.1.0

@@ -4,0 +20,0 @@

13

lib-es/ble/receiveAPDU.d.ts
/// <reference types="node" />
import { Observable } from "rxjs";
export declare const receiveAPDU: (rawStream: Observable<Buffer>) => Observable<Buffer>;
import { TraceContext } from "@ledgerhq/logs";
/**
* Parses a raw stream coming from a BLE communication into an APDU response
*
* @param rawStream An observable containing the raw stream as emitted buffers
* @param options Optional options containing:
* - context An optional context object for log/tracing strategy
* @returns An observable containing the APDU response as one emitted buffer
*/
export declare const receiveAPDU: (rawStream: Observable<Buffer | Error>, { context }?: {
context?: TraceContext | undefined;
}) => Observable<Buffer>;
//# sourceMappingURL=receiveAPDU.d.ts.map

77

lib-es/ble/receiveAPDU.js
import { TransportError, DisconnectedDevice } from "@ledgerhq/errors";
import { Observable } from "rxjs";
import { log } from "@ledgerhq/logs";
import { Observable, ReplaySubject, takeUntil } from "rxjs";
import { trace } from "@ledgerhq/logs";
const TagId = 0x05;
// operator that transform the input raw stream into one apdu response and finishes
export const receiveAPDU = (rawStream) => new Observable(o => {
/**
* Parses a raw stream coming from a BLE communication into an APDU response
*
* @param rawStream An observable containing the raw stream as emitted buffers
* @param options Optional options containing:
* - context An optional context object for log/tracing strategy
* @returns An observable containing the APDU response as one emitted buffer
*/
export const receiveAPDU = (rawStream, { context } = {}) => new Observable(o => {
let notifiedIndex = 0;
let notifiedDataLength = 0;
let notifiedData = Buffer.alloc(0);
const sub = rawStream.subscribe({
const subscriptionCleaner = new ReplaySubject();
// The raw stream is listened/subscribed to until a full message (that can be made of several frames) is received
rawStream.pipe(takeUntil(subscriptionCleaner)).subscribe({
complete: () => {
o.error(new DisconnectedDevice());
sub.unsubscribe();
},
error: e => {
log("ble-error", "in receiveAPDU " + String(e));
o.error(e);
sub.unsubscribe();
error: error => {
trace({
type: "ble-error",
message: `Error in receiveAPDU: ${error}`,
data: { error },
context,
});
o.error(error);
},
next: value => {
// Silences emitted errors in next
if (value instanceof Error) {
trace({
type: "ble-error",
message: `Error emitted to receiveAPDU next: ${value}`,
data: { error: value },
context,
});
return;
}
const tag = value.readUInt8(0);
const index = value.readUInt16BE(1);
let data = value.slice(3);
const chunkIndex = value.readUInt16BE(1);
// `slice` and not `subarray`: this is not a Node Buffer, but probably only a Uint8Array.
let chunkData = value.slice(3);
if (tag !== TagId) {

@@ -28,20 +51,19 @@ o.error(new TransportError("Invalid tag " + tag.toString(16), "InvalidTag"));

}
if (notifiedIndex !== index) {
o.error(new TransportError("BLE: Invalid sequence number. discontinued chunk. Received " +
index +
" but expected " +
notifiedIndex, "InvalidSequence"));
// A chunk was probably missed
if (notifiedIndex !== chunkIndex) {
o.error(new TransportError(`BLE: Invalid sequence number. discontinued chunk. Received ${chunkIndex} but expected ${notifiedIndex}`, "InvalidSequence"));
return;
}
if (index === 0) {
notifiedDataLength = data.readUInt16BE(0);
data = data.slice(2);
// The total length of the response is located on the next 2 bytes on the 1st chunk
if (chunkIndex === 0) {
notifiedDataLength = chunkData.readUInt16BE(0);
// `slice` and not `subarray`: this is not a Node Buffer, but probably only a Uint8Array.
chunkData = chunkData.slice(2);
}
notifiedIndex++;
notifiedData = Buffer.concat([notifiedData, data]);
// The cost of creating a new buffer for each received chunk is low because the response is often contained in 1 chunk.
// Allocating `notifiedData` buffer with the received total length and mutating it will probably not improve any performance.
notifiedData = Buffer.concat([notifiedData, chunkData]);
if (notifiedData.length > notifiedDataLength) {
o.error(new TransportError("BLE: received too much data. discontinued chunk. Received " +
notifiedData.length +
" but expected " +
notifiedDataLength, "BLETooMuchData"));
o.error(new TransportError(`BLE: received too much data. discontinued chunk. Received ${notifiedData.length} but expected ${notifiedDataLength}`, "BLETooMuchData"));
return;

@@ -52,3 +74,4 @@ }

o.complete();
sub.unsubscribe();
// Tries to unsubscribe from the raw stream as soon as we complete the parent observer
subscriptionCleaner.next();
}

@@ -58,5 +81,5 @@ },

return () => {
sub.unsubscribe();
subscriptionCleaner.next();
};
});
//# sourceMappingURL=receiveAPDU.js.map
/// <reference types="node" />
import { Observable } from "rxjs";
export declare const sendAPDU: (write: (arg0: Buffer) => Promise<void>, apdu: Buffer, mtuSize: number) => Observable<Buffer>;
import { TraceContext } from "@ledgerhq/logs";
/**
* Sends an APDU by encoding it into chunks and sending the chunks using the given `write` function
*
* @param write The function to send each chunk to the device
* @param apdu
* @param mtuSize The negotiated maximum size of the data to be sent in one chunk
* @param options Optional options containing:
* - context An optional context object for log/tracing strategy
* @returns An observable that will only emit if an error occurred, otherwise it will complete
*/
export declare const sendAPDU: (write: (arg0: Buffer) => Promise<void>, apdu: Buffer, mtuSize: number, { context }?: {
context?: TraceContext | undefined;
}) => Observable<Buffer>;
//# sourceMappingURL=sendAPDU.d.ts.map

@@ -11,7 +11,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

import { Observable } from "rxjs";
import { log } from "@ledgerhq/logs";
import { trace } from "@ledgerhq/logs";
const TagId = 0x05;
function chunkBuffer(buffer, sizeForIndex) {
/**
* Creates a list of chunked buffer from one buffer
*
* If this is using a Node buffer: the chunked buffers reference to the same memory as the original buffer.
* If this is using a Uint8Array: each part of the original buffer is copied into the chunked buffers
*
* @param buffer a Node Buffer, or a Uint8Array
* @param sizeForIndex A function that takes an index (on the buffer) and returns the size of the chunk at that index
* @returns a list of chunked buffers
*/
function createChunkedBuffers(buffer, sizeForIndex) {
const chunks = [];
for (let i = 0, size = sizeForIndex(0); i < buffer.length; i += size, size = sizeForIndex(i)) {
// If this is a Node buffer: this chunked buffer points to the same memory but with cropped starting and ending indices
// `slice` and not `subarray`: this might not be a Node Buffer, but probably only a Uint8Array.
chunks.push(buffer.slice(i, i + size));

@@ -21,10 +33,26 @@ }

}
export const sendAPDU = (write, apdu, mtuSize) => {
const chunks = chunkBuffer(apdu, i => mtuSize - (i === 0 ? 5 : 3)).map((buffer, i) => {
/**
* Sends an APDU by encoding it into chunks and sending the chunks using the given `write` function
*
* @param write The function to send each chunk to the device
* @param apdu
* @param mtuSize The negotiated maximum size of the data to be sent in one chunk
* @param options Optional options containing:
* - context An optional context object for log/tracing strategy
* @returns An observable that will only emit if an error occurred, otherwise it will complete
*/
export const sendAPDU = (write, apdu, mtuSize, { context } = {}) => {
// Prepares the data to be sent in chunks, by adding a header to chunked of the APDU
// The first chunk will contain the total length of the APDU, which reduces the size of the data written in the first chunk.
// The total length of the APDU is encoded in 2 bytes (so 5 bytes for the header with the tag id and the chunk index).
const chunks = createChunkedBuffers(apdu, i => mtuSize - (i === 0 ? 5 : 3)).map((buffer, i) => {
const head = Buffer.alloc(i === 0 ? 5 : 3);
head.writeUInt8(TagId, 0);
// Index of the chunk as the 2 next bytes
head.writeUInt16BE(i, 1);
// The total length of the APDU is written on the first chunk
if (i === 0) {
head.writeUInt16BE(apdu.length, 3);
}
// No 0-padding is needed
return Buffer.concat([head, buffer]);

@@ -46,10 +74,19 @@ });

o.complete();
}, e => {
}, error => {
terminated = true;
log("ble-error", "sendAPDU failure " + String(e));
o.error(e);
trace({
type: "ble-error",
message: `sendAPDU failure: ${error}`,
data: { error },
context,
});
o.error(error);
});
const unsubscribe = () => {
if (!terminated) {
log("ble-verbose", "sendAPDU interruption");
trace({
type: "ble-error",
message: "sendAPDU interruption",
context,
});
terminated = true;

@@ -56,0 +93,0 @@ }

@@ -8,7 +8,31 @@ /// <reference types="node" />

/**
* Object to handle HID frames (encoding and decoding)
*
* @param channel
* @param packetSize The HID protocol packet size in bytes (usually 64)
*/
declare const createHIDframing: (channel: number, packetSize: number) => {
/**
* Frames/encodes an APDU message into HID USB packets/frames
*
* @param apdu The APDU message to send, in a Buffer containing [cla, ins, p1, p2, data length, data(if not empty)]
* @returns an array of HID USB frames ready to be sent
*/
makeBlocks(apdu: Buffer): Buffer[];
/**
* Reduces HID USB packets/frames to one response.
*
* @param acc The value resulting from (accumulating) the previous call of reduceResponse.
* On first call initialized to `initialAcc`. The accumulator enables handling multi-frames messages.
* @param chunk Current chunk to reduce into accumulator
* @returns An accumulator value updated with the current chunk
*/
reduceResponse(acc: ResponseAcc, chunk: Buffer): ResponseAcc;
/**
* Returns the response message that has been reduced from the HID USB frames
*
* @param acc The accumulator
* @returns A Buffer containing the cleaned response message, or null if no response message, or undefined if the
* accumulator is incorrect (message length is not valid)
*/
getReducedResult(acc: ResponseAcc): Buffer | null | undefined;

@@ -15,0 +39,0 @@ };

@@ -14,14 +14,25 @@ import { TransportError } from "@ledgerhq/errors";

/**
* Object to handle HID frames (encoding and decoding)
*
* @param channel
* @param packetSize The HID protocol packet size in bytes (usually 64)
*/
const createHIDframing = (channel, packetSize) => {
return {
/**
* Frames/encodes an APDU message into HID USB packets/frames
*
* @param apdu The APDU message to send, in a Buffer containing [cla, ins, p1, p2, data length, data(if not empty)]
* @returns an array of HID USB frames ready to be sent
*/
makeBlocks(apdu) {
// Encodes the APDU length in 2 bytes before the APDU itself.
// The length is measured as the number of bytes.
// As the size of the APDU `data` should have been added in 1 byte just before `data`,
// the minimum size of an APDU is 5 bytes.
let data = Buffer.concat([asUInt16BE(apdu.length), apdu]);
const blockSize = packetSize - 5;
const nbBlocks = Math.ceil(data.length / blockSize);
data = Buffer.concat([
data,
Buffer.alloc(nbBlocks * blockSize - data.length + 1).fill(0),
]);
// Fills data with 0-padding
data = Buffer.concat([data, Buffer.alloc(nbBlocks * blockSize - data.length + 1).fill(0)]);
const blocks = [];

@@ -33,2 +44,3 @@ for (let i = 0; i < nbBlocks; i++) {

head.writeUInt16BE(i, 3);
// `slice` and not `subarray`: this might not be a Node Buffer, but probably only a Uint8Array
const chunk = data.slice(i * blockSize, (i + 1) * blockSize);

@@ -39,2 +51,10 @@ blocks.push(Buffer.concat([head, chunk]));

},
/**
* Reduces HID USB packets/frames to one response.
*
* @param acc The value resulting from (accumulating) the previous call of reduceResponse.
* On first call initialized to `initialAcc`. The accumulator enables handling multi-frames messages.
* @param chunk Current chunk to reduce into accumulator
* @returns An accumulator value updated with the current chunk
*/
reduceResponse(acc, chunk) {

@@ -51,2 +71,3 @@ let { data, dataLength, sequence } = acc || initialAcc;

}
// Gets the total length of the response from the 1st frame
if (!acc) {

@@ -56,4 +77,6 @@ dataLength = chunk.readUInt16BE(5);

sequence++;
// The total length on the 1st frame takes 2 more bytes
const chunkData = chunk.slice(acc ? 5 : 7);
data = Buffer.concat([data, chunkData]);
// Removes any 0 padding
if (data.length > dataLength) {

@@ -68,2 +91,9 @@ data = data.slice(0, dataLength);

},
/**
* Returns the response message that has been reduced from the HID USB frames
*
* @param acc The accumulator
* @returns A Buffer containing the cleaned response message, or null if no response message, or undefined if the
* accumulator is incorrect (message length is not valid)
*/
getReducedResult(acc) {

@@ -70,0 +100,0 @@ if (acc && acc.dataLength === acc.data.length) {

@@ -42,3 +42,6 @@ /**

/**
* From a given USB product id, return the deviceModel associated to it.
*
* The mapping from the product id is only based on the 2 most significant bytes.
* For example, Stax is defined with a product id of 0x60ii, a product id 0x6011 would be mapped to it.
*/

@@ -45,0 +48,0 @@ export declare const identifyUSBProductId: (usbProductId: number) => DeviceModel | null | undefined;

@@ -128,3 +128,6 @@ import semver from "semver";

/**
* From a given USB product id, return the deviceModel associated to it.
*
* The mapping from the product id is only based on the 2 most significant bytes.
* For example, Stax is defined with a product id of 0x60ii, a product id 0x6011 would be mapped to it.
*/

@@ -131,0 +134,0 @@ export const identifyUSBProductId = (usbProductId) => {

/// <reference types="node" />
import { Observable } from "rxjs";
export declare const receiveAPDU: (rawStream: Observable<Buffer>) => Observable<Buffer>;
import { TraceContext } from "@ledgerhq/logs";
/**
* Parses a raw stream coming from a BLE communication into an APDU response
*
* @param rawStream An observable containing the raw stream as emitted buffers
* @param options Optional options containing:
* - context An optional context object for log/tracing strategy
* @returns An observable containing the APDU response as one emitted buffer
*/
export declare const receiveAPDU: (rawStream: Observable<Buffer | Error>, { context }?: {
context?: TraceContext | undefined;
}) => Observable<Buffer>;
//# sourceMappingURL=receiveAPDU.d.ts.map

@@ -8,21 +8,44 @@ "use strict";

const TagId = 0x05;
// operator that transform the input raw stream into one apdu response and finishes
const receiveAPDU = (rawStream) => new rxjs_1.Observable(o => {
/**
* Parses a raw stream coming from a BLE communication into an APDU response
*
* @param rawStream An observable containing the raw stream as emitted buffers
* @param options Optional options containing:
* - context An optional context object for log/tracing strategy
* @returns An observable containing the APDU response as one emitted buffer
*/
const receiveAPDU = (rawStream, { context } = {}) => new rxjs_1.Observable(o => {
let notifiedIndex = 0;
let notifiedDataLength = 0;
let notifiedData = Buffer.alloc(0);
const sub = rawStream.subscribe({
const subscriptionCleaner = new rxjs_1.ReplaySubject();
// The raw stream is listened/subscribed to until a full message (that can be made of several frames) is received
rawStream.pipe((0, rxjs_1.takeUntil)(subscriptionCleaner)).subscribe({
complete: () => {
o.error(new errors_1.DisconnectedDevice());
sub.unsubscribe();
},
error: e => {
(0, logs_1.log)("ble-error", "in receiveAPDU " + String(e));
o.error(e);
sub.unsubscribe();
error: error => {
(0, logs_1.trace)({
type: "ble-error",
message: `Error in receiveAPDU: ${error}`,
data: { error },
context,
});
o.error(error);
},
next: value => {
// Silences emitted errors in next
if (value instanceof Error) {
(0, logs_1.trace)({
type: "ble-error",
message: `Error emitted to receiveAPDU next: ${value}`,
data: { error: value },
context,
});
return;
}
const tag = value.readUInt8(0);
const index = value.readUInt16BE(1);
let data = value.slice(3);
const chunkIndex = value.readUInt16BE(1);
// `slice` and not `subarray`: this is not a Node Buffer, but probably only a Uint8Array.
let chunkData = value.slice(3);
if (tag !== TagId) {

@@ -32,20 +55,19 @@ o.error(new errors_1.TransportError("Invalid tag " + tag.toString(16), "InvalidTag"));

}
if (notifiedIndex !== index) {
o.error(new errors_1.TransportError("BLE: Invalid sequence number. discontinued chunk. Received " +
index +
" but expected " +
notifiedIndex, "InvalidSequence"));
// A chunk was probably missed
if (notifiedIndex !== chunkIndex) {
o.error(new errors_1.TransportError(`BLE: Invalid sequence number. discontinued chunk. Received ${chunkIndex} but expected ${notifiedIndex}`, "InvalidSequence"));
return;
}
if (index === 0) {
notifiedDataLength = data.readUInt16BE(0);
data = data.slice(2);
// The total length of the response is located on the next 2 bytes on the 1st chunk
if (chunkIndex === 0) {
notifiedDataLength = chunkData.readUInt16BE(0);
// `slice` and not `subarray`: this is not a Node Buffer, but probably only a Uint8Array.
chunkData = chunkData.slice(2);
}
notifiedIndex++;
notifiedData = Buffer.concat([notifiedData, data]);
// The cost of creating a new buffer for each received chunk is low because the response is often contained in 1 chunk.
// Allocating `notifiedData` buffer with the received total length and mutating it will probably not improve any performance.
notifiedData = Buffer.concat([notifiedData, chunkData]);
if (notifiedData.length > notifiedDataLength) {
o.error(new errors_1.TransportError("BLE: received too much data. discontinued chunk. Received " +
notifiedData.length +
" but expected " +
notifiedDataLength, "BLETooMuchData"));
o.error(new errors_1.TransportError(`BLE: received too much data. discontinued chunk. Received ${notifiedData.length} but expected ${notifiedDataLength}`, "BLETooMuchData"));
return;

@@ -56,3 +78,4 @@ }

o.complete();
sub.unsubscribe();
// Tries to unsubscribe from the raw stream as soon as we complete the parent observer
subscriptionCleaner.next();
}

@@ -62,3 +85,3 @@ },

return () => {
sub.unsubscribe();
subscriptionCleaner.next();
};

@@ -65,0 +88,0 @@ });

/// <reference types="node" />
import { Observable } from "rxjs";
export declare const sendAPDU: (write: (arg0: Buffer) => Promise<void>, apdu: Buffer, mtuSize: number) => Observable<Buffer>;
import { TraceContext } from "@ledgerhq/logs";
/**
* Sends an APDU by encoding it into chunks and sending the chunks using the given `write` function
*
* @param write The function to send each chunk to the device
* @param apdu
* @param mtuSize The negotiated maximum size of the data to be sent in one chunk
* @param options Optional options containing:
* - context An optional context object for log/tracing strategy
* @returns An observable that will only emit if an error occurred, otherwise it will complete
*/
export declare const sendAPDU: (write: (arg0: Buffer) => Promise<void>, apdu: Buffer, mtuSize: number, { context }?: {
context?: TraceContext | undefined;
}) => Observable<Buffer>;
//# sourceMappingURL=sendAPDU.d.ts.map

@@ -16,5 +16,17 @@ "use strict";

const TagId = 0x05;
function chunkBuffer(buffer, sizeForIndex) {
/**
* Creates a list of chunked buffer from one buffer
*
* If this is using a Node buffer: the chunked buffers reference to the same memory as the original buffer.
* If this is using a Uint8Array: each part of the original buffer is copied into the chunked buffers
*
* @param buffer a Node Buffer, or a Uint8Array
* @param sizeForIndex A function that takes an index (on the buffer) and returns the size of the chunk at that index
* @returns a list of chunked buffers
*/
function createChunkedBuffers(buffer, sizeForIndex) {
const chunks = [];
for (let i = 0, size = sizeForIndex(0); i < buffer.length; i += size, size = sizeForIndex(i)) {
// If this is a Node buffer: this chunked buffer points to the same memory but with cropped starting and ending indices
// `slice` and not `subarray`: this might not be a Node Buffer, but probably only a Uint8Array.
chunks.push(buffer.slice(i, i + size));

@@ -24,10 +36,26 @@ }

}
const sendAPDU = (write, apdu, mtuSize) => {
const chunks = chunkBuffer(apdu, i => mtuSize - (i === 0 ? 5 : 3)).map((buffer, i) => {
/**
* Sends an APDU by encoding it into chunks and sending the chunks using the given `write` function
*
* @param write The function to send each chunk to the device
* @param apdu
* @param mtuSize The negotiated maximum size of the data to be sent in one chunk
* @param options Optional options containing:
* - context An optional context object for log/tracing strategy
* @returns An observable that will only emit if an error occurred, otherwise it will complete
*/
const sendAPDU = (write, apdu, mtuSize, { context } = {}) => {
// Prepares the data to be sent in chunks, by adding a header to chunked of the APDU
// The first chunk will contain the total length of the APDU, which reduces the size of the data written in the first chunk.
// The total length of the APDU is encoded in 2 bytes (so 5 bytes for the header with the tag id and the chunk index).
const chunks = createChunkedBuffers(apdu, i => mtuSize - (i === 0 ? 5 : 3)).map((buffer, i) => {
const head = Buffer.alloc(i === 0 ? 5 : 3);
head.writeUInt8(TagId, 0);
// Index of the chunk as the 2 next bytes
head.writeUInt16BE(i, 1);
// The total length of the APDU is written on the first chunk
if (i === 0) {
head.writeUInt16BE(apdu.length, 3);
}
// No 0-padding is needed
return Buffer.concat([head, buffer]);

@@ -49,10 +77,19 @@ });

o.complete();
}, e => {
}, error => {
terminated = true;
(0, logs_1.log)("ble-error", "sendAPDU failure " + String(e));
o.error(e);
(0, logs_1.trace)({
type: "ble-error",
message: `sendAPDU failure: ${error}`,
data: { error },
context,
});
o.error(error);
});
const unsubscribe = () => {
if (!terminated) {
(0, logs_1.log)("ble-verbose", "sendAPDU interruption");
(0, logs_1.trace)({
type: "ble-error",
message: "sendAPDU interruption",
context,
});
terminated = true;

@@ -59,0 +96,0 @@ }

@@ -8,7 +8,31 @@ /// <reference types="node" />

/**
* Object to handle HID frames (encoding and decoding)
*
* @param channel
* @param packetSize The HID protocol packet size in bytes (usually 64)
*/
declare const createHIDframing: (channel: number, packetSize: number) => {
/**
* Frames/encodes an APDU message into HID USB packets/frames
*
* @param apdu The APDU message to send, in a Buffer containing [cla, ins, p1, p2, data length, data(if not empty)]
* @returns an array of HID USB frames ready to be sent
*/
makeBlocks(apdu: Buffer): Buffer[];
/**
* Reduces HID USB packets/frames to one response.
*
* @param acc The value resulting from (accumulating) the previous call of reduceResponse.
* On first call initialized to `initialAcc`. The accumulator enables handling multi-frames messages.
* @param chunk Current chunk to reduce into accumulator
* @returns An accumulator value updated with the current chunk
*/
reduceResponse(acc: ResponseAcc, chunk: Buffer): ResponseAcc;
/**
* Returns the response message that has been reduced from the HID USB frames
*
* @param acc The accumulator
* @returns A Buffer containing the cleaned response message, or null if no response message, or undefined if the
* accumulator is incorrect (message length is not valid)
*/
getReducedResult(acc: ResponseAcc): Buffer | null | undefined;

@@ -15,0 +39,0 @@ };

@@ -16,14 +16,25 @@ "use strict";

/**
* Object to handle HID frames (encoding and decoding)
*
* @param channel
* @param packetSize The HID protocol packet size in bytes (usually 64)
*/
const createHIDframing = (channel, packetSize) => {
return {
/**
* Frames/encodes an APDU message into HID USB packets/frames
*
* @param apdu The APDU message to send, in a Buffer containing [cla, ins, p1, p2, data length, data(if not empty)]
* @returns an array of HID USB frames ready to be sent
*/
makeBlocks(apdu) {
// Encodes the APDU length in 2 bytes before the APDU itself.
// The length is measured as the number of bytes.
// As the size of the APDU `data` should have been added in 1 byte just before `data`,
// the minimum size of an APDU is 5 bytes.
let data = Buffer.concat([asUInt16BE(apdu.length), apdu]);
const blockSize = packetSize - 5;
const nbBlocks = Math.ceil(data.length / blockSize);
data = Buffer.concat([
data,
Buffer.alloc(nbBlocks * blockSize - data.length + 1).fill(0),
]);
// Fills data with 0-padding
data = Buffer.concat([data, Buffer.alloc(nbBlocks * blockSize - data.length + 1).fill(0)]);
const blocks = [];

@@ -35,2 +46,3 @@ for (let i = 0; i < nbBlocks; i++) {

head.writeUInt16BE(i, 3);
// `slice` and not `subarray`: this might not be a Node Buffer, but probably only a Uint8Array
const chunk = data.slice(i * blockSize, (i + 1) * blockSize);

@@ -41,2 +53,10 @@ blocks.push(Buffer.concat([head, chunk]));

},
/**
* Reduces HID USB packets/frames to one response.
*
* @param acc The value resulting from (accumulating) the previous call of reduceResponse.
* On first call initialized to `initialAcc`. The accumulator enables handling multi-frames messages.
* @param chunk Current chunk to reduce into accumulator
* @returns An accumulator value updated with the current chunk
*/
reduceResponse(acc, chunk) {

@@ -53,2 +73,3 @@ let { data, dataLength, sequence } = acc || initialAcc;

}
// Gets the total length of the response from the 1st frame
if (!acc) {

@@ -58,4 +79,6 @@ dataLength = chunk.readUInt16BE(5);

sequence++;
// The total length on the 1st frame takes 2 more bytes
const chunkData = chunk.slice(acc ? 5 : 7);
data = Buffer.concat([data, chunkData]);
// Removes any 0 padding
if (data.length > dataLength) {

@@ -70,2 +93,9 @@ data = data.slice(0, dataLength);

},
/**
* Returns the response message that has been reduced from the HID USB frames
*
* @param acc The accumulator
* @returns A Buffer containing the cleaned response message, or null if no response message, or undefined if the
* accumulator is incorrect (message length is not valid)
*/
getReducedResult(acc) {

@@ -72,0 +102,0 @@ if (acc && acc.dataLength === acc.data.length) {

@@ -42,3 +42,6 @@ /**

/**
* From a given USB product id, return the deviceModel associated to it.
*
* The mapping from the product id is only based on the 2 most significant bytes.
* For example, Stax is defined with a product id of 0x60ii, a product id 0x6011 would be mapped to it.
*/

@@ -45,0 +48,0 @@ export declare const identifyUSBProductId: (usbProductId: number) => DeviceModel | null | undefined;

@@ -136,3 +136,6 @@ "use strict";

/**
* From a given USB product id, return the deviceModel associated to it.
*
* The mapping from the product id is only based on the 2 most significant bytes.
* For example, Stax is defined with a product id of 0x60ii, a product id 0x6011 would be mapped to it.
*/

@@ -139,0 +142,0 @@ const identifyUSBProductId = (usbProductId) => {

{
"name": "@ledgerhq/devices",
"version": "8.1.0",
"version": "8.2.0-next.0",
"description": "Ledger devices",

@@ -58,14 +58,14 @@ "keywords": [

"semver": "^7.3.5",
"@ledgerhq/errors": "^6.16.0",
"@ledgerhq/errors": "^6.16.1-next.0",
"@ledgerhq/logs": "^6.12.0"
},
"devDependencies": {
"@types/jest": "^29.5.0",
"@types/jest": "^29.5.10",
"@types/node": "^20.8.10",
"@types/semver": "^7.3.9",
"documentation": "14.0.2",
"jest": "^28.1.1",
"jest": "^29.7.0",
"rimraf": "^4.4.1",
"source-map-support": "^0.5.21",
"ts-jest": "^28.0.5",
"ts-jest": "^29.1.1",
"ts-node": "^10.4.0"

@@ -72,0 +72,0 @@ },

@@ -13,25 +13,114 @@ <img src="https://user-images.githubusercontent.com/4631227/191834116-59cf590e-25cc-4956-ae5c-812ea464f324.png" height="100" />

* [receiveAPDU](#receiveapdu)
* [Parameters](#parameters)
* [createChunkedBuffers](#createchunkedbuffers)
* [Parameters](#parameters-1)
* [sendAPDU](#sendapdu)
* [Parameters](#parameters-2)
* [createHIDframing](#createhidframing)
* [Parameters](#parameters)
* [Parameters](#parameters-3)
* [makeBlocks](#makeblocks)
* [Parameters](#parameters-4)
* [reduceResponse](#reduceresponse)
* [Parameters](#parameters-5)
* [getReducedResult](#getreducedresult)
* [Parameters](#parameters-6)
* [IIGenericHID](#iigenerichid)
* [ledgerUSBVendorId](#ledgerusbvendorid)
* [getDeviceModel](#getdevicemodel)
* [Parameters](#parameters-1)
* [Parameters](#parameters-7)
* [identifyTargetId](#identifytargetid)
* [Parameters](#parameters-2)
* [Parameters](#parameters-8)
* [identifyUSBProductId](#identifyusbproductid)
* [Parameters](#parameters-3)
* [Parameters](#parameters-9)
* [getBluetoothServiceUuids](#getbluetoothserviceuuids)
* [getInfosForServiceUuid](#getinfosforserviceuuid)
* [Parameters](#parameters-4)
* [Parameters](#parameters-10)
* [DeviceModel](#devicemodel)
* [BluetoothInfos](#bluetoothinfos)
### receiveAPDU
Parses a raw stream coming from a BLE communication into an APDU response
#### Parameters
* `rawStream` **Observable<([Buffer](https://nodejs.org/api/buffer.html) | [Error](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error))>** An observable containing the raw stream as emitted buffers
* `options` **{context: TraceContext?}** Optional options containing:* context An optional context object for log/tracing strategy (optional, default `{}`)
* `options.context` &#x20;
Returns **Observable<[Buffer](https://nodejs.org/api/buffer.html)>** An observable containing the APDU response as one emitted buffer
### createChunkedBuffers
Creates a list of chunked buffer from one buffer
If this is using a Node buffer: the chunked buffers reference to the same memory as the original buffer.
If this is using a Uint8Array: each part of the original buffer is copied into the chunked buffers
#### Parameters
* `buffer` **[Buffer](https://nodejs.org/api/buffer.html)** a Node Buffer, or a Uint8Array
* `sizeForIndex` **function (arg0: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)): [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** A function that takes an index (on the buffer) and returns the size of the chunk at that index
Returns **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Buffer](https://nodejs.org/api/buffer.html)>** a list of chunked buffers
### sendAPDU
Sends an APDU by encoding it into chunks and sending the chunks using the given `write` function
#### Parameters
* `write` **function (arg0: [Buffer](https://nodejs.org/api/buffer.html)): [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<void>** The function to send each chunk to the device
* `apdu` **[Buffer](https://nodejs.org/api/buffer.html)**&#x20;
* `mtuSize` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** The negotiated maximum size of the data to be sent in one chunk
* `options` **{context: TraceContext?}** Optional options containing:* context An optional context object for log/tracing strategy (optional, default `{}`)
* `options.context` &#x20;
Returns **Observable<[Buffer](https://nodejs.org/api/buffer.html)>** An observable that will only emit if an error occurred, otherwise it will complete
### createHIDframing
Object to handle HID frames (encoding and decoding)
#### Parameters
* `channel` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)**&#x20;
* `packetSize` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)**&#x20;
* `packetSize` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** The HID protocol packet size in bytes (usually 64)
### makeBlocks
Frames/encodes an APDU message into HID USB packets/frames
#### Parameters
* `apdu` **[Buffer](https://nodejs.org/api/buffer.html)** The APDU message to send, in a Buffer containing \[cla, ins, p1, p2, data length, data(if not empty)]
Returns **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Buffer](https://nodejs.org/api/buffer.html)>** an array of HID USB frames ready to be sent
### reduceResponse
Reduces HID USB packets/frames to one response.
#### Parameters
* `acc` **ResponseAcc** The value resulting from (accumulating) the previous call of reduceResponse.
On first call initialized to `initialAcc`. The accumulator enables handling multi-frames messages.
* `chunk` **[Buffer](https://nodejs.org/api/buffer.html)** Current chunk to reduce into accumulator
Returns **ResponseAcc** An accumulator value updated with the current chunk
### getReducedResult
Returns the response message that has been reduced from the HID USB frames
#### Parameters
* `acc` **ResponseAcc** The accumulator
Returns **([Buffer](https://nodejs.org/api/buffer.html) | null | [undefined](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined))** A Buffer containing the cleaned response message, or null if no response message, or undefined if the
accumulator is incorrect (message length is not valid)
### IIGenericHID

@@ -80,2 +169,7 @@

From a given USB product id, return the deviceModel associated to it.
The mapping from the product id is only based on the 2 most significant bytes.
For example, Stax is defined with a product id of 0x60ii, a product id 0x6011 would be mapped to it.
#### Parameters

@@ -82,0 +176,0 @@

import { TransportError, DisconnectedDevice } from "@ledgerhq/errors";
import { Observable } from "rxjs";
import { log } from "@ledgerhq/logs";
import { Observable, ReplaySubject, takeUntil } from "rxjs";
import { TraceContext, trace } from "@ledgerhq/logs";
const TagId = 0x05;
// operator that transform the input raw stream into one apdu response and finishes
export const receiveAPDU = (rawStream: Observable<Buffer>): Observable<Buffer> =>
/**
* Parses a raw stream coming from a BLE communication into an APDU response
*
* @param rawStream An observable containing the raw stream as emitted buffers
* @param options Optional options containing:
* - context An optional context object for log/tracing strategy
* @returns An observable containing the APDU response as one emitted buffer
*/
export const receiveAPDU = (
rawStream: Observable<Buffer | Error>,
{ context }: { context?: TraceContext } = {},
): Observable<Buffer> =>
new Observable(o => {

@@ -11,16 +22,35 @@ let notifiedIndex = 0;

let notifiedData = Buffer.alloc(0);
const sub = rawStream.subscribe({
const subscriptionCleaner = new ReplaySubject<void>();
// The raw stream is listened/subscribed to until a full message (that can be made of several frames) is received
rawStream.pipe(takeUntil(subscriptionCleaner)).subscribe({
complete: () => {
o.error(new DisconnectedDevice());
sub.unsubscribe();
},
error: e => {
log("ble-error", "in receiveAPDU " + String(e));
o.error(e);
sub.unsubscribe();
error: error => {
trace({
type: "ble-error",
message: `Error in receiveAPDU: ${error}`,
data: { error },
context,
});
o.error(error);
},
next: value => {
// Silences emitted errors in next
if (value instanceof Error) {
trace({
type: "ble-error",
message: `Error emitted to receiveAPDU next: ${value}`,
data: { error: value },
context,
});
return;
}
const tag = value.readUInt8(0);
const index = value.readUInt16BE(1);
let data = value.slice(3);
const chunkIndex = value.readUInt16BE(1);
// `slice` and not `subarray`: this is not a Node Buffer, but probably only a Uint8Array.
let chunkData = value.slice(3);

@@ -32,9 +62,7 @@ if (tag !== TagId) {

if (notifiedIndex !== index) {
// A chunk was probably missed
if (notifiedIndex !== chunkIndex) {
o.error(
new TransportError(
"BLE: Invalid sequence number. discontinued chunk. Received " +
index +
" but expected " +
notifiedIndex,
`BLE: Invalid sequence number. discontinued chunk. Received ${chunkIndex} but expected ${notifiedIndex}`,
"InvalidSequence",

@@ -46,9 +74,13 @@ ),

if (index === 0) {
notifiedDataLength = data.readUInt16BE(0);
data = data.slice(2);
// The total length of the response is located on the next 2 bytes on the 1st chunk
if (chunkIndex === 0) {
notifiedDataLength = chunkData.readUInt16BE(0);
// `slice` and not `subarray`: this is not a Node Buffer, but probably only a Uint8Array.
chunkData = chunkData.slice(2);
}
notifiedIndex++;
notifiedData = Buffer.concat([notifiedData, data]);
// The cost of creating a new buffer for each received chunk is low because the response is often contained in 1 chunk.
// Allocating `notifiedData` buffer with the received total length and mutating it will probably not improve any performance.
notifiedData = Buffer.concat([notifiedData, chunkData]);

@@ -58,6 +90,3 @@ if (notifiedData.length > notifiedDataLength) {

new TransportError(
"BLE: received too much data. discontinued chunk. Received " +
notifiedData.length +
" but expected " +
notifiedDataLength,
`BLE: received too much data. discontinued chunk. Received ${notifiedData.length} but expected ${notifiedDataLength}`,
"BLETooMuchData",

@@ -72,9 +101,11 @@ ),

o.complete();
sub.unsubscribe();
// Tries to unsubscribe from the raw stream as soon as we complete the parent observer
subscriptionCleaner.next();
}
},
});
return () => {
sub.unsubscribe();
subscriptionCleaner.next();
};
});
import { Observable } from "rxjs";
import { log } from "@ledgerhq/logs";
import { TraceContext, trace } from "@ledgerhq/logs";
const TagId = 0x05;
function chunkBuffer(buffer: Buffer, sizeForIndex: (arg0: number) => number): Array<Buffer> {
/**
* Creates a list of chunked buffer from one buffer
*
* If this is using a Node buffer: the chunked buffers reference to the same memory as the original buffer.
* If this is using a Uint8Array: each part of the original buffer is copied into the chunked buffers
*
* @param buffer a Node Buffer, or a Uint8Array
* @param sizeForIndex A function that takes an index (on the buffer) and returns the size of the chunk at that index
* @returns a list of chunked buffers
*/
function createChunkedBuffers(
buffer: Buffer,
sizeForIndex: (arg0: number) => number,
): Array<Buffer> {
const chunks: Buffer[] = [];
for (let i = 0, size = sizeForIndex(0); i < buffer.length; i += size, size = sizeForIndex(i)) {
// If this is a Node buffer: this chunked buffer points to the same memory but with cropped starting and ending indices
// `slice` and not `subarray`: this might not be a Node Buffer, but probably only a Uint8Array.
chunks.push(buffer.slice(i, i + size));

@@ -15,2 +30,12 @@ }

/**
* Sends an APDU by encoding it into chunks and sending the chunks using the given `write` function
*
* @param write The function to send each chunk to the device
* @param apdu
* @param mtuSize The negotiated maximum size of the data to be sent in one chunk
* @param options Optional options containing:
* - context An optional context object for log/tracing strategy
* @returns An observable that will only emit if an error occurred, otherwise it will complete
*/
export const sendAPDU = (

@@ -20,8 +45,14 @@ write: (arg0: Buffer) => Promise<void>,

mtuSize: number,
{ context }: { context?: TraceContext } = {},
): Observable<Buffer> => {
const chunks = chunkBuffer(apdu, i => mtuSize - (i === 0 ? 5 : 3)).map((buffer, i) => {
// Prepares the data to be sent in chunks, by adding a header to chunked of the APDU
// The first chunk will contain the total length of the APDU, which reduces the size of the data written in the first chunk.
// The total length of the APDU is encoded in 2 bytes (so 5 bytes for the header with the tag id and the chunk index).
const chunks = createChunkedBuffers(apdu, i => mtuSize - (i === 0 ? 5 : 3)).map((buffer, i) => {
const head = Buffer.alloc(i === 0 ? 5 : 3);
head.writeUInt8(TagId, 0);
// Index of the chunk as the 2 next bytes
head.writeUInt16BE(i, 1);
// The total length of the APDU is written on the first chunk
if (i === 0) {

@@ -31,4 +62,6 @@ head.writeUInt16BE(apdu.length, 3);

// No 0-padding is needed
return Buffer.concat([head, buffer]);
});
return new Observable(o => {

@@ -49,6 +82,11 @@ let terminated = false;

},
e => {
error => {
terminated = true;
log("ble-error", "sendAPDU failure " + String(e));
o.error(e);
trace({
type: "ble-error",
message: `sendAPDU failure: ${error}`,
data: { error },
context,
});
o.error(error);
},

@@ -59,3 +97,7 @@ );

if (!terminated) {
log("ble-verbose", "sendAPDU interruption");
trace({
type: "ble-error",
message: "sendAPDU interruption",
context,
});
terminated = true;

@@ -62,0 +104,0 @@ }

import { TransportError } from "@ledgerhq/errors";
// Represents a response message from the device being reduced from HID USB frames/packets
export type ResponseAcc =

@@ -6,2 +8,3 @@ | {

dataLength: number;
// The current frame id/number
sequence: number;

@@ -11,2 +14,3 @@ }

| undefined;
const Tag = 0x05;

@@ -27,14 +31,28 @@

/**
* Object to handle HID frames (encoding and decoding)
*
* @param channel
* @param packetSize The HID protocol packet size in bytes (usually 64)
*/
const createHIDframing = (channel: number, packetSize: number) => {
return {
/**
* Frames/encodes an APDU message into HID USB packets/frames
*
* @param apdu The APDU message to send, in a Buffer containing [cla, ins, p1, p2, data length, data(if not empty)]
* @returns an array of HID USB frames ready to be sent
*/
makeBlocks(apdu: Buffer): Buffer[] {
// Encodes the APDU length in 2 bytes before the APDU itself.
// The length is measured as the number of bytes.
// As the size of the APDU `data` should have been added in 1 byte just before `data`,
// the minimum size of an APDU is 5 bytes.
let data = Buffer.concat([asUInt16BE(apdu.length), apdu]);
const blockSize = packetSize - 5;
const nbBlocks = Math.ceil(data.length / blockSize);
data = Buffer.concat([
data, // fill data with padding
Buffer.alloc(nbBlocks * blockSize - data.length + 1).fill(0),
]);
// Fills data with 0-padding
data = Buffer.concat([data, Buffer.alloc(nbBlocks * blockSize - data.length + 1).fill(0)]);
const blocks: Buffer[] = [];

@@ -47,3 +65,6 @@

head.writeUInt16BE(i, 3);
// `slice` and not `subarray`: this might not be a Node Buffer, but probably only a Uint8Array
const chunk = data.slice(i * blockSize, (i + 1) * blockSize);
blocks.push(Buffer.concat([head, chunk]));

@@ -55,2 +76,10 @@ }

/**
* Reduces HID USB packets/frames to one response.
*
* @param acc The value resulting from (accumulating) the previous call of reduceResponse.
* On first call initialized to `initialAcc`. The accumulator enables handling multi-frames messages.
* @param chunk Current chunk to reduce into accumulator
* @returns An accumulator value updated with the current chunk
*/
reduceResponse(acc: ResponseAcc, chunk: Buffer): ResponseAcc {

@@ -71,2 +100,3 @@ let { data, dataLength, sequence } = acc || initialAcc;

// Gets the total length of the response from the 1st frame
if (!acc) {

@@ -77,5 +107,7 @@ dataLength = chunk.readUInt16BE(5);

sequence++;
// The total length on the 1st frame takes 2 more bytes
const chunkData = chunk.slice(acc ? 5 : 7);
data = Buffer.concat([data, chunkData]);
// Removes any 0 padding
if (data.length > dataLength) {

@@ -92,2 +124,9 @@ data = data.slice(0, dataLength);

/**
* Returns the response message that has been reduced from the HID USB frames
*
* @param acc The accumulator
* @returns A Buffer containing the cleaned response message, or null if no response message, or undefined if the
* accumulator is incorrect (message length is not valid)
*/
getReducedResult(acc: ResponseAcc): Buffer | null | undefined {

@@ -94,0 +133,0 @@ if (acc && acc.dataLength === acc.data.length) {

@@ -139,3 +139,6 @@ import semver from "semver";

/**
* From a given USB product id, return the deviceModel associated to it.
*
* The mapping from the product id is only based on the 2 most significant bytes.
* For example, Stax is defined with a product id of 0x60ii, a product id 0x6011 would be mapped to it.
*/

@@ -142,0 +145,0 @@ export const identifyUSBProductId = (usbProductId: number): DeviceModel | null | undefined => {

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

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

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

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc