@ledgerhq/ledger-key-ring-protocol 0.5.1-nightly.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +33 -0
- package/.turbo/turbo-build.log +4 -0
- package/.unimportedrc.json +16 -0
- package/CHANGELOG.md +299 -0
- package/LICENSE.txt +21 -0
- package/README.md +3 -0
- package/jest.config.js +13 -0
- package/lib/HWDeviceProvider.d.ts +25 -0
- package/lib/HWDeviceProvider.d.ts.map +1 -0
- package/lib/HWDeviceProvider.js +88 -0
- package/lib/HWDeviceProvider.js.map +1 -0
- package/lib/api.d.ts +77 -0
- package/lib/api.d.ts.map +1 -0
- package/lib/api.js +150 -0
- package/lib/api.js.map +1 -0
- package/lib/auth.d.ts +3 -0
- package/lib/auth.d.ts.map +1 -0
- package/lib/auth.js +79 -0
- package/lib/auth.js.map +1 -0
- package/lib/errors.d.ts +40 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +18 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +17 -0
- package/lib/index.js.map +1 -0
- package/lib/mockSdk.d.ts +22 -0
- package/lib/mockSdk.d.ts.map +1 -0
- package/lib/mockSdk.js +208 -0
- package/lib/mockSdk.js.map +1 -0
- package/lib/qrcode/cipher.d.ts +12 -0
- package/lib/qrcode/cipher.d.ts.map +1 -0
- package/lib/qrcode/cipher.js +69 -0
- package/lib/qrcode/cipher.js.map +1 -0
- package/lib/qrcode/cipher.test.d.ts +2 -0
- package/lib/qrcode/cipher.test.d.ts.map +1 -0
- package/lib/qrcode/cipher.test.js +40 -0
- package/lib/qrcode/cipher.test.js.map +1 -0
- package/lib/qrcode/index.d.ts +70 -0
- package/lib/qrcode/index.d.ts.map +1 -0
- package/lib/qrcode/index.js +312 -0
- package/lib/qrcode/index.js.map +1 -0
- package/lib/qrcode/index.test.d.ts +2 -0
- package/lib/qrcode/index.test.d.ts.map +1 -0
- package/lib/qrcode/index.test.js +131 -0
- package/lib/qrcode/index.test.js.map +1 -0
- package/lib/qrcode/types.d.ts +69 -0
- package/lib/qrcode/types.d.ts.map +1 -0
- package/lib/qrcode/types.js +3 -0
- package/lib/qrcode/types.js.map +1 -0
- package/lib/sdk.d.ts +31 -0
- package/lib/sdk.d.ts.map +1 -0
- package/lib/sdk.js +380 -0
- package/lib/sdk.js.map +1 -0
- package/lib/store.d.ts +71 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/store.js +62 -0
- package/lib/store.js.map +1 -0
- package/lib/types.d.ts +181 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +10 -0
- package/lib/types.js.map +1 -0
- package/lib-es/HWDeviceProvider.d.ts +25 -0
- package/lib-es/HWDeviceProvider.d.ts.map +1 -0
- package/lib-es/HWDeviceProvider.js +81 -0
- package/lib-es/HWDeviceProvider.js.map +1 -0
- package/lib-es/api.d.ts +77 -0
- package/lib-es/api.d.ts.map +1 -0
- package/lib-es/api.js +145 -0
- package/lib-es/api.js.map +1 -0
- package/lib-es/auth.d.ts +3 -0
- package/lib-es/auth.d.ts.map +1 -0
- package/lib-es/auth.js +75 -0
- package/lib-es/auth.js.map +1 -0
- package/lib-es/errors.d.ts +40 -0
- package/lib-es/errors.d.ts.map +1 -0
- package/lib-es/errors.js +15 -0
- package/lib-es/errors.js.map +1 -0
- package/lib-es/index.d.ts +6 -0
- package/lib-es/index.d.ts.map +1 -0
- package/lib-es/index.js +13 -0
- package/lib-es/index.js.map +1 -0
- package/lib-es/mockSdk.d.ts +22 -0
- package/lib-es/mockSdk.d.ts.map +1 -0
- package/lib-es/mockSdk.js +201 -0
- package/lib-es/mockSdk.js.map +1 -0
- package/lib-es/qrcode/cipher.d.ts +12 -0
- package/lib-es/qrcode/cipher.d.ts.map +1 -0
- package/lib-es/qrcode/cipher.js +61 -0
- package/lib-es/qrcode/cipher.js.map +1 -0
- package/lib-es/qrcode/cipher.test.d.ts +2 -0
- package/lib-es/qrcode/cipher.test.d.ts.map +1 -0
- package/lib-es/qrcode/cipher.test.js +38 -0
- package/lib-es/qrcode/cipher.test.js.map +1 -0
- package/lib-es/qrcode/index.d.ts +70 -0
- package/lib-es/qrcode/index.d.ts.map +1 -0
- package/lib-es/qrcode/index.js +304 -0
- package/lib-es/qrcode/index.js.map +1 -0
- package/lib-es/qrcode/index.test.d.ts +2 -0
- package/lib-es/qrcode/index.test.d.ts.map +1 -0
- package/lib-es/qrcode/index.test.js +126 -0
- package/lib-es/qrcode/index.test.js.map +1 -0
- package/lib-es/qrcode/types.d.ts +69 -0
- package/lib-es/qrcode/types.d.ts.map +1 -0
- package/lib-es/qrcode/types.js +2 -0
- package/lib-es/qrcode/types.js.map +1 -0
- package/lib-es/sdk.d.ts +31 -0
- package/lib-es/sdk.d.ts.map +1 -0
- package/lib-es/sdk.js +371 -0
- package/lib-es/sdk.js.map +1 -0
- package/lib-es/store.d.ts +71 -0
- package/lib-es/store.d.ts.map +1 -0
- package/lib-es/store.js +51 -0
- package/lib-es/store.js.map +1 -0
- package/lib-es/types.d.ts +181 -0
- package/lib-es/types.d.ts.map +1 -0
- package/lib-es/types.js +7 -0
- package/lib-es/types.js.map +1 -0
- package/mocks/scenarios/addSameMemberMultipleTimes.json +426 -0
- package/mocks/scenarios/create2trustchainInARow.json +616 -0
- package/mocks/scenarios/getOrCreateTransactionCases.json +591 -0
- package/mocks/scenarios/member3implicitlyAdded.json +648 -0
- package/mocks/scenarios/membersManySelfAdd.json +1427 -0
- package/mocks/scenarios/randomMemberTryToDestroy.json +371 -0
- package/mocks/scenarios/removeMemberWithTheWrongSeed.json +510 -0
- package/mocks/scenarios/removedMemberEjectedOnDeletedTrustchain.json +481 -0
- package/mocks/scenarios/removedMemberEjectedOnGetMembers.json +648 -0
- package/mocks/scenarios/removedMemberEjectedOnRestore.json +648 -0
- package/mocks/scenarios/removingAMemberCreatesAnInteraction.json +593 -0
- package/mocks/scenarios/removingYourselfIsForbidden.json +397 -0
- package/mocks/scenarios/success.json +978 -0
- package/mocks/scenarios/tokenExpires.json +371 -0
- package/mocks/scenarios/twoAddMembersFollowedByDeviceAdd.json +705 -0
- package/mocks/scenarios/userRefusesAuth.json +40 -0
- package/mocks/scenarios/userRefusesRemoveMember.json +542 -0
- package/package.json +91 -0
- package/scripts/README.md +15 -0
- package/scripts/e2e.ts +57 -0
- package/src/HWDeviceProvider.ts +105 -0
- package/src/__tests__/integration/mock.sdk.test.ts +47 -0
- package/src/__tests__/integration/sdk.test.ts +20 -0
- package/src/__tests__/tsconfig.json +8 -0
- package/src/__tests__/unit/sdk.test.ts +236 -0
- package/src/api.ts +202 -0
- package/src/auth.ts +81 -0
- package/src/errors.ts +18 -0
- package/src/index.ts +20 -0
- package/src/mockSdk.ts +253 -0
- package/src/qrcode/cipher.test.ts +30 -0
- package/src/qrcode/cipher.ts +63 -0
- package/src/qrcode/index.test.ts +138 -0
- package/src/qrcode/index.ts +395 -0
- package/src/qrcode/types.ts +70 -0
- package/src/sdk.ts +542 -0
- package/src/store.ts +99 -0
- package/src/types.ts +242 -0
- package/tests/scenarios/_template.ts +18 -0
- package/tests/scenarios/addSameMemberMultipleTimes.ts +20 -0
- package/tests/scenarios/create2trustchainInARow.ts +14 -0
- package/tests/scenarios/getOrCreateTransactionCases.ts +74 -0
- package/tests/scenarios/member3implicitlyAdded.ts +51 -0
- package/tests/scenarios/membersManySelfAdd.ts +18 -0
- package/tests/scenarios/randomMemberTryToDestroy.ts +23 -0
- package/tests/scenarios/removeMemberWithTheWrongSeed.ts +28 -0
- package/tests/scenarios/removedMemberEjectedOnDeletedTrustchain.ts +31 -0
- package/tests/scenarios/removedMemberEjectedOnGetMembers.ts +29 -0
- package/tests/scenarios/removedMemberEjectedOnRestore.ts +31 -0
- package/tests/scenarios/removingAMemberCreatesAnInteraction.ts +42 -0
- package/tests/scenarios/removingYourselfIsForbidden.ts +11 -0
- package/tests/scenarios/success.ts +94 -0
- package/tests/scenarios/tokenExpires.ts +20 -0
- package/tests/scenarios/twoAddMembersFollowedByDeviceAdd.ts +49 -0
- package/tests/scenarios/userRefusesAuth.ts +28 -0
- package/tests/scenarios/userRefusesRemoveMember.ts +66 -0
- package/tests/test-helpers/recordTrustchainSdkTests.ts +178 -0
- package/tests/test-helpers/replayTrustchainSdkTests.ts +141 -0
- package/tests/test-helpers/types.ts +45 -0
- package/tests/tsconfig.json +8 -0
- package/tsconfig.json +15 -0
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ledgerhq/ledger-key-ring-protocol",
|
|
3
|
+
"version": "0.5.1-nightly.0",
|
|
4
|
+
"description": "Ledger Key Ring Protocol layer",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"Ledger"
|
|
7
|
+
],
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/LedgerHQ/ledger-live.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/LedgerHQ/ledger-live/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/LedgerHQ/ledger-live/tree/develop/libs/ledger-key-ring-protocol",
|
|
16
|
+
"main": "lib/index.js",
|
|
17
|
+
"module": "lib-es/index.js",
|
|
18
|
+
"types": "lib/index.d.ts",
|
|
19
|
+
"typesVersions": {
|
|
20
|
+
"*": {
|
|
21
|
+
"*.json": [
|
|
22
|
+
"*.json"
|
|
23
|
+
],
|
|
24
|
+
"*": [
|
|
25
|
+
"lib/*"
|
|
26
|
+
],
|
|
27
|
+
"lib/*": [
|
|
28
|
+
"lib/*"
|
|
29
|
+
],
|
|
30
|
+
"lib-es/*": [
|
|
31
|
+
"lib-es/*"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"exports": {
|
|
36
|
+
"./lib/*": "./lib/*.js",
|
|
37
|
+
"./lib/*.js": "./lib/*.js",
|
|
38
|
+
"./lib-es/*": "./lib-es/*.js",
|
|
39
|
+
"./lib-es/*.js": "./lib-es/*.js",
|
|
40
|
+
"./*": {
|
|
41
|
+
"require": "./lib/*.js",
|
|
42
|
+
"default": "./lib-es/*.js"
|
|
43
|
+
},
|
|
44
|
+
"./*.js": {
|
|
45
|
+
"require": "./lib/*.js",
|
|
46
|
+
"default": "./lib-es/*.js"
|
|
47
|
+
},
|
|
48
|
+
".": {
|
|
49
|
+
"require": "./lib/index.js",
|
|
50
|
+
"default": "./lib-es/index.js"
|
|
51
|
+
},
|
|
52
|
+
"./package.json": "./package.json"
|
|
53
|
+
},
|
|
54
|
+
"license": "Apache-2.0",
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"base64-js": "1",
|
|
57
|
+
"isomorphic-ws": "5",
|
|
58
|
+
"rxjs": "^7.8.1",
|
|
59
|
+
"ws": "8",
|
|
60
|
+
"@ledgerhq/errors": "6.19.1",
|
|
61
|
+
"@ledgerhq/hw-transport": "6.31.4",
|
|
62
|
+
"@ledgerhq/hw-transport-mocker": "6.29.4",
|
|
63
|
+
"@ledgerhq/hw-ledger-key-ring-protocol": "0.2.1-nightly.0",
|
|
64
|
+
"@ledgerhq/live-env": "2.4.1-nightly.0",
|
|
65
|
+
"@ledgerhq/live-network": "2.0.3-nightly.0",
|
|
66
|
+
"@ledgerhq/logs": "6.12.0",
|
|
67
|
+
"@ledgerhq/speculos-transport": "0.1.8-nightly.0",
|
|
68
|
+
"@ledgerhq/types-devices": "^6.25.3"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@types/jest": "^29.5.10",
|
|
72
|
+
"bip39": "^3.0.4",
|
|
73
|
+
"expect": "^27.5.1",
|
|
74
|
+
"jest": "^29.7.0",
|
|
75
|
+
"msw": "^2.2.13",
|
|
76
|
+
"ts-jest": "^29.1.1",
|
|
77
|
+
"ts-node": "^10.9.2"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"clean": "rimraf lib lib-es",
|
|
81
|
+
"build": "tsc && tsc -m ES6 --outDir lib-es",
|
|
82
|
+
"prewatch": "pnpm build",
|
|
83
|
+
"watch": "tsc --watch",
|
|
84
|
+
"lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache",
|
|
85
|
+
"lint:fix": "pnpm lint --fix",
|
|
86
|
+
"typecheck": "tsc --noEmit",
|
|
87
|
+
"unimported": "unimported",
|
|
88
|
+
"test": "jest",
|
|
89
|
+
"e2e": "ts-node scripts/e2e.ts"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
### Run end 2 end tests and generate missing integration tests
|
|
2
|
+
|
|
3
|
+
the e2e script will run all end 2 end tests and record the APDUs, network calls and crypto randomness in order to replay them deterministically in integration tests.
|
|
4
|
+
|
|
5
|
+
Just run the tests that miss snapshot mocks.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pnpm trustchain e2e
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Run all the end2end tests regardless if there are snapshot generated.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
RUN_EVEN_IF_SNAPSHOT_EXISTS=1 pnpm trustchain e2e
|
|
15
|
+
```
|
package/scripts/e2e.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import fsPromises from "fs/promises";
|
|
3
|
+
import { listen } from "@ledgerhq/logs";
|
|
4
|
+
import { recordTestTrustchainSdk } from "../tests/test-helpers/recordTrustchainSdkTests";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import expect from "expect";
|
|
7
|
+
|
|
8
|
+
// we force inject expect to allow us to have expect() code in the test-scenarios
|
|
9
|
+
(global as any).expect = expect;
|
|
10
|
+
|
|
11
|
+
const coinapps = process.env.COIN_APPS;
|
|
12
|
+
if (!coinapps) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"COIN_APPS env is required. it should be the path to the coinapps folder ( cloned from https://github.com/LedgerHQ/coin-apps )",
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
const scenarioFolder = path.join(__dirname, "../tests/scenarios");
|
|
20
|
+
const mockFolder = path.join(__dirname, "../mocks/scenarios");
|
|
21
|
+
const files = await fsPromises.readdir(scenarioFolder);
|
|
22
|
+
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
if (file.endsWith(".ts") && !file.startsWith("_")) {
|
|
25
|
+
const slug = file.slice(0, -3);
|
|
26
|
+
const e2eFile = path.join(scenarioFolder, file);
|
|
27
|
+
const mod = await import(e2eFile);
|
|
28
|
+
const scenario = mod.scenario;
|
|
29
|
+
const snapshotFile = path.join(mockFolder, slug + ".json");
|
|
30
|
+
const snapshotFileExists = await exists(snapshotFile);
|
|
31
|
+
if (snapshotFileExists && !process.env.RUN_EVEN_IF_SNAPSHOT_EXISTS) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
console.log("RUNNING E2E ON TEST SCENARIO", slug);
|
|
35
|
+
// otherwise we always run the e2e but don't touch existing snapshots
|
|
36
|
+
await recordTestTrustchainSdk(snapshotFileExists ? null : snapshotFile, scenario, {
|
|
37
|
+
coinapps,
|
|
38
|
+
...mod.recorderConfig,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
main();
|
|
45
|
+
|
|
46
|
+
function exists(file: string): Promise<boolean> {
|
|
47
|
+
return fsPromises.access(file).then(
|
|
48
|
+
() => true,
|
|
49
|
+
() => false,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (process.env.VERBOSE) {
|
|
54
|
+
listen(log => {
|
|
55
|
+
console.log(log.type + ": " + log.message);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { from, lastValueFrom } from "rxjs";
|
|
2
|
+
import { UserRefusedOnDevice } from "@ledgerhq/errors";
|
|
3
|
+
import { ApduDevice } from "@ledgerhq/hw-ledger-key-ring-protocol/ApduDevice";
|
|
4
|
+
import { StatusCodes, TransportStatusError } from "@ledgerhq/hw-transport";
|
|
5
|
+
import { crypto, device } from "@ledgerhq/hw-ledger-key-ring-protocol";
|
|
6
|
+
import getApi from "./api";
|
|
7
|
+
import { genericWithJWT } from "./auth";
|
|
8
|
+
import { AuthCachePolicy, JWT, TrustchainDeviceCallbacks, WithDevice } from "./types";
|
|
9
|
+
import { TrustchainNotAllowed } from "./errors";
|
|
10
|
+
|
|
11
|
+
export class HWDeviceProvider {
|
|
12
|
+
/**
|
|
13
|
+
* TODO withDevice should be imported statically from @ledgerhq/live-common/hw/deviceAccess
|
|
14
|
+
*
|
|
15
|
+
* but ATM making @ledgerhq/live-common a dependency of @ledgerhq/ledger-key-ring-protocol causes:
|
|
16
|
+
* > Turbo error: Invalid package dependency graph: cyclic dependency detected:
|
|
17
|
+
* > @ledgerhq/ledger-key-ring-protocol,@ledgerhq/live-wallet,@ledgerhq/live-common
|
|
18
|
+
*
|
|
19
|
+
* Maybe hw/deviceAccess.ts and hw/index.ts could be moved to @ledgerhq/devices
|
|
20
|
+
* This would break the cyclic dependency as @ledgerhq/live-common would depend on @ledgerhq/devices
|
|
21
|
+
* but not the other way around.
|
|
22
|
+
*/
|
|
23
|
+
private withDevice: WithDevice;
|
|
24
|
+
private jwt?: JWT;
|
|
25
|
+
private api: ReturnType<typeof getApi>;
|
|
26
|
+
|
|
27
|
+
constructor(apiBaseURL: string, withDevice: WithDevice) {
|
|
28
|
+
this.api = getApi(apiBaseURL);
|
|
29
|
+
this.withDevice = withDevice;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public withJwt<T>(
|
|
33
|
+
deviceId: string,
|
|
34
|
+
job: (jwt: JWT) => Promise<T>,
|
|
35
|
+
policy?: AuthCachePolicy,
|
|
36
|
+
callbacks?: TrustchainDeviceCallbacks,
|
|
37
|
+
): Promise<T> {
|
|
38
|
+
return genericWithJWT(
|
|
39
|
+
jwt => {
|
|
40
|
+
this.jwt = jwt;
|
|
41
|
+
return job(jwt);
|
|
42
|
+
},
|
|
43
|
+
this.jwt,
|
|
44
|
+
() => this._authWithDevice(deviceId, callbacks),
|
|
45
|
+
(jwt: JWT) => this.api.refreshAuth(jwt),
|
|
46
|
+
policy,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async withHw<T>(
|
|
51
|
+
deviceId: string,
|
|
52
|
+
job: (hw: ApduDevice) => Promise<T>,
|
|
53
|
+
callbacks?: TrustchainDeviceCallbacks,
|
|
54
|
+
): Promise<T> {
|
|
55
|
+
callbacks?.onStartRequestUserInteraction?.();
|
|
56
|
+
const runWithDevice = this.withDevice(deviceId);
|
|
57
|
+
try {
|
|
58
|
+
return await lastValueFrom(runWithDevice(transport => from(job(device.apdu(transport)))));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!(error instanceof TransportStatusError)) {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
switch (error.statusCode) {
|
|
64
|
+
case StatusCodes.USER_REFUSED_ON_DEVICE:
|
|
65
|
+
case StatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED:
|
|
66
|
+
throw new UserRefusedOnDevice();
|
|
67
|
+
|
|
68
|
+
case StatusCodes.TRUSTCHAIN_WRONG_SEED:
|
|
69
|
+
this.clearJwt();
|
|
70
|
+
throw new TrustchainNotAllowed();
|
|
71
|
+
|
|
72
|
+
default:
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
callbacks?.onEndRequestUserInteraction?.();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public async refreshJwt(deviceId: string, callbacks?: TrustchainDeviceCallbacks): Promise<void> {
|
|
81
|
+
this.jwt = await this.withJwt(deviceId, this.api.refreshAuth, "cache", callbacks);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public clearJwt() {
|
|
85
|
+
this.jwt = undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async _authWithDevice(
|
|
89
|
+
deviceId: string,
|
|
90
|
+
callbacks?: TrustchainDeviceCallbacks,
|
|
91
|
+
): Promise<JWT> {
|
|
92
|
+
const challenge = await this.api.getAuthenticationChallenge();
|
|
93
|
+
const data = crypto.from_hex(challenge.tlv);
|
|
94
|
+
const seedId = await this.withHw(deviceId, hw => hw.getSeedId(data), callbacks);
|
|
95
|
+
const signature = crypto.to_hex(seedId.signature);
|
|
96
|
+
return this.api.postChallengeResponse({
|
|
97
|
+
challenge: challenge.json,
|
|
98
|
+
signature: {
|
|
99
|
+
credential: seedId.pubkeyCredential.toJSON(),
|
|
100
|
+
signature,
|
|
101
|
+
attestation: crypto.to_hex(seedId.attestationResult),
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { TransportReplayer } from "@ledgerhq/hw-transport-mocker/lib/openTransportReplayer";
|
|
4
|
+
import { RecordStore } from "@ledgerhq/hw-transport-mocker";
|
|
5
|
+
import { getEnv, setEnv } from "@ledgerhq/live-env";
|
|
6
|
+
import { ScenarioOptions } from "../../../tests/test-helpers/types";
|
|
7
|
+
import { getSdk } from "../..";
|
|
8
|
+
import { WithDevice } from "../../types";
|
|
9
|
+
|
|
10
|
+
setEnv("MOCK", "true");
|
|
11
|
+
|
|
12
|
+
const nonMockableScenarios = [
|
|
13
|
+
"randomMemberTryToDestroy", // can't simulate seed<>trustchain relationship
|
|
14
|
+
"removeMemberWithTheWrongSeed", // can't simulate seed<>trustchain relationship
|
|
15
|
+
"tokenExpires", // can't simulate token expiration
|
|
16
|
+
"userRefusesAuth", // can't simulate device interaction at the moment
|
|
17
|
+
"userRefusesRemoveMember", // can't simulate device interaction at the moment
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const scenarioFolder = path.join(__dirname, "../../../tests/scenarios");
|
|
21
|
+
fs.readdirSync(scenarioFolder).forEach(file => {
|
|
22
|
+
if (file.endsWith(".ts") && !file.startsWith("_")) {
|
|
23
|
+
const slug = file.slice(0, -3);
|
|
24
|
+
if (nonMockableScenarios.includes(slug)) return;
|
|
25
|
+
const e2eFile = path.join(scenarioFolder, file);
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
27
|
+
const mod = require(e2eFile);
|
|
28
|
+
test(slug, async () => {
|
|
29
|
+
const scenario = mod.scenario;
|
|
30
|
+
const transport = new TransportReplayer(new RecordStore());
|
|
31
|
+
const device = { id: "", transport };
|
|
32
|
+
const withDevice: WithDevice = () => fn => fn(device.transport);
|
|
33
|
+
const options: ScenarioOptions = {
|
|
34
|
+
withDevice,
|
|
35
|
+
sdkForName: name =>
|
|
36
|
+
getSdk(
|
|
37
|
+
!!getEnv("MOCK"),
|
|
38
|
+
{ applicationId: 16, name, apiBaseUrl: getEnv("TRUSTCHAIN_API_STAGING") },
|
|
39
|
+
withDevice,
|
|
40
|
+
),
|
|
41
|
+
pauseRecorder: () => Promise.resolve(), // replayer don't need to pause
|
|
42
|
+
switchDeviceSeed: async () => device, // nothing to actually do, we will continue replaying
|
|
43
|
+
};
|
|
44
|
+
await scenario(device.id, options);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { replayTrustchainSdkTests } from "../../../tests/test-helpers/replayTrustchainSdkTests";
|
|
5
|
+
|
|
6
|
+
const mockFolder = path.join(__dirname, "../../../mocks/scenarios");
|
|
7
|
+
const scenarioFolder = path.join(__dirname, "../../../tests/scenarios");
|
|
8
|
+
|
|
9
|
+
fs.readdirSync(mockFolder).forEach(file => {
|
|
10
|
+
if (!file.endsWith(".json")) return;
|
|
11
|
+
|
|
12
|
+
const slug = file.slice(0, -5);
|
|
13
|
+
const json = require(path.join(mockFolder, file));
|
|
14
|
+
const mod = require(path.join(scenarioFolder, slug + ".ts"));
|
|
15
|
+
|
|
16
|
+
test(slug, async () => {
|
|
17
|
+
const scenario = mod.scenario;
|
|
18
|
+
await replayTrustchainSdkTests(json, scenario);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { DefaultBodyType, http, HttpResponse, PathParams, StrictRequest } from "msw";
|
|
2
|
+
import { setupServer } from "msw/node";
|
|
3
|
+
import {
|
|
4
|
+
CommandBlock,
|
|
5
|
+
crypto,
|
|
6
|
+
Device,
|
|
7
|
+
Permissions,
|
|
8
|
+
SoftwareDevice,
|
|
9
|
+
StreamTree,
|
|
10
|
+
} from "@ledgerhq/hw-ledger-key-ring-protocol";
|
|
11
|
+
import { getEnv } from "@ledgerhq/live-env";
|
|
12
|
+
import { PutCommandsRequest } from "../../api";
|
|
13
|
+
import { HWDeviceProvider } from "../../HWDeviceProvider";
|
|
14
|
+
import { convertLiveCredentialsToKeyPair, SDK } from "../../sdk";
|
|
15
|
+
import { TrustchainResultType } from "../../types";
|
|
16
|
+
|
|
17
|
+
describe("Trustchain SDK", () => {
|
|
18
|
+
// Setup API calls mocks
|
|
19
|
+
const apiMocks = {
|
|
20
|
+
getTrustchainsMock: jest.fn(),
|
|
21
|
+
getTrustchainByIdMock: jest.fn(),
|
|
22
|
+
getChalenge: jest.fn(),
|
|
23
|
+
postAuthenticate: jest.fn(),
|
|
24
|
+
postSeed: jest.fn<object, [StrictRequest<CommandBlock[]>]>(),
|
|
25
|
+
postDerivation: jest.fn<object, [StrictRequest<CommandBlock[]>]>(),
|
|
26
|
+
putCommands: jest.fn<object, [StrictRequest<PutCommandsRequest>]>(),
|
|
27
|
+
};
|
|
28
|
+
const mswServer = setupServer(
|
|
29
|
+
http.get("*/v1/trustchains", () => HttpResponse.json(apiMocks.getTrustchainsMock())),
|
|
30
|
+
http.get("*/v1/trustchain/ROOTID", () => HttpResponse.json(apiMocks.getTrustchainByIdMock())),
|
|
31
|
+
http.get("*/v1/challenge", () => HttpResponse.json(apiMocks.getChalenge())),
|
|
32
|
+
http.post("*/v1/authenticate", () => HttpResponse.json(apiMocks.postAuthenticate())),
|
|
33
|
+
http.post<PathParams, CommandBlock[]>("*/v1/seed", ({ request }) =>
|
|
34
|
+
HttpResponse.json(apiMocks.postSeed(request)),
|
|
35
|
+
),
|
|
36
|
+
http.post<PathParams, CommandBlock[]>("*/v1/trustchain/ROOTID/derivation", ({ request }) =>
|
|
37
|
+
HttpResponse.json(apiMocks.postDerivation(request)),
|
|
38
|
+
),
|
|
39
|
+
http.put<PathParams, PutCommandsRequest>("*/v1/trustchain/ROOTID/commands", ({ request }) =>
|
|
40
|
+
HttpResponse.json(apiMocks.putCommands(request)),
|
|
41
|
+
),
|
|
42
|
+
http.all("*", () => HttpResponse.json({})),
|
|
43
|
+
);
|
|
44
|
+
mswServer.listen();
|
|
45
|
+
|
|
46
|
+
// Setup APDU device interactions mocks
|
|
47
|
+
const HWDeviceProviderMethodsMocks = {
|
|
48
|
+
withJwt: jest.fn(),
|
|
49
|
+
withHw: jest.fn(),
|
|
50
|
+
refreshJwt: jest.fn(),
|
|
51
|
+
clearJwt: jest.fn(),
|
|
52
|
+
} satisfies Partial<HWDeviceProvider>;
|
|
53
|
+
const hwDeviceProviderMock = HWDeviceProviderMethodsMocks as unknown as HWDeviceProvider;
|
|
54
|
+
|
|
55
|
+
afterAll(() => {
|
|
56
|
+
mswServer.close();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const apiBaseUrl = getEnv("TRUSTCHAIN_API_STAGING");
|
|
60
|
+
const sdkContext = { applicationId: 16, name: "alice", apiBaseUrl };
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
Object.values(apiMocks).forEach(mock => mock.mockClear());
|
|
64
|
+
Object.values(HWDeviceProviderMethodsMocks).forEach(mock => mock.mockClear());
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("encryptUserData + decryptUserData", async () => {
|
|
68
|
+
const sdk = new SDK(sdkContext, hwDeviceProviderMock);
|
|
69
|
+
const obj = new Uint8Array([1, 2, 3, 4, 5]);
|
|
70
|
+
const keypair = await crypto.randomKeypair();
|
|
71
|
+
const trustchain = {
|
|
72
|
+
rootId: "",
|
|
73
|
+
walletSyncEncryptionKey: crypto.to_hex(keypair.privateKey),
|
|
74
|
+
applicationPath: "m/0'/16'/0'",
|
|
75
|
+
};
|
|
76
|
+
const encrypted = await sdk.encryptUserData(trustchain, obj);
|
|
77
|
+
const decrypted = await sdk.decryptUserData(trustchain, encrypted);
|
|
78
|
+
expect(decrypted).toEqual(obj);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should create Trustchain", async () => {
|
|
82
|
+
const { alice } = MOCK_DATA.members;
|
|
83
|
+
|
|
84
|
+
// Mock trustchain states:
|
|
85
|
+
const device = new SoftwareDevice(convertLiveCredentialsToKeyPair(alice));
|
|
86
|
+
const initialTree = await createTrustChain(device);
|
|
87
|
+
const oneMemberTree = await addMember(device, "m/0'/16'/0'", "alice")(initialTree);
|
|
88
|
+
|
|
89
|
+
// Mock API calls:
|
|
90
|
+
apiMocks.getTrustchainsMock.mockReturnValueOnce({});
|
|
91
|
+
apiMocks.getTrustchainsMock.mockReturnValueOnce({ ROOTID: initialTree.serialize() });
|
|
92
|
+
apiMocks.getTrustchainByIdMock.mockReturnValue(initialTree.serialize());
|
|
93
|
+
|
|
94
|
+
// Mock APDU device interactions:
|
|
95
|
+
HWDeviceProviderMethodsMocks.withJwt.mockImplementation(async (deviceId, job) =>
|
|
96
|
+
job({ accessToken: "ACCESS TOKEN" }),
|
|
97
|
+
);
|
|
98
|
+
HWDeviceProviderMethodsMocks.withHw.mockResolvedValueOnce(initialTree);
|
|
99
|
+
HWDeviceProviderMethodsMocks.withHw.mockResolvedValueOnce(oneMemberTree);
|
|
100
|
+
|
|
101
|
+
// Run the test:
|
|
102
|
+
const sdk = new SDK(sdkContext, hwDeviceProviderMock);
|
|
103
|
+
const { type, trustchain } = await sdk.getOrCreateTrustchain("", alice);
|
|
104
|
+
|
|
105
|
+
// Check expectations:
|
|
106
|
+
expect(type).toBe(TrustchainResultType.created);
|
|
107
|
+
expect(trustchain).toEqual({
|
|
108
|
+
applicationPath: "m/0'/16'/0'",
|
|
109
|
+
rootId: "ROOTID",
|
|
110
|
+
walletSyncEncryptionKey: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(await jsonRequestContent(apiMocks.postSeed)).toEqual([oneMemberTree.serialize()["m/"]]);
|
|
114
|
+
expect(await jsonRequestContent(apiMocks.postDerivation)).toEqual([
|
|
115
|
+
oneMemberTree.serialize()["m/0'/16'/0'"],
|
|
116
|
+
]);
|
|
117
|
+
expect(apiMocks.putCommands).not.toHaveBeenCalled();
|
|
118
|
+
|
|
119
|
+
expect(HWDeviceProviderMethodsMocks.withJwt).toHaveBeenCalled();
|
|
120
|
+
expect(HWDeviceProviderMethodsMocks.withHw).toHaveBeenCalledTimes(2);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should remove a member from the Trustchain", async () => {
|
|
124
|
+
const { alice, bob, charlie } = MOCK_DATA.members;
|
|
125
|
+
|
|
126
|
+
// Mock trustchain states:
|
|
127
|
+
const device = new SoftwareDevice(convertLiveCredentialsToKeyPair(alice));
|
|
128
|
+
const closedStreamTree = await createTrustChain(device)
|
|
129
|
+
.then(addMember(device, "m/0'/16'/0'", "alice"))
|
|
130
|
+
.then(addMember(device, "m/0'/16'/0'", "bob"))
|
|
131
|
+
.then(addMember(device, "m/0'/16'/0'", "charlie"))
|
|
132
|
+
.then(tree => tree.close("m/0'/16'/0'", device));
|
|
133
|
+
const rmMembersTree = await addMember(device, "m/0'/16'/1'", "alice")(closedStreamTree);
|
|
134
|
+
|
|
135
|
+
// Mock API calls:
|
|
136
|
+
apiMocks.getTrustchainByIdMock.mockReturnValue(closedStreamTree.serialize());
|
|
137
|
+
apiMocks.getChalenge.mockReturnValue({ json: {}, tlv: MOCK_DATA.challengeTlv });
|
|
138
|
+
apiMocks.postAuthenticate.mockReturnValue({
|
|
139
|
+
accessToken: "BACKEND JWT",
|
|
140
|
+
permissions: { ROOTID: { "m/0'/16'/1'": ["owner"] } },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Mock APDU device interactions:
|
|
144
|
+
HWDeviceProviderMethodsMocks.withJwt.mockImplementation(async (deviceId, job) =>
|
|
145
|
+
job({ accessToken: "ACCESS TOKEN" }),
|
|
146
|
+
);
|
|
147
|
+
HWDeviceProviderMethodsMocks.withHw.mockResolvedValueOnce(closedStreamTree);
|
|
148
|
+
HWDeviceProviderMethodsMocks.withHw.mockResolvedValueOnce(rmMembersTree);
|
|
149
|
+
|
|
150
|
+
// Mock the lifecycle callback:
|
|
151
|
+
const afterRotation = jest.fn();
|
|
152
|
+
const onTrustchainRotation = jest.fn();
|
|
153
|
+
onTrustchainRotation.mockResolvedValueOnce(afterRotation);
|
|
154
|
+
|
|
155
|
+
// Run the test:
|
|
156
|
+
const sdk = new SDK(sdkContext, hwDeviceProviderMock, { onTrustchainRotation });
|
|
157
|
+
|
|
158
|
+
const trustchain = {
|
|
159
|
+
applicationPath: "m/0'/16'/0'",
|
|
160
|
+
rootId: "ROOTID",
|
|
161
|
+
walletSyncEncryptionKey: "",
|
|
162
|
+
};
|
|
163
|
+
const memberToRemove = {
|
|
164
|
+
name: "bob",
|
|
165
|
+
id: bob.pubkey,
|
|
166
|
+
permissions: Permissions.OWNER,
|
|
167
|
+
};
|
|
168
|
+
const newTrustchain = await sdk.removeMember("", trustchain, alice, memberToRemove);
|
|
169
|
+
|
|
170
|
+
// Check expectations:
|
|
171
|
+
expect(newTrustchain).toEqual({
|
|
172
|
+
applicationPath: "m/0'/16'/1'",
|
|
173
|
+
rootId: "ROOTID",
|
|
174
|
+
walletSyncEncryptionKey: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(await jsonRequestContent(apiMocks.postDerivation)).toEqual([
|
|
178
|
+
rmMembersTree.serialize()["m/0'/16'/1'"],
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
const putCommands = await jsonRequestContent(apiMocks.putCommands);
|
|
182
|
+
const closeBlock = putCommands.find(_ => _.path === "m/0'/16'/0'")?.blocks[0] ?? "";
|
|
183
|
+
const pushMemberBlock = putCommands.find(_ => _.path === "m/0'/16'/1'")?.blocks[0] ?? "";
|
|
184
|
+
expect(putCommands).toEqual([
|
|
185
|
+
{ path: "m/0'/16'/1'", blocks: [pushMemberBlock] },
|
|
186
|
+
{ path: "m/0'/16'/0'", blocks: [closeBlock] }, // The closed stream command is sent last
|
|
187
|
+
]);
|
|
188
|
+
expect(closeBlock).toBe(closedStreamTree.serialize()["m/0'/16'/0'"].slice(-closeBlock.length));
|
|
189
|
+
expect(pushMemberBlock).not.toContain(bob.pubkey);
|
|
190
|
+
expect(pushMemberBlock).toContain(charlie.pubkey);
|
|
191
|
+
|
|
192
|
+
expect(HWDeviceProviderMethodsMocks.withJwt).toHaveBeenCalled();
|
|
193
|
+
expect(HWDeviceProviderMethodsMocks.withHw).toHaveBeenCalledTimes(2);
|
|
194
|
+
expect(HWDeviceProviderMethodsMocks.refreshJwt).toHaveBeenCalledTimes(1);
|
|
195
|
+
|
|
196
|
+
expect(onTrustchainRotation).toHaveBeenCalledWith(sdk, trustchain, alice);
|
|
197
|
+
expect(afterRotation).toHaveBeenCalledWith(newTrustchain);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const MOCK_DATA = {
|
|
202
|
+
members: {
|
|
203
|
+
alice: {
|
|
204
|
+
pubkey: "02e3311a12c450604725f02d1a775ef5cdb4a1b832eb41ac6b1302adbe92a612fc",
|
|
205
|
+
privatekey: "873f500bd20783224f7e78d4f8cce3d2bf69eb8008fbd697d20bbea31a721a03",
|
|
206
|
+
},
|
|
207
|
+
bob: {
|
|
208
|
+
pubkey: "034ac6813695b0d5e033a2a19061c83951e2241aad62ec8fa347a944831c07ea82",
|
|
209
|
+
},
|
|
210
|
+
charlie: {
|
|
211
|
+
pubkey: "03b4165cddf39e58f3a89682fdf1ccba213167084fecdbb3b86669d40d201df1cf",
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
challengeTlv:
|
|
216
|
+
"010107020100121053801a35c2e24b627d6e4925ce318980140101154630440220319b42a416512437e48d9c9bf204daea7da03d452c50a8caa4c2d152407ffd0c02201f121b0e99df1d30f4757b6a00b8d974d70996771893ac49c4a245c147cc1d8f160466a90248202b7472757374636861696e2d6261636b656e642e6170692e6177732e7374672e6c64672d746563682e636f6d320121332103cb7628e7248ddf9c07da54b979f16bf081fb3d173aac0992ad2a44ef6a388ae2600401000000",
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
function createTrustChain(device: Device): Promise<StreamTree> {
|
|
220
|
+
return StreamTree.createNewTree(device);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function addMember(
|
|
224
|
+
device: Device,
|
|
225
|
+
path: string,
|
|
226
|
+
name: keyof typeof MOCK_DATA.members,
|
|
227
|
+
): (tree: StreamTree) => Promise<StreamTree> {
|
|
228
|
+
const memberId = crypto.from_hex(MOCK_DATA.members[name].pubkey);
|
|
229
|
+
return (tree: StreamTree) => tree.share(path, device, memberId, name, Permissions.OWNER);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function jsonRequestContent<T extends DefaultBodyType>(
|
|
233
|
+
mock: jest.Mock<object, [StrictRequest<T>]>,
|
|
234
|
+
): Promise<T[]> {
|
|
235
|
+
return Promise.all(mock.mock.calls.map(([request]) => request.json()));
|
|
236
|
+
}
|