@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/src/sdk.ts
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JWT,
|
|
3
|
+
MemberCredentials,
|
|
4
|
+
TrustchainSDKContext,
|
|
5
|
+
Trustchain,
|
|
6
|
+
TrustchainMember,
|
|
7
|
+
TrustchainSDK,
|
|
8
|
+
TrustchainDeviceCallbacks,
|
|
9
|
+
AuthCachePolicy,
|
|
10
|
+
TrustchainResult,
|
|
11
|
+
TrustchainResultType,
|
|
12
|
+
TrustchainLifecycle,
|
|
13
|
+
GetOrCreateTrustchainCallbacks,
|
|
14
|
+
} from "./types";
|
|
15
|
+
import {
|
|
16
|
+
crypto,
|
|
17
|
+
Challenge,
|
|
18
|
+
CommandStreamEncoder,
|
|
19
|
+
StreamTree,
|
|
20
|
+
Permissions,
|
|
21
|
+
DerivationPath,
|
|
22
|
+
SoftwareDevice,
|
|
23
|
+
Device,
|
|
24
|
+
} from "@ledgerhq/hw-ledger-key-ring-protocol";
|
|
25
|
+
import getApi from "./api";
|
|
26
|
+
import { KeyPair as CryptoKeyPair } from "@ledgerhq/hw-ledger-key-ring-protocol/Crypto";
|
|
27
|
+
import { log } from "@ledgerhq/logs";
|
|
28
|
+
import { LedgerAPI4xx } from "@ledgerhq/errors";
|
|
29
|
+
import {
|
|
30
|
+
TrustchainAlreadyInitialized,
|
|
31
|
+
TrustchainAlreadyInitializedWithOtherSeed,
|
|
32
|
+
TrustchainEjected,
|
|
33
|
+
TrustchainNotAllowed,
|
|
34
|
+
TrustchainOutdated,
|
|
35
|
+
} from "./errors";
|
|
36
|
+
import { HWDeviceProvider } from "./HWDeviceProvider";
|
|
37
|
+
import { genericWithJWT } from "./auth";
|
|
38
|
+
|
|
39
|
+
type WithJwt = <T>(job: (jwt: JWT) => Promise<T>) => Promise<T>;
|
|
40
|
+
type WithDevice = <T>(job: (device: Device) => Promise<T>) => Promise<T>;
|
|
41
|
+
|
|
42
|
+
export class SDK implements TrustchainSDK {
|
|
43
|
+
private context: TrustchainSDKContext;
|
|
44
|
+
private hwDeviceProvider: HWDeviceProvider;
|
|
45
|
+
private lifecycle?: TrustchainLifecycle;
|
|
46
|
+
private api: ReturnType<typeof getApi>;
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
context: TrustchainSDKContext,
|
|
50
|
+
hwDeviceProvider: HWDeviceProvider,
|
|
51
|
+
lifecyle?: TrustchainLifecycle,
|
|
52
|
+
) {
|
|
53
|
+
this.context = context;
|
|
54
|
+
this.hwDeviceProvider = hwDeviceProvider;
|
|
55
|
+
this.lifecycle = lifecyle;
|
|
56
|
+
this.api = getApi(context.apiBaseUrl);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private jwt: JWT | undefined = undefined;
|
|
60
|
+
private jwtHash = "";
|
|
61
|
+
withAuth<T>(
|
|
62
|
+
trustchain: Trustchain,
|
|
63
|
+
memberCredentials: MemberCredentials,
|
|
64
|
+
job: (jwt: JWT) => Promise<T>,
|
|
65
|
+
policy?: AuthCachePolicy,
|
|
66
|
+
ignorePermissionsChecks?: boolean,
|
|
67
|
+
): Promise<T> {
|
|
68
|
+
const hash = trustchain.rootId + " " + memberCredentials.pubkey;
|
|
69
|
+
if (this.jwtHash !== hash) {
|
|
70
|
+
this.jwt = undefined;
|
|
71
|
+
this.jwtHash = hash;
|
|
72
|
+
}
|
|
73
|
+
return genericWithJWT(
|
|
74
|
+
jwt => {
|
|
75
|
+
this.jwt = jwt;
|
|
76
|
+
if (!ignorePermissionsChecks) {
|
|
77
|
+
const permissions = jwt.permissions[trustchain.rootId];
|
|
78
|
+
if (!permissions) {
|
|
79
|
+
throw new TrustchainNotAllowed("permissions not available for current trustchain");
|
|
80
|
+
}
|
|
81
|
+
// check if the application path is allowed
|
|
82
|
+
if (!permissions[trustchain.applicationPath]) {
|
|
83
|
+
throw new TrustchainOutdated(
|
|
84
|
+
`expected ${trustchain.applicationPath}, got: ${Object.keys(permissions)[0]}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return job(jwt);
|
|
89
|
+
},
|
|
90
|
+
this.jwt,
|
|
91
|
+
() => this.auth(trustchain, memberCredentials),
|
|
92
|
+
jwt => this.api.refreshAuth(jwt),
|
|
93
|
+
policy,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async initMemberCredentials(): Promise<MemberCredentials> {
|
|
98
|
+
const kp = await crypto.randomKeypair();
|
|
99
|
+
return convertKeyPairToLiveCredentials(kp);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getOrCreateTrustchain(
|
|
103
|
+
deviceId: string,
|
|
104
|
+
memberCredentials: MemberCredentials,
|
|
105
|
+
callbacks?: GetOrCreateTrustchainCallbacks,
|
|
106
|
+
topic?: Uint8Array,
|
|
107
|
+
currentTrustchain?: Trustchain,
|
|
108
|
+
): Promise<TrustchainResult> {
|
|
109
|
+
this.invalidateJwt();
|
|
110
|
+
|
|
111
|
+
let type = TrustchainResultType.restored;
|
|
112
|
+
|
|
113
|
+
const withJwt: WithJwt = job =>
|
|
114
|
+
this.hwDeviceProvider.withJwt(deviceId, job, "cache", callbacks);
|
|
115
|
+
const withHw: WithDevice = job => this.hwDeviceProvider.withHw(deviceId, job, callbacks);
|
|
116
|
+
|
|
117
|
+
let trustchains = await withJwt(this.api.getTrustchains);
|
|
118
|
+
|
|
119
|
+
callbacks?.onInitialResponse?.(trustchains);
|
|
120
|
+
|
|
121
|
+
if (currentTrustchain) {
|
|
122
|
+
await this.restoreTrustchain(currentTrustchain, memberCredentials).then(
|
|
123
|
+
() => {
|
|
124
|
+
throw Object.keys(trustchains).includes(currentTrustchain.rootId)
|
|
125
|
+
? new TrustchainAlreadyInitialized()
|
|
126
|
+
: new TrustchainAlreadyInitializedWithOtherSeed();
|
|
127
|
+
},
|
|
128
|
+
() => {
|
|
129
|
+
// The user was ejected from the trustchain therefore we can continue
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Object.keys(trustchains).length === 0) {
|
|
135
|
+
log("trustchain", "getOrCreateTrustchain: no trustchain yet, let's create one");
|
|
136
|
+
type = TrustchainResultType.created;
|
|
137
|
+
const streamTree = await this.hwDeviceProvider.withHw(deviceId, hw =>
|
|
138
|
+
StreamTree.createNewTree(hw, { topic }),
|
|
139
|
+
);
|
|
140
|
+
await streamTree.getRoot().resolve(); // double checks the signatures are correct before sending to the backend
|
|
141
|
+
const commandStream = CommandStreamEncoder.encode(streamTree.getRoot().blocks);
|
|
142
|
+
await withJwt(jwt => this.api.postSeed(jwt, crypto.to_hex(commandStream)));
|
|
143
|
+
// deviceJwt have changed, proactively refresh it
|
|
144
|
+
await this.hwDeviceProvider.refreshJwt(deviceId, callbacks);
|
|
145
|
+
trustchains = await withJwt(this.api.getTrustchains);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// we find our trustchain root id
|
|
149
|
+
let trustchainRootId: string | undefined;
|
|
150
|
+
const trustchainRootPath = "m/";
|
|
151
|
+
for (const [trustchainId, info] of Object.entries(trustchains)) {
|
|
152
|
+
for (const path in info) {
|
|
153
|
+
if (path === trustchainRootPath) {
|
|
154
|
+
trustchainRootId = trustchainId;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
invariant(trustchainRootId, "trustchainRootId should be defined");
|
|
160
|
+
log("trustchain", "getOrCreateTrustchain rootId=" + trustchainRootId);
|
|
161
|
+
|
|
162
|
+
// make a stream tree from all the trustchains associated to this root id
|
|
163
|
+
let { streamTree } = await withJwt(jwt => this.fetchTrustchain(jwt, trustchainRootId));
|
|
164
|
+
const path = streamTree.getApplicationRootPath(this.context.applicationId);
|
|
165
|
+
const child = streamTree.getChild(path);
|
|
166
|
+
let shouldShare = true;
|
|
167
|
+
|
|
168
|
+
if (child) {
|
|
169
|
+
const resolved = await child.resolve();
|
|
170
|
+
const members = resolved.getMembers();
|
|
171
|
+
|
|
172
|
+
shouldShare = !members.some(m => crypto.to_hex(m) === memberCredentials.pubkey); // not already a member
|
|
173
|
+
}
|
|
174
|
+
if (shouldShare) {
|
|
175
|
+
if (type === TrustchainResultType.restored) type = TrustchainResultType.updated;
|
|
176
|
+
streamTree = await this.pushMember(streamTree, path, trustchainRootId, withJwt, withHw, {
|
|
177
|
+
id: memberCredentials.pubkey,
|
|
178
|
+
name: this.context.name,
|
|
179
|
+
permissions: Permissions.OWNER,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const walletSyncEncryptionKey = await extractEncryptionKey(streamTree, path, memberCredentials);
|
|
184
|
+
|
|
185
|
+
const trustchain = {
|
|
186
|
+
rootId: trustchainRootId,
|
|
187
|
+
walletSyncEncryptionKey,
|
|
188
|
+
applicationPath: path,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return { type, trustchain };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async restoreTrustchain(
|
|
195
|
+
trustchain: Trustchain,
|
|
196
|
+
memberCredentials: MemberCredentials,
|
|
197
|
+
): Promise<Trustchain> {
|
|
198
|
+
const { streamTree, applicationRootPath } = await this.withAuth(
|
|
199
|
+
trustchain,
|
|
200
|
+
memberCredentials,
|
|
201
|
+
jwt => this.fetchTrustchainAndResolve(jwt, trustchain.rootId, this.context.applicationId),
|
|
202
|
+
"refresh",
|
|
203
|
+
true,
|
|
204
|
+
);
|
|
205
|
+
const walletSyncEncryptionKey = await extractEncryptionKey(
|
|
206
|
+
streamTree,
|
|
207
|
+
applicationRootPath,
|
|
208
|
+
memberCredentials,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
rootId: trustchain.rootId,
|
|
213
|
+
walletSyncEncryptionKey,
|
|
214
|
+
applicationPath: applicationRootPath,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async getMembers(
|
|
219
|
+
trustchain: Trustchain,
|
|
220
|
+
memberCredentials: MemberCredentials,
|
|
221
|
+
): Promise<TrustchainMember[]> {
|
|
222
|
+
const { resolved } = await this.withAuth(trustchain, memberCredentials, jwt =>
|
|
223
|
+
this.fetchTrustchainAndResolve(jwt, trustchain.rootId, this.context.applicationId),
|
|
224
|
+
);
|
|
225
|
+
const members = resolved.getMembersData();
|
|
226
|
+
if (!members.some(m => m.id === memberCredentials.pubkey)) {
|
|
227
|
+
throw new TrustchainEjected("Not a member of trustchain");
|
|
228
|
+
}
|
|
229
|
+
return members;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async removeMember(
|
|
233
|
+
deviceId: string,
|
|
234
|
+
trustchain: Trustchain,
|
|
235
|
+
memberCredentials: MemberCredentials,
|
|
236
|
+
member: TrustchainMember,
|
|
237
|
+
callbacks?: TrustchainDeviceCallbacks,
|
|
238
|
+
): Promise<Trustchain> {
|
|
239
|
+
this.invalidateJwt();
|
|
240
|
+
|
|
241
|
+
const withJwt: WithJwt = job =>
|
|
242
|
+
this.hwDeviceProvider.withJwt(deviceId, job, "cache", callbacks);
|
|
243
|
+
const withHw: WithDevice = job => this.hwDeviceProvider.withHw(deviceId, job, callbacks);
|
|
244
|
+
|
|
245
|
+
// invariant because the sdk does not support this case, and the UI should not allows it.
|
|
246
|
+
invariant(
|
|
247
|
+
memberCredentials.pubkey !== member.id,
|
|
248
|
+
"removeMember must not be used to remove the current member.",
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const afterRotation = await this.lifecycle?.onTrustchainRotation(
|
|
252
|
+
this,
|
|
253
|
+
trustchain,
|
|
254
|
+
memberCredentials,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const applicationId = this.context.applicationId;
|
|
258
|
+
const trustchainId = trustchain.rootId;
|
|
259
|
+
// eslint-disable-next-line prefer-const
|
|
260
|
+
let { resolved, streamTree, applicationRootPath } = await withJwt(jwt =>
|
|
261
|
+
this.fetchTrustchainAndResolve(jwt, trustchainId, applicationId),
|
|
262
|
+
);
|
|
263
|
+
const members = resolved.getMembersData();
|
|
264
|
+
const withoutMember = members.filter(m => m.id !== member.id);
|
|
265
|
+
invariant(withoutMember.length < members.length, "member not found"); // invariant because the UI should not allow this case.
|
|
266
|
+
const withoutMemberOrMe = withoutMember.filter(m => m.id !== memberCredentials.pubkey);
|
|
267
|
+
const softwareDevice = getSoftwareDevice(memberCredentials);
|
|
268
|
+
|
|
269
|
+
const newPath = streamTree.getApplicationRootPath(applicationId, 1);
|
|
270
|
+
|
|
271
|
+
// We close the current trustchain with the hardware wallet in order to get a user confirmation of the action
|
|
272
|
+
const sendCloseStreamToAPI = await this.closeStream(
|
|
273
|
+
streamTree,
|
|
274
|
+
applicationRootPath,
|
|
275
|
+
trustchainId,
|
|
276
|
+
withJwt,
|
|
277
|
+
withHw,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// derive a new branch of the tree on the new path
|
|
281
|
+
streamTree = await this.pushMember(streamTree, newPath, trustchainId, withJwt, withHw, {
|
|
282
|
+
id: memberCredentials.pubkey,
|
|
283
|
+
name: this.context.name,
|
|
284
|
+
permissions: Permissions.OWNER,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// add the remaining members
|
|
288
|
+
const withSw = (job: (device: Device) => Promise<StreamTree>) => job(softwareDevice);
|
|
289
|
+
for (const m of withoutMemberOrMe) {
|
|
290
|
+
streamTree = await this.pushMember(streamTree, newPath, trustchainId, withJwt, withSw, m);
|
|
291
|
+
}
|
|
292
|
+
const walletSyncEncryptionKey = await extractEncryptionKey(
|
|
293
|
+
streamTree,
|
|
294
|
+
newPath,
|
|
295
|
+
memberCredentials,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// we send the close stream to the API only after the new stream is created in case user cancelled the process in the middle.
|
|
299
|
+
await sendCloseStreamToAPI();
|
|
300
|
+
|
|
301
|
+
// deviceJwt have changed, proactively refresh it
|
|
302
|
+
await this.hwDeviceProvider.refreshJwt(deviceId, callbacks);
|
|
303
|
+
|
|
304
|
+
const newTrustchain: Trustchain = {
|
|
305
|
+
rootId: trustchainId,
|
|
306
|
+
walletSyncEncryptionKey,
|
|
307
|
+
applicationPath: newPath,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
if (afterRotation) await afterRotation(newTrustchain);
|
|
311
|
+
|
|
312
|
+
// refresh the jwt with the new trustchain
|
|
313
|
+
this.jwt = await this.withAuth(
|
|
314
|
+
newTrustchain,
|
|
315
|
+
memberCredentials,
|
|
316
|
+
jwt => Promise.resolve(jwt),
|
|
317
|
+
"refresh",
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
return newTrustchain;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async addMember(
|
|
324
|
+
trustchain: Trustchain,
|
|
325
|
+
memberCredentials: MemberCredentials,
|
|
326
|
+
member: TrustchainMember,
|
|
327
|
+
): Promise<void> {
|
|
328
|
+
const withJwt: WithJwt = f => this.withAuth(trustchain, memberCredentials, f);
|
|
329
|
+
const { streamTree, applicationRootPath } = await withJwt(jwt =>
|
|
330
|
+
this.fetchTrustchainAndResolve(jwt, trustchain.rootId, this.context.applicationId),
|
|
331
|
+
);
|
|
332
|
+
const softwareDevice = getSoftwareDevice(memberCredentials);
|
|
333
|
+
const withSw = (job: (device: Device) => Promise<StreamTree>) => job(softwareDevice);
|
|
334
|
+
await this.pushMember(
|
|
335
|
+
streamTree,
|
|
336
|
+
applicationRootPath,
|
|
337
|
+
trustchain.rootId,
|
|
338
|
+
withJwt,
|
|
339
|
+
withSw,
|
|
340
|
+
member,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
invalidateJwt() {
|
|
345
|
+
this.jwt = undefined;
|
|
346
|
+
this.hwDeviceProvider.clearJwt();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async destroyTrustchain(
|
|
350
|
+
trustchain: Trustchain,
|
|
351
|
+
memberCredentials: MemberCredentials,
|
|
352
|
+
): Promise<void> {
|
|
353
|
+
await this.withAuth(trustchain, memberCredentials, jwt =>
|
|
354
|
+
this.api.deleteTrustchain(jwt, trustchain.rootId),
|
|
355
|
+
);
|
|
356
|
+
this.invalidateJwt();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async encryptUserData(trustchain: Trustchain, input: Uint8Array): Promise<Uint8Array> {
|
|
360
|
+
const key = crypto.from_hex(trustchain.walletSyncEncryptionKey);
|
|
361
|
+
const encrypted = await crypto.encryptUserData(key, input);
|
|
362
|
+
return encrypted;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async decryptUserData(trustchain: Trustchain, data: Uint8Array): Promise<Uint8Array> {
|
|
366
|
+
const key = crypto.from_hex(trustchain.walletSyncEncryptionKey);
|
|
367
|
+
const decrypted = await crypto.decryptUserData(key, data);
|
|
368
|
+
return decrypted;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private async fetchTrustchain(jwt: JWT, trustchainId: string) {
|
|
372
|
+
const trustchainData = await this.api.getTrustchain(jwt, trustchainId);
|
|
373
|
+
const streamTree = StreamTree.deserialize(trustchainData);
|
|
374
|
+
return { streamTree };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private async fetchTrustchainAndResolve(jwt: JWT, trustchainId: string, applicationId: number) {
|
|
378
|
+
const { streamTree } = await this.fetchTrustchain(jwt, trustchainId);
|
|
379
|
+
const applicationRootPath = streamTree.getApplicationRootPath(applicationId);
|
|
380
|
+
const applicationNode = streamTree.getChild(applicationRootPath);
|
|
381
|
+
invariant(applicationNode, "could not find the application stream.");
|
|
382
|
+
const resolved = await applicationNode.resolve();
|
|
383
|
+
return { resolved, streamTree, applicationRootPath, applicationNode };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private async auth(trustchain: Trustchain, memberCredentials: MemberCredentials): Promise<JWT> {
|
|
387
|
+
const challenge = await this.api.getAuthenticationChallenge();
|
|
388
|
+
const data = crypto.from_hex(challenge.tlv);
|
|
389
|
+
const [parsed, _] = Challenge.fromBytes(data);
|
|
390
|
+
const hash = await crypto.hash(parsed.getUnsignedTLV());
|
|
391
|
+
const keypair = convertLiveCredentialsToKeyPair(memberCredentials);
|
|
392
|
+
const response = await this.api
|
|
393
|
+
.postChallengeResponse({
|
|
394
|
+
challenge: challenge.json,
|
|
395
|
+
signature: {
|
|
396
|
+
credential: credentialForPubKey(memberCredentials.pubkey),
|
|
397
|
+
signature: crypto.to_hex(await crypto.sign(hash, keypair)),
|
|
398
|
+
attestation: crypto.to_hex(liveAuthentication(trustchain.rootId)),
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
.catch(e => {
|
|
402
|
+
if (
|
|
403
|
+
e instanceof LedgerAPI4xx &&
|
|
404
|
+
(e.message.includes("Not a member of trustchain") ||
|
|
405
|
+
e.message.includes("You are not member"))
|
|
406
|
+
) {
|
|
407
|
+
throw new TrustchainEjected(e.message);
|
|
408
|
+
}
|
|
409
|
+
throw e;
|
|
410
|
+
});
|
|
411
|
+
return response;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private async pushMember(
|
|
415
|
+
streamTree: StreamTree,
|
|
416
|
+
path: string,
|
|
417
|
+
trustchainId: string,
|
|
418
|
+
withJwt: WithJwt,
|
|
419
|
+
withDevice: (job: (device: Device) => Promise<StreamTree>) => Promise<StreamTree>,
|
|
420
|
+
member: TrustchainMember,
|
|
421
|
+
) {
|
|
422
|
+
const isMemberAlreadyInStreamTree = await isMemberInStreamTree(streamTree, path, member);
|
|
423
|
+
if (isMemberAlreadyInStreamTree) {
|
|
424
|
+
return streamTree;
|
|
425
|
+
}
|
|
426
|
+
const isNewDerivation = !streamTree.getChild(path);
|
|
427
|
+
streamTree = await withDevice(device =>
|
|
428
|
+
streamTree.share(path, device, crypto.from_hex(member.id), member.name, member.permissions),
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const child = streamTree.getChild(path);
|
|
432
|
+
invariant(child, "StreamTree.share failed to create the child stream.");
|
|
433
|
+
await child.resolve(); // double checks the signatures are correct before sending to the backend
|
|
434
|
+
if (isNewDerivation) {
|
|
435
|
+
const commandStream = CommandStreamEncoder.encode(child.blocks);
|
|
436
|
+
await withJwt(jwt =>
|
|
437
|
+
this.api.postDerivation(jwt, trustchainId, crypto.to_hex(commandStream)),
|
|
438
|
+
);
|
|
439
|
+
} else {
|
|
440
|
+
const commandStream = CommandStreamEncoder.encode([child.blocks[child.blocks.length - 1]]);
|
|
441
|
+
const request = {
|
|
442
|
+
path,
|
|
443
|
+
blocks: [crypto.to_hex(commandStream)],
|
|
444
|
+
};
|
|
445
|
+
await withJwt(jwt => this.api.putCommands(jwt, trustchainId, request));
|
|
446
|
+
}
|
|
447
|
+
return streamTree;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private async closeStream(
|
|
451
|
+
streamTree: StreamTree,
|
|
452
|
+
path: string,
|
|
453
|
+
trustchainId: string,
|
|
454
|
+
withJwt: WithJwt,
|
|
455
|
+
withDevice: (job: (device: Device) => Promise<StreamTree>) => Promise<StreamTree>,
|
|
456
|
+
) {
|
|
457
|
+
streamTree = await withDevice(device => streamTree.close(path, device));
|
|
458
|
+
const child = streamTree.getChild(path);
|
|
459
|
+
invariant(child, "StreamTree.close failed to create the child stream.");
|
|
460
|
+
await child.resolve(); // double checks the signatures are correct before sending to the backend
|
|
461
|
+
const commandStream = CommandStreamEncoder.encode([child.blocks[child.blocks.length - 1]]);
|
|
462
|
+
const request = {
|
|
463
|
+
path,
|
|
464
|
+
blocks: [crypto.to_hex(commandStream)],
|
|
465
|
+
};
|
|
466
|
+
return () => withJwt(jwt => this.api.putCommands(jwt, trustchainId, request));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function convertKeyPairToLiveCredentials(keyPair: CryptoKeyPair): MemberCredentials {
|
|
471
|
+
return {
|
|
472
|
+
pubkey: crypto.to_hex(keyPair.publicKey),
|
|
473
|
+
privatekey: crypto.to_hex(keyPair.privateKey),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function convertLiveCredentialsToKeyPair(
|
|
478
|
+
memberCredentials: MemberCredentials,
|
|
479
|
+
): CryptoKeyPair {
|
|
480
|
+
return {
|
|
481
|
+
publicKey: crypto.from_hex(memberCredentials.pubkey),
|
|
482
|
+
privateKey: crypto.from_hex(memberCredentials.privatekey),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function getSoftwareDevice(memberCredentials: MemberCredentials): SoftwareDevice {
|
|
487
|
+
const kp = convertLiveCredentialsToKeyPair(memberCredentials);
|
|
488
|
+
return new SoftwareDevice(kp);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function extractEncryptionKey(
|
|
492
|
+
streamTree: StreamTree,
|
|
493
|
+
path: string,
|
|
494
|
+
memberCredentials: MemberCredentials,
|
|
495
|
+
): Promise<string> {
|
|
496
|
+
const softwareDevice = getSoftwareDevice(memberCredentials);
|
|
497
|
+
const pathNumbers = DerivationPath.toIndexArray(path);
|
|
498
|
+
try {
|
|
499
|
+
const key = await softwareDevice.readKey(streamTree, pathNumbers);
|
|
500
|
+
// private key is in the first 32 bytes
|
|
501
|
+
return crypto.to_hex(key.slice(0, 32));
|
|
502
|
+
} catch (e) {
|
|
503
|
+
if (e instanceof Error) {
|
|
504
|
+
throw new TrustchainEjected(e.message);
|
|
505
|
+
}
|
|
506
|
+
throw e;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// spec https://ledgerhq.atlassian.net/wiki/spaces/TA/pages/4335960138/ARCH+LedgerLive+Auth+specifications
|
|
511
|
+
function liveAuthentication(rootId: string): Uint8Array {
|
|
512
|
+
const trustchainId = new TextEncoder().encode(rootId);
|
|
513
|
+
const att = new Uint8Array(2 + trustchainId.length);
|
|
514
|
+
att[0] = 0x02; // Prefix tag
|
|
515
|
+
att[1] = trustchainId.length;
|
|
516
|
+
att.set(trustchainId, 2);
|
|
517
|
+
return att;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function credentialForPubKey(publicKey: string) {
|
|
521
|
+
return { version: 0, curveId: 33, signAlgorithm: 1, publicKey };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function invariant(condition: unknown, message: string): asserts condition {
|
|
525
|
+
if (!condition) {
|
|
526
|
+
throw new Error(message);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function isMemberInStreamTree(
|
|
531
|
+
streamTree: StreamTree,
|
|
532
|
+
path: string,
|
|
533
|
+
member: TrustchainMember,
|
|
534
|
+
): Promise<boolean> {
|
|
535
|
+
const child = streamTree.getChild(path);
|
|
536
|
+
if (!child) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
const resolved = await child.resolve();
|
|
540
|
+
const members = resolved.getMembersData();
|
|
541
|
+
return members.some(m => m.id === member.id);
|
|
542
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This exports all the logic related to the Trustchain store.
|
|
3
|
+
* The Trustchain store is a store that contains the data related to trustchain.
|
|
4
|
+
* It essentially is the client's credentials that are only stored on the
|
|
5
|
+
* client side and the trustchain returned by the backend.
|
|
6
|
+
*/
|
|
7
|
+
import { MemberCredentials, Trustchain } from "./types";
|
|
8
|
+
|
|
9
|
+
export type TrustchainStore = {
|
|
10
|
+
trustchain: Trustchain | null;
|
|
11
|
+
memberCredentials: MemberCredentials | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const INITIAL_STATE: TrustchainStore = {
|
|
15
|
+
trustchain: null,
|
|
16
|
+
memberCredentials: null,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getInitialStore = (): TrustchainStore => {
|
|
20
|
+
return INITIAL_STATE;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const trustchainStoreActionTypePrefix = "TRUSTCHAIN_STORE_";
|
|
24
|
+
|
|
25
|
+
export enum TrustchainHandlerType {
|
|
26
|
+
TRUSTCHAIN_STORE_IMPORT_STATE = `${trustchainStoreActionTypePrefix}IMPORT_STATE`,
|
|
27
|
+
TRUSTCHAIN_STORE_RESET = `${trustchainStoreActionTypePrefix}RESET`,
|
|
28
|
+
TRUSTCHAIN_STORE_SET_TRUSTCHAIN = `${trustchainStoreActionTypePrefix}SET_TRUSTCHAIN`,
|
|
29
|
+
TRUSTCHAIN_STORE_SET_MEMBER_CREDENTIALS = `${trustchainStoreActionTypePrefix}SET_MEMBER_CREDENTIALS`,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type TrustchainHandlersPayloads = {
|
|
33
|
+
TRUSTCHAIN_STORE_IMPORT_STATE: { trustchain: TrustchainStore };
|
|
34
|
+
TRUSTCHAIN_STORE_RESET: never;
|
|
35
|
+
TRUSTCHAIN_STORE_SET_TRUSTCHAIN: { trustchain: Trustchain };
|
|
36
|
+
TRUSTCHAIN_STORE_SET_MEMBER_CREDENTIALS: { memberCredentials: MemberCredentials };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type Handlers<State, Types, PreciseKey = true> = {
|
|
40
|
+
[Key in keyof Types]: (
|
|
41
|
+
state: State,
|
|
42
|
+
body: { payload: Types[PreciseKey extends true ? Key : keyof Types] },
|
|
43
|
+
) => State;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type TrustchainHandlers<PreciseKey = true> = Handlers<
|
|
47
|
+
TrustchainStore,
|
|
48
|
+
TrustchainHandlersPayloads,
|
|
49
|
+
PreciseKey
|
|
50
|
+
>;
|
|
51
|
+
|
|
52
|
+
export const trustchainHandlers: TrustchainHandlers = {
|
|
53
|
+
TRUSTCHAIN_STORE_IMPORT_STATE: (_, { payload: { trustchain } }) => {
|
|
54
|
+
return trustchain;
|
|
55
|
+
},
|
|
56
|
+
TRUSTCHAIN_STORE_RESET: (): TrustchainStore => {
|
|
57
|
+
return { ...getInitialStore() };
|
|
58
|
+
},
|
|
59
|
+
TRUSTCHAIN_STORE_SET_TRUSTCHAIN: (state, { payload: { trustchain } }) => {
|
|
60
|
+
return { ...state, trustchain };
|
|
61
|
+
},
|
|
62
|
+
TRUSTCHAIN_STORE_SET_MEMBER_CREDENTIALS: (state, { payload: { memberCredentials } }) => {
|
|
63
|
+
return { ...state, memberCredentials };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// actions
|
|
68
|
+
|
|
69
|
+
export const importTrustchainStoreState = (trustchain: TrustchainStore) => ({
|
|
70
|
+
type: `${trustchainStoreActionTypePrefix}IMPORT_STATE`,
|
|
71
|
+
payload: { trustchain },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const resetTrustchainStore = () => ({
|
|
75
|
+
type: `${trustchainStoreActionTypePrefix}RESET`,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const setTrustchain = (trustchain: Trustchain) => ({
|
|
79
|
+
type: `${trustchainStoreActionTypePrefix}SET_TRUSTCHAIN`,
|
|
80
|
+
payload: { trustchain },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const setMemberCredentials = (memberCredentials: MemberCredentials) => ({
|
|
84
|
+
type: `${trustchainStoreActionTypePrefix}SET_MEMBER_CREDENTIALS`,
|
|
85
|
+
payload: { memberCredentials },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Local Selectors
|
|
89
|
+
// FIXME: these are not actually local Selector, a localSelector takes a TrustchainStore in param. we will need to rework this.
|
|
90
|
+
|
|
91
|
+
export const trustchainStoreSelector = (state: { trustchain: TrustchainStore }): TrustchainStore =>
|
|
92
|
+
state.trustchain;
|
|
93
|
+
|
|
94
|
+
export const trustchainSelector = (state: { trustchain: TrustchainStore }): Trustchain | null =>
|
|
95
|
+
state.trustchain.trustchain;
|
|
96
|
+
|
|
97
|
+
export const memberCredentialsSelector = (state: {
|
|
98
|
+
trustchain: TrustchainStore;
|
|
99
|
+
}): MemberCredentials | null => state.trustchain.memberCredentials;
|