Skip to content

Commit

Permalink
feat: add mobile keyring bridge (#225)
Browse files Browse the repository at this point in the history
* feat: add mobile keyring bridge

* chore: update version number

* feat: upgrade @ethereumjs/tx library to match the same version used in TransactionController 8.0.1 to fix all transaction broken issue.

Fix the signPersonalMessage in ledger always return 0x0 in v value in test-dapp

* feat: upgrade ledgerHQ library to latest to resolve vulnerablity report from socket-security

* Feat/add mobile keyring bridge flat version (#3)

* feat: this is a backup of flat directory version of eth-ledger-bridge-keyring to successfully solve the metamask-mobile not able to resovle all the module under the subdirectory during build process.

* feat: improve the unit tests coverage for some API.

* feat: run `yarn lint:fix` and update the correct import in index.ts file.

* feat: add unit tests coverage

* feat: fix lint error in test file.

* feat: Remove unit tests which failed.

* feat: Add extra test to cover more lines in ledger-keyring.ts

* feat: Add yarn lint:fix

* feat: Increase the threshold coverage to pass the pipeline.

* feat: revert the version of package..

* Update src/ledger-transport-middleware.ts

Co-authored-by: Gustavo Antunes <[email protected]>

* Update src/ledger-mobile-bridge.test.ts

Co-authored-by: Gustavo Antunes <[email protected]>

* Update src/ledger-mobile-bridge.test.ts

Co-authored-by: Gustavo Antunes <[email protected]>

* Update src/ledger-mobile-bridge.test.ts

Co-authored-by: Gustavo Antunes <[email protected]>

* Apply suggestions from code review

Co-authored-by: Gustavo Antunes <[email protected]>

* feat: remove the comment code in `prepack.sh`

* feat: change the jsDoc for deviceSignMessage method.

* feat: fix the lint issue.

* Apply suggestions from code review

Co-authored-by: Gustavo Antunes <[email protected]>

* Apply suggestions from code review

Co-authored-by: Gustavo Antunes <[email protected]>

* feat: change setAccountToUnlock method signature to enforce index is number type.

* feat: change setAccountToUnlock method signature to enforce index is number type.

* feat: commit the new coverageThreshold to pass the test.

---------

Co-authored-by: Xiaoming Wang <[email protected]>
Co-authored-by: Gustavo Antunes <[email protected]>
  • Loading branch information
3 people authored Jun 24, 2024
1 parent 127eb74 commit 8979b63
Show file tree
Hide file tree
Showing 14 changed files with 2,678 additions and 1,841 deletions.
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 69.76,
functions: 88.73,
lines: 81.93,
statements: 81.84,
branches: 87.94,
functions: 95.91,
lines: 91.13,
statements: 91.22,
},
},

Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,16 @@
},
"dependencies": {
"@ethereumjs/rlp": "^4.0.0",
"@ethereumjs/tx": "^4.1.1",
"@ethereumjs/tx": "^4.2.0",
"@ethereumjs/util": "^8.0.0",
"@metamask/eth-sig-util": "^7.0.0",
"@ledgerhq/hw-app-eth": "6.26.1",
"@metamask/eth-sig-util": "^7.0.1",
"hdkey": "^2.1.0"
},
"devDependencies": {
"@ethereumjs/common": "^3.1.1",
"@ethereumjs/common": "^3.2.0",
"@lavamoat/allow-scripts": "^2.5.1",
"@ledgerhq/hw-app-eth": "^6.32.0",
"@ledgerhq/hw-transport": "^6.24.1",
"@ledgerhq/types-cryptoassets": "^7.6.0",
"@ledgerhq/types-devices": "^6.22.4",
"@metamask/auto-changelog": "^3.1.0",
Expand Down Expand Up @@ -95,7 +96,8 @@
"@ledgerhq/hw-app-eth>@ledgerhq/domain-service>eip55>keccak": false,
"ethereumjs-tx>ethereumjs-util>keccak": false,
"ethereumjs-tx>ethereumjs-util>secp256k1": false,
"hdkey>secp256k1": false
"hdkey>secp256k1": false,
"ethereumjs-tx>ethereumjs-util>ethereum-cryptography>keccak": false
}
}
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export * from './ledger-keyring';
export * from './ledger-iframe-bridge';
export * from './ledger-mobile-bridge';
export * from './ledger-bridge';
export * from './ledger-transport-middleware';
export * from './type';
export * from './ledger-hw-app';
7 changes: 3 additions & 4 deletions src/ledger-bridge.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type LedgerHwAppEth from '@ledgerhq/hw-app-eth';
import type Transport from '@ledgerhq/hw-transport';

