@idos-network/enclave 1.1.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/base-0xSNl4rW.d.mts +140 -0
  4. package/dist/base-0xSNl4rW.d.mts.map +1 -0
  5. package/dist/client-BSkUuHYm.mjs +1196 -0
  6. package/dist/client-BSkUuHYm.mjs.map +1 -0
  7. package/dist/client-Bu0aO5sT.d.mts +38 -0
  8. package/dist/client-Bu0aO5sT.d.mts.map +1 -0
  9. package/dist/index.d.mts +16 -0
  10. package/dist/index.d.mts.map +1 -0
  11. package/dist/index.mjs +2 -0
  12. package/dist/keys-RuC1M-_K.mjs +187 -0
  13. package/dist/keys-RuC1M-_K.mjs.map +1 -0
  14. package/dist/local.d.mts +96 -0
  15. package/dist/local.d.mts.map +1 -0
  16. package/dist/local.mjs +253 -0
  17. package/dist/local.mjs.map +1 -0
  18. package/dist/mpc/index.d.mts +3 -0
  19. package/dist/mpc/index.mjs +2 -0
  20. package/dist/types-DKsOwtWL.d.mts +73 -0
  21. package/dist/types-DKsOwtWL.d.mts.map +1 -0
  22. package/package.json +76 -0
  23. package/src/base.test.ts +137 -0
  24. package/src/base.ts +306 -0
  25. package/src/index.ts +3 -0
  26. package/src/keys.ts +10 -0
  27. package/src/local.test.ts +113 -0
  28. package/src/local.ts +495 -0
  29. package/src/mpc/api.ts +122 -0
  30. package/src/mpc/client.ts +411 -0
  31. package/src/mpc/engine-client.ts +144 -0
  32. package/src/mpc/generated/IdosContract.ts +92 -0
  33. package/src/mpc/index.ts +2 -0
  34. package/src/mpc/secretsharing/f256.ts +122 -0
  35. package/src/mpc/secretsharing/finite-field-element.ts +50 -0
  36. package/src/mpc/secretsharing/lagrange.ts +163 -0
  37. package/src/mpc/secretsharing/polynomial.ts +84 -0
  38. package/src/mpc/secretsharing/secret-share-factory.ts +30 -0
  39. package/src/mpc/secretsharing/secret-shares.ts +20 -0
  40. package/src/mpc/secretsharing/shamir-secret-shares.ts +164 -0
  41. package/src/mpc/secretsharing/xor-secret-shares.ts +68 -0
  42. package/src/mpc/types.ts +117 -0
  43. package/src/types.ts +37 -0
