@bandeira-tech/b3nd-web 0.2.3 → 0.2.4
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/chunk-F3W5GZU6.js +1775 -0
- package/dist/{chunk-K3ZSSVHR.js → chunk-JN75UL5C.js} +2 -0
- package/dist/core-CgxQpSVM.d.ts +275 -0
- package/dist/encrypt/mod.d.ts +1 -1
- package/dist/encrypt/mod.js +1 -1
- package/dist/{mod-D02790g_.d.ts → mod-CII9wqu2.d.ts} +1 -1
- package/dist/src/mod.web.d.ts +1 -1
- package/dist/src/mod.web.js +4 -4
- package/dist/wallet-server/adapters/browser.d.ts +76 -0
- package/dist/wallet-server/adapters/browser.js +105 -0
- package/dist/wallet-server/mod.d.ts +320 -0
- package/dist/wallet-server/mod.js +77 -0
- package/package.json +13 -3
|
@@ -0,0 +1,1775 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAuthenticatedMessage,
|
|
3
|
+
createSignedEncryptedMessage,
|
|
4
|
+
decodeHex,
|
|
5
|
+
decrypt,
|
|
6
|
+
encodeHex,
|
|
7
|
+
encrypt,
|
|
8
|
+
verifyPayload
|
|
9
|
+
} from "./chunk-JN75UL5C.js";
|
|
10
|
+
import {
|
|
11
|
+
HttpClient
|
|
12
|
+
} from "./chunk-LFUC4ETD.js";
|
|
13
|
+
|
|
14
|
+
// wallet-server/interfaces.ts
|
|
15
|
+
var defaultLogger = {
|
|
16
|
+
log: (...args) => console.log(...args),
|
|
17
|
+
warn: (...args) => console.warn(...args),
|
|
18
|
+
error: (...args) => console.error(...args)
|
|
19
|
+
};
|
|
20
|
+
var MemoryFileStorage = class {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.storage = /* @__PURE__ */ new Map();
|
|
23
|
+
}
|
|
24
|
+
async readTextFile(path) {
|
|
25
|
+
const content = this.storage.get(path);
|
|
26
|
+
if (content === void 0) {
|
|
27
|
+
throw new Error(`File not found: ${path}`);
|
|
28
|
+
}
|
|
29
|
+
return content;
|
|
30
|
+
}
|
|
31
|
+
async writeTextFile(path, content) {
|
|
32
|
+
this.storage.set(path, content);
|
|
33
|
+
}
|
|
34
|
+
async exists(path) {
|
|
35
|
+
return this.storage.has(path);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var ConfigEnvironment = class {
|
|
39
|
+
constructor(config = {}) {
|
|
40
|
+
this.config = config;
|
|
41
|
+
}
|
|
42
|
+
get(key) {
|
|
43
|
+
return this.config[key];
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// wallet-server/jwt.ts
|
|
48
|
+
function base64urlEncode(data) {
|
|
49
|
+
const base64 = btoa(data);
|
|
50
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
51
|
+
}
|
|
52
|
+
function base64urlDecode(encoded) {
|
|
53
|
+
const padded = encoded.replace(/-/g, "+").replace(/_/g, "/").padEnd(encoded.length + (4 - encoded.length % 4) % 4, "=");
|
|
54
|
+
return atob(padded);
|
|
55
|
+
}
|
|
56
|
+
async function createJwt(username, secret, expirationSeconds) {
|
|
57
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
58
|
+
const header = {
|
|
59
|
+
alg: "HS256",
|
|
60
|
+
typ: "JWT"
|
|
61
|
+
};
|
|
62
|
+
const payload = {
|
|
63
|
+
username,
|
|
64
|
+
iat: now,
|
|
65
|
+
exp: now + expirationSeconds,
|
|
66
|
+
type: "access"
|
|
67
|
+
};
|
|
68
|
+
const headerEncoded = base64urlEncode(JSON.stringify(header));
|
|
69
|
+
const payloadEncoded = base64urlEncode(JSON.stringify(payload));
|
|
70
|
+
const message = `${headerEncoded}.${payloadEncoded}`;
|
|
71
|
+
const encoder = new TextEncoder();
|
|
72
|
+
const key = await crypto.subtle.importKey(
|
|
73
|
+
"raw",
|
|
74
|
+
encoder.encode(secret),
|
|
75
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
76
|
+
false,
|
|
77
|
+
["sign"]
|
|
78
|
+
);
|
|
79
|
+
const signature = await crypto.subtle.sign(
|
|
80
|
+
"HMAC",
|
|
81
|
+
key,
|
|
82
|
+
encoder.encode(message)
|
|
83
|
+
);
|
|
84
|
+
const signatureEncoded = base64urlEncode(
|
|
85
|
+
String.fromCharCode(...new Uint8Array(signature))
|
|
86
|
+
);
|
|
87
|
+
return `${message}.${signatureEncoded}`;
|
|
88
|
+
}
|
|
89
|
+
async function verifyJwt(token, secret) {
|
|
90
|
+
const parts = token.split(".");
|
|
91
|
+
if (parts.length !== 3) {
|
|
92
|
+
throw new Error("Invalid JWT format");
|
|
93
|
+
}
|
|
94
|
+
const [headerEncoded, payloadEncoded, signatureEncoded] = parts;
|
|
95
|
+
const message = `${headerEncoded}.${payloadEncoded}`;
|
|
96
|
+
const encoder = new TextEncoder();
|
|
97
|
+
const key = await crypto.subtle.importKey(
|
|
98
|
+
"raw",
|
|
99
|
+
encoder.encode(secret),
|
|
100
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
101
|
+
false,
|
|
102
|
+
["verify"]
|
|
103
|
+
);
|
|
104
|
+
const signaturePadded = signatureEncoded.replace(/-/g, "+").replace(/_/g, "/").padEnd(signatureEncoded.length + (4 - signatureEncoded.length % 4) % 4, "=");
|
|
105
|
+
const signatureBytes = new Uint8Array(
|
|
106
|
+
atob(signaturePadded).split("").map((c) => c.charCodeAt(0))
|
|
107
|
+
);
|
|
108
|
+
const isValid = await crypto.subtle.verify(
|
|
109
|
+
"HMAC",
|
|
110
|
+
key,
|
|
111
|
+
signatureBytes,
|
|
112
|
+
encoder.encode(message)
|
|
113
|
+
);
|
|
114
|
+
if (!isValid) {
|
|
115
|
+
throw new Error("Invalid JWT signature");
|
|
116
|
+
}
|
|
117
|
+
const payloadJson = base64urlDecode(payloadEncoded);
|
|
118
|
+
const payload = JSON.parse(payloadJson);
|
|
119
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
120
|
+
if (payload.exp < now) {
|
|
121
|
+
throw new Error("JWT token has expired");
|
|
122
|
+
}
|
|
123
|
+
if (payload.type !== "access") {
|
|
124
|
+
throw new Error("Invalid token type");
|
|
125
|
+
}
|
|
126
|
+
return payload;
|
|
127
|
+
}
|
|
128
|
+
function extractUsernameFromJwt(token) {
|
|
129
|
+
try {
|
|
130
|
+
const parts = token.split(".");
|
|
131
|
+
if (parts.length !== 3) return null;
|
|
132
|
+
const payloadJson = base64urlDecode(parts[1]);
|
|
133
|
+
const payload = JSON.parse(payloadJson);
|
|
134
|
+
return payload.username;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// wallet-server/obfuscation.ts
|
|
141
|
+
async function deriveObfuscatedPath(serverPublicKey, username, operationType, ...params) {
|
|
142
|
+
const encoder = new TextEncoder();
|
|
143
|
+
const parts = [username, operationType, serverPublicKey, ...params];
|
|
144
|
+
const input = parts.join("|");
|
|
145
|
+
const key = await crypto.subtle.importKey(
|
|
146
|
+
"raw",
|
|
147
|
+
encoder.encode(serverPublicKey),
|
|
148
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
149
|
+
false,
|
|
150
|
+
["sign"]
|
|
151
|
+
);
|
|
152
|
+
const signature = await crypto.subtle.sign(
|
|
153
|
+
"HMAC",
|
|
154
|
+
key,
|
|
155
|
+
encoder.encode(input)
|
|
156
|
+
);
|
|
157
|
+
return encodeHex(new Uint8Array(signature)).substring(0, 32);
|
|
158
|
+
}
|
|
159
|
+
async function pemToCryptoKey(pem, algorithm = "Ed25519") {
|
|
160
|
+
const base64 = pem.split("\n").filter((line) => !line.startsWith("-----")).join("");
|
|
161
|
+
const binaryString = atob(base64);
|
|
162
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
163
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
164
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
165
|
+
}
|
|
166
|
+
const buffer = bytes.buffer.slice(
|
|
167
|
+
bytes.byteOffset,
|
|
168
|
+
bytes.byteOffset + bytes.byteLength
|
|
169
|
+
);
|
|
170
|
+
if (algorithm === "Ed25519") {
|
|
171
|
+
return await crypto.subtle.importKey(
|
|
172
|
+
"pkcs8",
|
|
173
|
+
buffer,
|
|
174
|
+
{ name: "Ed25519", namedCurve: "Ed25519" },
|
|
175
|
+
false,
|
|
176
|
+
["sign"]
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
return await crypto.subtle.importKey(
|
|
180
|
+
"pkcs8",
|
|
181
|
+
buffer,
|
|
182
|
+
{ name: "X25519", namedCurve: "X25519" },
|
|
183
|
+
false,
|
|
184
|
+
["deriveBits"]
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function createSignedEncryptedPayload(data, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex) {
|
|
189
|
+
const identityPrivateKey = await pemToCryptoKey(
|
|
190
|
+
serverIdentityPrivateKeyPem,
|
|
191
|
+
"Ed25519"
|
|
192
|
+
);
|
|
193
|
+
return await createSignedEncryptedMessage(
|
|
194
|
+
data,
|
|
195
|
+
[{ privateKey: identityPrivateKey, publicKeyHex: serverIdentityPublicKeyHex }],
|
|
196
|
+
serverEncryptionPublicKeyHex
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
async function decryptSignedEncryptedPayload(signedMessage, serverEncryptionPrivateKeyPem) {
|
|
200
|
+
const encryptionPrivateKey = await pemToCryptoKey(
|
|
201
|
+
serverEncryptionPrivateKeyPem,
|
|
202
|
+
"X25519"
|
|
203
|
+
);
|
|
204
|
+
const { verified, signers } = await verifyPayload({
|
|
205
|
+
payload: signedMessage.payload,
|
|
206
|
+
auth: signedMessage.auth
|
|
207
|
+
});
|
|
208
|
+
const data = await decrypt(signedMessage.payload, encryptionPrivateKey);
|
|
209
|
+
return { data, verified, signers };
|
|
210
|
+
}
|
|
211
|
+
async function encryptForBackend(data, serverEncryptionPublicKeyHex) {
|
|
212
|
+
return await encrypt(data, serverEncryptionPublicKeyHex);
|
|
213
|
+
}
|
|
214
|
+
async function decryptFromBackend(encryptedPayload, serverEncryptionPrivateKeyPem) {
|
|
215
|
+
const privateKey = await pemToCryptoKey(
|
|
216
|
+
serverEncryptionPrivateKeyPem,
|
|
217
|
+
"X25519"
|
|
218
|
+
);
|
|
219
|
+
return await decrypt(encryptedPayload, privateKey);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// wallet-server/keys.ts
|
|
223
|
+
function bytesToBase64(bytes) {
|
|
224
|
+
return btoa(String.fromCharCode(...bytes));
|
|
225
|
+
}
|
|
226
|
+
async function generateAccountKeyPair() {
|
|
227
|
+
const keyPair = await crypto.subtle.generateKey("Ed25519", true, [
|
|
228
|
+
"sign",
|
|
229
|
+
"verify"
|
|
230
|
+
]);
|
|
231
|
+
const privateKeyBuffer = await crypto.subtle.exportKey(
|
|
232
|
+
"pkcs8",
|
|
233
|
+
keyPair.privateKey
|
|
234
|
+
);
|
|
235
|
+
const publicKeyBuffer = await crypto.subtle.exportKey(
|
|
236
|
+
"raw",
|
|
237
|
+
keyPair.publicKey
|
|
238
|
+
);
|
|
239
|
+
const privateKeyBase64 = bytesToBase64(new Uint8Array(privateKeyBuffer));
|
|
240
|
+
const privateKeyPem = `-----BEGIN PRIVATE KEY-----
|
|
241
|
+
${privateKeyBase64.match(/.{1,64}/g)?.join("\n")}
|
|
242
|
+
-----END PRIVATE KEY-----`;
|
|
243
|
+
const publicKeyHex = encodeHex(new Uint8Array(publicKeyBuffer));
|
|
244
|
+
return { privateKeyPem, publicKeyHex };
|
|
245
|
+
}
|
|
246
|
+
async function generateEncryptionKeyPair() {
|
|
247
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
248
|
+
{
|
|
249
|
+
name: "X25519",
|
|
250
|
+
namedCurve: "X25519"
|
|
251
|
+
},
|
|
252
|
+
true,
|
|
253
|
+
["deriveBits"]
|
|
254
|
+
);
|
|
255
|
+
const privateKeyBuffer = await crypto.subtle.exportKey(
|
|
256
|
+
"pkcs8",
|
|
257
|
+
keyPair.privateKey
|
|
258
|
+
);
|
|
259
|
+
const publicKeyBuffer = await crypto.subtle.exportKey(
|
|
260
|
+
"raw",
|
|
261
|
+
keyPair.publicKey
|
|
262
|
+
);
|
|
263
|
+
const privateKeyBase64 = bytesToBase64(new Uint8Array(privateKeyBuffer));
|
|
264
|
+
const privateKeyPem = `-----BEGIN PRIVATE KEY-----
|
|
265
|
+
${privateKeyBase64.match(/.{1,64}/g)?.join("\n")}
|
|
266
|
+
-----END PRIVATE KEY-----`;
|
|
267
|
+
const publicKeyHex = encodeHex(new Uint8Array(publicKeyBuffer));
|
|
268
|
+
return { privateKeyPem, publicKeyHex };
|
|
269
|
+
}
|
|
270
|
+
async function generateUserKeys(client, serverPublicKey, username, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex) {
|
|
271
|
+
const [accountKey, encryptionKey] = await Promise.all([
|
|
272
|
+
generateAccountKeyPair(),
|
|
273
|
+
generateEncryptionKeyPair()
|
|
274
|
+
]);
|
|
275
|
+
await Promise.all([
|
|
276
|
+
(async () => {
|
|
277
|
+
const path = await deriveObfuscatedPath(
|
|
278
|
+
serverPublicKey,
|
|
279
|
+
username,
|
|
280
|
+
"account-key"
|
|
281
|
+
);
|
|
282
|
+
const signed = await createSignedEncryptedPayload(
|
|
283
|
+
accountKey,
|
|
284
|
+
serverIdentityPrivateKeyPem,
|
|
285
|
+
serverIdentityPublicKeyHex,
|
|
286
|
+
serverEncryptionPublicKeyHex
|
|
287
|
+
);
|
|
288
|
+
await client.write(
|
|
289
|
+
`mutable://accounts/${serverPublicKey}/${path}`,
|
|
290
|
+
signed
|
|
291
|
+
);
|
|
292
|
+
})(),
|
|
293
|
+
(async () => {
|
|
294
|
+
const path = await deriveObfuscatedPath(
|
|
295
|
+
serverPublicKey,
|
|
296
|
+
username,
|
|
297
|
+
"encryption-key"
|
|
298
|
+
);
|
|
299
|
+
const signed = await createSignedEncryptedPayload(
|
|
300
|
+
encryptionKey,
|
|
301
|
+
serverIdentityPrivateKeyPem,
|
|
302
|
+
serverIdentityPublicKeyHex,
|
|
303
|
+
serverEncryptionPublicKeyHex
|
|
304
|
+
);
|
|
305
|
+
await client.write(
|
|
306
|
+
`mutable://accounts/${serverPublicKey}/${path}`,
|
|
307
|
+
signed
|
|
308
|
+
);
|
|
309
|
+
})()
|
|
310
|
+
]);
|
|
311
|
+
return { accountKey, encryptionKey };
|
|
312
|
+
}
|
|
313
|
+
async function loadUserAccountKey(client, serverPublicKey, username, serverEncryptionPrivateKeyPem, logger) {
|
|
314
|
+
const path = await deriveObfuscatedPath(
|
|
315
|
+
serverPublicKey,
|
|
316
|
+
username,
|
|
317
|
+
"account-key"
|
|
318
|
+
);
|
|
319
|
+
const result = await client.read(
|
|
320
|
+
`mutable://accounts/${serverPublicKey}/${path}`
|
|
321
|
+
);
|
|
322
|
+
if (!result.success || !result.record?.data) {
|
|
323
|
+
throw new Error("Account key not found");
|
|
324
|
+
}
|
|
325
|
+
const { data, verified } = await decryptSignedEncryptedPayload(
|
|
326
|
+
result.record.data,
|
|
327
|
+
serverEncryptionPrivateKeyPem
|
|
328
|
+
);
|
|
329
|
+
if (!verified) {
|
|
330
|
+
logger?.warn(
|
|
331
|
+
"Account key signature verification failed for user:",
|
|
332
|
+
username
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
return data;
|
|
336
|
+
}
|
|
337
|
+
async function loadUserEncryptionKey(client, serverPublicKey, username, serverEncryptionPrivateKeyPem, logger) {
|
|
338
|
+
const path = await deriveObfuscatedPath(
|
|
339
|
+
serverPublicKey,
|
|
340
|
+
username,
|
|
341
|
+
"encryption-key"
|
|
342
|
+
);
|
|
343
|
+
const result = await client.read(
|
|
344
|
+
`mutable://accounts/${serverPublicKey}/${path}`
|
|
345
|
+
);
|
|
346
|
+
if (!result.success || !result.record?.data) {
|
|
347
|
+
throw new Error("Encryption key not found");
|
|
348
|
+
}
|
|
349
|
+
const { data, verified } = await decryptSignedEncryptedPayload(
|
|
350
|
+
result.record.data,
|
|
351
|
+
serverEncryptionPrivateKeyPem
|
|
352
|
+
);
|
|
353
|
+
if (!verified) {
|
|
354
|
+
logger?.warn(
|
|
355
|
+
"Encryption key signature verification failed for user:",
|
|
356
|
+
username
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
return data;
|
|
360
|
+
}
|
|
361
|
+
async function loadUserKeys(client, serverPublicKey, username, serverEncryptionPrivateKeyPem, logger) {
|
|
362
|
+
const [accountKey, encryptionKey] = await Promise.all([
|
|
363
|
+
loadUserAccountKey(
|
|
364
|
+
client,
|
|
365
|
+
serverPublicKey,
|
|
366
|
+
username,
|
|
367
|
+
serverEncryptionPrivateKeyPem,
|
|
368
|
+
logger
|
|
369
|
+
),
|
|
370
|
+
loadUserEncryptionKey(
|
|
371
|
+
client,
|
|
372
|
+
serverPublicKey,
|
|
373
|
+
username,
|
|
374
|
+
serverEncryptionPrivateKeyPem,
|
|
375
|
+
logger
|
|
376
|
+
)
|
|
377
|
+
]);
|
|
378
|
+
return { accountKey, encryptionKey };
|
|
379
|
+
}
|
|
380
|
+
async function getUserPublicKeys(client, serverPublicKey, username, serverEncryptionPrivateKeyPem, logger) {
|
|
381
|
+
const { accountKey, encryptionKey } = await loadUserKeys(
|
|
382
|
+
client,
|
|
383
|
+
serverPublicKey,
|
|
384
|
+
username,
|
|
385
|
+
serverEncryptionPrivateKeyPem,
|
|
386
|
+
logger
|
|
387
|
+
);
|
|
388
|
+
return {
|
|
389
|
+
accountPublicKeyHex: accountKey.publicKeyHex,
|
|
390
|
+
encryptionPublicKeyHex: encryptionKey.publicKeyHex
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// wallet-server/proxy.ts
|
|
395
|
+
async function proxyWrite(proxyClient, credentialClient, serverPublicKey, username, serverEncryptionPrivateKeyPem, request) {
|
|
396
|
+
try {
|
|
397
|
+
const [accountKey, encryptionKey] = await Promise.all([
|
|
398
|
+
loadUserAccountKey(
|
|
399
|
+
credentialClient,
|
|
400
|
+
serverPublicKey,
|
|
401
|
+
username,
|
|
402
|
+
serverEncryptionPrivateKeyPem
|
|
403
|
+
),
|
|
404
|
+
loadUserEncryptionKey(
|
|
405
|
+
credentialClient,
|
|
406
|
+
serverPublicKey,
|
|
407
|
+
username,
|
|
408
|
+
serverEncryptionPrivateKeyPem
|
|
409
|
+
)
|
|
410
|
+
]);
|
|
411
|
+
const resolvedUri = request.uri.replace(/:key/g, accountKey.publicKeyHex);
|
|
412
|
+
const userPrivateKey = await pemToCryptoKey(
|
|
413
|
+
accountKey.privateKeyPem,
|
|
414
|
+
"Ed25519"
|
|
415
|
+
);
|
|
416
|
+
const signer = {
|
|
417
|
+
privateKey: userPrivateKey,
|
|
418
|
+
publicKeyHex: accountKey.publicKeyHex
|
|
419
|
+
};
|
|
420
|
+
let signedMessage;
|
|
421
|
+
if (request.encrypt) {
|
|
422
|
+
signedMessage = await createSignedEncryptedMessage(
|
|
423
|
+
request.data,
|
|
424
|
+
[signer],
|
|
425
|
+
encryptionKey.publicKeyHex
|
|
426
|
+
);
|
|
427
|
+
} else {
|
|
428
|
+
signedMessage = await createAuthenticatedMessage(request.data, [signer]);
|
|
429
|
+
}
|
|
430
|
+
const result = await proxyClient.write(resolvedUri, signedMessage);
|
|
431
|
+
if (!result.success) {
|
|
432
|
+
return {
|
|
433
|
+
success: false,
|
|
434
|
+
error: result.error || "Write failed"
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
success: true,
|
|
439
|
+
resolvedUri,
|
|
440
|
+
record: result.record
|
|
441
|
+
};
|
|
442
|
+
} catch (error) {
|
|
443
|
+
return {
|
|
444
|
+
success: false,
|
|
445
|
+
error: `Proxy write failed: ${error instanceof Error ? error.message : String(error)}`
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function proxyRead(proxyClient, uri, serverEncryptionPrivateKeyPem) {
|
|
450
|
+
try {
|
|
451
|
+
const result = await proxyClient.read(uri);
|
|
452
|
+
if (!result.success) {
|
|
453
|
+
return {
|
|
454
|
+
success: false,
|
|
455
|
+
error: result.error || "Read failed"
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const response = {
|
|
459
|
+
success: true,
|
|
460
|
+
record: result.record
|
|
461
|
+
};
|
|
462
|
+
if (serverEncryptionPrivateKeyPem && result.record?.data) {
|
|
463
|
+
try {
|
|
464
|
+
const data = result.record.data;
|
|
465
|
+
if (typeof data === "object" && data.payload && typeof data.payload === "object") {
|
|
466
|
+
const payload = data.payload;
|
|
467
|
+
if (payload.data && payload.nonce && payload.ephemeralPublicKey) {
|
|
468
|
+
const privateKey = await pemToCryptoKey(
|
|
469
|
+
serverEncryptionPrivateKeyPem,
|
|
470
|
+
"X25519"
|
|
471
|
+
);
|
|
472
|
+
const encryptedPayload = {
|
|
473
|
+
data: payload.data,
|
|
474
|
+
nonce: payload.nonce,
|
|
475
|
+
ephemeralPublicKey: payload.ephemeralPublicKey
|
|
476
|
+
};
|
|
477
|
+
const decrypted = await decrypt(
|
|
478
|
+
encryptedPayload,
|
|
479
|
+
privateKey
|
|
480
|
+
);
|
|
481
|
+
return {
|
|
482
|
+
...response,
|
|
483
|
+
decrypted
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} catch {
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return response;
|
|
491
|
+
} catch (error) {
|
|
492
|
+
return {
|
|
493
|
+
success: false,
|
|
494
|
+
error: `Proxy read failed: ${error instanceof Error ? error.message : String(error)}`
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// wallet-server/auth.ts
|
|
500
|
+
function generateSalt() {
|
|
501
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
502
|
+
return encodeHex(bytes);
|
|
503
|
+
}
|
|
504
|
+
async function hashPassword(password, salt) {
|
|
505
|
+
const encoder = new TextEncoder();
|
|
506
|
+
const data = encoder.encode(password);
|
|
507
|
+
const saltBytes = new Uint8Array(decodeHex(salt));
|
|
508
|
+
const key = await crypto.subtle.importKey("raw", data, "PBKDF2", false, [
|
|
509
|
+
"deriveBits"
|
|
510
|
+
]);
|
|
511
|
+
const derived = await crypto.subtle.deriveBits(
|
|
512
|
+
{
|
|
513
|
+
name: "PBKDF2",
|
|
514
|
+
salt: saltBytes.buffer,
|
|
515
|
+
iterations: 1e5,
|
|
516
|
+
hash: "SHA-256"
|
|
517
|
+
},
|
|
518
|
+
key,
|
|
519
|
+
256
|
|
520
|
+
);
|
|
521
|
+
return encodeHex(new Uint8Array(derived));
|
|
522
|
+
}
|
|
523
|
+
async function verifyPassword(password, salt, hash) {
|
|
524
|
+
const computedHash = await hashPassword(password, salt);
|
|
525
|
+
return computedHash === hash;
|
|
526
|
+
}
|
|
527
|
+
async function userExists(client, serverPublicKey, username, appScope) {
|
|
528
|
+
const path = await deriveObfuscatedPath(
|
|
529
|
+
serverPublicKey,
|
|
530
|
+
username,
|
|
531
|
+
"profile",
|
|
532
|
+
...appScope ? [appScope] : []
|
|
533
|
+
);
|
|
534
|
+
const result = await client.read(
|
|
535
|
+
`mutable://accounts/${serverPublicKey}/${path}`
|
|
536
|
+
);
|
|
537
|
+
return result.success;
|
|
538
|
+
}
|
|
539
|
+
async function createUser(client, serverPublicKey, username, password, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex, appScope) {
|
|
540
|
+
if (await userExists(client, serverPublicKey, username, appScope)) {
|
|
541
|
+
throw new Error("User already exists");
|
|
542
|
+
}
|
|
543
|
+
const salt = generateSalt();
|
|
544
|
+
const hash = await hashPassword(password, salt);
|
|
545
|
+
const profilePath = await deriveObfuscatedPath(
|
|
546
|
+
serverPublicKey,
|
|
547
|
+
username,
|
|
548
|
+
"profile",
|
|
549
|
+
...appScope ? [appScope] : []
|
|
550
|
+
);
|
|
551
|
+
const profileData = {
|
|
552
|
+
username,
|
|
553
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
554
|
+
};
|
|
555
|
+
const profileSigned = await createSignedEncryptedPayload(
|
|
556
|
+
profileData,
|
|
557
|
+
serverIdentityPrivateKeyPem,
|
|
558
|
+
serverIdentityPublicKeyHex,
|
|
559
|
+
serverEncryptionPublicKeyHex
|
|
560
|
+
);
|
|
561
|
+
await client.write(
|
|
562
|
+
`mutable://accounts/${serverPublicKey}/${profilePath}`,
|
|
563
|
+
profileSigned
|
|
564
|
+
);
|
|
565
|
+
const passwordPath = await deriveObfuscatedPath(
|
|
566
|
+
serverPublicKey,
|
|
567
|
+
username,
|
|
568
|
+
"password",
|
|
569
|
+
...appScope ? [appScope] : []
|
|
570
|
+
);
|
|
571
|
+
const passwordData = { hash, salt };
|
|
572
|
+
const passwordSigned = await createSignedEncryptedPayload(
|
|
573
|
+
passwordData,
|
|
574
|
+
serverIdentityPrivateKeyPem,
|
|
575
|
+
serverIdentityPublicKeyHex,
|
|
576
|
+
serverEncryptionPublicKeyHex
|
|
577
|
+
);
|
|
578
|
+
await client.write(
|
|
579
|
+
`mutable://accounts/${serverPublicKey}/${passwordPath}`,
|
|
580
|
+
passwordSigned
|
|
581
|
+
);
|
|
582
|
+
return { salt, hash };
|
|
583
|
+
}
|
|
584
|
+
async function authenticateUser(client, serverPublicKey, username, password, serverIdentityPublicKeyHex, serverEncryptionPrivateKeyPem, appScope, logger) {
|
|
585
|
+
const passwordPath = await deriveObfuscatedPath(
|
|
586
|
+
serverPublicKey,
|
|
587
|
+
username,
|
|
588
|
+
"password",
|
|
589
|
+
...appScope ? [appScope] : []
|
|
590
|
+
);
|
|
591
|
+
const result = await client.read(
|
|
592
|
+
`mutable://accounts/${serverPublicKey}/${passwordPath}`
|
|
593
|
+
);
|
|
594
|
+
if (!result.success || !result.record?.data) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
const { data, verified } = await decryptSignedEncryptedPayload(
|
|
598
|
+
result.record.data,
|
|
599
|
+
serverEncryptionPrivateKeyPem
|
|
600
|
+
);
|
|
601
|
+
if (!verified) {
|
|
602
|
+
logger?.warn(
|
|
603
|
+
"Password credential signature verification failed for user:",
|
|
604
|
+
username
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const { salt, hash } = data;
|
|
608
|
+
return await verifyPassword(password, salt, hash);
|
|
609
|
+
}
|
|
610
|
+
async function changePassword(client, serverPublicKey, username, oldPassword, newPassword, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex, serverEncryptionPrivateKeyPem, appScope) {
|
|
611
|
+
const isValid = await authenticateUser(
|
|
612
|
+
client,
|
|
613
|
+
serverPublicKey,
|
|
614
|
+
username,
|
|
615
|
+
oldPassword,
|
|
616
|
+
serverIdentityPublicKeyHex,
|
|
617
|
+
serverEncryptionPrivateKeyPem,
|
|
618
|
+
appScope
|
|
619
|
+
);
|
|
620
|
+
if (!isValid) {
|
|
621
|
+
throw new Error("Current password is incorrect");
|
|
622
|
+
}
|
|
623
|
+
const salt = generateSalt();
|
|
624
|
+
const hash = await hashPassword(newPassword, salt);
|
|
625
|
+
const passwordPath = await deriveObfuscatedPath(
|
|
626
|
+
serverPublicKey,
|
|
627
|
+
username,
|
|
628
|
+
"password",
|
|
629
|
+
...appScope ? [appScope] : []
|
|
630
|
+
);
|
|
631
|
+
const passwordData = { hash, salt };
|
|
632
|
+
const passwordSigned = await createSignedEncryptedPayload(
|
|
633
|
+
passwordData,
|
|
634
|
+
serverIdentityPrivateKeyPem,
|
|
635
|
+
serverIdentityPublicKeyHex,
|
|
636
|
+
serverEncryptionPublicKeyHex
|
|
637
|
+
);
|
|
638
|
+
await client.write(
|
|
639
|
+
`mutable://accounts/${serverPublicKey}/${passwordPath}`,
|
|
640
|
+
passwordSigned
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
async function createPasswordResetToken(client, serverPublicKey, username, ttlSeconds, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex, appScope) {
|
|
644
|
+
if (!await userExists(client, serverPublicKey, username)) {
|
|
645
|
+
throw new Error("User not found");
|
|
646
|
+
}
|
|
647
|
+
const tokenBytes = crypto.getRandomValues(new Uint8Array(32));
|
|
648
|
+
const token = encodeHex(tokenBytes);
|
|
649
|
+
const now = /* @__PURE__ */ new Date();
|
|
650
|
+
const expiresAt = new Date(now.getTime() + ttlSeconds * 1e3);
|
|
651
|
+
const tokenPath = await deriveObfuscatedPath(
|
|
652
|
+
serverPublicKey,
|
|
653
|
+
username,
|
|
654
|
+
"reset-tokens",
|
|
655
|
+
token,
|
|
656
|
+
...appScope ? [appScope] : []
|
|
657
|
+
);
|
|
658
|
+
const tokenData = {
|
|
659
|
+
username,
|
|
660
|
+
createdAt: now.toISOString(),
|
|
661
|
+
expiresAt: expiresAt.toISOString()
|
|
662
|
+
};
|
|
663
|
+
const tokenSigned = await createSignedEncryptedPayload(
|
|
664
|
+
tokenData,
|
|
665
|
+
serverIdentityPrivateKeyPem,
|
|
666
|
+
serverIdentityPublicKeyHex,
|
|
667
|
+
serverEncryptionPublicKeyHex
|
|
668
|
+
);
|
|
669
|
+
await client.write(
|
|
670
|
+
`mutable://accounts/${serverPublicKey}/${tokenPath}`,
|
|
671
|
+
tokenSigned
|
|
672
|
+
);
|
|
673
|
+
return token;
|
|
674
|
+
}
|
|
675
|
+
async function resetPasswordWithToken(client, serverPublicKey, token, newPassword, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex, serverEncryptionPrivateKeyPem, username, appScope, logger) {
|
|
676
|
+
const tokenPath = await deriveObfuscatedPath(
|
|
677
|
+
serverPublicKey,
|
|
678
|
+
username,
|
|
679
|
+
"reset-tokens",
|
|
680
|
+
token,
|
|
681
|
+
...appScope ? [appScope] : []
|
|
682
|
+
);
|
|
683
|
+
const result = await client.read(
|
|
684
|
+
`mutable://accounts/${serverPublicKey}/${tokenPath}`
|
|
685
|
+
);
|
|
686
|
+
if (!result.success || !result.record?.data) {
|
|
687
|
+
throw new Error("Invalid or expired reset token");
|
|
688
|
+
}
|
|
689
|
+
const { data: tokenData, verified } = await decryptSignedEncryptedPayload(
|
|
690
|
+
result.record.data,
|
|
691
|
+
serverEncryptionPrivateKeyPem
|
|
692
|
+
);
|
|
693
|
+
if (!verified) {
|
|
694
|
+
logger?.warn(
|
|
695
|
+
"Reset token signature verification failed for user:",
|
|
696
|
+
username
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
const { username: tokenUsername, expiresAt } = tokenData;
|
|
700
|
+
if (tokenUsername !== username) {
|
|
701
|
+
throw new Error("Invalid reset token");
|
|
702
|
+
}
|
|
703
|
+
if (new Date(expiresAt) < /* @__PURE__ */ new Date()) {
|
|
704
|
+
throw new Error("Reset token has expired");
|
|
705
|
+
}
|
|
706
|
+
const salt = generateSalt();
|
|
707
|
+
const hash = await hashPassword(newPassword, salt);
|
|
708
|
+
const passwordPath = await deriveObfuscatedPath(
|
|
709
|
+
serverPublicKey,
|
|
710
|
+
username,
|
|
711
|
+
"password",
|
|
712
|
+
...appScope ? [appScope] : []
|
|
713
|
+
);
|
|
714
|
+
const passwordData = { hash, salt };
|
|
715
|
+
const passwordSigned = await createSignedEncryptedPayload(
|
|
716
|
+
passwordData,
|
|
717
|
+
serverIdentityPrivateKeyPem,
|
|
718
|
+
serverIdentityPublicKeyHex,
|
|
719
|
+
serverEncryptionPublicKeyHex
|
|
720
|
+
);
|
|
721
|
+
await client.write(
|
|
722
|
+
`mutable://accounts/${serverPublicKey}/${passwordPath}`,
|
|
723
|
+
passwordSigned
|
|
724
|
+
);
|
|
725
|
+
await client.delete(`mutable://accounts/${serverPublicKey}/${tokenPath}`);
|
|
726
|
+
return tokenUsername;
|
|
727
|
+
}
|
|
728
|
+
async function googleUserExists(client, serverPublicKey, googleSub, appScope) {
|
|
729
|
+
const path = await deriveObfuscatedPath(
|
|
730
|
+
serverPublicKey,
|
|
731
|
+
googleSub,
|
|
732
|
+
"google-profile",
|
|
733
|
+
...appScope ? [appScope] : []
|
|
734
|
+
);
|
|
735
|
+
const result = await client.read(
|
|
736
|
+
`mutable://accounts/${serverPublicKey}/${path}`
|
|
737
|
+
);
|
|
738
|
+
return result.success;
|
|
739
|
+
}
|
|
740
|
+
async function createGoogleUser(client, serverPublicKey, username, googlePayload, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex, appScope) {
|
|
741
|
+
if (await googleUserExists(client, serverPublicKey, googlePayload.sub, appScope)) {
|
|
742
|
+
throw new Error("Google account already registered");
|
|
743
|
+
}
|
|
744
|
+
if (await userExists(client, serverPublicKey, username, appScope)) {
|
|
745
|
+
throw new Error("Username already exists");
|
|
746
|
+
}
|
|
747
|
+
const profilePath = await deriveObfuscatedPath(
|
|
748
|
+
serverPublicKey,
|
|
749
|
+
username,
|
|
750
|
+
"profile",
|
|
751
|
+
...appScope ? [appScope] : []
|
|
752
|
+
);
|
|
753
|
+
const profileData = {
|
|
754
|
+
username,
|
|
755
|
+
authProvider: "google",
|
|
756
|
+
googleSub: googlePayload.sub,
|
|
757
|
+
email: googlePayload.email,
|
|
758
|
+
name: googlePayload.name,
|
|
759
|
+
picture: googlePayload.picture,
|
|
760
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
761
|
+
};
|
|
762
|
+
const profileSigned = await createSignedEncryptedPayload(
|
|
763
|
+
profileData,
|
|
764
|
+
serverIdentityPrivateKeyPem,
|
|
765
|
+
serverIdentityPublicKeyHex,
|
|
766
|
+
serverEncryptionPublicKeyHex
|
|
767
|
+
);
|
|
768
|
+
await client.write(
|
|
769
|
+
`mutable://accounts/${serverPublicKey}/${profilePath}`,
|
|
770
|
+
profileSigned
|
|
771
|
+
);
|
|
772
|
+
const googleProfilePath = await deriveObfuscatedPath(
|
|
773
|
+
serverPublicKey,
|
|
774
|
+
googlePayload.sub,
|
|
775
|
+
"google-profile",
|
|
776
|
+
...appScope ? [appScope] : []
|
|
777
|
+
);
|
|
778
|
+
const googleProfileData = {
|
|
779
|
+
googleSub: googlePayload.sub,
|
|
780
|
+
username,
|
|
781
|
+
email: googlePayload.email,
|
|
782
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
783
|
+
};
|
|
784
|
+
const googleProfileSigned = await createSignedEncryptedPayload(
|
|
785
|
+
googleProfileData,
|
|
786
|
+
serverIdentityPrivateKeyPem,
|
|
787
|
+
serverIdentityPublicKeyHex,
|
|
788
|
+
serverEncryptionPublicKeyHex
|
|
789
|
+
);
|
|
790
|
+
await client.write(
|
|
791
|
+
`mutable://accounts/${serverPublicKey}/${googleProfilePath}`,
|
|
792
|
+
googleProfileSigned
|
|
793
|
+
);
|
|
794
|
+
return { username, googleSub: googlePayload.sub };
|
|
795
|
+
}
|
|
796
|
+
async function authenticateGoogleUser(client, serverPublicKey, googleSub, serverEncryptionPrivateKeyPem, appScope, logger) {
|
|
797
|
+
const googleProfilePath = await deriveObfuscatedPath(
|
|
798
|
+
serverPublicKey,
|
|
799
|
+
googleSub,
|
|
800
|
+
"google-profile",
|
|
801
|
+
...appScope ? [appScope] : []
|
|
802
|
+
);
|
|
803
|
+
const result = await client.read(
|
|
804
|
+
`mutable://accounts/${serverPublicKey}/${googleProfilePath}`
|
|
805
|
+
);
|
|
806
|
+
if (!result.success || !result.record?.data) {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
const { data, verified } = await decryptSignedEncryptedPayload(
|
|
810
|
+
result.record.data,
|
|
811
|
+
serverEncryptionPrivateKeyPem
|
|
812
|
+
);
|
|
813
|
+
if (!verified) {
|
|
814
|
+
logger?.warn(
|
|
815
|
+
"Google profile signature verification failed for sub:",
|
|
816
|
+
googleSub
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
const { username } = data;
|
|
820
|
+
return username;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// wallet-server/google-oauth.ts
|
|
824
|
+
var cachedKeys = [];
|
|
825
|
+
var cacheExpiry = 0;
|
|
826
|
+
async function getGooglePublicKeys(fetchImpl = fetch) {
|
|
827
|
+
const now = Date.now();
|
|
828
|
+
if (cachedKeys.length > 0 && now < cacheExpiry) {
|
|
829
|
+
return cachedKeys;
|
|
830
|
+
}
|
|
831
|
+
const response = await fetchImpl(
|
|
832
|
+
"https://www.googleapis.com/oauth2/v3/certs"
|
|
833
|
+
);
|
|
834
|
+
if (!response.ok) {
|
|
835
|
+
throw new Error("Failed to fetch Google public keys");
|
|
836
|
+
}
|
|
837
|
+
const cacheControl = response.headers.get("cache-control");
|
|
838
|
+
let maxAge = 3600;
|
|
839
|
+
if (cacheControl) {
|
|
840
|
+
const match = cacheControl.match(/max-age=(\d+)/);
|
|
841
|
+
if (match) {
|
|
842
|
+
maxAge = parseInt(match[1], 10);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const data = await response.json();
|
|
846
|
+
cachedKeys = data.keys;
|
|
847
|
+
cacheExpiry = now + maxAge * 1e3;
|
|
848
|
+
return cachedKeys;
|
|
849
|
+
}
|
|
850
|
+
function clearGooglePublicKeyCache() {
|
|
851
|
+
cachedKeys = [];
|
|
852
|
+
cacheExpiry = 0;
|
|
853
|
+
}
|
|
854
|
+
function base64UrlDecode(str) {
|
|
855
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
856
|
+
while (base64.length % 4) {
|
|
857
|
+
base64 += "=";
|
|
858
|
+
}
|
|
859
|
+
const binary = atob(base64);
|
|
860
|
+
const bytes = new Uint8Array(binary.length);
|
|
861
|
+
for (let i = 0; i < binary.length; i++) {
|
|
862
|
+
bytes[i] = binary.charCodeAt(i);
|
|
863
|
+
}
|
|
864
|
+
return bytes;
|
|
865
|
+
}
|
|
866
|
+
async function importRsaPublicKey(key) {
|
|
867
|
+
const jwk = {
|
|
868
|
+
kty: key.kty,
|
|
869
|
+
n: key.n,
|
|
870
|
+
e: key.e,
|
|
871
|
+
alg: key.alg,
|
|
872
|
+
use: key.use
|
|
873
|
+
};
|
|
874
|
+
return await crypto.subtle.importKey(
|
|
875
|
+
"jwk",
|
|
876
|
+
jwk,
|
|
877
|
+
{
|
|
878
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
879
|
+
hash: { name: "SHA-256" }
|
|
880
|
+
},
|
|
881
|
+
false,
|
|
882
|
+
["verify"]
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
async function verifyGoogleIdToken(idToken, clientId, fetchImpl = fetch) {
|
|
886
|
+
const parts = idToken.split(".");
|
|
887
|
+
if (parts.length !== 3) {
|
|
888
|
+
throw new Error("Invalid Google ID token format");
|
|
889
|
+
}
|
|
890
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
891
|
+
const headerJson = new TextDecoder().decode(base64UrlDecode(headerB64));
|
|
892
|
+
const header = JSON.parse(headerJson);
|
|
893
|
+
const kid = header.kid;
|
|
894
|
+
if (!kid) {
|
|
895
|
+
throw new Error("Google ID token missing key ID");
|
|
896
|
+
}
|
|
897
|
+
let keys = await getGooglePublicKeys(fetchImpl);
|
|
898
|
+
let key = keys.find((k) => k.kid === kid);
|
|
899
|
+
if (!key) {
|
|
900
|
+
clearGooglePublicKeyCache();
|
|
901
|
+
keys = await getGooglePublicKeys(fetchImpl);
|
|
902
|
+
key = keys.find((k) => k.kid === kid);
|
|
903
|
+
if (!key) {
|
|
904
|
+
throw new Error("Google public key not found for token");
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
const publicKey = await importRsaPublicKey(key);
|
|
908
|
+
const signatureData = base64UrlDecode(signatureB64);
|
|
909
|
+
const signedData = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
|
|
910
|
+
const isValid = await crypto.subtle.verify(
|
|
911
|
+
"RSASSA-PKCS1-v1_5",
|
|
912
|
+
publicKey,
|
|
913
|
+
signatureData.buffer,
|
|
914
|
+
signedData
|
|
915
|
+
);
|
|
916
|
+
if (!isValid) {
|
|
917
|
+
throw new Error("Google ID token signature verification failed");
|
|
918
|
+
}
|
|
919
|
+
const payloadJson = new TextDecoder().decode(base64UrlDecode(payloadB64));
|
|
920
|
+
const payload = JSON.parse(payloadJson);
|
|
921
|
+
if (payload.iss !== "accounts.google.com" && payload.iss !== "https://accounts.google.com") {
|
|
922
|
+
throw new Error("Invalid Google ID token issuer");
|
|
923
|
+
}
|
|
924
|
+
if (payload.aud !== clientId) {
|
|
925
|
+
throw new Error("Google ID token audience mismatch");
|
|
926
|
+
}
|
|
927
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
928
|
+
if (payload.exp < now) {
|
|
929
|
+
throw new Error("Google ID token has expired");
|
|
930
|
+
}
|
|
931
|
+
if (!payload.email_verified) {
|
|
932
|
+
throw new Error("Google email not verified");
|
|
933
|
+
}
|
|
934
|
+
return payload;
|
|
935
|
+
}
|
|
936
|
+
async function generateGoogleUsername(googleSub) {
|
|
937
|
+
const encoder = new TextEncoder();
|
|
938
|
+
const data = encoder.encode(`google:${googleSub}`);
|
|
939
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
940
|
+
const hashHex = encodeHex(new Uint8Array(hashBuffer));
|
|
941
|
+
return `g_${hashHex.substring(0, 12)}`;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// wallet-server/credentials.ts
|
|
945
|
+
var PasswordCredentialHandler = class {
|
|
946
|
+
async signup(payload, context) {
|
|
947
|
+
const { username, password } = payload;
|
|
948
|
+
if (!username || typeof username !== "string") {
|
|
949
|
+
throw new Error("username is required");
|
|
950
|
+
}
|
|
951
|
+
if (!password || typeof password !== "string" || password.length < 8) {
|
|
952
|
+
throw new Error(
|
|
953
|
+
"password is required and must be at least 8 characters"
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
context.logger?.log(`Creating password user: ${username}`);
|
|
957
|
+
await createUser(
|
|
958
|
+
context.client,
|
|
959
|
+
context.serverPublicKey,
|
|
960
|
+
username,
|
|
961
|
+
password,
|
|
962
|
+
context.serverIdentityPrivateKeyPem,
|
|
963
|
+
context.serverIdentityPublicKeyHex,
|
|
964
|
+
context.serverEncryptionPublicKeyHex,
|
|
965
|
+
context.appKey
|
|
966
|
+
);
|
|
967
|
+
context.logger?.log(`Password user created: ${username}`);
|
|
968
|
+
context.logger?.log(`Generating keys for user: ${username}`);
|
|
969
|
+
await generateUserKeys(
|
|
970
|
+
context.client,
|
|
971
|
+
context.serverPublicKey,
|
|
972
|
+
username,
|
|
973
|
+
context.serverIdentityPrivateKeyPem,
|
|
974
|
+
context.serverIdentityPublicKeyHex,
|
|
975
|
+
context.serverEncryptionPublicKeyHex
|
|
976
|
+
);
|
|
977
|
+
context.logger?.log(`Keys generated for user: ${username}`);
|
|
978
|
+
return { username };
|
|
979
|
+
}
|
|
980
|
+
async login(payload, context) {
|
|
981
|
+
const { username, password } = payload;
|
|
982
|
+
if (!username || !password) {
|
|
983
|
+
throw new Error("username and password are required");
|
|
984
|
+
}
|
|
985
|
+
const isValid = await authenticateUser(
|
|
986
|
+
context.client,
|
|
987
|
+
context.serverPublicKey,
|
|
988
|
+
username,
|
|
989
|
+
password,
|
|
990
|
+
context.serverIdentityPublicKeyHex,
|
|
991
|
+
context.serverEncryptionPrivateKeyPem,
|
|
992
|
+
context.appKey,
|
|
993
|
+
context.logger
|
|
994
|
+
);
|
|
995
|
+
if (!isValid) {
|
|
996
|
+
throw new Error("Invalid username or password");
|
|
997
|
+
}
|
|
998
|
+
return { username };
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
var GoogleCredentialHandler = class {
|
|
1002
|
+
async signup(payload, context) {
|
|
1003
|
+
if (!context.googleClientId) {
|
|
1004
|
+
throw new Error("Google OAuth is not configured");
|
|
1005
|
+
}
|
|
1006
|
+
const { googleIdToken } = payload;
|
|
1007
|
+
if (!googleIdToken || typeof googleIdToken !== "string") {
|
|
1008
|
+
throw new Error("googleIdToken is required");
|
|
1009
|
+
}
|
|
1010
|
+
context.logger?.log("Verifying Google ID token...");
|
|
1011
|
+
const googlePayload = await verifyGoogleIdToken(
|
|
1012
|
+
googleIdToken,
|
|
1013
|
+
context.googleClientId,
|
|
1014
|
+
context.fetch
|
|
1015
|
+
);
|
|
1016
|
+
context.logger?.log(`Google token verified for: ${googlePayload.email}`);
|
|
1017
|
+
const username = await generateGoogleUsername(googlePayload.sub);
|
|
1018
|
+
context.logger?.log(`Creating Google user: ${username}`);
|
|
1019
|
+
await createGoogleUser(
|
|
1020
|
+
context.client,
|
|
1021
|
+
context.serverPublicKey,
|
|
1022
|
+
username,
|
|
1023
|
+
googlePayload,
|
|
1024
|
+
context.serverIdentityPrivateKeyPem,
|
|
1025
|
+
context.serverIdentityPublicKeyHex,
|
|
1026
|
+
context.serverEncryptionPublicKeyHex,
|
|
1027
|
+
context.appKey
|
|
1028
|
+
);
|
|
1029
|
+
context.logger?.log(`Google user created: ${username}`);
|
|
1030
|
+
context.logger?.log(`Generating keys for Google user: ${username}`);
|
|
1031
|
+
await generateUserKeys(
|
|
1032
|
+
context.client,
|
|
1033
|
+
context.serverPublicKey,
|
|
1034
|
+
username,
|
|
1035
|
+
context.serverIdentityPrivateKeyPem,
|
|
1036
|
+
context.serverIdentityPublicKeyHex,
|
|
1037
|
+
context.serverEncryptionPublicKeyHex
|
|
1038
|
+
);
|
|
1039
|
+
context.logger?.log(`Keys generated for Google user: ${username}`);
|
|
1040
|
+
return {
|
|
1041
|
+
username,
|
|
1042
|
+
metadata: {
|
|
1043
|
+
email: googlePayload.email,
|
|
1044
|
+
name: googlePayload.name,
|
|
1045
|
+
picture: googlePayload.picture
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
async login(payload, context) {
|
|
1050
|
+
if (!context.googleClientId) {
|
|
1051
|
+
throw new Error("Google OAuth is not configured");
|
|
1052
|
+
}
|
|
1053
|
+
const { googleIdToken } = payload;
|
|
1054
|
+
if (!googleIdToken || typeof googleIdToken !== "string") {
|
|
1055
|
+
throw new Error("googleIdToken is required");
|
|
1056
|
+
}
|
|
1057
|
+
context.logger?.log("Verifying Google ID token for login...");
|
|
1058
|
+
const googlePayload = await verifyGoogleIdToken(
|
|
1059
|
+
googleIdToken,
|
|
1060
|
+
context.googleClientId,
|
|
1061
|
+
context.fetch
|
|
1062
|
+
);
|
|
1063
|
+
context.logger?.log(`Google token verified for: ${googlePayload.email}`);
|
|
1064
|
+
const username = await authenticateGoogleUser(
|
|
1065
|
+
context.client,
|
|
1066
|
+
context.serverPublicKey,
|
|
1067
|
+
googlePayload.sub,
|
|
1068
|
+
context.serverEncryptionPrivateKeyPem,
|
|
1069
|
+
context.appKey,
|
|
1070
|
+
context.logger
|
|
1071
|
+
);
|
|
1072
|
+
if (!username) {
|
|
1073
|
+
throw new Error("Google account not registered. Please sign up first.");
|
|
1074
|
+
}
|
|
1075
|
+
context.logger?.log(`Google login successful for user: ${username}`);
|
|
1076
|
+
return {
|
|
1077
|
+
username,
|
|
1078
|
+
metadata: {
|
|
1079
|
+
email: googlePayload.email,
|
|
1080
|
+
name: googlePayload.name,
|
|
1081
|
+
picture: googlePayload.picture
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
var credentialHandlers = /* @__PURE__ */ new Map([
|
|
1087
|
+
["password", new PasswordCredentialHandler()],
|
|
1088
|
+
["google", new GoogleCredentialHandler()]
|
|
1089
|
+
]);
|
|
1090
|
+
function getCredentialHandler(type) {
|
|
1091
|
+
const handler = credentialHandlers.get(type);
|
|
1092
|
+
if (!handler) {
|
|
1093
|
+
throw new Error(`Unknown credential type: ${type}`);
|
|
1094
|
+
}
|
|
1095
|
+
return handler;
|
|
1096
|
+
}
|
|
1097
|
+
function registerCredentialHandler(type, handler) {
|
|
1098
|
+
credentialHandlers.set(type, handler);
|
|
1099
|
+
}
|
|
1100
|
+
function getSupportedCredentialTypes() {
|
|
1101
|
+
return Array.from(credentialHandlers.keys());
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// wallet-server/core.ts
|
|
1105
|
+
import { Hono } from "hono";
|
|
1106
|
+
import { cors } from "hono/cors";
|
|
1107
|
+
var WalletServerCore = class {
|
|
1108
|
+
constructor(userConfig) {
|
|
1109
|
+
this.validateConfig(userConfig);
|
|
1110
|
+
this.serverKeys = userConfig.serverKeys;
|
|
1111
|
+
this.config = this.applyDefaults(userConfig);
|
|
1112
|
+
this.logger = userConfig.deps?.logger ?? defaultLogger;
|
|
1113
|
+
this.storage = userConfig.deps?.storage ?? new MemoryFileStorage();
|
|
1114
|
+
this.fetchImpl = userConfig.deps?.fetch ?? fetch;
|
|
1115
|
+
this.credentialClient = userConfig.deps?.credentialClient ?? new HttpClient({ url: userConfig.credentialNodeUrl });
|
|
1116
|
+
this.proxyClient = userConfig.deps?.proxyClient ?? new HttpClient({ url: userConfig.proxyNodeUrl });
|
|
1117
|
+
this.app = this.createApp();
|
|
1118
|
+
}
|
|
1119
|
+
validateConfig(config) {
|
|
1120
|
+
if (!config.serverKeys?.identityKey?.privateKeyPem) {
|
|
1121
|
+
throw new Error("serverKeys.identityKey.privateKeyPem is required");
|
|
1122
|
+
}
|
|
1123
|
+
if (!config.serverKeys?.identityKey?.publicKeyHex) {
|
|
1124
|
+
throw new Error("serverKeys.identityKey.publicKeyHex is required");
|
|
1125
|
+
}
|
|
1126
|
+
if (!config.serverKeys?.encryptionKey?.privateKeyPem) {
|
|
1127
|
+
throw new Error("serverKeys.encryptionKey.privateKeyPem is required");
|
|
1128
|
+
}
|
|
1129
|
+
if (!config.serverKeys?.encryptionKey?.publicKeyHex) {
|
|
1130
|
+
throw new Error("serverKeys.encryptionKey.publicKeyHex is required");
|
|
1131
|
+
}
|
|
1132
|
+
if (!config.jwtSecret || config.jwtSecret.length < 32) {
|
|
1133
|
+
throw new Error("jwtSecret is required and must be at least 32 characters");
|
|
1134
|
+
}
|
|
1135
|
+
if (!config.credentialNodeUrl && !config.deps?.credentialClient) {
|
|
1136
|
+
throw new Error(
|
|
1137
|
+
"Either credentialNodeUrl or deps.credentialClient is required"
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
if (!config.proxyNodeUrl && !config.deps?.proxyClient) {
|
|
1141
|
+
throw new Error("Either proxyNodeUrl or deps.proxyClient is required");
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
applyDefaults(config) {
|
|
1145
|
+
return {
|
|
1146
|
+
serverKeys: config.serverKeys,
|
|
1147
|
+
jwtSecret: config.jwtSecret,
|
|
1148
|
+
jwtExpirationSeconds: config.jwtExpirationSeconds ?? 86400,
|
|
1149
|
+
allowedOrigins: config.allowedOrigins ?? ["*"],
|
|
1150
|
+
passwordResetTokenTtlSeconds: config.passwordResetTokenTtlSeconds ?? 3600,
|
|
1151
|
+
googleClientId: config.googleClientId ?? null,
|
|
1152
|
+
bootstrapStatePath: config.bootstrapStatePath ?? null,
|
|
1153
|
+
appBackend: config.appBackend ?? null
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Get the Hono app instance
|
|
1158
|
+
*/
|
|
1159
|
+
getApp() {
|
|
1160
|
+
return this.app;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Get the fetch handler for use with various runtimes
|
|
1164
|
+
*/
|
|
1165
|
+
getFetchHandler() {
|
|
1166
|
+
return this.app.fetch.bind(this.app);
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Get server public keys
|
|
1170
|
+
*/
|
|
1171
|
+
getServerKeys() {
|
|
1172
|
+
return {
|
|
1173
|
+
identityPublicKeyHex: this.serverKeys.identityKey.publicKeyHex,
|
|
1174
|
+
encryptionPublicKeyHex: this.serverKeys.encryptionKey.publicKeyHex
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Bootstrap wallet app registration (optional)
|
|
1179
|
+
*/
|
|
1180
|
+
async bootstrap() {
|
|
1181
|
+
if (!this.config.appBackend) {
|
|
1182
|
+
this.logger.warn(
|
|
1183
|
+
"App backend not configured; skipping wallet app bootstrap"
|
|
1184
|
+
);
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
const appKey = this.serverKeys.identityKey.publicKeyHex;
|
|
1188
|
+
const apiBasePath = this.normalizeApiBasePath(
|
|
1189
|
+
this.config.appBackend.apiBasePath ?? "/api/v1"
|
|
1190
|
+
);
|
|
1191
|
+
const appServerUrl = this.config.appBackend.url.replace(/\/$/, "");
|
|
1192
|
+
if (this.config.bootstrapStatePath) {
|
|
1193
|
+
try {
|
|
1194
|
+
const existingState = await this.readBootstrapState(
|
|
1195
|
+
this.config.bootstrapStatePath
|
|
1196
|
+
);
|
|
1197
|
+
if (existingState) {
|
|
1198
|
+
if (existingState.appKey !== appKey) {
|
|
1199
|
+
throw new Error(
|
|
1200
|
+
`Bootstrap state belongs to ${existingState.appKey}, expected ${appKey}`
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
return existingState;
|
|
1204
|
+
}
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const bootstrapJwt = await createJwt(
|
|
1209
|
+
"__wallet_bootstrap__",
|
|
1210
|
+
this.config.jwtSecret,
|
|
1211
|
+
this.config.jwtExpirationSeconds
|
|
1212
|
+
);
|
|
1213
|
+
const signMessage = async (payload) => {
|
|
1214
|
+
const privateKey = await pemToCryptoKey(
|
|
1215
|
+
this.serverKeys.identityKey.privateKeyPem,
|
|
1216
|
+
"Ed25519"
|
|
1217
|
+
);
|
|
1218
|
+
return await createAuthenticatedMessage(payload, [
|
|
1219
|
+
{ privateKey, publicKeyHex: appKey }
|
|
1220
|
+
]);
|
|
1221
|
+
};
|
|
1222
|
+
const originsMessage = await signMessage({
|
|
1223
|
+
allowedOrigins: this.config.allowedOrigins,
|
|
1224
|
+
encryptionPublicKeyHex: this.serverKeys.encryptionKey.publicKeyHex
|
|
1225
|
+
});
|
|
1226
|
+
const originsRes = await this.fetchImpl(
|
|
1227
|
+
`${appServerUrl}${apiBasePath}/apps/origins/${appKey}`,
|
|
1228
|
+
{
|
|
1229
|
+
method: "POST",
|
|
1230
|
+
headers: {
|
|
1231
|
+
"Content-Type": "application/json",
|
|
1232
|
+
Authorization: `Bearer ${bootstrapJwt}`
|
|
1233
|
+
},
|
|
1234
|
+
body: JSON.stringify(originsMessage)
|
|
1235
|
+
}
|
|
1236
|
+
);
|
|
1237
|
+
const originsBody = await originsRes.json().catch(() => ({}));
|
|
1238
|
+
if (!originsRes.ok || !originsBody?.success) {
|
|
1239
|
+
throw new Error(
|
|
1240
|
+
`Wallet app origins bootstrap failed: ${originsBody?.error || originsRes.statusText}`
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
const schemaMessage = await signMessage({
|
|
1244
|
+
actions: [],
|
|
1245
|
+
encryptionPublicKeyHex: this.serverKeys.encryptionKey.publicKeyHex
|
|
1246
|
+
});
|
|
1247
|
+
const schemaRes = await this.fetchImpl(
|
|
1248
|
+
`${appServerUrl}${apiBasePath}/apps/schema/${appKey}`,
|
|
1249
|
+
{
|
|
1250
|
+
method: "POST",
|
|
1251
|
+
headers: {
|
|
1252
|
+
"Content-Type": "application/json",
|
|
1253
|
+
Authorization: `Bearer ${bootstrapJwt}`
|
|
1254
|
+
},
|
|
1255
|
+
body: JSON.stringify(schemaMessage)
|
|
1256
|
+
}
|
|
1257
|
+
);
|
|
1258
|
+
const schemaBody = await schemaRes.json().catch(() => ({}));
|
|
1259
|
+
if (!schemaRes.ok || !schemaBody?.success) {
|
|
1260
|
+
throw new Error(
|
|
1261
|
+
`Wallet app schema bootstrap failed: ${schemaBody?.error || schemaRes.statusText}`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
const state = {
|
|
1265
|
+
appKey,
|
|
1266
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1267
|
+
appServerUrl,
|
|
1268
|
+
apiBasePath
|
|
1269
|
+
};
|
|
1270
|
+
if (this.config.bootstrapStatePath) {
|
|
1271
|
+
await this.writeBootstrapState(this.config.bootstrapStatePath, state);
|
|
1272
|
+
}
|
|
1273
|
+
return state;
|
|
1274
|
+
}
|
|
1275
|
+
normalizeApiBasePath(path) {
|
|
1276
|
+
if (!path || typeof path !== "string") {
|
|
1277
|
+
return "/api/v1";
|
|
1278
|
+
}
|
|
1279
|
+
const base = path.startsWith("/") ? path : `/${path}`;
|
|
1280
|
+
return base.replace(/\/$/, "");
|
|
1281
|
+
}
|
|
1282
|
+
async readBootstrapState(path) {
|
|
1283
|
+
try {
|
|
1284
|
+
const text = await this.storage.readTextFile(path);
|
|
1285
|
+
const parsed = JSON.parse(text);
|
|
1286
|
+
if (!parsed.appKey || !parsed.createdAt || !parsed.appServerUrl || !parsed.apiBasePath) {
|
|
1287
|
+
throw new Error(`Invalid bootstrap state file at ${path}`);
|
|
1288
|
+
}
|
|
1289
|
+
return parsed;
|
|
1290
|
+
} catch {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
async writeBootstrapState(path, state) {
|
|
1295
|
+
await this.storage.writeTextFile(path, JSON.stringify(state, null, 2));
|
|
1296
|
+
}
|
|
1297
|
+
async sessionExists(appKey, sessionKey) {
|
|
1298
|
+
const input = new TextEncoder().encode(sessionKey);
|
|
1299
|
+
const digest = await crypto.subtle.digest("SHA-256", input);
|
|
1300
|
+
const sigHex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("").substring(0, 32);
|
|
1301
|
+
const uri = `mutable://accounts/${appKey}/sessions/${sigHex}`;
|
|
1302
|
+
const res = await this.proxyClient.read(uri);
|
|
1303
|
+
return res.success;
|
|
1304
|
+
}
|
|
1305
|
+
createApp() {
|
|
1306
|
+
const app = new Hono();
|
|
1307
|
+
const serverPublicKey = this.serverKeys.identityKey.publicKeyHex;
|
|
1308
|
+
const serverIdentityPrivateKeyPem = this.serverKeys.identityKey.privateKeyPem;
|
|
1309
|
+
const serverIdentityPublicKeyHex = this.serverKeys.identityKey.publicKeyHex;
|
|
1310
|
+
const serverEncryptionPublicKeyHex = this.serverKeys.encryptionKey.publicKeyHex;
|
|
1311
|
+
const serverEncryptionPrivateKeyPem = this.serverKeys.encryptionKey.privateKeyPem;
|
|
1312
|
+
app.use(
|
|
1313
|
+
"/*",
|
|
1314
|
+
cors({
|
|
1315
|
+
origin: (origin) => this.config.allowedOrigins[0] === "*" ? origin : this.config.allowedOrigins.join(","),
|
|
1316
|
+
allowMethods: ["GET", "POST", "OPTIONS"],
|
|
1317
|
+
allowHeaders: ["Content-Type", "Authorization"]
|
|
1318
|
+
})
|
|
1319
|
+
);
|
|
1320
|
+
app.use(async (c, next) => {
|
|
1321
|
+
const start = Date.now();
|
|
1322
|
+
await next();
|
|
1323
|
+
const duration = Date.now() - start;
|
|
1324
|
+
this.logger.log(
|
|
1325
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] ${c.req.method} ${c.req.path} ${c.res.status} - ${duration}ms`
|
|
1326
|
+
);
|
|
1327
|
+
});
|
|
1328
|
+
app.get("/api/v1/health", (c) => {
|
|
1329
|
+
return c.json({
|
|
1330
|
+
success: true,
|
|
1331
|
+
status: "ok",
|
|
1332
|
+
server: "b3nd-wallet-server",
|
|
1333
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1334
|
+
});
|
|
1335
|
+
});
|
|
1336
|
+
app.get("/api/v1/server-keys", (c) => {
|
|
1337
|
+
return c.json({
|
|
1338
|
+
success: true,
|
|
1339
|
+
identityPublicKeyHex: serverIdentityPublicKeyHex,
|
|
1340
|
+
encryptionPublicKeyHex: serverEncryptionPublicKeyHex
|
|
1341
|
+
});
|
|
1342
|
+
});
|
|
1343
|
+
app.get("/api/v1/auth/public-keys/:appKey", async (c) => {
|
|
1344
|
+
try {
|
|
1345
|
+
const appKey = c.req.param("appKey");
|
|
1346
|
+
const authHeader = c.req.header("Authorization");
|
|
1347
|
+
if (!appKey) {
|
|
1348
|
+
return c.json({ success: false, error: "appKey is required" }, 400);
|
|
1349
|
+
}
|
|
1350
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
1351
|
+
return c.json({ success: false, error: "Authorization required" }, 401);
|
|
1352
|
+
}
|
|
1353
|
+
const token = authHeader.substring(7);
|
|
1354
|
+
const payload = await verifyJwt(token, this.config.jwtSecret);
|
|
1355
|
+
const keys = await getUserPublicKeys(
|
|
1356
|
+
this.credentialClient,
|
|
1357
|
+
serverPublicKey,
|
|
1358
|
+
payload.username,
|
|
1359
|
+
serverEncryptionPrivateKeyPem,
|
|
1360
|
+
this.logger
|
|
1361
|
+
);
|
|
1362
|
+
return c.json({
|
|
1363
|
+
success: true,
|
|
1364
|
+
accountPublicKeyHex: keys.accountPublicKeyHex,
|
|
1365
|
+
encryptionPublicKeyHex: keys.encryptionPublicKeyHex
|
|
1366
|
+
});
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
return c.json({
|
|
1369
|
+
success: false,
|
|
1370
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1371
|
+
}, 400);
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
app.get("/api/v1/auth/verify/:appKey", async (c) => {
|
|
1375
|
+
try {
|
|
1376
|
+
const appKey = c.req.param("appKey");
|
|
1377
|
+
const authHeader = c.req.header("Authorization");
|
|
1378
|
+
if (!appKey) {
|
|
1379
|
+
return c.json({ success: false, error: "appKey is required" }, 400);
|
|
1380
|
+
}
|
|
1381
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
1382
|
+
return c.json({ success: false, error: "Authorization required" }, 401);
|
|
1383
|
+
}
|
|
1384
|
+
const token = authHeader.substring(7);
|
|
1385
|
+
const payload = await verifyJwt(token, this.config.jwtSecret);
|
|
1386
|
+
return c.json({
|
|
1387
|
+
success: true,
|
|
1388
|
+
username: payload.username,
|
|
1389
|
+
exp: payload.exp
|
|
1390
|
+
});
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
return c.json({
|
|
1393
|
+
success: false,
|
|
1394
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1395
|
+
}, 401);
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
app.post("/api/v1/auth/signup/:appKey", async (c) => {
|
|
1399
|
+
try {
|
|
1400
|
+
const appKey = c.req.param("appKey");
|
|
1401
|
+
const payload = await c.req.json();
|
|
1402
|
+
if (!appKey) {
|
|
1403
|
+
return c.json({ success: false, error: "appKey is required" }, 400);
|
|
1404
|
+
}
|
|
1405
|
+
if (!payload.type) {
|
|
1406
|
+
return c.json({
|
|
1407
|
+
success: false,
|
|
1408
|
+
error: `type is required. Supported: ${getSupportedCredentialTypes().join(", ")}`
|
|
1409
|
+
}, 400);
|
|
1410
|
+
}
|
|
1411
|
+
const handler = getCredentialHandler(payload.type);
|
|
1412
|
+
let googleClientId;
|
|
1413
|
+
if (payload.type === "google") {
|
|
1414
|
+
const appProfileUri = `mutable://accounts/${appKey}/app-profile`;
|
|
1415
|
+
const appProfileResult = await this.credentialClient.read(appProfileUri);
|
|
1416
|
+
if (appProfileResult.success && appProfileResult.record?.data) {
|
|
1417
|
+
const appProfile = appProfileResult.record.data;
|
|
1418
|
+
if (appProfile.payload && typeof appProfile.payload === "object") {
|
|
1419
|
+
googleClientId = appProfile.payload.googleClientId;
|
|
1420
|
+
} else {
|
|
1421
|
+
googleClientId = appProfile.googleClientId;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (!googleClientId) {
|
|
1425
|
+
throw new Error("Google Client ID not configured for this app");
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
const context = {
|
|
1429
|
+
client: this.credentialClient,
|
|
1430
|
+
serverPublicKey,
|
|
1431
|
+
serverIdentityPrivateKeyPem,
|
|
1432
|
+
serverIdentityPublicKeyHex,
|
|
1433
|
+
serverEncryptionPublicKeyHex,
|
|
1434
|
+
serverEncryptionPrivateKeyPem,
|
|
1435
|
+
appKey,
|
|
1436
|
+
googleClientId,
|
|
1437
|
+
logger: this.logger,
|
|
1438
|
+
fetch: this.fetchImpl
|
|
1439
|
+
};
|
|
1440
|
+
const result = await handler.signup(payload, context);
|
|
1441
|
+
const jwt = await createJwt(
|
|
1442
|
+
result.username,
|
|
1443
|
+
this.config.jwtSecret,
|
|
1444
|
+
this.config.jwtExpirationSeconds
|
|
1445
|
+
);
|
|
1446
|
+
return c.json({
|
|
1447
|
+
success: true,
|
|
1448
|
+
username: result.username,
|
|
1449
|
+
token: jwt,
|
|
1450
|
+
expiresIn: this.config.jwtExpirationSeconds,
|
|
1451
|
+
...result.metadata
|
|
1452
|
+
});
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1455
|
+
if (message.includes("already exists") || message.includes("already registered")) {
|
|
1456
|
+
return c.json({ success: false, error: message }, 409);
|
|
1457
|
+
}
|
|
1458
|
+
if (message.includes("Unknown credential type")) {
|
|
1459
|
+
return c.json({ success: false, error: message }, 400);
|
|
1460
|
+
}
|
|
1461
|
+
this.logger.error("Signup error:", error);
|
|
1462
|
+
return c.json({ success: false, error: message }, 500);
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
app.post("/api/v1/auth/login/:appKey", async (c) => {
|
|
1466
|
+
try {
|
|
1467
|
+
const appKey = c.req.param("appKey");
|
|
1468
|
+
const payload = await c.req.json();
|
|
1469
|
+
if (!appKey) {
|
|
1470
|
+
return c.json({ success: false, error: "appKey is required" }, 400);
|
|
1471
|
+
}
|
|
1472
|
+
if (!payload.session) {
|
|
1473
|
+
return c.json({ success: false, error: "session is required" }, 400);
|
|
1474
|
+
}
|
|
1475
|
+
if (!payload.type) {
|
|
1476
|
+
return c.json({
|
|
1477
|
+
success: false,
|
|
1478
|
+
error: `type is required. Supported: ${getSupportedCredentialTypes().join(", ")}`
|
|
1479
|
+
}, 400);
|
|
1480
|
+
}
|
|
1481
|
+
if (!await this.sessionExists(appKey, payload.session)) {
|
|
1482
|
+
return c.json({ success: false, error: "Invalid session" }, 401);
|
|
1483
|
+
}
|
|
1484
|
+
const handler = getCredentialHandler(payload.type);
|
|
1485
|
+
let googleClientId;
|
|
1486
|
+
if (payload.type === "google") {
|
|
1487
|
+
const appProfileUri = `mutable://accounts/${appKey}/app-profile`;
|
|
1488
|
+
const appProfileResult = await this.credentialClient.read(appProfileUri);
|
|
1489
|
+
if (appProfileResult.success && appProfileResult.record?.data) {
|
|
1490
|
+
const appProfile = appProfileResult.record.data;
|
|
1491
|
+
if (appProfile.payload && typeof appProfile.payload === "object") {
|
|
1492
|
+
googleClientId = appProfile.payload.googleClientId;
|
|
1493
|
+
} else {
|
|
1494
|
+
googleClientId = appProfile.googleClientId;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
if (!googleClientId) {
|
|
1498
|
+
throw new Error("Google Client ID not configured for this app");
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
const context = {
|
|
1502
|
+
client: this.credentialClient,
|
|
1503
|
+
serverPublicKey,
|
|
1504
|
+
serverIdentityPrivateKeyPem,
|
|
1505
|
+
serverIdentityPublicKeyHex,
|
|
1506
|
+
serverEncryptionPublicKeyHex,
|
|
1507
|
+
serverEncryptionPrivateKeyPem,
|
|
1508
|
+
appKey,
|
|
1509
|
+
googleClientId,
|
|
1510
|
+
logger: this.logger,
|
|
1511
|
+
fetch: this.fetchImpl
|
|
1512
|
+
};
|
|
1513
|
+
const result = await handler.login(payload, context);
|
|
1514
|
+
const jwt = await createJwt(
|
|
1515
|
+
result.username,
|
|
1516
|
+
this.config.jwtSecret,
|
|
1517
|
+
this.config.jwtExpirationSeconds
|
|
1518
|
+
);
|
|
1519
|
+
return c.json({
|
|
1520
|
+
success: true,
|
|
1521
|
+
username: result.username,
|
|
1522
|
+
token: jwt,
|
|
1523
|
+
expiresIn: this.config.jwtExpirationSeconds,
|
|
1524
|
+
...result.metadata
|
|
1525
|
+
});
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1528
|
+
if (message.includes("Unknown credential type")) {
|
|
1529
|
+
return c.json({ success: false, error: message }, 400);
|
|
1530
|
+
}
|
|
1531
|
+
this.logger.error("Login error:", error);
|
|
1532
|
+
return c.json({ success: false, error: message }, 500);
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
app.post("/api/v1/auth/credentials/change-password/:appKey", async (c) => {
|
|
1536
|
+
try {
|
|
1537
|
+
const appKey = c.req.param("appKey");
|
|
1538
|
+
const authHeader = c.req.header("Authorization");
|
|
1539
|
+
if (!appKey) {
|
|
1540
|
+
return c.json({ success: false, error: "appKey is required" }, 400);
|
|
1541
|
+
}
|
|
1542
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
1543
|
+
return c.json({ success: false, error: "Authorization required" }, 401);
|
|
1544
|
+
}
|
|
1545
|
+
const token = authHeader.substring(7);
|
|
1546
|
+
const jwtPayload = await verifyJwt(token, this.config.jwtSecret);
|
|
1547
|
+
const { oldPassword, newPassword } = await c.req.json();
|
|
1548
|
+
if (!oldPassword || !newPassword) {
|
|
1549
|
+
return c.json({ success: false, error: "oldPassword and newPassword are required" }, 400);
|
|
1550
|
+
}
|
|
1551
|
+
if (newPassword.length < 8) {
|
|
1552
|
+
return c.json({ success: false, error: "newPassword must be at least 8 characters" }, 400);
|
|
1553
|
+
}
|
|
1554
|
+
await changePassword(
|
|
1555
|
+
this.credentialClient,
|
|
1556
|
+
serverPublicKey,
|
|
1557
|
+
jwtPayload.username,
|
|
1558
|
+
oldPassword,
|
|
1559
|
+
newPassword,
|
|
1560
|
+
serverIdentityPrivateKeyPem,
|
|
1561
|
+
serverIdentityPublicKeyHex,
|
|
1562
|
+
serverEncryptionPublicKeyHex,
|
|
1563
|
+
serverEncryptionPrivateKeyPem,
|
|
1564
|
+
appKey
|
|
1565
|
+
);
|
|
1566
|
+
return c.json({ success: true, message: "Password changed successfully" });
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1569
|
+
if (message.includes("expired") || message.includes("incorrect")) {
|
|
1570
|
+
return c.json({ success: false, error: message }, 401);
|
|
1571
|
+
}
|
|
1572
|
+
this.logger.error("Change password error:", error);
|
|
1573
|
+
return c.json({ success: false, error: message }, 500);
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
app.post("/api/v1/auth/credentials/request-password-reset/:appKey", async (c) => {
|
|
1577
|
+
try {
|
|
1578
|
+
const appKey = c.req.param("appKey");
|
|
1579
|
+
const { username } = await c.req.json();
|
|
1580
|
+
if (!appKey) {
|
|
1581
|
+
return c.json({ success: false, error: "appKey is required" }, 400);
|
|
1582
|
+
}
|
|
1583
|
+
if (!username) {
|
|
1584
|
+
return c.json({ success: false, error: "username is required" }, 400);
|
|
1585
|
+
}
|
|
1586
|
+
const resetToken = await createPasswordResetToken(
|
|
1587
|
+
this.credentialClient,
|
|
1588
|
+
serverPublicKey,
|
|
1589
|
+
username,
|
|
1590
|
+
this.config.passwordResetTokenTtlSeconds,
|
|
1591
|
+
serverIdentityPrivateKeyPem,
|
|
1592
|
+
serverIdentityPublicKeyHex,
|
|
1593
|
+
serverEncryptionPublicKeyHex,
|
|
1594
|
+
appKey
|
|
1595
|
+
);
|
|
1596
|
+
return c.json({
|
|
1597
|
+
success: true,
|
|
1598
|
+
message: "Password reset token created",
|
|
1599
|
+
resetToken,
|
|
1600
|
+
expiresIn: this.config.passwordResetTokenTtlSeconds
|
|
1601
|
+
});
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1604
|
+
if (message.includes("not found")) {
|
|
1605
|
+
return c.json({ success: false, error: "User not found" }, 404);
|
|
1606
|
+
}
|
|
1607
|
+
this.logger.error("Request password reset error:", error);
|
|
1608
|
+
return c.json({ success: false, error: message }, 500);
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
app.post("/api/v1/auth/credentials/reset-password/:appKey", async (c) => {
|
|
1612
|
+
try {
|
|
1613
|
+
const appKey = c.req.param("appKey");
|
|
1614
|
+
const { username, resetToken, newPassword } = await c.req.json();
|
|
1615
|
+
if (!appKey) {
|
|
1616
|
+
return c.json({ success: false, error: "appKey is required" }, 400);
|
|
1617
|
+
}
|
|
1618
|
+
if (!username || !resetToken || !newPassword) {
|
|
1619
|
+
return c.json({ success: false, error: "username, resetToken and newPassword are required" }, 400);
|
|
1620
|
+
}
|
|
1621
|
+
if (newPassword.length < 8) {
|
|
1622
|
+
return c.json({ success: false, error: "newPassword must be at least 8 characters" }, 400);
|
|
1623
|
+
}
|
|
1624
|
+
const resetUsername = await resetPasswordWithToken(
|
|
1625
|
+
this.credentialClient,
|
|
1626
|
+
serverPublicKey,
|
|
1627
|
+
resetToken,
|
|
1628
|
+
newPassword,
|
|
1629
|
+
serverIdentityPrivateKeyPem,
|
|
1630
|
+
serverIdentityPublicKeyHex,
|
|
1631
|
+
serverEncryptionPublicKeyHex,
|
|
1632
|
+
serverEncryptionPrivateKeyPem,
|
|
1633
|
+
username,
|
|
1634
|
+
appKey,
|
|
1635
|
+
this.logger
|
|
1636
|
+
);
|
|
1637
|
+
const newToken = await createJwt(
|
|
1638
|
+
resetUsername,
|
|
1639
|
+
this.config.jwtSecret,
|
|
1640
|
+
this.config.jwtExpirationSeconds
|
|
1641
|
+
);
|
|
1642
|
+
return c.json({
|
|
1643
|
+
success: true,
|
|
1644
|
+
message: "Password reset successful",
|
|
1645
|
+
username: resetUsername,
|
|
1646
|
+
token: newToken,
|
|
1647
|
+
expiresIn: this.config.jwtExpirationSeconds
|
|
1648
|
+
});
|
|
1649
|
+
} catch (error) {
|
|
1650
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1651
|
+
if (message.includes("invalid") || message.includes("expired")) {
|
|
1652
|
+
return c.json({ success: false, error: message }, 400);
|
|
1653
|
+
}
|
|
1654
|
+
this.logger.error("Reset password error:", error);
|
|
1655
|
+
return c.json({ success: false, error: message }, 500);
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
app.post("/api/v1/proxy/write", async (c) => {
|
|
1659
|
+
try {
|
|
1660
|
+
const authHeader = c.req.header("Authorization");
|
|
1661
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
1662
|
+
return c.json({ success: false, error: "Authorization required" }, 401);
|
|
1663
|
+
}
|
|
1664
|
+
const token = authHeader.substring(7);
|
|
1665
|
+
const payload = await verifyJwt(token, this.config.jwtSecret);
|
|
1666
|
+
const { uri, data, encrypt: encrypt2 } = await c.req.json();
|
|
1667
|
+
if (!uri) {
|
|
1668
|
+
return c.json({ success: false, error: "uri is required" }, 400);
|
|
1669
|
+
}
|
|
1670
|
+
if (data === void 0) {
|
|
1671
|
+
return c.json({ success: false, error: "data is required" }, 400);
|
|
1672
|
+
}
|
|
1673
|
+
const result = await proxyWrite(
|
|
1674
|
+
this.proxyClient,
|
|
1675
|
+
this.credentialClient,
|
|
1676
|
+
serverPublicKey,
|
|
1677
|
+
payload.username,
|
|
1678
|
+
serverEncryptionPrivateKeyPem,
|
|
1679
|
+
{ uri, data, encrypt: encrypt2 === true }
|
|
1680
|
+
);
|
|
1681
|
+
if (!result.success) {
|
|
1682
|
+
return c.json({ success: false, error: result.error }, 400);
|
|
1683
|
+
}
|
|
1684
|
+
return c.json({
|
|
1685
|
+
success: true,
|
|
1686
|
+
uri,
|
|
1687
|
+
resolvedUri: result.resolvedUri,
|
|
1688
|
+
data,
|
|
1689
|
+
record: result.record
|
|
1690
|
+
});
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1693
|
+
if (message.includes("expired")) {
|
|
1694
|
+
return c.json({ success: false, error: message }, 401);
|
|
1695
|
+
}
|
|
1696
|
+
this.logger.error("Proxy write error:", error);
|
|
1697
|
+
return c.json({ success: false, error: message }, 500);
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
app.get("/api/v1/proxy/read", async (c) => {
|
|
1701
|
+
try {
|
|
1702
|
+
const authHeader = c.req.header("Authorization");
|
|
1703
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
1704
|
+
return c.json({ success: false, error: "Authorization required" }, 401);
|
|
1705
|
+
}
|
|
1706
|
+
const token = authHeader.substring(7);
|
|
1707
|
+
await verifyJwt(token, this.config.jwtSecret);
|
|
1708
|
+
const uri = c.req.query("uri");
|
|
1709
|
+
if (!uri) {
|
|
1710
|
+
return c.json({ success: false, error: "uri query parameter is required" }, 400);
|
|
1711
|
+
}
|
|
1712
|
+
const result = await proxyRead(
|
|
1713
|
+
this.proxyClient,
|
|
1714
|
+
uri,
|
|
1715
|
+
serverEncryptionPrivateKeyPem
|
|
1716
|
+
);
|
|
1717
|
+
if (!result.success) {
|
|
1718
|
+
return c.json({ success: false, error: result.error }, 400);
|
|
1719
|
+
}
|
|
1720
|
+
return c.json({
|
|
1721
|
+
success: true,
|
|
1722
|
+
uri,
|
|
1723
|
+
record: result.record,
|
|
1724
|
+
decrypted: result.decrypted
|
|
1725
|
+
});
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1728
|
+
if (message.includes("expired")) {
|
|
1729
|
+
return c.json({ success: false, error: message }, 401);
|
|
1730
|
+
}
|
|
1731
|
+
this.logger.error("Proxy read error:", error);
|
|
1732
|
+
return c.json({ success: false, error: message }, 500);
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
return app;
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
export {
|
|
1740
|
+
defaultLogger,
|
|
1741
|
+
MemoryFileStorage,
|
|
1742
|
+
ConfigEnvironment,
|
|
1743
|
+
createJwt,
|
|
1744
|
+
verifyJwt,
|
|
1745
|
+
extractUsernameFromJwt,
|
|
1746
|
+
deriveObfuscatedPath,
|
|
1747
|
+
pemToCryptoKey,
|
|
1748
|
+
createSignedEncryptedPayload,
|
|
1749
|
+
decryptSignedEncryptedPayload,
|
|
1750
|
+
encryptForBackend,
|
|
1751
|
+
decryptFromBackend,
|
|
1752
|
+
generateUserKeys,
|
|
1753
|
+
loadUserAccountKey,
|
|
1754
|
+
loadUserEncryptionKey,
|
|
1755
|
+
loadUserKeys,
|
|
1756
|
+
getUserPublicKeys,
|
|
1757
|
+
proxyWrite,
|
|
1758
|
+
proxyRead,
|
|
1759
|
+
userExists,
|
|
1760
|
+
createUser,
|
|
1761
|
+
authenticateUser,
|
|
1762
|
+
changePassword,
|
|
1763
|
+
createPasswordResetToken,
|
|
1764
|
+
resetPasswordWithToken,
|
|
1765
|
+
googleUserExists,
|
|
1766
|
+
createGoogleUser,
|
|
1767
|
+
authenticateGoogleUser,
|
|
1768
|
+
clearGooglePublicKeyCache,
|
|
1769
|
+
verifyGoogleIdToken,
|
|
1770
|
+
generateGoogleUsername,
|
|
1771
|
+
getCredentialHandler,
|
|
1772
|
+
registerCredentialHandler,
|
|
1773
|
+
getSupportedCredentialTypes,
|
|
1774
|
+
WalletServerCore
|
|
1775
|
+
};
|