export type GetPublicKeyParams = { hdPath: string };
export type GetPublicKeyResponse = Awaited<
ReturnType<LedgerHwAppEth['getAddress']>
> & {
chainCode: string;
};
>;

export type LedgerSignTransactionParams = { hdPath: string; tx: string };
export type LedgerSignTransactionResponse = Awaited<
Expand Down Expand Up @@ -49,7 +48,7 @@ export type LedgerBridge<T extends LedgerBridgeOptions> = {

attemptMakeApp(): Promise<boolean>;

updateTransportMethod(transportType: string): Promise<boolean>;
updateTransportMethod(transportType: string | Transport): Promise<boolean>;

getPublicKey(params: GetPublicKeyParams): Promise<GetPublicKeyResponse>;

Expand Down
128 changes: 128 additions & 0 deletions src/ledger-hw-app.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import Transport from '@ledgerhq/hw-transport';

import { MetaMaskLedgerHwAppEth } from './ledger-hw-app';

const DEVICE_ID = 'DEVICE_ID';

const mockTransport = {
deviceModel: {
id: DEVICE_ID,
},
send: jest.fn(),
close: jest.fn(),
decorateAppAPIMethods: jest.fn(),
};

describe('MetaMaskLedgerHwAppEth', function () {
afterEach(function () {
jest.clearAllMocks();
});

describe('openEthApp', function () {
it('sends "open ETH app" command correctly', async function () {
const ethApp = new MetaMaskLedgerHwAppEth(
mockTransport as unknown as Transport,
);

const transportSpy = jest
.spyOn(mockTransport, 'send')
.mockImplementation(async () => Promise.resolve(Buffer.alloc(1)));

await ethApp.openEthApp();
expect(transportSpy).toHaveBeenCalledTimes(1);
expect(transportSpy).toHaveBeenCalledWith(
0xe0,
0xd8,
0x00,
0x00,
Buffer.from('Ethereum', 'ascii'),
);
});
});

describe('closeApps', function () {
it('sends "closeApp" command correctly', async function () {
const ethApp = new MetaMaskLedgerHwAppEth(
mockTransport as unknown as Transport,
);

const transportSpy = jest
.spyOn(mockTransport, 'send')
.mockImplementation(async () => Promise.resolve(Buffer.alloc(1)));

await ethApp.closeApps();
expect(transportSpy).toHaveBeenCalledTimes(1);
expect(transportSpy).toHaveBeenCalledWith(0xb0, 0xa7, 0x00, 0x00);
});
});

describe('getAppNameAndVersion', function () {
it('gets appName and appVersion correctly', async function () {
const appNameBuf = Buffer.alloc(7, 'appName', 'ascii');
const verionBuf = Buffer.alloc(10, 'appVersion', 'ascii');
const buffer = Buffer.alloc(20);
buffer[0] = 1;
buffer[1] = appNameBuf.length;
let j = 2;
for (let i = 0; i < appNameBuf.length; i++, j++) {
buffer[j] = appNameBuf[i] ?? 0;
}
buffer[j] = verionBuf.length;
j += 1;
for (let i = 0; i < verionBuf.length; i++, j++) {
buffer[j] = verionBuf[i] ?? 0;
}

const ethApp = new MetaMaskLedgerHwAppEth(
mockTransport as unknown as Transport,
);

const transportSpy = jest
.spyOn(mockTransport, 'send')
.mockImplementation(async () => Promise.resolve(buffer));

const result = await ethApp.getAppNameAndVersion();
expect(transportSpy).toHaveBeenCalledTimes(1);
expect(transportSpy).toHaveBeenCalledWith(0xb0, 0x01, 0x00, 0x00);
expect(result).toStrictEqual({
appName: 'appName',
version: 'appVersion',
});
});

it('does not throw an error when the result length is less than expected', async function () {
const buffer = Buffer.alloc(1);
buffer[0] = 1;
const ethApp = new MetaMaskLedgerHwAppEth(
mockTransport as unknown as Transport,
);
const transportSpy = jest
.spyOn(mockTransport, 'send')
.mockImplementation(async () => Promise.resolve(buffer));

const result = await ethApp.getAppNameAndVersion();
expect(transportSpy).toHaveBeenCalledTimes(1);
expect(transportSpy).toHaveBeenCalledWith(0xb0, 0x01, 0x00, 0x00);
expect(result).toStrictEqual({
appName: '',
version: '',
});
});

it('throws an error when first byte is not 1', async function () {
const ethApp = new MetaMaskLedgerHwAppEth(
mockTransport as unknown as Transport,
);

const transportSpy = jest
.spyOn(mockTransport, 'send')
.mockImplementation(async () => Promise.resolve(Buffer.alloc(1)));

await expect(ethApp.getAppNameAndVersion()).rejects.toThrow(
'Incorrect format return from getAppNameAndVersion.',
);
expect(transportSpy).toHaveBeenCalledTimes(1);
expect(transportSpy).toHaveBeenCalledWith(0xb0, 0x01, 0x00, 0x00);
});
});
});
77 changes: 77 additions & 0 deletions src/ledger-hw-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import LedgerHwAppEth from '@ledgerhq/hw-app-eth';
// eslint-disable-next-line import/no-nodejs-modules
import { Buffer } from 'buffer';

import { GetAppNameAndVersionResponse } from './type';

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export interface MetaMaskLedgerHwAppEth extends LedgerHwAppEth {
openEthApp(): void;
closeApps(): void;
getAppNameAndVersion(): Promise<GetAppNameAndVersionResponse>;
}

export class MetaMaskLedgerHwAppEth
extends LedgerHwAppEth
implements MetaMaskLedgerHwAppEth
{
readonly mainAppName = 'BOLOS';

readonly ethAppName = 'Ethereum';

readonly transportEncoding = 'ascii';

/**
* Method to open ethereum application on ledger device.
*
*/
async openEthApp(): Promise<void> {
await this.transport.send(
0xe0,
0xd8,
0x00,
0x00,
Buffer.from(this.ethAppName, this.transportEncoding),
);
}

/**
* Method to close all running application on ledger device.
*
*/
async closeApps(): Promise<void> {
await this.transport.send(0xb0, 0xa7, 0x00, 0x00);
}

/**
* Method to retrieve the name and version of the running application in ledger device.
*
* @returns An object contains appName and version.
*/
async getAppNameAndVersion(): Promise<GetAppNameAndVersionResponse> {
const response = await this.transport.send(0xb0, 0x01, 0x00, 0x00);
if (response[0] !== 1) {
throw new Error('Incorrect format return from getAppNameAndVersion.');
}

let i = 1;
const nameLength = response[i] ?? 0;
i += 1;

const appName = response
.slice(i, (i += nameLength))
.toString(this.transportEncoding);

const versionLength = response[i] ?? 0;
i += 1;

const version = response
.slice(i, (i += versionLength))
.toString(this.transportEncoding);

return {
appName,
version,
};
}
}
Loading

0 comments on commit 8979b63

Please sign in to comment.