@enbox/browser 0.1.25 → 0.2.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/dist/esm/dweb-connect-client.js +47 -4
- package/dist/esm/dweb-connect-client.js.map +1 -1
- package/dist/esm/dweb-connect-crypto.js +112 -0
- package/dist/esm/dweb-connect-crypto.js.map +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/types/dweb-connect-client.d.ts.map +1 -1
- package/dist/types/dweb-connect-crypto.d.ts +52 -0
- package/dist/types/dweb-connect-crypto.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/dweb-connect-client.ts +63 -9
- package/src/dweb-connect-crypto.ts +156 -0
- package/src/index.ts +3 -1
|
@@ -16,6 +16,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
16
16
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
17
17
|
});
|
|
18
18
|
};
|
|
19
|
+
import { decryptPostMessagePayload, generateEphemeralKeyPair } from './dweb-connect-crypto.js';
|
|
19
20
|
/**
|
|
20
21
|
* Open a wallet popup and run the DWeb Connect postMessage flow.
|
|
21
22
|
*
|
|
@@ -42,6 +43,19 @@ function initClient(options) {
|
|
|
42
43
|
if (!popup) {
|
|
43
44
|
throw new Error('[@enbox/auth] Popup blocked. Allow popups for this site to connect to a wallet.');
|
|
44
45
|
}
|
|
46
|
+
// Generate an ephemeral ECDH keypair for this connect session.
|
|
47
|
+
// The public key is sent to the wallet so it can encrypt the response
|
|
48
|
+
// containing delegate private keys and decryption material.
|
|
49
|
+
let dappKeyPair;
|
|
50
|
+
let dappPublicKeyBase64url;
|
|
51
|
+
try {
|
|
52
|
+
const ephemeral = yield generateEphemeralKeyPair();
|
|
53
|
+
dappKeyPair = ephemeral.keyPair;
|
|
54
|
+
dappPublicKeyBase64url = ephemeral.publicKeyBase64url;
|
|
55
|
+
}
|
|
56
|
+
catch (_a) {
|
|
57
|
+
// crypto.subtle unavailable (e.g. non-secure context) — skip encryption.
|
|
58
|
+
}
|
|
45
59
|
return new Promise((resolve, reject) => {
|
|
46
60
|
let settled = false;
|
|
47
61
|
const cleanup = () => {
|
|
@@ -77,28 +91,57 @@ function initClient(options) {
|
|
|
77
91
|
}
|
|
78
92
|
const { type } = (_a = event.data) !== null && _a !== void 0 ? _a : {};
|
|
79
93
|
if (type === 'dweb-connect-loaded') {
|
|
80
|
-
// Wallet is ready — send the authorization request.
|
|
94
|
+
// Wallet is ready — send the authorization request with ephemeral public key.
|
|
81
95
|
popup.postMessage({
|
|
82
96
|
type: 'dweb-connect-authorization-request',
|
|
83
97
|
did,
|
|
84
98
|
permissions: permissionRequests,
|
|
99
|
+
ephemeralPublicKey: dappPublicKeyBase64url,
|
|
85
100
|
}, walletOrigin);
|
|
86
101
|
return;
|
|
87
102
|
}
|
|
88
103
|
if (type === 'dweb-connect-authorization-response') {
|
|
89
104
|
clearInterval(pollClosed);
|
|
90
105
|
cleanup();
|
|
91
|
-
|
|
106
|
+
// Handle encrypted response (wallet supports ECDH channel encryption).
|
|
107
|
+
const encrypted = event.data.encryptedPayload;
|
|
108
|
+
if (encrypted && dappKeyPair) {
|
|
109
|
+
decryptPostMessagePayload(encrypted, dappKeyPair).then((payload) => {
|
|
110
|
+
var _a, _b, _c, _d, _e, _f;
|
|
111
|
+
const p = payload;
|
|
112
|
+
if (!p.delegateDid || !p.grants) {
|
|
113
|
+
resolve(undefined);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
resolve({
|
|
117
|
+
delegatePortableDid: p.delegateDid,
|
|
118
|
+
delegateGrants: p.grants,
|
|
119
|
+
connectedDid: (_b = (_a = p.connectedDid) !== null && _a !== void 0 ? _a : did) !== null && _b !== void 0 ? _b : p.delegateDid.uri,
|
|
120
|
+
delegateDecryptionKeys: (_c = p.delegateDecryptionKeys) !== null && _c !== void 0 ? _c : undefined,
|
|
121
|
+
delegateContextKeys: (_d = p.delegateContextKeys) !== null && _d !== void 0 ? _d : undefined,
|
|
122
|
+
delegateMultiPartyProtocols: (_e = p.delegateMultiPartyProtocols) !== null && _e !== void 0 ? _e : undefined,
|
|
123
|
+
sessionRevocations: (_f = p.sessionRevocations) !== null && _f !== void 0 ? _f : undefined,
|
|
124
|
+
});
|
|
125
|
+
}).catch(() => {
|
|
126
|
+
// Decryption failed — treat as denied.
|
|
127
|
+
resolve(undefined);
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Plaintext fallback (wallet doesn't support encryption yet).
|
|
132
|
+
const { delegateDid, connectedDid: walletConnectedDid, grants, delegateDecryptionKeys, delegateContextKeys, delegateMultiPartyProtocols, sessionRevocations, } = event.data;
|
|
92
133
|
if (!delegateDid || !grants) {
|
|
93
|
-
// User denied the request.
|
|
94
134
|
resolve(undefined);
|
|
95
135
|
return;
|
|
96
136
|
}
|
|
97
|
-
// connectedDid priority: wallet response > dapp-provided > delegate DID
|
|
98
137
|
resolve({
|
|
99
138
|
delegatePortableDid: delegateDid,
|
|
100
139
|
delegateGrants: grants,
|
|
101
140
|
connectedDid: (_b = walletConnectedDid !== null && walletConnectedDid !== void 0 ? walletConnectedDid : did) !== null && _b !== void 0 ? _b : delegateDid.uri,
|
|
141
|
+
delegateDecryptionKeys: delegateDecryptionKeys !== null && delegateDecryptionKeys !== void 0 ? delegateDecryptionKeys : undefined,
|
|
142
|
+
delegateContextKeys: delegateContextKeys !== null && delegateContextKeys !== void 0 ? delegateContextKeys : undefined,
|
|
143
|
+
delegateMultiPartyProtocols: delegateMultiPartyProtocols !== null && delegateMultiPartyProtocols !== void 0 ? delegateMultiPartyProtocols : undefined,
|
|
144
|
+
sessionRevocations: sessionRevocations !== null && sessionRevocations !== void 0 ? sessionRevocations : undefined,
|
|
102
145
|
});
|
|
103
146
|
}
|
|
104
147
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dweb-connect-client.js","sourceRoot":"","sources":["../../src/dweb-connect-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"dweb-connect-client.js","sourceRoot":"","sources":["../../src/dweb-connect-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECDH + AES-256-GCM encryption for the DWeb Connect postMessage channel.
|
|
3
|
+
*
|
|
4
|
+
* Protects the authorization response (delegate private keys, decryption
|
|
5
|
+
* keys, grants) from same-origin XSS interception. Both the dapp and
|
|
6
|
+
* wallet generate ephemeral P-256 keypairs, exchange public keys during
|
|
7
|
+
* the handshake, derive a shared AES-256-GCM key via ECDH + HKDF, and
|
|
8
|
+
* encrypt/decrypt the payload.
|
|
9
|
+
*
|
|
10
|
+
* P-256 is used (instead of X25519) because `crypto.subtle.deriveKey`
|
|
11
|
+
* with ECDH requires a NIST curve in all major browsers. The keys are
|
|
12
|
+
* ephemeral — generated fresh for each connect session.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
17
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
18
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
19
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
20
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
21
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
22
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
const ECDH_PARAMS = { name: 'ECDH', namedCurve: 'P-256' };
|
|
26
|
+
const AES_KEY_PARAMS = { name: 'AES-GCM', length: 256 };
|
|
27
|
+
const HKDF_INFO = new TextEncoder().encode('dweb-connect-v1');
|
|
28
|
+
/**
|
|
29
|
+
* Generate an ephemeral P-256 ECDH keypair for a single connect session.
|
|
30
|
+
*
|
|
31
|
+
* @returns The keypair and the public key as a base64url-encoded raw export.
|
|
32
|
+
*/
|
|
33
|
+
export function generateEphemeralKeyPair() {
|
|
34
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
const keyPair = yield crypto.subtle.generateKey(ECDH_PARAMS, false, ['deriveKey']);
|
|
36
|
+
const rawPub = yield crypto.subtle.exportKey('raw', keyPair.publicKey);
|
|
37
|
+
return { keyPair, publicKeyBase64url: toBase64url(new Uint8Array(rawPub)) };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Derive a shared AES-256-GCM key from a local ECDH private key and a
|
|
42
|
+
* remote public key.
|
|
43
|
+
*/
|
|
44
|
+
function deriveSharedKey(localPrivateKey, remotePublicKeyBase64url) {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
const remotePubRaw = fromBase64url(remotePublicKeyBase64url);
|
|
47
|
+
const remotePublicKey = yield crypto.subtle.importKey('raw', remotePubRaw, ECDH_PARAMS, false, []);
|
|
48
|
+
// ECDH → raw shared secret, then HKDF → AES-256-GCM key.
|
|
49
|
+
const sharedBits = yield crypto.subtle.deriveBits({ name: 'ECDH', public: remotePublicKey }, localPrivateKey, 256);
|
|
50
|
+
const hkdfKey = yield crypto.subtle.importKey('raw', sharedBits, 'HKDF', false, ['deriveKey']);
|
|
51
|
+
return crypto.subtle.deriveKey({ name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(32), info: HKDF_INFO }, hkdfKey, AES_KEY_PARAMS, false, ['encrypt', 'decrypt']);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Encrypt a JSON-serializable payload for postMessage transport.
|
|
56
|
+
*
|
|
57
|
+
* Called by the **wallet** side. Uses the wallet's ephemeral private key
|
|
58
|
+
* and the dapp's ephemeral public key to derive the shared AES key.
|
|
59
|
+
*
|
|
60
|
+
* @returns The encrypted payload plus the wallet's public key (for the dapp to derive the same shared key).
|
|
61
|
+
*/
|
|
62
|
+
export function encryptPostMessagePayload(payload, walletKeyPair, walletPublicKeyBase64url, dappPublicKeyBase64url) {
|
|
63
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
const sharedKey = yield deriveSharedKey(walletKeyPair.privateKey, dappPublicKeyBase64url);
|
|
65
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
66
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
67
|
+
const ciphertextBuf = yield crypto.subtle.encrypt({ name: 'AES-GCM', iv }, sharedKey, plaintext);
|
|
68
|
+
return {
|
|
69
|
+
ciphertext: toBase64url(new Uint8Array(ciphertextBuf)),
|
|
70
|
+
iv: toBase64url(iv),
|
|
71
|
+
walletPublicKey: walletPublicKeyBase64url,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Decrypt a postMessage payload received from the wallet.
|
|
77
|
+
*
|
|
78
|
+
* Called by the **dapp** side. Uses the dapp's ephemeral private key
|
|
79
|
+
* and the wallet's ephemeral public key to derive the same shared key.
|
|
80
|
+
*/
|
|
81
|
+
export function decryptPostMessagePayload(encrypted, dappKeyPair) {
|
|
82
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
83
|
+
const sharedKey = yield deriveSharedKey(dappKeyPair.privateKey, encrypted.walletPublicKey);
|
|
84
|
+
const iv = fromBase64url(encrypted.iv);
|
|
85
|
+
const ciphertext = fromBase64url(encrypted.ciphertext);
|
|
86
|
+
const plaintextBuf = yield crypto.subtle.decrypt({ name: 'AES-GCM', iv }, sharedKey, ciphertext);
|
|
87
|
+
return JSON.parse(new TextDecoder().decode(plaintextBuf));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// ── Base64url helpers ────────────────────────────────────────────
|
|
91
|
+
function toBase64url(bytes) {
|
|
92
|
+
let binary = '';
|
|
93
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
94
|
+
binary += String.fromCharCode(bytes[i]);
|
|
95
|
+
}
|
|
96
|
+
let b64 = btoa(binary).replace(/\+/g, '-').replace(/\//g, '_');
|
|
97
|
+
// Strip trailing padding without a backtracking-vulnerable regex.
|
|
98
|
+
while (b64.endsWith('=')) {
|
|
99
|
+
b64 = b64.slice(0, -1);
|
|
100
|
+
}
|
|
101
|
+
return b64;
|
|
102
|
+
}
|
|
103
|
+
function fromBase64url(str) {
|
|
104
|
+
const padded = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
105
|
+
const binary = atob(padded);
|
|
106
|
+
const bytes = new Uint8Array(binary.length);
|
|
107
|
+
for (let i = 0; i < binary.length; i++) {
|
|
108
|
+
bytes[i] = binary.charCodeAt(i);
|
|
109
|
+
}
|
|
110
|
+
return bytes;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=dweb-connect-crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dweb-connect-crypto.js","sourceRoot":"","sources":["../../src/dweb-connect-crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;;;;;;;;;;AAeH,MAAM,WAAW,GAAmB,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;AAC1E,MAAM,cAAc,GAAwB,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AAC7E,MAAM,SAAS,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAE9D;;;;GAIG;AACH,MAAM,UAAgB,wBAAwB;;QAI5C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;QACnF,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QACvE,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,WAAW,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;IAC9E,CAAC;CAAA;AAED;;;GAGG;AACH,SAAe,eAAe,CAC5B,eAA0B,EAC1B,wBAAgC;;QAEhC,MAAM,YAAY,GAAG,aAAa,CAAC,wBAAwB,CAAC,CAAC;QAC7D,MAAM,eAAe,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACnD,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,CAC5C,CAAC;QAEF,yDAAyD;QACzD,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,UAAU,CAC/C,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAyB,EAChE,eAAe,EACf,GAAG,CACJ,CAAC;QAEF,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAC3C,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,WAAW,CAAC,CAChD,CAAC;QAEF,OAAO,MAAM,CAAC,MAAM,CAAC,SAAS,CAC5B,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,UAAU,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAC5E,OAAO,EACP,cAAc,EACd,KAAK,EACL,CAAC,SAAS,EAAE,SAAS,CAAC,CACvB,CAAC;IACJ,CAAC;CAAA;AAED;;;;;;;GAOG;AACH,MAAM,UAAgB,yBAAyB,CAC7C,OAAgC,EAChC,aAA4B,EAC5B,wBAAgC,EAChC,sBAA8B;;QAE9B,MAAM,SAAS,GAAG,MAAM,eAAe,CAAC,aAAa,CAAC,UAAU,EAAE,sBAAsB,CAAC,CAAC;QAC1F,MAAM,EAAE,GAAG,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;QACtD,MAAM,SAAS,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;QAEpE,MAAM,aAAa,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,OAAO,CAC/C,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAkB,EACvC,SAAS,EACT,SAAS,CACV,CAAC;QAEF,OAAO;YACL,UAAU,EAAQ,WAAW,CAAC,IAAI,UAAU,CAAC,aAAa,CAAC,CAAC;YAC5D,EAAE,EAAgB,WAAW,CAAC,EAAE,CAAC;YACjC,eAAe,EAAG,wBAAwB;SAC3C,CAAC;IACJ,CAAC;CAAA;AAED;;;;;GAKG;AACH,MAAM,UAAgB,yBAAyB,CAC7C,SAAsC,EACtC,WAA0B;;QAE1B,MAAM,SAAS,GAAG,MAAM,eAAe,CAAC,WAAW,CAAC,UAAU,EAAE,SAAS,CAAC,eAAe,CAAC,CAAC;QAC3F,MAAM,EAAE,GAAG,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QACvC,MAAM,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QAEvD,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,OAAO,CAC9C,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAkB,EACvC,SAAS,EACT,UAAU,CACX,CAAC;QAEF,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;IAC5D,CAAC;CAAA;AAED,oEAAoE;AAEpE,SAAS,WAAW,CAAC,KAAiB;IACpC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,IAAI,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC/D,kEAAkE;IAClE,OAAO,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAAC,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IACrD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,aAAa,CAAC,GAAW;IAChC,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './web-features.js';
|
|
2
2
|
export { BrowserConnectHandler, DEFAULT_WALLETS } from './browser-connect-handler.js';
|
|
3
3
|
export { DWebConnect } from './dweb-connect-client.js';
|
|
4
|
+
export { encryptPostMessagePayload, generateEphemeralKeyPair } from './dweb-connect-crypto.js';
|
|
4
5
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAEtF,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAEtF,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,OAAO,EAAE,yBAAyB,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dweb-connect-client.d.ts","sourceRoot":"","sources":["../../src/dweb-connect-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,KAAK,EAAE,wBAAwB,EAAqC,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"dweb-connect-client.d.ts","sourceRoot":"","sources":["../../src/dweb-connect-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,KAAK,EAAE,wBAAwB,EAAqC,MAAM,cAAc,CAAC;AAKhG,4DAA4D;AAC5D,MAAM,WAAW,wBAAwB;IACvC,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAElB,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,kBAAkB,EAAE,wBAAwB,EAAE,CAAC;IAE/C;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,iBAAe,UAAU,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAoJ/F;AAED;;;;;;;;;;;GAWG;AACH,iBAAe,kBAAkB,CAC/B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,OAAO,SAAQ,GACd,OAAO,CAAC,OAAO,CAAC,CA+ClB;AAED,eAAO,MAAM,WAAW;;;CAAqC,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECDH + AES-256-GCM encryption for the DWeb Connect postMessage channel.
|
|
3
|
+
*
|
|
4
|
+
* Protects the authorization response (delegate private keys, decryption
|
|
5
|
+
* keys, grants) from same-origin XSS interception. Both the dapp and
|
|
6
|
+
* wallet generate ephemeral P-256 keypairs, exchange public keys during
|
|
7
|
+
* the handshake, derive a shared AES-256-GCM key via ECDH + HKDF, and
|
|
8
|
+
* encrypt/decrypt the payload.
|
|
9
|
+
*
|
|
10
|
+
* P-256 is used (instead of X25519) because `crypto.subtle.deriveKey`
|
|
11
|
+
* with ECDH requires a NIST curve in all major browsers. The keys are
|
|
12
|
+
* ephemeral — generated fresh for each connect session.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
/** Exported public key as raw bytes (base64url-encoded for postMessage). */
|
|
17
|
+
export type ExportedPublicKey = string;
|
|
18
|
+
/** Encrypted payload sent via postMessage. */
|
|
19
|
+
export interface EncryptedPostMessagePayload {
|
|
20
|
+
/** The encrypted JSON payload as base64url. */
|
|
21
|
+
ciphertext: string;
|
|
22
|
+
/** AES-GCM initialization vector as base64url. */
|
|
23
|
+
iv: string;
|
|
24
|
+
/** The wallet's ephemeral P-256 public key as base64url raw bytes. */
|
|
25
|
+
walletPublicKey: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate an ephemeral P-256 ECDH keypair for a single connect session.
|
|
29
|
+
*
|
|
30
|
+
* @returns The keypair and the public key as a base64url-encoded raw export.
|
|
31
|
+
*/
|
|
32
|
+
export declare function generateEphemeralKeyPair(): Promise<{
|
|
33
|
+
keyPair: CryptoKeyPair;
|
|
34
|
+
publicKeyBase64url: ExportedPublicKey;
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Encrypt a JSON-serializable payload for postMessage transport.
|
|
38
|
+
*
|
|
39
|
+
* Called by the **wallet** side. Uses the wallet's ephemeral private key
|
|
40
|
+
* and the dapp's ephemeral public key to derive the shared AES key.
|
|
41
|
+
*
|
|
42
|
+
* @returns The encrypted payload plus the wallet's public key (for the dapp to derive the same shared key).
|
|
43
|
+
*/
|
|
44
|
+
export declare function encryptPostMessagePayload(payload: Record<string, unknown>, walletKeyPair: CryptoKeyPair, walletPublicKeyBase64url: string, dappPublicKeyBase64url: string): Promise<EncryptedPostMessagePayload>;
|
|
45
|
+
/**
|
|
46
|
+
* Decrypt a postMessage payload received from the wallet.
|
|
47
|
+
*
|
|
48
|
+
* Called by the **dapp** side. Uses the dapp's ephemeral private key
|
|
49
|
+
* and the wallet's ephemeral public key to derive the same shared key.
|
|
50
|
+
*/
|
|
51
|
+
export declare function decryptPostMessagePayload(encrypted: EncryptedPostMessagePayload, dappKeyPair: CryptoKeyPair): Promise<Record<string, unknown>>;
|
|
52
|
+
//# sourceMappingURL=dweb-connect-crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dweb-connect-crypto.d.ts","sourceRoot":"","sources":["../../src/dweb-connect-crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAEvC,8CAA8C;AAC9C,MAAM,WAAW,2BAA2B;IAC1C,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,EAAE,EAAE,MAAM,CAAC;IACX,sEAAsE;IACtE,eAAe,EAAE,MAAM,CAAC;CACzB;AAMD;;;;GAIG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC;IACxD,OAAO,EAAE,aAAa,CAAC;IACvB,kBAAkB,EAAE,iBAAiB,CAAC;CACvC,CAAC,CAID;AAmCD;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,aAAa,EAAE,aAAa,EAC5B,wBAAwB,EAAE,MAAM,EAChC,sBAAsB,EAAE,MAAM,GAC7B,OAAO,CAAC,2BAA2B,CAAC,CAgBtC;AAED;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,2BAA2B,EACtC,WAAW,EAAE,aAAa,GACzB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAYlC"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -3,4 +3,6 @@ export { BrowserConnectHandler, DEFAULT_WALLETS } from './browser-connect-handle
|
|
|
3
3
|
export type { BrowserConnectHandlerOptions, WalletOption } from './browser-connect-handler.js';
|
|
4
4
|
export { DWebConnect } from './dweb-connect-client.js';
|
|
5
5
|
export type { DWebConnectClientOptions } from './dweb-connect-client.js';
|
|
6
|
+
export { encryptPostMessagePayload, generateEphemeralKeyPair } from './dweb-connect-crypto.js';
|
|
7
|
+
export type { EncryptedPostMessagePayload } from './dweb-connect-crypto.js';
|
|
6
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AACtF,YAAY,EAAE,4BAA4B,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC/F,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AACtF,YAAY,EAAE,4BAA4B,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC/F,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAE,yBAAyB,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAC/F,YAAY,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enbox/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Enbox tools and features to use in the browser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/esm/index.js",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@enbox/agent": "0.6.
|
|
59
|
-
"@enbox/auth": "0.6.
|
|
58
|
+
"@enbox/agent": "0.6.2",
|
|
59
|
+
"@enbox/auth": "0.6.22",
|
|
60
60
|
"@enbox/dids": "0.1.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
@@ -12,6 +12,9 @@ import type { ConnectResult } from '@enbox/auth';
|
|
|
12
12
|
import type { PortableDid } from '@enbox/dids';
|
|
13
13
|
import type { ConnectPermissionRequest, DwnDataEncodedRecordsWriteMessage } from '@enbox/agent';
|
|
14
14
|
|
|
15
|
+
import type { EncryptedPostMessagePayload } from './dweb-connect-crypto.js';
|
|
16
|
+
import { decryptPostMessagePayload, generateEphemeralKeyPair } from './dweb-connect-crypto.js';
|
|
17
|
+
|
|
15
18
|
/** Options for initiating a DWeb Connect flow via popup. */
|
|
16
19
|
export interface DWebConnectClientOptions {
|
|
17
20
|
/** Base URL of the wallet app (e.g. "https://wallet.enbox.org"). */
|
|
@@ -76,6 +79,20 @@ async function initClient(options: DWebConnectClientOptions): Promise<ConnectRes
|
|
|
76
79
|
);
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
// Generate an ephemeral ECDH keypair for this connect session.
|
|
83
|
+
// The public key is sent to the wallet so it can encrypt the response
|
|
84
|
+
// containing delegate private keys and decryption material.
|
|
85
|
+
let dappKeyPair: CryptoKeyPair | undefined;
|
|
86
|
+
let dappPublicKeyBase64url: string | undefined;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const ephemeral = await generateEphemeralKeyPair();
|
|
90
|
+
dappKeyPair = ephemeral.keyPair;
|
|
91
|
+
dappPublicKeyBase64url = ephemeral.publicKeyBase64url;
|
|
92
|
+
} catch {
|
|
93
|
+
// crypto.subtle unavailable (e.g. non-secure context) — skip encryption.
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
return new Promise<ConnectResult | undefined>((resolve, reject) => {
|
|
80
97
|
let settled = false;
|
|
81
98
|
|
|
@@ -113,11 +130,12 @@ async function initClient(options: DWebConnectClientOptions): Promise<ConnectRes
|
|
|
113
130
|
const { type } = event.data ?? {};
|
|
114
131
|
|
|
115
132
|
if (type === 'dweb-connect-loaded') {
|
|
116
|
-
// Wallet is ready — send the authorization request.
|
|
133
|
+
// Wallet is ready — send the authorization request with ephemeral public key.
|
|
117
134
|
popup.postMessage({
|
|
118
|
-
type
|
|
135
|
+
type : 'dweb-connect-authorization-request',
|
|
119
136
|
did,
|
|
120
|
-
permissions
|
|
137
|
+
permissions : permissionRequests,
|
|
138
|
+
ephemeralPublicKey : dappPublicKeyBase64url,
|
|
121
139
|
}, walletOrigin);
|
|
122
140
|
return;
|
|
123
141
|
}
|
|
@@ -126,19 +144,55 @@ async function initClient(options: DWebConnectClientOptions): Promise<ConnectRes
|
|
|
126
144
|
clearInterval(pollClosed);
|
|
127
145
|
cleanup();
|
|
128
146
|
|
|
129
|
-
|
|
147
|
+
// Handle encrypted response (wallet supports ECDH channel encryption).
|
|
148
|
+
const encrypted = event.data.encryptedPayload as EncryptedPostMessagePayload | undefined;
|
|
149
|
+
if (encrypted && dappKeyPair) {
|
|
150
|
+
decryptPostMessagePayload(encrypted, dappKeyPair).then((payload: Record<string, unknown>) => {
|
|
151
|
+
const p = payload as any;
|
|
152
|
+
if (!p.delegateDid || !p.grants) {
|
|
153
|
+
resolve(undefined);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
resolve({
|
|
157
|
+
delegatePortableDid : p.delegateDid as PortableDid,
|
|
158
|
+
delegateGrants : p.grants as DwnDataEncodedRecordsWriteMessage[],
|
|
159
|
+
connectedDid : p.connectedDid ?? did ?? (p.delegateDid as PortableDid).uri,
|
|
160
|
+
delegateDecryptionKeys : p.delegateDecryptionKeys ?? undefined,
|
|
161
|
+
delegateContextKeys : p.delegateContextKeys ?? undefined,
|
|
162
|
+
delegateMultiPartyProtocols : p.delegateMultiPartyProtocols ?? undefined,
|
|
163
|
+
sessionRevocations : p.sessionRevocations ?? undefined,
|
|
164
|
+
});
|
|
165
|
+
}).catch(() => {
|
|
166
|
+
// Decryption failed — treat as denied.
|
|
167
|
+
resolve(undefined);
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Plaintext fallback (wallet doesn't support encryption yet).
|
|
173
|
+
const {
|
|
174
|
+
delegateDid,
|
|
175
|
+
connectedDid: walletConnectedDid,
|
|
176
|
+
grants,
|
|
177
|
+
delegateDecryptionKeys,
|
|
178
|
+
delegateContextKeys,
|
|
179
|
+
delegateMultiPartyProtocols,
|
|
180
|
+
sessionRevocations,
|
|
181
|
+
} = event.data;
|
|
130
182
|
|
|
131
183
|
if (!delegateDid || !grants) {
|
|
132
|
-
// User denied the request.
|
|
133
184
|
resolve(undefined);
|
|
134
185
|
return;
|
|
135
186
|
}
|
|
136
187
|
|
|
137
|
-
// connectedDid priority: wallet response > dapp-provided > delegate DID
|
|
138
188
|
resolve({
|
|
139
|
-
delegatePortableDid
|
|
140
|
-
delegateGrants
|
|
141
|
-
connectedDid
|
|
189
|
+
delegatePortableDid : delegateDid as PortableDid,
|
|
190
|
+
delegateGrants : grants as DwnDataEncodedRecordsWriteMessage[],
|
|
191
|
+
connectedDid : walletConnectedDid ?? did ?? delegateDid.uri,
|
|
192
|
+
delegateDecryptionKeys : delegateDecryptionKeys ?? undefined,
|
|
193
|
+
delegateContextKeys : delegateContextKeys ?? undefined,
|
|
194
|
+
delegateMultiPartyProtocols : delegateMultiPartyProtocols ?? undefined,
|
|
195
|
+
sessionRevocations : sessionRevocations ?? undefined,
|
|
142
196
|
});
|
|
143
197
|
}
|
|
144
198
|
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECDH + AES-256-GCM encryption for the DWeb Connect postMessage channel.
|
|
3
|
+
*
|
|
4
|
+
* Protects the authorization response (delegate private keys, decryption
|
|
5
|
+
* keys, grants) from same-origin XSS interception. Both the dapp and
|
|
6
|
+
* wallet generate ephemeral P-256 keypairs, exchange public keys during
|
|
7
|
+
* the handshake, derive a shared AES-256-GCM key via ECDH + HKDF, and
|
|
8
|
+
* encrypt/decrypt the payload.
|
|
9
|
+
*
|
|
10
|
+
* P-256 is used (instead of X25519) because `crypto.subtle.deriveKey`
|
|
11
|
+
* with ECDH requires a NIST curve in all major browsers. The keys are
|
|
12
|
+
* ephemeral — generated fresh for each connect session.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Exported public key as raw bytes (base64url-encoded for postMessage). */
|
|
18
|
+
export type ExportedPublicKey = string;
|
|
19
|
+
|
|
20
|
+
/** Encrypted payload sent via postMessage. */
|
|
21
|
+
export interface EncryptedPostMessagePayload {
|
|
22
|
+
/** The encrypted JSON payload as base64url. */
|
|
23
|
+
ciphertext: string;
|
|
24
|
+
/** AES-GCM initialization vector as base64url. */
|
|
25
|
+
iv: string;
|
|
26
|
+
/** The wallet's ephemeral P-256 public key as base64url raw bytes. */
|
|
27
|
+
walletPublicKey: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ECDH_PARAMS: EcKeyGenParams = { name: 'ECDH', namedCurve: 'P-256' };
|
|
31
|
+
const AES_KEY_PARAMS: AesDerivedKeyParams = { name: 'AES-GCM', length: 256 };
|
|
32
|
+
const HKDF_INFO = new TextEncoder().encode('dweb-connect-v1');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate an ephemeral P-256 ECDH keypair for a single connect session.
|
|
36
|
+
*
|
|
37
|
+
* @returns The keypair and the public key as a base64url-encoded raw export.
|
|
38
|
+
*/
|
|
39
|
+
export async function generateEphemeralKeyPair(): Promise<{
|
|
40
|
+
keyPair: CryptoKeyPair;
|
|
41
|
+
publicKeyBase64url: ExportedPublicKey;
|
|
42
|
+
}> {
|
|
43
|
+
const keyPair = await crypto.subtle.generateKey(ECDH_PARAMS, false, ['deriveKey']);
|
|
44
|
+
const rawPub = await crypto.subtle.exportKey('raw', keyPair.publicKey);
|
|
45
|
+
return { keyPair, publicKeyBase64url: toBase64url(new Uint8Array(rawPub)) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Derive a shared AES-256-GCM key from a local ECDH private key and a
|
|
50
|
+
* remote public key.
|
|
51
|
+
*/
|
|
52
|
+
async function deriveSharedKey(
|
|
53
|
+
localPrivateKey: CryptoKey,
|
|
54
|
+
remotePublicKeyBase64url: string,
|
|
55
|
+
): Promise<CryptoKey> {
|
|
56
|
+
const remotePubRaw = fromBase64url(remotePublicKeyBase64url);
|
|
57
|
+
const remotePublicKey = await crypto.subtle.importKey(
|
|
58
|
+
'raw', remotePubRaw, ECDH_PARAMS, false, [],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// ECDH → raw shared secret, then HKDF → AES-256-GCM key.
|
|
62
|
+
const sharedBits = await crypto.subtle.deriveBits(
|
|
63
|
+
{ name: 'ECDH', public: remotePublicKey } as EcdhKeyDeriveParams,
|
|
64
|
+
localPrivateKey,
|
|
65
|
+
256,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const hkdfKey = await crypto.subtle.importKey(
|
|
69
|
+
'raw', sharedBits, 'HKDF', false, ['deriveKey'],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return crypto.subtle.deriveKey(
|
|
73
|
+
{ name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(32), info: HKDF_INFO },
|
|
74
|
+
hkdfKey,
|
|
75
|
+
AES_KEY_PARAMS,
|
|
76
|
+
false,
|
|
77
|
+
['encrypt', 'decrypt'],
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Encrypt a JSON-serializable payload for postMessage transport.
|
|
83
|
+
*
|
|
84
|
+
* Called by the **wallet** side. Uses the wallet's ephemeral private key
|
|
85
|
+
* and the dapp's ephemeral public key to derive the shared AES key.
|
|
86
|
+
*
|
|
87
|
+
* @returns The encrypted payload plus the wallet's public key (for the dapp to derive the same shared key).
|
|
88
|
+
*/
|
|
89
|
+
export async function encryptPostMessagePayload(
|
|
90
|
+
payload: Record<string, unknown>,
|
|
91
|
+
walletKeyPair: CryptoKeyPair,
|
|
92
|
+
walletPublicKeyBase64url: string,
|
|
93
|
+
dappPublicKeyBase64url: string,
|
|
94
|
+
): Promise<EncryptedPostMessagePayload> {
|
|
95
|
+
const sharedKey = await deriveSharedKey(walletKeyPair.privateKey, dappPublicKeyBase64url);
|
|
96
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
97
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(payload));
|
|
98
|
+
|
|
99
|
+
const ciphertextBuf = await crypto.subtle.encrypt(
|
|
100
|
+
{ name: 'AES-GCM', iv } as AesGcmParams,
|
|
101
|
+
sharedKey,
|
|
102
|
+
plaintext,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
ciphertext : toBase64url(new Uint8Array(ciphertextBuf)),
|
|
107
|
+
iv : toBase64url(iv),
|
|
108
|
+
walletPublicKey : walletPublicKeyBase64url,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Decrypt a postMessage payload received from the wallet.
|
|
114
|
+
*
|
|
115
|
+
* Called by the **dapp** side. Uses the dapp's ephemeral private key
|
|
116
|
+
* and the wallet's ephemeral public key to derive the same shared key.
|
|
117
|
+
*/
|
|
118
|
+
export async function decryptPostMessagePayload(
|
|
119
|
+
encrypted: EncryptedPostMessagePayload,
|
|
120
|
+
dappKeyPair: CryptoKeyPair,
|
|
121
|
+
): Promise<Record<string, unknown>> {
|
|
122
|
+
const sharedKey = await deriveSharedKey(dappKeyPair.privateKey, encrypted.walletPublicKey);
|
|
123
|
+
const iv = fromBase64url(encrypted.iv);
|
|
124
|
+
const ciphertext = fromBase64url(encrypted.ciphertext);
|
|
125
|
+
|
|
126
|
+
const plaintextBuf = await crypto.subtle.decrypt(
|
|
127
|
+
{ name: 'AES-GCM', iv } as AesGcmParams,
|
|
128
|
+
sharedKey,
|
|
129
|
+
ciphertext,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return JSON.parse(new TextDecoder().decode(plaintextBuf));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Base64url helpers ────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function toBase64url(bytes: Uint8Array): string {
|
|
138
|
+
let binary = '';
|
|
139
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
140
|
+
binary += String.fromCharCode(bytes[i]);
|
|
141
|
+
}
|
|
142
|
+
let b64 = btoa(binary).replace(/\+/g, '-').replace(/\//g, '_');
|
|
143
|
+
// Strip trailing padding without a backtracking-vulnerable regex.
|
|
144
|
+
while (b64.endsWith('=')) { b64 = b64.slice(0, -1); }
|
|
145
|
+
return b64;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function fromBase64url(str: string): Uint8Array {
|
|
149
|
+
const padded = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
150
|
+
const binary = atob(padded);
|
|
151
|
+
const bytes = new Uint8Array(binary.length);
|
|
152
|
+
for (let i = 0; i < binary.length; i++) {
|
|
153
|
+
bytes[i] = binary.charCodeAt(i);
|
|
154
|
+
}
|
|
155
|
+
return bytes;
|
|
156
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,4 +2,6 @@ export * from './web-features.js';
|
|
|
2
2
|
export { BrowserConnectHandler, DEFAULT_WALLETS } from './browser-connect-handler.js';
|
|
3
3
|
export type { BrowserConnectHandlerOptions, WalletOption } from './browser-connect-handler.js';
|
|
4
4
|
export { DWebConnect } from './dweb-connect-client.js';
|
|
5
|
-
export type { DWebConnectClientOptions } from './dweb-connect-client.js';
|
|
5
|
+
export type { DWebConnectClientOptions } from './dweb-connect-client.js';
|
|
6
|
+
export { encryptPostMessagePayload, generateEphemeralKeyPair } from './dweb-connect-crypto.js';
|
|
7
|
+
export type { EncryptedPostMessagePayload } from './dweb-connect-crypto.js';
|