package/src/base.ts ADDED
@@ -0,0 +1,306 @@
1
+ import type { idOSCredential } from "@idos-network/credentials/types";
2
+
3
+ import { recordFilter } from "@idos-network/credentials/utils";
4
+ import { base64Decode, fromBytesToJson, hexEncode } from "@idos-network/utils/codecs";
5
+
6
+ import type {
7
+ AddAddressMessageToSign,
8
+ AddAddressSignatureMessage,
9
+ RemoveAddressMessageToSign,
10
+ RemoveAddressSignatureMessage,
11
+ } from "./mpc/types";
12
+ import type { EnclaveOptions, PublicEncryptionProfile } from "./types";
13
+
14
+ export abstract class BaseProvider<K extends EnclaveOptions = EnclaveOptions> {
15
+ readonly options: K;
16
+
17
+ // oxlint-disable-next-line typescript/no-explicit-any -- TODO: Change this when we know how to MPC & other chains
18
+ protected _signMethod?: (domain: any, types: any, value: any) => Promise<string>;
19
+ private _signMethodType?: "signTypedData" | "signMessage" | "signer";
20
+
21
+ constructor(options: K) {
22
+ this.options = options;
23
+ }
24
+
25
+ /**
26
+ * Sets the signer for the enclave.
27
+ *
28
+ * @param signer - The signer to set, is later used for MPC if allowed.
29
+ */
30
+ setSigner(signer: {
31
+ signTypedData?: (domain: string, types: string[], value: string) => Promise<string>;
32
+ // oxlint-disable-next-line typescript/no-explicit-any -- TODO: Change this when we know how to MPC & other chains
33
+ signMessage?: (message: string) => Promise<any>;
34
+ // oxlint-disable-next-line typescript/no-explicit-any -- TODO: Change this when we know how to MPC & other chains
35
+ signer?: (message: string) => Promise<any>;
36
+ }): void {
37
+ if (signer.signTypedData) {
38
+ // EVM wallet
39
+ this._signMethod = signer.signTypedData.bind(signer);
40
+ this._signMethodType = "signTypedData";
41
+ } else if (signer.signMessage) {
42
+ // XRPL wallet
43
+ this._signMethod = signer.signMessage.bind(signer);
44
+ this._signMethodType = "signMessage";
45
+ } else if (signer.signer) {
46
+ // Stellar wallet
47
+ this._signMethod = signer.signer.bind(signer);
48
+ this._signMethodType = "signer";
49
+ } else {
50
+ if (["signMessage", "signer"].some((key) => key in signer)) {
51
+ this._signMethod = signer.signMessage
52
+ ? // @ts-expect-error - signMessage for xrpl and near, signer for stellar
53
+ signer.signMessage.bind(signer)
54
+ : // @ts-expect-error - signMessage for xrpl and near, signer for stellar
55
+ signer.signer?.bind(signer);
56
+ } else {
57
+ throw new Error("No sign method found in passed signer");
58
+ }
59
+ }
60
+ }
61
+
62
+ // oxlint-disable-next-line typescript/no-explicit-any -- TODO: Change this when we know how to MPC & other chains
63
+ async signTypedData(domain: any, types: any, value: any): Promise<string> {
64
+ if (!this._signMethod || !this._signMethodType) {
65
+ throw new Error("Signer is not set");
66
+ }
67
+
68
+ let signature: string;
69
+
70
+ // Handle different signing methods based on wallet type
71
+ if (this._signMethodType === "signTypedData") {
72
+ // EVM wallets: use all 3 arguments
73
+ signature =
74
+ await // oxlint-disable-next-line typescript/no-explicit-any -- TODO: Change this when we know how to MPC & other chains
75
+ (this._signMethod as (domain: any, types: any, value: any) => Promise<string>)(
76
+ domain,
77
+ types,
78
+ value,
79
+ );
80
+ } else if (this._signMethodType === "signMessage" || this._signMethodType === "signer") {
81
+ // XRPL/NEAR/Stellar/FaceSign wallets: use only the value as message
82
+ const messageString = JSON.stringify(value);
83
+ // oxlint-disable-next-line typescript/no-explicit-any -- TODO: Change this when we know how to MPC & other chains
84
+ const response = await (this._signMethod as (message: any) => Promise<any>)(messageString);
85
+
86
+ // Extract signature from response object
87
+ if (typeof response === "string") {
88
+ signature = response;
89
+ } else if (response?.result?.signedMessage) {
90
+ signature = response.result.signedMessage;
91
+ } else if (response?.signedMessage) {
92
+ signature = response.signedMessage;
93
+ } else if (response instanceof Uint8Array && response.length === 64) {
94
+ // ed25519 signature size
95
+ return hexEncode(response);
96
+ } else {
97
+ throw new Error(
98
+ `Unexpected response format from ${this._signMethodType}: ${JSON.stringify(response)}`,
99
+ );
100
+ }
101
+ } else {
102
+ throw new Error(`Unknown sign method type: ${this._signMethodType}`);
103
+ }
104
+
105
+ return signature;
106
+ }
107
+
108
+ /**
109
+ * Helper method to get the user ID.
110
+ *
111
+ * @returns The user ID.
112
+ */
113
+ get userId(): string {
114
+ if (!this.options.userId) {
115
+ throw new Error("User ID is not present");
116
+ }
117
+ return this.options.userId;
118
+ }
119
+
120
+ /**
121
+ * Resets the enclave (storage etc.)
122
+ */
123
+ async reset(): Promise<void> {
124
+ // Implement this if needed in child classes
125
+ }
126
+
127
+ /**
128
+ * Reconfigures the enclave (theme etc).
129
+ *
130
+ * @param options - The options to reconfigure.
131
+ */
132
+ async reconfigure(options: Partial<K> = {}): Promise<void> {
133
+ Object.assign(this.options, options);
134
+ }
135
+
136
+ /**
137
+ * Loads the enclave (create iframe, open connection, etc.)
138
+ */
139
+ async load(): Promise<void> {
140
+ await this.reconfigure();
141
+ }
142
+
143
+ /**
144
+ * Encrypts a message to a receiver.
145
+ * This method also checks if the user is authorized to use the keys.
146
+ *
147
+ * @param _message - The message to encrypt.
148
+ * @param _receiverPublicKey - The public key of the receiver.
149
+ *
150
+ * @returns The encrypted message.
151
+ */
152
+ async encrypt(
153
+ _message: Uint8Array,
154
+ _receiverPublicKey: Uint8Array,
155
+ ): Promise<{ content: Uint8Array; encryptorPublicKey: Uint8Array }> {
156
+ throw new Error("Method 'encrypt' has to be implemented in the subclass.");
157
+ }
158
+
159
+ /**
160
+ * Decrypts a message from a sender.
161
+ * This method also checks if the user is authorized to use the keys.
162
+ *
163
+ * @param _message - The message to decrypt.
164
+ * @param _senderPublicKey - The public key of the sender.
165
+ *
166
+ * @returns The decrypted message.
167
+ */
168
+ async decrypt(
169
+ _message: Uint8Array,
170
+ _senderPublicKey: Uint8Array,
171
+ ): Promise<Uint8Array<ArrayBufferLike>> {
172
+ throw new Error("Method 'decrypt' has to be implemented in the subclass.");
173
+ }
174
+
175
+ /**
176
+ * This method is used to confirm the user action.
177
+ *
178
+ * @param _message - The message to confirm.
179
+ *
180
+ * @returns `true` if the user action is confirmed, `false` otherwise.
181
+ */
182
+ async confirm(_message: string): Promise<boolean> {
183
+ throw new Error("Method 'confirm' has to be implemented in the subclass.");
184
+ }
185
+
186
+ /**
187
+ * This method is used to backup the password context.
188
+ */
189
+ async backupUserEncryptionProfile(): Promise<void> {
190
+ throw new Error("Method 'backupUserEncryptionProfile' has to be implemented in the subclass.");
191
+ }
192
+
193
+ /**
194
+ * Gets the public encryption profile.
195
+ *
196
+ * @returns The public encryption profile.
197
+ */
198
+ async ensureUserEncryptionProfile(): Promise<PublicEncryptionProfile> {
199
+ throw new Error("Method 'ensureUserEncryptionProfile' has to be implemented in the subclass.");
200
+ }
201
+
202
+ /**
203
+ * This method authorizes the origin in case of enclave
204
+ * to use the keys, without user providing the password or MPC again.
205
+ *
206
+ * @returns `true` if the user action is authorized, `false` otherwise.
207
+ */
208
+ async guardKeys(): Promise<boolean> {
209
+ return true;
210
+ }
211
+
212
+ /**
213
+ * Filters the credentials based on the private field filters.
214
+ *
215
+ * @param credentials - The credentials to filter.
216
+ * @param privateFieldFilters - The private field filters.
217
+ *
218
+ * @returns The filtered credentials without the content.
219
+ */
220
+ async filterCredentials(
221
+ credentials: idOSCredential[],
222
+ privateFieldFilters: { pick: Record<string, unknown[]>; omit: Record<string, unknown[]> },
223
+ ): Promise<idOSCredential[]> {
224
+ if (
225
+ Object.keys(privateFieldFilters.pick).length === 0 &&
226
+ Object.keys(privateFieldFilters.omit).length === 0
227
+ ) {
228
+ // No filtering needed
229
+ return credentials.map((credential) => ({
230
+ ...credential,
231
+ content: "", // Content should never leave the enclave!!!
232
+ }));
233
+ }
234
+
235
+ // We need to be sure first that we have the encryption profile
236
+ // otherwise we can end up with a lot of signatures for each credential
237
+ await this.ensureUserEncryptionProfile();
238
+
239
+ const decrypted = await Promise.all(
240
+ credentials.map(async (credential: idOSCredential) => {
241
+ const content = await this.decrypt(
242
+ base64Decode(credential.content),
243
+ base64Decode(credential.encryptor_public_key),
244
+ );
245
+
246
+ let json: Record<string, unknown>;
247
+
248
+ try {
249
+ json = fromBytesToJson(content);
250
+ } catch (_e) {
251
+ throw new Error(`Credential ${credential.id} decrypted contents are not valid JSON`);
252
+ }
253
+
254
+ return {
255
+ ...credential,
256
+ content: json,
257
+ };
258
+ }),
259
+ );
260
+
261
+ return decrypted
262
+ .filter(({ content }) =>
263
+ recordFilter(
264
+ content as Record<string, unknown>,
265
+ privateFieldFilters.pick,
266
+ privateFieldFilters.omit,
267
+ ),
268
+ )
269
+ .map((credential) => ({
270
+ ...credential,
271
+ content: "", // Content should never leave the enclave!!!
272
+ }));
273
+ }
274
+
275
+ async addAddressMessageToSign(
276
+ _address: string,
277
+ _publicKey: string | undefined,
278
+ _addressToAddType: string,
279
+ ): Promise<AddAddressMessageToSign> {
280
+ throw new Error("Method 'addAddressMessageToSign' has to be implemented in the subclass.");
281
+ }
282
+
283
+ async removeAddressMessageToSign(
284
+ _address: string,
285
+ _publicKey: string | undefined,
286
+ _addressToRemoveType: string,
287
+ ): Promise<RemoveAddressMessageToSign> {
288
+ throw new Error("Method 'removeAddressMessageToSign' has to be implemented in the subclass.");
289
+ }
290
+
291
+ async addAddressToMpcSecret(
292
+ _userId: string,
293
+ _message: AddAddressSignatureMessage,
294
+ _signature: string,
295
+ ): Promise<string> {
296
+ throw new Error("Method 'addAddressToMpcSecret' has to be implemented in the subclass.");
297
+ }
298
+
299
+ async removeAddressFromMpcSecret(
300
+ _userId: string,
301
+ _message: RemoveAddressSignatureMessage,
302
+ _signature: string,
303
+ ): Promise<string> {
304
+ throw new Error("Method 'removeAddressFromMpcSecret' has to be implemented in the subclass.");
305
+ }
306
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { BaseProvider } from "./base";
2
+ export * from "./keys";
3
+ export * from "./types";
package/src/keys.ts ADDED
@@ -0,0 +1,10 @@
1
+ export const STORAGE_KEYS = {
2
+ DEPRECATED___ENCRYPTION_PRIVATE_KEY: "encryption-private-key",
3
+ DEPRECATED___ENCRYPTION_SECRET_KEY: "encryption-secret-key",
4
+ OBFUSCATED_BASE64_ENCRYPTION_SECRET_KEY: "obfuscated-base64-encryption-secret-key",
5
+ ENCRYPTION_PUBLIC_KEY: "encryption-public-key",
6
+ DEPRECATED___PASSWORD: "password",
7
+ OBFUSCATED_PASSWORD: "obfuscated-password",
8
+ USER_ID: "user-id",
9
+ ENCRYPTION_PASSWORD_STORE: "encryption-password-store",
10
+ };
@@ -0,0 +1,113 @@
1
+ import type { idOSCredential } from "@idos-network/credentials/types";
2
+
3
+ import { base64Decode, base64Encode, utf8Encode } from "@idos-network/utils/codecs";
4
+ import { encryptContent } from "@idos-network/utils/cryptography";
5
+ import { MemoryStore } from "@idos-network/utils/store";
6
+ import tweetnacl from "tweetnacl";
7
+ import { describe, expect, it, vi } from "vitest";
8
+
9
+ import { LocalEnclave, type LocalEnclaveOptions } from "./local.js";
10
+
11
+ class TestEnclave extends LocalEnclave {
12
+ async getPasswordContext() {
13
+ return {
14
+ encryptionPasswordStore: "user",
15
+ password: "super-secret",
16
+ duration: 1,
17
+ } as const;
18
+ }
19
+ }
20
+
21
+ describe("LocalEnclave", () => {
22
+ const userId = "9f51b3b2-4cbe-4c2b-8ea3-0b0c1b2f1a11";
23
+
24
+ it("creates and exposes a public encryption profile", async () => {
25
+ const store = new MemoryStore();
26
+ const enclave = new TestEnclave({ userId, store } as LocalEnclaveOptions);
27
+
28
+ const profile = await enclave.createEncryptionProfileFromPassword(
29
+ "super-secret",
30
+ userId,
31
+ "user",
32
+ );
33
+
34
+ expect(profile.userId).toBe(userId);
35
+ expect(profile.encryptionPasswordStore).toBe("user");
36
+
37
+ const publicProfile = await enclave.ensureUserEncryptionProfile();
38
+ const publicKeyBytes = base64Decode(publicProfile.userEncryptionPublicKey);
39
+
40
+ expect(publicProfile.userId).toBe(userId);
41
+ expect(publicProfile.encryptionPasswordStore).toBe("user");
42
+ expect(publicKeyBytes).toHaveLength(32);
43
+ });
44
+
45
+ it("encrypts and decrypts with its keypair", async () => {
46
+ const store = new MemoryStore();
47
+ const enclave = new TestEnclave({ userId, store } as LocalEnclaveOptions);
48
+ const { keyPair } = await enclave.getPrivateEncryptionProfile();
49
+
50
+ const message = new Uint8Array([1, 2, 3, 4]);
51
+ const { content, encryptorPublicKey } = await enclave.encrypt(message, keyPair.publicKey);
52
+ const decrypted = await enclave.decrypt(content, encryptorPublicKey);
53
+
54
+ expect(decrypted).toEqual(message);
55
+ });
56
+
57
+ it("filters credentials and strips content", async () => {
58
+ const store = new MemoryStore();
59
+ const enclave = new TestEnclave({ userId, store });
60
+ const { keyPair } = await enclave.getPrivateEncryptionProfile();
61
+
62
+ // Reset enclave locals
63
+ // @ts-expect-error This is fine, it's a custom method simulates the private encryption profile
64
+ enclave.storedEncryptionProfile = undefined;
65
+
66
+ const matches = { type: "kyc", level: "basic" };
67
+ const misses = { type: "aml", level: "basic" };
68
+
69
+ const ephemeralKeyPair = tweetnacl.box.keyPair();
70
+
71
+ const matchesEncryption = encryptContent(
72
+ utf8Encode(JSON.stringify(matches)),
73
+ keyPair.publicKey,
74
+ ephemeralKeyPair.secretKey,
75
+ );
76
+ const missesEncryption = encryptContent(
77
+ utf8Encode(JSON.stringify(misses)),
78
+ keyPair.publicKey,
79
+ ephemeralKeyPair.secretKey,
80
+ );
81
+
82
+ const passwordContextSpy = vi.spyOn(enclave, "getPasswordContext");
83
+
84
+ const credentials: idOSCredential[] = [
85
+ {
86
+ id: "cred-1",
87
+ user_id: "user-1",
88
+ public_notes: "{}",
89
+ content: base64Encode(matchesEncryption),
90
+ encryptor_public_key: base64Encode(ephemeralKeyPair.publicKey),
91
+ issuer_auth_public_key: "issuer",
92
+ },
93
+ {
94
+ id: "cred-2",
95
+ user_id: "user-1",
96
+ public_notes: "{}",
97
+ content: base64Encode(missesEncryption),
98
+ encryptor_public_key: base64Encode(ephemeralKeyPair.publicKey),
99
+ issuer_auth_public_key: "issuer",
100
+ },
101
+ ];
102
+
103
+ const filtered = await enclave.filterCredentials(credentials, {
104
+ pick: { type: ["kyc"] },
105
+ omit: {},
106
+ });
107
+
108
+ expect(filtered).toHaveLength(1);
109
+ expect(filtered[0]?.id).toBe("cred-1");
110
+ expect(filtered[0]?.content).toBe("");
111
+ expect(passwordContextSpy).toHaveBeenCalledTimes(1);
112
+ });
113
+ });