@textrp/briij-js-sdk 42.0.0 → 43.0.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/README.md +71 -0
- package/lib/@types/auth.d.ts +53 -0
- package/lib/@types/auth.d.ts.map +1 -1
- package/lib/@types/auth.js +2 -0
- package/lib/@types/auth.js.map +1 -1
- package/lib/@types/event.d.ts +32 -0
- package/lib/@types/event.d.ts.map +1 -1
- package/lib/@types/event.js.map +1 -1
- package/lib/briij.d.ts +1 -0
- package/lib/briij.d.ts.map +1 -1
- package/lib/briij.js +1 -0
- package/lib/briij.js.map +1 -1
- package/lib/client.d.ts +36 -6
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +210 -119
- package/lib/client.js.map +1 -1
- package/lib/wallet-recovery.d.ts +24 -0
- package/lib/wallet-recovery.d.ts.map +1 -0
- package/lib/wallet-recovery.js +232 -0
- package/lib/wallet-recovery.js.map +1 -0
- package/package.json +129 -127
- package/src/@types/auth.ts +60 -0
- package/src/@types/event.ts +32 -0
- package/src/briij.ts +1 -0
- package/src/client.ts +106 -13
- package/src/wallet-recovery.ts +252 -0
package/src/@types/auth.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface ILoginFlowsResponse {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export const XRPL_WALLET_LOGIN_TYPE = "io.briij.login.xrpl";
|
|
42
|
+
export const WALLET_E2EE_RECOVERY_ACCOUNT_DATA_TYPE = "org.textrp.wallet.e2ee_recovery.v1";
|
|
43
|
+
export const WALLET_IDENTITY_ACCOUNT_DATA_TYPE = "org.textrp.wallet.identity";
|
|
42
44
|
|
|
43
45
|
export type LoginFlow = ISSOFlow | IPasswordFlow | IXrplWalletLoginFlow | ILoginFlow;
|
|
44
46
|
|
|
@@ -206,6 +208,7 @@ export interface LoginRequest {
|
|
|
206
208
|
export type ILoginParams = LoginRequest;
|
|
207
209
|
|
|
208
210
|
export interface XrplWalletChallengePayload {
|
|
211
|
+
session?: string;
|
|
209
212
|
nonce: string;
|
|
210
213
|
timestamp: number | string;
|
|
211
214
|
message: string;
|
|
@@ -213,6 +216,63 @@ export interface XrplWalletChallengePayload {
|
|
|
213
216
|
algorithm?: "secp256k1" | "ed25519" | string;
|
|
214
217
|
}
|
|
215
218
|
|
|
219
|
+
export interface XrplAuthChallengeRequest extends Omit<LoginRequest, "type"> {
|
|
220
|
+
type: typeof XRPL_WALLET_LOGIN_TYPE;
|
|
221
|
+
address: string;
|
|
222
|
+
network: string;
|
|
223
|
+
preferred_localpart?: string;
|
|
224
|
+
username?: string;
|
|
225
|
+
display_name?: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface XrplAuthChallengeResponse {
|
|
229
|
+
session: string;
|
|
230
|
+
challenge: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface XrplAuthCompleteRequest extends Omit<LoginRequest, "type"> {
|
|
234
|
+
type: typeof XRPL_WALLET_LOGIN_TYPE;
|
|
235
|
+
session: string;
|
|
236
|
+
address: string;
|
|
237
|
+
signature: string;
|
|
238
|
+
public_key?: string;
|
|
239
|
+
network?: string;
|
|
240
|
+
wallet_e2ee_recovery?: WalletE2eeRecoveryEnvelope;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface WalletRecoveryWrap {
|
|
244
|
+
alg: string;
|
|
245
|
+
kdf: string;
|
|
246
|
+
salt: string;
|
|
247
|
+
nonce: string;
|
|
248
|
+
ciphertext: string;
|
|
249
|
+
aad?: string;
|
|
250
|
+
params?: Record<string, unknown>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface WalletRecoveryPasswordWrap extends WalletRecoveryWrap {
|
|
254
|
+
kdf: string;
|
|
255
|
+
params?: Record<string, unknown>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface WalletE2eeRecoveryEnvelope {
|
|
259
|
+
envelope_version: number;
|
|
260
|
+
chain_id: string;
|
|
261
|
+
account_id: string;
|
|
262
|
+
created_at_ms: number;
|
|
263
|
+
key_id: string;
|
|
264
|
+
wallet_wrap: WalletRecoveryWrap;
|
|
265
|
+
password_wrap: WalletRecoveryPasswordWrap;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface WalletIdentityAccountData {
|
|
269
|
+
chain_id: string;
|
|
270
|
+
account_id: string;
|
|
271
|
+
public_key: string | null;
|
|
272
|
+
network?: string | null;
|
|
273
|
+
key_type?: string | null;
|
|
274
|
+
}
|
|
275
|
+
|
|
216
276
|
export interface XrplWalletLoginRequest extends Omit<LoginRequest, "type"> {
|
|
217
277
|
type: typeof XRPL_WALLET_LOGIN_TYPE;
|
|
218
278
|
wallet_address: string;
|
package/src/@types/event.ts
CHANGED
|
@@ -455,6 +455,38 @@ export interface AccountDataEvents extends SecretStorageAccountDataEvents {
|
|
|
455
455
|
[POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any };
|
|
456
456
|
|
|
457
457
|
[EventType.InvitePermissionConfig]: { default_action?: string };
|
|
458
|
+
"org.textrp.wallet.e2ee_recovery.v1": {
|
|
459
|
+
envelope_version: number;
|
|
460
|
+
chain_id: string;
|
|
461
|
+
account_id: string;
|
|
462
|
+
created_at_ms: number;
|
|
463
|
+
key_id: string;
|
|
464
|
+
wallet_wrap: {
|
|
465
|
+
alg: string;
|
|
466
|
+
kdf: string;
|
|
467
|
+
salt: string;
|
|
468
|
+
nonce: string;
|
|
469
|
+
ciphertext: string;
|
|
470
|
+
aad?: string;
|
|
471
|
+
params?: Record<string, unknown>;
|
|
472
|
+
};
|
|
473
|
+
password_wrap: {
|
|
474
|
+
alg: string;
|
|
475
|
+
kdf: string;
|
|
476
|
+
salt: string;
|
|
477
|
+
nonce: string;
|
|
478
|
+
ciphertext: string;
|
|
479
|
+
aad?: string;
|
|
480
|
+
params?: Record<string, unknown>;
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
"org.textrp.wallet.identity": {
|
|
484
|
+
chain_id: string;
|
|
485
|
+
account_id: string;
|
|
486
|
+
public_key: string | null;
|
|
487
|
+
network?: string | null;
|
|
488
|
+
key_type?: string | null;
|
|
489
|
+
};
|
|
458
490
|
}
|
|
459
491
|
|
|
460
492
|
/**
|
package/src/briij.ts
CHANGED
|
@@ -52,6 +52,7 @@ export * from "./interactive-auth.ts";
|
|
|
52
52
|
export * from "./xrpl/identity.ts";
|
|
53
53
|
export * from "./xrpl/trust.ts";
|
|
54
54
|
export * from "./xrpl/verification.ts";
|
|
55
|
+
export * from "./wallet-recovery.ts";
|
|
55
56
|
export * from "./version-support.ts";
|
|
56
57
|
export * from "./service-types.ts";
|
|
57
58
|
export * from "./store/memory.ts";
|
package/src/client.ts
CHANGED
|
@@ -198,7 +198,14 @@ import {
|
|
|
198
198
|
type LoginResponse,
|
|
199
199
|
type LoginTokenPostResponse,
|
|
200
200
|
type SSOAction,
|
|
201
|
+
WALLET_E2EE_RECOVERY_ACCOUNT_DATA_TYPE,
|
|
202
|
+
WALLET_IDENTITY_ACCOUNT_DATA_TYPE,
|
|
201
203
|
XRPL_WALLET_LOGIN_TYPE,
|
|
204
|
+
type WalletE2eeRecoveryEnvelope,
|
|
205
|
+
type WalletIdentityAccountData,
|
|
206
|
+
type XrplAuthChallengeRequest,
|
|
207
|
+
type XrplAuthChallengeResponse,
|
|
208
|
+
type XrplAuthCompleteRequest,
|
|
202
209
|
type XrplWalletChallengePayload,
|
|
203
210
|
} from "./@types/auth.ts";
|
|
204
211
|
import { TypedEventEmitter } from "./models/typed-event-emitter.ts";
|
|
@@ -6724,31 +6731,117 @@ export class BriijClient extends TypedEventEmitter<EmittedEvents, ClientEventHan
|
|
|
6724
6731
|
}
|
|
6725
6732
|
|
|
6726
6733
|
/**
|
|
6727
|
-
*
|
|
6728
|
-
*
|
|
6729
|
-
*
|
|
6730
|
-
*
|
|
6731
|
-
|
|
6734
|
+
* Request an XRPL login challenge from the homeserver.
|
|
6735
|
+
*
|
|
6736
|
+
* The briij homeserver responds with HTTP 401 as the successful challenge
|
|
6737
|
+
* response, so this helper normalizes that into a resolved promise.
|
|
6738
|
+
*/
|
|
6739
|
+
public async getXrplAuthChallenge(
|
|
6740
|
+
request: Omit<XrplAuthChallengeRequest, "type">,
|
|
6741
|
+
): Promise<XrplAuthChallengeResponse> {
|
|
6742
|
+
try {
|
|
6743
|
+
const response = (await this.loginRequest({
|
|
6744
|
+
...request,
|
|
6745
|
+
type: XRPL_WALLET_LOGIN_TYPE,
|
|
6746
|
+
})) as unknown as XrplAuthChallengeResponse;
|
|
6747
|
+
if (typeof response.session === "string" && typeof response.challenge === "string") {
|
|
6748
|
+
return response;
|
|
6749
|
+
}
|
|
6750
|
+
} catch (error) {
|
|
6751
|
+
if (error instanceof BriijError && error.httpStatus === 401) {
|
|
6752
|
+
const session = error.data?.session;
|
|
6753
|
+
const challenge = error.data?.challenge;
|
|
6754
|
+
if (typeof session === "string" && typeof challenge === "string") {
|
|
6755
|
+
return { session, challenge };
|
|
6756
|
+
}
|
|
6757
|
+
}
|
|
6758
|
+
throw error;
|
|
6759
|
+
}
|
|
6760
|
+
|
|
6761
|
+
throw new Error("XRPL challenge response was not returned by homeserver");
|
|
6762
|
+
}
|
|
6763
|
+
|
|
6764
|
+
/**
|
|
6765
|
+
* Complete XRPL login using the previously issued challenge session.
|
|
6766
|
+
*
|
|
6732
6767
|
* @returns Promise which resolves to a LoginResponse object.
|
|
6733
6768
|
* @returns Rejects: with an error response.
|
|
6734
6769
|
*/
|
|
6735
|
-
public
|
|
6770
|
+
public completeXrplAuth(request: Omit<XrplAuthCompleteRequest, "type">): Promise<LoginResponse> {
|
|
6771
|
+
return this.loginRequest({
|
|
6772
|
+
...request,
|
|
6773
|
+
type: XRPL_WALLET_LOGIN_TYPE,
|
|
6774
|
+
});
|
|
6775
|
+
}
|
|
6776
|
+
|
|
6777
|
+
/**
|
|
6778
|
+
* Store a chain-agnostic wallet recovery envelope in account data.
|
|
6779
|
+
*
|
|
6780
|
+
* This uses a stable account-data type and does not affect core E2EE state.
|
|
6781
|
+
*
|
|
6782
|
+
* @param envelope - Wallet recovery envelope payload.
|
|
6783
|
+
*/
|
|
6784
|
+
public setWalletRecoveryEnvelope(envelope: WalletE2eeRecoveryEnvelope): Promise<EmptyObject> {
|
|
6785
|
+
return this.setAccountDataRaw(WALLET_E2EE_RECOVERY_ACCOUNT_DATA_TYPE, envelope);
|
|
6786
|
+
}
|
|
6787
|
+
|
|
6788
|
+
/**
|
|
6789
|
+
* Fetch wallet recovery envelope directly from the homeserver.
|
|
6790
|
+
*
|
|
6791
|
+
* @returns The stored envelope, or null if not found.
|
|
6792
|
+
*/
|
|
6793
|
+
public getWalletRecoveryEnvelopeFromServer(): Promise<WalletE2eeRecoveryEnvelope | null> {
|
|
6794
|
+
return this.getAccountDataFromServer(WALLET_E2EE_RECOVERY_ACCOUNT_DATA_TYPE);
|
|
6795
|
+
}
|
|
6796
|
+
|
|
6797
|
+
/**
|
|
6798
|
+
* Fetch canonical wallet identity metadata from the homeserver.
|
|
6799
|
+
*
|
|
6800
|
+
* @returns Wallet identity metadata, or null if not found.
|
|
6801
|
+
*/
|
|
6802
|
+
public getWalletIdentityFromServer(): Promise<WalletIdentityAccountData | null> {
|
|
6803
|
+
return this.getAccountDataFromServer(WALLET_IDENTITY_ACCOUNT_DATA_TYPE);
|
|
6804
|
+
}
|
|
6805
|
+
|
|
6806
|
+
/**
|
|
6807
|
+
* @deprecated Use `getXrplAuthChallenge` + `completeXrplAuth` for explicit
|
|
6808
|
+
* two-step XRPL login flow. This wrapper now expects `challenge` to include
|
|
6809
|
+
* the `session` and optional `public_key`.
|
|
6810
|
+
*/
|
|
6811
|
+
public async loginWithXrplWallet(
|
|
6736
6812
|
user: string,
|
|
6737
6813
|
walletAddress: string,
|
|
6738
6814
|
signature: string,
|
|
6739
6815
|
challenge: string | XrplWalletChallengePayload,
|
|
6740
6816
|
network = "xrpl",
|
|
6741
6817
|
): Promise<LoginResponse> {
|
|
6742
|
-
|
|
6743
|
-
|
|
6818
|
+
let parsedChallenge: Partial<XrplWalletChallengePayload> | null = null;
|
|
6819
|
+
if (typeof challenge === "string") {
|
|
6820
|
+
try {
|
|
6821
|
+
parsedChallenge = JSON.parse(challenge) as Partial<XrplWalletChallengePayload>;
|
|
6822
|
+
} catch {
|
|
6823
|
+
throw new Error("XRPL challenge must be JSON with a session field");
|
|
6824
|
+
}
|
|
6825
|
+
} else {
|
|
6826
|
+
parsedChallenge = challenge;
|
|
6827
|
+
}
|
|
6828
|
+
|
|
6829
|
+
const session = parsedChallenge?.session;
|
|
6830
|
+
if (!session) {
|
|
6831
|
+
throw new Error("XRPL challenge payload is missing session; call getXrplAuthChallenge first");
|
|
6832
|
+
}
|
|
6833
|
+
|
|
6834
|
+
return this.completeXrplAuth({
|
|
6835
|
+
user,
|
|
6744
6836
|
identifier: {
|
|
6745
6837
|
type: "m.id.user",
|
|
6746
|
-
user
|
|
6838
|
+
user,
|
|
6747
6839
|
},
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6840
|
+
session,
|
|
6841
|
+
address: walletAddress,
|
|
6842
|
+
signature,
|
|
6843
|
+
public_key: parsedChallenge?.public_key,
|
|
6844
|
+
network,
|
|
6752
6845
|
});
|
|
6753
6846
|
}
|
|
6754
6847
|
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 TextRP
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type WalletE2eeRecoveryEnvelope, type WalletRecoveryWrap } from "./@types/auth.ts";
|
|
18
|
+
|
|
19
|
+
const ENVELOPE_VERSION = 1;
|
|
20
|
+
const KEY_BYTES = 32;
|
|
21
|
+
const NONCE_BYTES = 12;
|
|
22
|
+
const SALT_BYTES = 16;
|
|
23
|
+
const PASSWORD_KDF_ITERATIONS = 210_000;
|
|
24
|
+
|
|
25
|
+
export interface CreateDualWrapEnvelopeParams {
|
|
26
|
+
chainId: string;
|
|
27
|
+
accountId: string;
|
|
28
|
+
backupPassword: string;
|
|
29
|
+
walletWrapKey: Uint8Array;
|
|
30
|
+
createdAtMs?: number;
|
|
31
|
+
keyId?: string;
|
|
32
|
+
recoveryKey?: Uint8Array;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UnwrapWithWalletParams {
|
|
36
|
+
envelope: WalletE2eeRecoveryEnvelope;
|
|
37
|
+
walletWrapKey: Uint8Array;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface UnwrapWithPasswordParams {
|
|
41
|
+
envelope: WalletE2eeRecoveryEnvelope;
|
|
42
|
+
backupPassword: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assert(condition: boolean, message: string): asserts condition {
|
|
46
|
+
if (!condition) throw new Error(message);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toBase64(bytes: Uint8Array): string {
|
|
50
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
51
|
+
let binary = "";
|
|
52
|
+
bytes.forEach((b) => (binary += String.fromCodePoint(b)));
|
|
53
|
+
return btoa(binary);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fromBase64(value: string): Uint8Array {
|
|
57
|
+
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(value, "base64"));
|
|
58
|
+
const binary = atob(value);
|
|
59
|
+
const out = new Uint8Array(binary.length);
|
|
60
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.codePointAt(i)!;
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toUtf8(value: string): Uint8Array {
|
|
65
|
+
return new TextEncoder().encode(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toArrayBuffer(value: Uint8Array): ArrayBuffer {
|
|
69
|
+
return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function randomBytes(len: number): Uint8Array {
|
|
73
|
+
const out = new Uint8Array(len);
|
|
74
|
+
globalThis.crypto.getRandomValues(out);
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function importAesGcmKey(key: Uint8Array): Promise<CryptoKey> {
|
|
79
|
+
return await globalThis.crypto.subtle.importKey(
|
|
80
|
+
"raw",
|
|
81
|
+
toArrayBuffer(key),
|
|
82
|
+
{ name: "AES-GCM" },
|
|
83
|
+
false,
|
|
84
|
+
["encrypt", "decrypt"],
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function encryptWrap(
|
|
89
|
+
plaintext: Uint8Array,
|
|
90
|
+
{
|
|
91
|
+
key,
|
|
92
|
+
aad,
|
|
93
|
+
kdf,
|
|
94
|
+
params,
|
|
95
|
+
}: {
|
|
96
|
+
key: Uint8Array;
|
|
97
|
+
aad: Uint8Array;
|
|
98
|
+
kdf: string;
|
|
99
|
+
params?: Record<string, unknown>;
|
|
100
|
+
},
|
|
101
|
+
): Promise<WalletRecoveryWrap> {
|
|
102
|
+
assert(key.length >= KEY_BYTES, "wallet recovery key must be at least 32 bytes");
|
|
103
|
+
const salt = randomBytes(SALT_BYTES);
|
|
104
|
+
const nonce = randomBytes(NONCE_BYTES);
|
|
105
|
+
const cryptoKey = await importAesGcmKey(key.slice(0, KEY_BYTES));
|
|
106
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
107
|
+
{
|
|
108
|
+
name: "AES-GCM",
|
|
109
|
+
iv: toArrayBuffer(nonce),
|
|
110
|
+
additionalData: toArrayBuffer(aad),
|
|
111
|
+
tagLength: 128,
|
|
112
|
+
},
|
|
113
|
+
cryptoKey,
|
|
114
|
+
toArrayBuffer(plaintext),
|
|
115
|
+
);
|
|
116
|
+
return {
|
|
117
|
+
alg: "aes-256-gcm",
|
|
118
|
+
kdf,
|
|
119
|
+
salt: toBase64(salt),
|
|
120
|
+
nonce: toBase64(nonce),
|
|
121
|
+
ciphertext: toBase64(new Uint8Array(ciphertext)),
|
|
122
|
+
aad: toBase64(aad),
|
|
123
|
+
params,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function decryptWrap(wrap: WalletRecoveryWrap, key: Uint8Array): Promise<Uint8Array> {
|
|
128
|
+
const nonce = fromBase64(wrap.nonce);
|
|
129
|
+
const aad = wrap.aad ? fromBase64(wrap.aad) : new Uint8Array();
|
|
130
|
+
const ciphertext = fromBase64(wrap.ciphertext);
|
|
131
|
+
const cryptoKey = await importAesGcmKey(key.slice(0, KEY_BYTES));
|
|
132
|
+
const plaintext = await globalThis.crypto.subtle.decrypt(
|
|
133
|
+
{
|
|
134
|
+
name: "AES-GCM",
|
|
135
|
+
iv: toArrayBuffer(nonce),
|
|
136
|
+
additionalData: toArrayBuffer(aad),
|
|
137
|
+
tagLength: 128,
|
|
138
|
+
},
|
|
139
|
+
cryptoKey,
|
|
140
|
+
toArrayBuffer(ciphertext),
|
|
141
|
+
);
|
|
142
|
+
return new Uint8Array(plaintext);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function derivePasswordWrapKey(password: string, salt: Uint8Array): Promise<Uint8Array> {
|
|
146
|
+
const keyMaterial = await globalThis.crypto.subtle.importKey("raw", toArrayBuffer(toUtf8(password)), "PBKDF2", false, [
|
|
147
|
+
"deriveBits",
|
|
148
|
+
]);
|
|
149
|
+
const bits = await globalThis.crypto.subtle.deriveBits(
|
|
150
|
+
{
|
|
151
|
+
name: "PBKDF2",
|
|
152
|
+
hash: "SHA-256",
|
|
153
|
+
salt: toArrayBuffer(salt),
|
|
154
|
+
iterations: PASSWORD_KDF_ITERATIONS,
|
|
155
|
+
},
|
|
156
|
+
keyMaterial,
|
|
157
|
+
KEY_BYTES * 8,
|
|
158
|
+
);
|
|
159
|
+
return new Uint8Array(bits);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function deriveWalletWrapKeyFromSecret(
|
|
163
|
+
walletSecret: string,
|
|
164
|
+
chainId: string,
|
|
165
|
+
accountId: string,
|
|
166
|
+
homeserver: string,
|
|
167
|
+
): Promise<Uint8Array> {
|
|
168
|
+
const context = `briij-wallet-auth-wrap-v1:${homeserver}:${chainId}:${accountId}`;
|
|
169
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
170
|
+
"SHA-256",
|
|
171
|
+
toArrayBuffer(toUtf8(`${context}:${walletSecret}`)),
|
|
172
|
+
);
|
|
173
|
+
return new Uint8Array(digest);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function validateRecoveryEnvelopeShape(
|
|
177
|
+
value: unknown,
|
|
178
|
+
): asserts value is WalletE2eeRecoveryEnvelope {
|
|
179
|
+
assert(!!value && typeof value === "object", "recovery envelope must be an object");
|
|
180
|
+
const envelope = value as WalletE2eeRecoveryEnvelope;
|
|
181
|
+
assert(typeof envelope.envelope_version === "number", "envelope_version must be a number");
|
|
182
|
+
assert(typeof envelope.chain_id === "string" && envelope.chain_id.length > 0, "chain_id is required");
|
|
183
|
+
assert(typeof envelope.account_id === "string" && envelope.account_id.length > 0, "account_id is required");
|
|
184
|
+
assert(typeof envelope.created_at_ms === "number", "created_at_ms must be a number");
|
|
185
|
+
assert(typeof envelope.key_id === "string" && envelope.key_id.length > 0, "key_id is required");
|
|
186
|
+
assert(!!envelope.wallet_wrap && typeof envelope.wallet_wrap === "object", "wallet_wrap is required");
|
|
187
|
+
assert(!!envelope.password_wrap && typeof envelope.password_wrap === "object", "password_wrap is required");
|
|
188
|
+
for (const wrap of [envelope.wallet_wrap, envelope.password_wrap]) {
|
|
189
|
+
assert(typeof wrap.alg === "string" && wrap.alg.length > 0, "wrap.alg is required");
|
|
190
|
+
assert(typeof wrap.kdf === "string" && wrap.kdf.length > 0, "wrap.kdf is required");
|
|
191
|
+
assert(typeof wrap.salt === "string" && wrap.salt.length > 0, "wrap.salt is required");
|
|
192
|
+
assert(typeof wrap.nonce === "string" && wrap.nonce.length > 0, "wrap.nonce is required");
|
|
193
|
+
assert(typeof wrap.ciphertext === "string" && wrap.ciphertext.length > 0, "wrap.ciphertext is required");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function createDualWrapEnvelope(params: CreateDualWrapEnvelopeParams): Promise<WalletE2eeRecoveryEnvelope> {
|
|
198
|
+
assert(params.chainId.length > 0, "chainId is required");
|
|
199
|
+
assert(params.accountId.length > 0, "accountId is required");
|
|
200
|
+
assert(params.backupPassword.length > 0, "backupPassword is required");
|
|
201
|
+
assert(params.walletWrapKey.length >= KEY_BYTES, "walletWrapKey must be at least 32 bytes");
|
|
202
|
+
|
|
203
|
+
const createdAtMs = params.createdAtMs ?? Date.now();
|
|
204
|
+
const keyId = params.keyId ?? `k-${toBase64(randomBytes(8)).replace(/=+$/g, "")}`;
|
|
205
|
+
const recoveryKey = params.recoveryKey ?? randomBytes(KEY_BYTES);
|
|
206
|
+
assert(recoveryKey.length === KEY_BYTES, "recoveryKey must be 32 bytes");
|
|
207
|
+
|
|
208
|
+
const aad = toUtf8(`briij-recovery-envelope-v1:${params.chainId}:${params.accountId}:${keyId}:${createdAtMs}`);
|
|
209
|
+
const walletWrap = await encryptWrap(recoveryKey, {
|
|
210
|
+
key: params.walletWrapKey,
|
|
211
|
+
aad,
|
|
212
|
+
kdf: "sha256-context-v1",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const passwordSalt = randomBytes(SALT_BYTES);
|
|
216
|
+
const passwordWrapKey = await derivePasswordWrapKey(params.backupPassword, passwordSalt);
|
|
217
|
+
const passwordWrap = await encryptWrap(recoveryKey, {
|
|
218
|
+
key: passwordWrapKey,
|
|
219
|
+
aad,
|
|
220
|
+
kdf: "pbkdf2-sha256-v1",
|
|
221
|
+
params: {
|
|
222
|
+
iterations: PASSWORD_KDF_ITERATIONS,
|
|
223
|
+
hash: "SHA-256",
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
passwordWrap.salt = toBase64(passwordSalt);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
envelope_version: ENVELOPE_VERSION,
|
|
230
|
+
chain_id: params.chainId,
|
|
231
|
+
account_id: params.accountId,
|
|
232
|
+
created_at_ms: createdAtMs,
|
|
233
|
+
key_id: keyId,
|
|
234
|
+
wallet_wrap: walletWrap,
|
|
235
|
+
password_wrap: passwordWrap,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function unwrapWithWallet({ envelope, walletWrapKey }: UnwrapWithWalletParams): Promise<Uint8Array> {
|
|
240
|
+
validateRecoveryEnvelopeShape(envelope);
|
|
241
|
+
return await decryptWrap(envelope.wallet_wrap, walletWrapKey);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function unwrapWithPassword({
|
|
245
|
+
envelope,
|
|
246
|
+
backupPassword,
|
|
247
|
+
}: UnwrapWithPasswordParams): Promise<Uint8Array> {
|
|
248
|
+
validateRecoveryEnvelopeShape(envelope);
|
|
249
|
+
const passwordSalt = fromBase64(envelope.password_wrap.salt);
|
|
250
|
+
const passwordWrapKey = await derivePasswordWrapKey(backupPassword, passwordSalt);
|
|
251
|
+
return await decryptWrap(envelope.password_wrap, passwordWrapKey);
|
|
252
|
+
}
|