@bandeira-tech/b3nd-save 0.8.1
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/LICENSE +21 -0
- package/esm/_dnt.shims.d.ts +2 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/_dnt.shims.js +57 -0
- package/esm/byte-entity-shim.d.ts +47 -0
- package/esm/byte-entity-shim.d.ts.map +1 -0
- package/esm/byte-entity-shim.js +138 -0
- package/esm/clients/mod.d.ts +15 -0
- package/esm/clients/mod.d.ts.map +1 -0
- package/esm/clients/mod.js +14 -0
- package/esm/clients/save-client.d.ts +87 -0
- package/esm/clients/save-client.d.ts.map +1 -0
- package/esm/clients/save-client.js +174 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.d.ts +40 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.js +31 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/client-console/client.d.ts +30 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/client-console/client.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/client-console/client.js +70 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encoding/encoding.d.ts +16 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encoding/encoding.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encoding/encoding.js +50 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encrypt/mod.d.ts +235 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encrypt/mod.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encrypt/mod.js +619 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/functional-client/functional-client.d.ts +43 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/functional-client/functional-client.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/functional-client/functional-client.js +54 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/match-pattern/match-pattern.d.ts +47 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/match-pattern/match-pattern.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/match-pattern/match-pattern.js +134 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/decorators.d.ts +20 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/decorators.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/decorators.js +34 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/network.d.ts +46 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/network.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/network.js +131 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/peer.d.ts +27 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/peer.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/peer.js +25 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/flood.d.ts +59 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/flood.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/flood.js +188 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/path-vector.d.ts +35 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/path-vector.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/path-vector.js +57 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/tell-and-read.d.ts +117 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/tell-and-read.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/tell-and-read.js +125 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/types.d.ts +112 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/types.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/types.js +21 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/observe-emitter/observe-emitter.d.ts +35 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/observe-emitter/observe-emitter.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/observe-emitter/observe-emitter.js +106 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/connection.d.ts +88 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/connection.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/connection.js +86 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/events.d.ts +83 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/events.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/events.js +115 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/hooks.d.ts +164 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/hooks.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/hooks.js +67 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/identity.d.ts +99 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/identity.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/identity.js +190 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/operation-handle.d.ts +185 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/operation-handle.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/operation-handle.js +103 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/reactions.d.ts +58 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/reactions.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/reactions.js +64 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/rig.d.ts +295 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/rig.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/rig.js +898 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/types.d.ts +167 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/types.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/types.js +5 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.d.ts +278 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.js +89 -0
- package/esm/dispatch.d.ts +35 -0
- package/esm/dispatch.d.ts.map +1 -0
- package/esm/dispatch.js +40 -0
- package/esm/elasticsearch/mod.d.ts +31 -0
- package/esm/elasticsearch/mod.d.ts.map +1 -0
- package/esm/elasticsearch/mod.js +8 -0
- package/esm/elasticsearch/store.d.ts +44 -0
- package/esm/elasticsearch/store.d.ts.map +1 -0
- package/esm/elasticsearch/store.js +213 -0
- package/esm/entity-store.d.ts +106 -0
- package/esm/entity-store.d.ts.map +1 -0
- package/esm/entity-store.js +46 -0
- package/esm/entity.d.ts +125 -0
- package/esm/entity.d.ts.map +1 -0
- package/esm/entity.js +66 -0
- package/esm/errors.d.ts +23 -0
- package/esm/errors.d.ts.map +1 -0
- package/esm/errors.js +25 -0
- package/esm/fs/mod.d.ts +28 -0
- package/esm/fs/mod.d.ts.map +1 -0
- package/esm/fs/mod.js +13 -0
- package/esm/fs/store.d.ts +45 -0
- package/esm/fs/store.d.ts.map +1 -0
- package/esm/fs/store.js +194 -0
- package/esm/indexeddb/mod.d.ts +9 -0
- package/esm/indexeddb/mod.d.ts.map +1 -0
- package/esm/indexeddb/mod.js +8 -0
- package/esm/indexeddb/store.d.ts +47 -0
- package/esm/indexeddb/store.d.ts.map +1 -0
- package/esm/indexeddb/store.js +299 -0
- package/esm/ipfs/mod.d.ts +24 -0
- package/esm/ipfs/mod.d.ts.map +1 -0
- package/esm/ipfs/mod.js +13 -0
- package/esm/ipfs/store.d.ts +42 -0
- package/esm/ipfs/store.d.ts.map +1 -0
- package/esm/ipfs/store.js +197 -0
- package/esm/localstorage/mod.d.ts +9 -0
- package/esm/localstorage/mod.d.ts.map +1 -0
- package/esm/localstorage/mod.js +8 -0
- package/esm/localstorage/store.d.ts +45 -0
- package/esm/localstorage/store.d.ts.map +1 -0
- package/esm/localstorage/store.js +147 -0
- package/esm/memory/mod.d.ts +12 -0
- package/esm/memory/mod.d.ts.map +1 -0
- package/esm/memory/mod.js +11 -0
- package/esm/memory/store.d.ts +73 -0
- package/esm/memory/store.d.ts.map +1 -0
- package/esm/memory/store.js +300 -0
- package/esm/mod.d.ts +36 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +31 -0
- package/esm/mongo/mod.d.ts +35 -0
- package/esm/mongo/mod.d.ts.map +1 -0
- package/esm/mongo/mod.js +8 -0
- package/esm/mongo/store.d.ts +42 -0
- package/esm/mongo/store.d.ts.map +1 -0
- package/esm/mongo/store.js +185 -0
- package/esm/package.json +3 -0
- package/esm/payload.d.ts +24 -0
- package/esm/payload.d.ts.map +1 -0
- package/esm/payload.js +30 -0
- package/esm/postgres/columns.d.ts +33 -0
- package/esm/postgres/columns.d.ts.map +1 -0
- package/esm/postgres/columns.js +51 -0
- package/esm/postgres/mod.d.ts +19 -0
- package/esm/postgres/mod.d.ts.map +1 -0
- package/esm/postgres/mod.js +9 -0
- package/esm/postgres/store.d.ts +75 -0
- package/esm/postgres/store.d.ts.map +1 -0
- package/esm/postgres/store.js +364 -0
- package/esm/read.d.ts +40 -0
- package/esm/read.d.ts.map +1 -0
- package/esm/read.js +67 -0
- package/esm/s3/mod.d.ts +23 -0
- package/esm/s3/mod.d.ts.map +1 -0
- package/esm/s3/mod.js +13 -0
- package/esm/s3/store.d.ts +46 -0
- package/esm/s3/store.d.ts.map +1 -0
- package/esm/s3/store.js +183 -0
- package/esm/sqlite/mod.d.ts +18 -0
- package/esm/sqlite/mod.d.ts.map +1 -0
- package/esm/sqlite/mod.js +8 -0
- package/esm/sqlite/schema.d.ts +10 -0
- package/esm/sqlite/schema.d.ts.map +1 -0
- package/esm/sqlite/schema.js +26 -0
- package/esm/sqlite/store.d.ts +42 -0
- package/esm/sqlite/store.d.ts.map +1 -0
- package/esm/sqlite/store.js +184 -0
- package/esm/types.d.ts +101 -0
- package/esm/types.d.ts.map +1 -0
- package/esm/types.js +21 -0
- package/esm/url.d.ts +108 -0
- package/esm/url.d.ts.map +1 -0
- package/esm/url.js +151 -0
- package/package.json +93 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { decodeBase64, decodeHex, encodeBase64, encodeHex, } from "../encoding/encoding.js";
|
|
2
|
+
import _canonicalize from "canonicalize";
|
|
3
|
+
// CJS/ESM interop: cast to callable
|
|
4
|
+
const canonicalize = _canonicalize;
|
|
5
|
+
export class IdentityKey {
|
|
6
|
+
privateKey;
|
|
7
|
+
publicKeyHex;
|
|
8
|
+
constructor(privateKey, publicKeyHex) {
|
|
9
|
+
this.privateKey = privateKey;
|
|
10
|
+
this.publicKeyHex = publicKeyHex;
|
|
11
|
+
}
|
|
12
|
+
static async generate() {
|
|
13
|
+
const pair = await generateSigningKeyPair();
|
|
14
|
+
const privateKeyPem = await exportPrivateKeyPem(pair.privateKey, "PRIVATE KEY");
|
|
15
|
+
return {
|
|
16
|
+
key: new IdentityKey(pair.privateKey, pair.publicKeyHex),
|
|
17
|
+
privateKeyPem,
|
|
18
|
+
publicKeyHex: pair.publicKeyHex,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
static async fromPem(pem, publicKeyHex) {
|
|
22
|
+
const privateKey = await pemToCryptoKey(pem, "Ed25519");
|
|
23
|
+
return new IdentityKey(privateKey, publicKeyHex);
|
|
24
|
+
}
|
|
25
|
+
static async fromHex(params) {
|
|
26
|
+
const privateKeyBytes = decodeHex(params.privateKeyHex).buffer;
|
|
27
|
+
const privateKey = await crypto.subtle.importKey("pkcs8", privateKeyBytes, { name: "Ed25519", namedCurve: "Ed25519" }, false, ["sign"]);
|
|
28
|
+
return new IdentityKey(privateKey, params.publicKeyHex);
|
|
29
|
+
}
|
|
30
|
+
async sign(payload) {
|
|
31
|
+
return await sign(this.privateKey, payload);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class PublicEncryptionKey {
|
|
35
|
+
publicKeyHex;
|
|
36
|
+
publicKey;
|
|
37
|
+
constructor(publicKeyHex, publicKey) {
|
|
38
|
+
this.publicKeyHex = publicKeyHex;
|
|
39
|
+
this.publicKey = publicKey;
|
|
40
|
+
}
|
|
41
|
+
static async fromHex(publicKeyHex) {
|
|
42
|
+
const publicKeyBytes = decodeHex(publicKeyHex).buffer;
|
|
43
|
+
const publicKey = await crypto.subtle.importKey("raw", publicKeyBytes, { name: "X25519", namedCurve: "X25519" }, false, []);
|
|
44
|
+
return new PublicEncryptionKey(publicKeyHex, publicKey);
|
|
45
|
+
}
|
|
46
|
+
static async generatePair() {
|
|
47
|
+
const pair = await generateEncryptionKeyPair();
|
|
48
|
+
const privateKeyBytes = new Uint8Array(await crypto.subtle.exportKey("pkcs8", pair.privateKey));
|
|
49
|
+
return {
|
|
50
|
+
publicKey: new PublicEncryptionKey(pair.publicKeyHex, pair.publicKey),
|
|
51
|
+
privateKeyHex: encodeHex(privateKeyBytes),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async encrypt(data) {
|
|
55
|
+
return await encrypt(data, this.publicKeyHex);
|
|
56
|
+
}
|
|
57
|
+
toHex() {
|
|
58
|
+
return this.publicKeyHex;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export class SecretEncryptionKey {
|
|
62
|
+
keyHex;
|
|
63
|
+
constructor(keyHex) {
|
|
64
|
+
this.keyHex = keyHex;
|
|
65
|
+
}
|
|
66
|
+
static async fromSecret(params) {
|
|
67
|
+
const keyHex = await deriveKeyFromSeed(params.secret, params.salt, params.iterations ?? 600000);
|
|
68
|
+
return new SecretEncryptionKey(keyHex);
|
|
69
|
+
}
|
|
70
|
+
static fromHex(keyHex) {
|
|
71
|
+
return new SecretEncryptionKey(keyHex);
|
|
72
|
+
}
|
|
73
|
+
async encrypt(data) {
|
|
74
|
+
return await encryptSymmetric(data, this.keyHex);
|
|
75
|
+
}
|
|
76
|
+
async decrypt(payload) {
|
|
77
|
+
return await decryptSymmetric(payload, this.keyHex);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export class PrivateEncryptionKey {
|
|
81
|
+
privateKey;
|
|
82
|
+
privateKeyHex;
|
|
83
|
+
publicKeyHex;
|
|
84
|
+
constructor(privateKey, privateKeyHex, publicKeyHex) {
|
|
85
|
+
this.privateKey = privateKey;
|
|
86
|
+
this.privateKeyHex = privateKeyHex;
|
|
87
|
+
this.publicKeyHex = publicKeyHex;
|
|
88
|
+
}
|
|
89
|
+
static async fromHex(params) {
|
|
90
|
+
const { privateKeyHex, publicKeyHex } = params;
|
|
91
|
+
const privateKeyBytes = decodeHex(privateKeyHex).buffer;
|
|
92
|
+
const privateKey = await crypto.subtle.importKey("pkcs8", privateKeyBytes, { name: "X25519", namedCurve: "X25519" }, true, // extractable — needed for HKDF salt derivation in decrypt()
|
|
93
|
+
["deriveBits"]);
|
|
94
|
+
return new PrivateEncryptionKey(privateKey, privateKeyHex, publicKeyHex);
|
|
95
|
+
}
|
|
96
|
+
static async generatePair() {
|
|
97
|
+
const pair = await generateEncryptionKeyPair();
|
|
98
|
+
const privateKeyBytes = new Uint8Array(await crypto.subtle.exportKey("pkcs8", pair.privateKey));
|
|
99
|
+
const privateKeyHex = encodeHex(privateKeyBytes);
|
|
100
|
+
const publicKey = new PublicEncryptionKey(pair.publicKeyHex, pair.publicKey);
|
|
101
|
+
return {
|
|
102
|
+
privateKey: new PrivateEncryptionKey(pair.privateKey, privateKeyHex, pair.publicKeyHex),
|
|
103
|
+
publicKey,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
toPublic() {
|
|
107
|
+
return new PublicEncryptionKey(this.publicKeyHex, null);
|
|
108
|
+
}
|
|
109
|
+
async decrypt(payload) {
|
|
110
|
+
return await decrypt(payload, this.privateKey, this.publicKeyHex);
|
|
111
|
+
}
|
|
112
|
+
toHex() {
|
|
113
|
+
return this.privateKeyHex;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Convert a PEM-encoded private key to a CryptoKey
|
|
118
|
+
*/
|
|
119
|
+
export async function pemToCryptoKey(pem, algorithm, extractable = false) {
|
|
120
|
+
const lines = pem
|
|
121
|
+
.split("\n")
|
|
122
|
+
.map((l) => l.trim())
|
|
123
|
+
.filter((l) => l.length > 0 && !l.startsWith("---"));
|
|
124
|
+
if (lines.length === 0) {
|
|
125
|
+
throw new Error("Invalid PEM: no key data");
|
|
126
|
+
}
|
|
127
|
+
const base64 = lines.join("");
|
|
128
|
+
const binary = atob(base64);
|
|
129
|
+
const bytes = new Uint8Array(binary.length);
|
|
130
|
+
for (let i = 0; i < binary.length; i++)
|
|
131
|
+
bytes[i] = binary.charCodeAt(i);
|
|
132
|
+
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
133
|
+
if (algorithm === "Ed25519") {
|
|
134
|
+
return await crypto.subtle.importKey("pkcs8", buffer, { name: "Ed25519", namedCurve: "Ed25519" }, extractable, ["sign"]);
|
|
135
|
+
}
|
|
136
|
+
return await crypto.subtle.importKey("pkcs8", buffer, { name: "X25519", namedCurve: "X25519" }, extractable, ["deriveBits"]);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Generate an Ed25519 keypair for signing
|
|
140
|
+
*/
|
|
141
|
+
export async function generateSigningKeyPair() {
|
|
142
|
+
const keyPair = await crypto.subtle.generateKey({
|
|
143
|
+
name: "Ed25519",
|
|
144
|
+
namedCurve: "Ed25519",
|
|
145
|
+
}, true, ["sign", "verify"]);
|
|
146
|
+
const publicKeyBytes = await crypto.subtle.exportKey("raw", keyPair.publicKey);
|
|
147
|
+
const privateKeyBytes = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
|
|
148
|
+
return {
|
|
149
|
+
publicKey: keyPair.publicKey,
|
|
150
|
+
privateKey: keyPair.privateKey,
|
|
151
|
+
publicKeyHex: encodeHex(new Uint8Array(publicKeyBytes)),
|
|
152
|
+
privateKeyHex: encodeHex(new Uint8Array(privateKeyBytes)),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Generate an X25519 keypair for encryption (ECDH)
|
|
157
|
+
*/
|
|
158
|
+
export async function generateEncryptionKeyPair() {
|
|
159
|
+
const keyPair = await crypto.subtle.generateKey({
|
|
160
|
+
name: "X25519",
|
|
161
|
+
namedCurve: "X25519",
|
|
162
|
+
}, true, ["deriveBits"]);
|
|
163
|
+
const publicKeyBytes = await crypto.subtle.exportKey("raw", keyPair.publicKey);
|
|
164
|
+
return {
|
|
165
|
+
publicKey: keyPair.publicKey,
|
|
166
|
+
privateKey: keyPair.privateKey,
|
|
167
|
+
publicKeyHex: encodeHex(new Uint8Array(publicKeyBytes)),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Sign a payload with an Ed25519 private key
|
|
172
|
+
*/
|
|
173
|
+
export async function sign(privateKey, payload) {
|
|
174
|
+
const encoder = new TextEncoder();
|
|
175
|
+
const canonical = canonicalize(payload);
|
|
176
|
+
if (canonical === undefined) {
|
|
177
|
+
throw new Error("Payload is not canonicalizable (contains non-JSON-serializable values)");
|
|
178
|
+
}
|
|
179
|
+
const data = encoder.encode(canonical);
|
|
180
|
+
const signature = await crypto.subtle.sign("Ed25519", privateKey, data);
|
|
181
|
+
return encodeHex(new Uint8Array(signature));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Sign a payload with an Ed25519 private key from hex
|
|
185
|
+
*/
|
|
186
|
+
export async function signWithHex(privateKeyHex, payload) {
|
|
187
|
+
const privateKeyBytes = decodeHex(privateKeyHex).buffer;
|
|
188
|
+
const privateKey = await crypto.subtle.importKey("pkcs8", privateKeyBytes, {
|
|
189
|
+
name: "Ed25519",
|
|
190
|
+
namedCurve: "Ed25519",
|
|
191
|
+
}, false, ["sign"]);
|
|
192
|
+
return await sign(privateKey, payload);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Verify a signature using Ed25519 public key
|
|
196
|
+
*/
|
|
197
|
+
export async function verify(publicKeyHex, signatureHex, payload) {
|
|
198
|
+
try {
|
|
199
|
+
const publicKeyBytes = decodeHex(publicKeyHex).buffer;
|
|
200
|
+
const publicKey = await crypto.subtle.importKey("raw", publicKeyBytes, {
|
|
201
|
+
name: "Ed25519",
|
|
202
|
+
namedCurve: "Ed25519",
|
|
203
|
+
}, false, ["verify"]);
|
|
204
|
+
const encoder = new TextEncoder();
|
|
205
|
+
const canonical = canonicalize(payload);
|
|
206
|
+
if (canonical === undefined) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const data = encoder.encode(canonical);
|
|
210
|
+
const signatureBytes = decodeHex(signatureHex).buffer;
|
|
211
|
+
return await crypto.subtle.verify("Ed25519", publicKey, signatureBytes, data);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
console.error("Verification error:", error);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Derive an AES-GCM key from ECDH shared secret using HKDF-SHA256 (RFC 5869).
|
|
220
|
+
* Provides domain separation and defense-in-depth per NIST SP 800-56C.
|
|
221
|
+
*
|
|
222
|
+
* @param sharedSecret - Raw ECDH shared secret (ArrayBuffer)
|
|
223
|
+
* @param pubkey1Hex - First public key hex (sorted deterministically for salt)
|
|
224
|
+
* @param pubkey2Hex - Second public key hex
|
|
225
|
+
* @param usage - "encrypt" or "decrypt"
|
|
226
|
+
*/
|
|
227
|
+
async function deriveAesKeyFromEcdh(sharedSecret, pubkey1Hex, pubkey2Hex, usage) {
|
|
228
|
+
const encoder = new TextEncoder();
|
|
229
|
+
// Deterministic salt from sorted public keys, hashed to 32 bytes
|
|
230
|
+
// (raw concatenation exceeds HKDF salt length limits in some implementations)
|
|
231
|
+
const [sortedA, sortedB] = [pubkey1Hex, pubkey2Hex].sort();
|
|
232
|
+
const saltInput = encoder.encode(sortedA + sortedB);
|
|
233
|
+
const salt = new Uint8Array(await crypto.subtle.digest("SHA-256", saltInput));
|
|
234
|
+
const info = encoder.encode("b3nd-aes-gcm-key-v1");
|
|
235
|
+
// Import shared secret as HKDF key material
|
|
236
|
+
const hkdfKey = await crypto.subtle.importKey("raw", sharedSecret, "HKDF", false, ["deriveKey"]);
|
|
237
|
+
// Derive AES-256-GCM key via HKDF-SHA256
|
|
238
|
+
return await crypto.subtle.deriveKey({
|
|
239
|
+
name: "HKDF",
|
|
240
|
+
hash: "SHA-256",
|
|
241
|
+
salt,
|
|
242
|
+
info,
|
|
243
|
+
}, hkdfKey, { name: "AES-GCM", length: 256 }, false, [usage]);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Encrypt data using X25519 ECDH + HKDF + AES-GCM
|
|
247
|
+
* Uses ephemeral keypair for forward secrecy.
|
|
248
|
+
* HKDF provides domain separation per NIST SP 800-56C / RFC 5869.
|
|
249
|
+
*/
|
|
250
|
+
export async function encrypt(data, recipientPublicKeyHex) {
|
|
251
|
+
// Generate ephemeral keypair for ECDH
|
|
252
|
+
const ephemeralKeyPair = await generateEncryptionKeyPair();
|
|
253
|
+
// Import recipient's public key
|
|
254
|
+
const recipientPublicKeyBytes = decodeHex(recipientPublicKeyHex).buffer;
|
|
255
|
+
const recipientPublicKey = await crypto.subtle.importKey("raw", recipientPublicKeyBytes, {
|
|
256
|
+
name: "X25519",
|
|
257
|
+
namedCurve: "X25519",
|
|
258
|
+
}, false, []);
|
|
259
|
+
// Derive shared secret using ECDH
|
|
260
|
+
const sharedSecret = await crypto.subtle.deriveBits({
|
|
261
|
+
name: "X25519",
|
|
262
|
+
public: recipientPublicKey,
|
|
263
|
+
}, ephemeralKeyPair.privateKey, 256);
|
|
264
|
+
// Derive AES key via HKDF (not raw ECDH output)
|
|
265
|
+
const aesKey = await deriveAesKeyFromEcdh(sharedSecret, ephemeralKeyPair.publicKeyHex, recipientPublicKeyHex, "encrypt");
|
|
266
|
+
// Generate nonce
|
|
267
|
+
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
|
268
|
+
// Encrypt data
|
|
269
|
+
const ciphertext = await crypto.subtle.encrypt({
|
|
270
|
+
name: "AES-GCM",
|
|
271
|
+
iv: nonce,
|
|
272
|
+
}, aesKey, data);
|
|
273
|
+
return {
|
|
274
|
+
data: encodeBase64(new Uint8Array(ciphertext)),
|
|
275
|
+
nonce: encodeBase64(nonce),
|
|
276
|
+
ephemeralPublicKey: ephemeralKeyPair.publicKeyHex,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Decrypt data using X25519 ECDH + HKDF + AES-GCM
|
|
281
|
+
*/
|
|
282
|
+
export async function decrypt(encryptedPayload, recipientPrivateKey, recipientPublicKeyHex) {
|
|
283
|
+
if (!encryptedPayload.ephemeralPublicKey) {
|
|
284
|
+
throw new Error("Missing ephemeral public key");
|
|
285
|
+
}
|
|
286
|
+
// We need the recipient's public key hex for HKDF salt.
|
|
287
|
+
// If caller provided it, use it directly; otherwise extract via JWK round-trip
|
|
288
|
+
// (requires the key to have been imported with extractable: true).
|
|
289
|
+
if (!recipientPublicKeyHex) {
|
|
290
|
+
const jwk = await crypto.subtle.exportKey("jwk", recipientPrivateKey);
|
|
291
|
+
const recipientPub = await crypto.subtle.importKey("jwk", { kty: jwk.kty, crv: jwk.crv, x: jwk.x, key_ops: [] }, { name: "X25519", namedCurve: "X25519" }, true, []);
|
|
292
|
+
const recipientPubBytes = new Uint8Array(await crypto.subtle.exportKey("raw", recipientPub));
|
|
293
|
+
recipientPublicKeyHex = encodeHex(recipientPubBytes);
|
|
294
|
+
}
|
|
295
|
+
// Import ephemeral public key
|
|
296
|
+
const ephemeralPublicKeyBytes = decodeHex(encryptedPayload.ephemeralPublicKey).buffer;
|
|
297
|
+
const ephemeralPublicKey = await crypto.subtle.importKey("raw", ephemeralPublicKeyBytes, {
|
|
298
|
+
name: "X25519",
|
|
299
|
+
namedCurve: "X25519",
|
|
300
|
+
}, false, []);
|
|
301
|
+
// Derive shared secret using ECDH
|
|
302
|
+
const sharedSecret = await crypto.subtle.deriveBits({
|
|
303
|
+
name: "X25519",
|
|
304
|
+
public: ephemeralPublicKey,
|
|
305
|
+
}, recipientPrivateKey, 256);
|
|
306
|
+
// Derive AES key via HKDF (not raw ECDH output)
|
|
307
|
+
const aesKey = await deriveAesKeyFromEcdh(sharedSecret, encryptedPayload.ephemeralPublicKey, recipientPublicKeyHex, "decrypt");
|
|
308
|
+
// Decrypt data
|
|
309
|
+
const ciphertext = new Uint8Array(decodeBase64(encryptedPayload.data));
|
|
310
|
+
const nonce = new Uint8Array(decodeBase64(encryptedPayload.nonce));
|
|
311
|
+
const plaintext = await crypto.subtle.decrypt({
|
|
312
|
+
name: "AES-GCM",
|
|
313
|
+
iv: nonce,
|
|
314
|
+
}, aesKey, ciphertext);
|
|
315
|
+
return new Uint8Array(plaintext);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Decrypt data using private key from hex
|
|
319
|
+
*/
|
|
320
|
+
export async function decryptWithHex(encryptedPayload, recipientPrivateKeyHex) {
|
|
321
|
+
const privateKeyBytes = decodeHex(recipientPrivateKeyHex).buffer;
|
|
322
|
+
const privateKey = await crypto.subtle.importKey("pkcs8", privateKeyBytes, {
|
|
323
|
+
name: "X25519",
|
|
324
|
+
namedCurve: "X25519",
|
|
325
|
+
}, true, // extractable — needed for HKDF salt derivation in decrypt()
|
|
326
|
+
["deriveBits"]);
|
|
327
|
+
return await decrypt(encryptedPayload, privateKey);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Create an authenticated message (signed but not encrypted)
|
|
331
|
+
*/
|
|
332
|
+
export async function createAuthenticatedMessage(payload, signers) {
|
|
333
|
+
const auth = await Promise.all(signers.map(async (signer) => {
|
|
334
|
+
const signature = await sign(signer.privateKey, payload);
|
|
335
|
+
return {
|
|
336
|
+
pubkey: signer.publicKeyHex,
|
|
337
|
+
signature,
|
|
338
|
+
};
|
|
339
|
+
}));
|
|
340
|
+
return {
|
|
341
|
+
auth,
|
|
342
|
+
payload,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Create an authenticated message with hex-encoded keys (convenience wrapper)
|
|
347
|
+
*/
|
|
348
|
+
export async function createAuthenticatedMessageWithHex(payload, pubkeyHex, privateKeyHex) {
|
|
349
|
+
const signature = await signWithHex(privateKeyHex, payload);
|
|
350
|
+
return {
|
|
351
|
+
auth: [{ pubkey: pubkeyHex, signature }],
|
|
352
|
+
payload,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Derive an encryption key from seed and salt using PBKDF2
|
|
357
|
+
* Returns hex-encoded key suitable for encrypt/decrypt functions
|
|
358
|
+
*/
|
|
359
|
+
export async function deriveKeyFromSeed(seed, salt, iterations = 600000) {
|
|
360
|
+
const encoder = new TextEncoder();
|
|
361
|
+
// Import seed as key material
|
|
362
|
+
const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(seed), "PBKDF2", false, ["deriveBits"]);
|
|
363
|
+
// Derive 256-bit key
|
|
364
|
+
const derivedBits = await crypto.subtle.deriveBits({
|
|
365
|
+
name: "PBKDF2",
|
|
366
|
+
salt: encoder.encode(salt),
|
|
367
|
+
iterations,
|
|
368
|
+
hash: "SHA-256",
|
|
369
|
+
}, keyMaterial, 256);
|
|
370
|
+
return encodeHex(new Uint8Array(derivedBits));
|
|
371
|
+
}
|
|
372
|
+
// Utility functions
|
|
373
|
+
export function generateNonce(length = 12) {
|
|
374
|
+
return crypto.getRandomValues(new Uint8Array(length));
|
|
375
|
+
}
|
|
376
|
+
export function generateRandomData(size) {
|
|
377
|
+
return crypto.getRandomValues(new Uint8Array(size));
|
|
378
|
+
}
|
|
379
|
+
export async function exportPrivateKeyPem(privateKey, label) {
|
|
380
|
+
const der = new Uint8Array(await crypto.subtle.exportKey("pkcs8", privateKey));
|
|
381
|
+
return toPem(der, label);
|
|
382
|
+
}
|
|
383
|
+
function toPem(der, label) {
|
|
384
|
+
const base64 = encodeBase64(der);
|
|
385
|
+
const formatted = base64.match(/.{1,64}/g)?.join("\n") ?? base64;
|
|
386
|
+
return `-----BEGIN ${label}-----\n${formatted}\n-----END ${label}-----`;
|
|
387
|
+
}
|
|
388
|
+
export async function signPayload(params) {
|
|
389
|
+
const { payload, identity } = params;
|
|
390
|
+
const signature = await identity.sign(payload);
|
|
391
|
+
return [{ pubkey: identity.publicKeyHex, signature }];
|
|
392
|
+
}
|
|
393
|
+
export async function verifyPayload(params) {
|
|
394
|
+
const { payload, auth } = params;
|
|
395
|
+
const results = await Promise.all(auth.map(async (entry) => {
|
|
396
|
+
const ok = await verify(entry.pubkey, entry.signature, payload);
|
|
397
|
+
return { pubkey: entry.pubkey, ok };
|
|
398
|
+
}));
|
|
399
|
+
const verified = results.every((r) => r.ok);
|
|
400
|
+
const signers = results.filter((r) => r.ok).map((r) => r.pubkey);
|
|
401
|
+
return { verified, signers };
|
|
402
|
+
}
|
|
403
|
+
export async function createSignedEncryptedMessage(paramsOrData, signers, recipientPublicKeyHex) {
|
|
404
|
+
if (typeof paramsOrData === "object" && paramsOrData !== null &&
|
|
405
|
+
!(paramsOrData instanceof Uint8Array) &&
|
|
406
|
+
"encryptionKey" in paramsOrData) {
|
|
407
|
+
const { data, identity, encryptionKey } = paramsOrData;
|
|
408
|
+
const payload = await encryptionKey.encrypt(data);
|
|
409
|
+
const auth = await signPayload({ payload, identity });
|
|
410
|
+
return { auth, payload };
|
|
411
|
+
}
|
|
412
|
+
if (!signers || !recipientPublicKeyHex) {
|
|
413
|
+
throw new Error("Invalid arguments for legacy createSignedEncryptedMessage");
|
|
414
|
+
}
|
|
415
|
+
const encrypted = await encrypt(paramsOrData, recipientPublicKeyHex);
|
|
416
|
+
const auth = await Promise.all(signers.map(async (signer) => {
|
|
417
|
+
const signature = await sign(signer.privateKey, encrypted);
|
|
418
|
+
return { pubkey: signer.publicKeyHex, signature };
|
|
419
|
+
}));
|
|
420
|
+
return { auth, payload: encrypted };
|
|
421
|
+
}
|
|
422
|
+
export async function verifyAndDecryptMessage(params) {
|
|
423
|
+
const { message, encryptionKey } = params;
|
|
424
|
+
const { verified, signers } = await verifyPayload({
|
|
425
|
+
payload: message.payload,
|
|
426
|
+
auth: message.auth,
|
|
427
|
+
});
|
|
428
|
+
const data = encryptionKey instanceof SecretEncryptionKey
|
|
429
|
+
? await encryptionKey.decrypt(message.payload)
|
|
430
|
+
: await encryptionKey.decrypt(message.payload);
|
|
431
|
+
return { data, verified, signers };
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Encrypt data using a symmetric key (AES-GCM) provided as hex
|
|
435
|
+
*/
|
|
436
|
+
export async function encryptSymmetric(data, keyHex) {
|
|
437
|
+
const keyBytes = decodeHex(keyHex).buffer;
|
|
438
|
+
const aesKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt"]);
|
|
439
|
+
const nonce = generateNonce();
|
|
440
|
+
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, aesKey, data);
|
|
441
|
+
return {
|
|
442
|
+
data: encodeBase64(new Uint8Array(ciphertext)),
|
|
443
|
+
nonce: encodeBase64(nonce),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Decrypt data using a symmetric key (AES-GCM) provided as hex
|
|
448
|
+
*/
|
|
449
|
+
export async function decryptSymmetric(payload, keyHex) {
|
|
450
|
+
const keyBytes = decodeHex(keyHex).buffer;
|
|
451
|
+
const aesKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["decrypt"]);
|
|
452
|
+
const ciphertext = new Uint8Array(decodeBase64(payload.data));
|
|
453
|
+
const nonce = new Uint8Array(decodeBase64(payload.nonce));
|
|
454
|
+
const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv: nonce }, aesKey, ciphertext);
|
|
455
|
+
return new Uint8Array(plaintext);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Create a signed symmetric message (signs encrypted payload)
|
|
459
|
+
*/
|
|
460
|
+
export async function createSignedSymmetricMessage(data, signers, keyHex) {
|
|
461
|
+
const encryptedPayload = await encryptSymmetric(data, keyHex);
|
|
462
|
+
const auth = await Promise.all(signers.map(async (signer) => {
|
|
463
|
+
const signature = await sign(signer.privateKey, encryptedPayload);
|
|
464
|
+
return {
|
|
465
|
+
pubkey: signer.publicKeyHex,
|
|
466
|
+
signature,
|
|
467
|
+
};
|
|
468
|
+
}));
|
|
469
|
+
return {
|
|
470
|
+
auth,
|
|
471
|
+
payload: encryptedPayload,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Compute HMAC-SHA256 of data with a key
|
|
476
|
+
* Returns hex-encoded 32-byte result
|
|
477
|
+
*/
|
|
478
|
+
export async function hmac(key, data) {
|
|
479
|
+
const encoder = new TextEncoder();
|
|
480
|
+
const cryptoKey = await crypto.subtle.importKey("raw", encoder.encode(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
481
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(data));
|
|
482
|
+
return encodeHex(new Uint8Array(signature));
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Derive a deterministic Ed25519 signing keypair from a seed string.
|
|
486
|
+
* Uses HKDF to expand the seed into key material suitable for Ed25519.
|
|
487
|
+
*
|
|
488
|
+
* The same seed always produces the same keypair.
|
|
489
|
+
*/
|
|
490
|
+
export async function deriveSigningKeyPairFromSeed(seed) {
|
|
491
|
+
const encoder = new TextEncoder();
|
|
492
|
+
// Import seed as HKDF key material
|
|
493
|
+
const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(seed), "HKDF", false, ["deriveBits"]);
|
|
494
|
+
// Derive 32 bytes for Ed25519 private key seed
|
|
495
|
+
const derivedBits = await crypto.subtle.deriveBits({
|
|
496
|
+
name: "HKDF",
|
|
497
|
+
hash: "SHA-256",
|
|
498
|
+
salt: encoder.encode("b3nd-signing-key"),
|
|
499
|
+
info: encoder.encode("Ed25519"),
|
|
500
|
+
}, keyMaterial, 256);
|
|
501
|
+
const privateKeyBytes = new Uint8Array(derivedBits);
|
|
502
|
+
// Ed25519 PKCS8 wrapper: ASN.1 prefix for a 32-byte Ed25519 private key
|
|
503
|
+
const pkcs8Prefix = new Uint8Array([
|
|
504
|
+
0x30,
|
|
505
|
+
0x2e,
|
|
506
|
+
0x02,
|
|
507
|
+
0x01,
|
|
508
|
+
0x00,
|
|
509
|
+
0x30,
|
|
510
|
+
0x05,
|
|
511
|
+
0x06,
|
|
512
|
+
0x03,
|
|
513
|
+
0x2b,
|
|
514
|
+
0x65,
|
|
515
|
+
0x70,
|
|
516
|
+
0x04,
|
|
517
|
+
0x22,
|
|
518
|
+
0x04,
|
|
519
|
+
0x20,
|
|
520
|
+
]);
|
|
521
|
+
const pkcs8Key = new Uint8Array(pkcs8Prefix.length + privateKeyBytes.length);
|
|
522
|
+
pkcs8Key.set(pkcs8Prefix);
|
|
523
|
+
pkcs8Key.set(privateKeyBytes, pkcs8Prefix.length);
|
|
524
|
+
const privateKey = await crypto.subtle.importKey("pkcs8", pkcs8Key.buffer, { name: "Ed25519", namedCurve: "Ed25519" }, true, ["sign"]);
|
|
525
|
+
// Extract public key from the JWK representation
|
|
526
|
+
const jwk = await crypto.subtle.exportKey("jwk", privateKey);
|
|
527
|
+
// Import just the public part (x only, no d) as a verify key
|
|
528
|
+
const publicKey = await crypto.subtle.importKey("jwk", { kty: jwk.kty, crv: jwk.crv, x: jwk.x }, { name: "Ed25519", namedCurve: "Ed25519" }, true, ["verify"]);
|
|
529
|
+
const publicKeyBytes = await crypto.subtle.exportKey("raw", publicKey);
|
|
530
|
+
const exportedPrivateKey = await crypto.subtle.exportKey("pkcs8", privateKey);
|
|
531
|
+
return {
|
|
532
|
+
publicKey,
|
|
533
|
+
privateKey,
|
|
534
|
+
publicKeyHex: encodeHex(new Uint8Array(publicKeyBytes)),
|
|
535
|
+
privateKeyHex: encodeHex(new Uint8Array(exportedPrivateKey)),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Derive a deterministic X25519 encryption keypair from a seed string.
|
|
540
|
+
* Uses HKDF to expand the seed into key material suitable for X25519.
|
|
541
|
+
*
|
|
542
|
+
* The same seed always produces the same keypair.
|
|
543
|
+
*/
|
|
544
|
+
export async function deriveEncryptionKeyPairFromSeed(seed) {
|
|
545
|
+
const encoder = new TextEncoder();
|
|
546
|
+
// Import seed as HKDF key material
|
|
547
|
+
const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(seed), "HKDF", false, ["deriveBits"]);
|
|
548
|
+
// Derive 32 bytes for X25519 private key
|
|
549
|
+
// Use different info than signing to get a different key from the same seed
|
|
550
|
+
const derivedBits = await crypto.subtle.deriveBits({
|
|
551
|
+
name: "HKDF",
|
|
552
|
+
hash: "SHA-256",
|
|
553
|
+
salt: encoder.encode("b3nd-encryption-key"),
|
|
554
|
+
info: encoder.encode("X25519"),
|
|
555
|
+
}, keyMaterial, 256);
|
|
556
|
+
const privateKeyBytes = new Uint8Array(derivedBits);
|
|
557
|
+
// X25519 PKCS8 wrapper: ASN.1 prefix for a 32-byte X25519 private key
|
|
558
|
+
const pkcs8Prefix = new Uint8Array([
|
|
559
|
+
0x30,
|
|
560
|
+
0x2e,
|
|
561
|
+
0x02,
|
|
562
|
+
0x01,
|
|
563
|
+
0x00,
|
|
564
|
+
0x30,
|
|
565
|
+
0x05,
|
|
566
|
+
0x06,
|
|
567
|
+
0x03,
|
|
568
|
+
0x2b,
|
|
569
|
+
0x65,
|
|
570
|
+
0x6e,
|
|
571
|
+
0x04,
|
|
572
|
+
0x22,
|
|
573
|
+
0x04,
|
|
574
|
+
0x20,
|
|
575
|
+
]);
|
|
576
|
+
const pkcs8Key = new Uint8Array(pkcs8Prefix.length + privateKeyBytes.length);
|
|
577
|
+
pkcs8Key.set(pkcs8Prefix);
|
|
578
|
+
pkcs8Key.set(privateKeyBytes, pkcs8Prefix.length);
|
|
579
|
+
const privateKey = await crypto.subtle.importKey("pkcs8", pkcs8Key.buffer, { name: "X25519", namedCurve: "X25519" }, true, ["deriveBits"]);
|
|
580
|
+
// Get public key via JWK round-trip
|
|
581
|
+
const jwk = await crypto.subtle.exportKey("jwk", privateKey);
|
|
582
|
+
const publicKey = await crypto.subtle.importKey("jwk", { kty: jwk.kty, crv: jwk.crv, x: jwk.x, key_ops: [] }, { name: "X25519", namedCurve: "X25519" }, true, []);
|
|
583
|
+
const publicKeyBytes = await crypto.subtle.exportKey("raw", publicKey);
|
|
584
|
+
return {
|
|
585
|
+
publicKey,
|
|
586
|
+
privateKey,
|
|
587
|
+
publicKeyHex: encodeHex(new Uint8Array(publicKeyBytes)),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Extract the Ed25519 public key hex from an Ed25519 private CryptoKey.
|
|
592
|
+
* Useful for deriving a node ID from a PEM-imported private key.
|
|
593
|
+
*/
|
|
594
|
+
export async function extractPublicKeyHex(privateKey) {
|
|
595
|
+
const jwk = await crypto.subtle.exportKey("jwk", privateKey);
|
|
596
|
+
const pub = await crypto.subtle.importKey("jwk", { kty: jwk.kty, crv: jwk.crv, x: jwk.x }, { name: "Ed25519", namedCurve: "Ed25519" }, true, ["verify"]);
|
|
597
|
+
const raw = await crypto.subtle.exportKey("raw", pub);
|
|
598
|
+
return encodeHex(new Uint8Array(raw));
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Generate a PKCE code verifier (RFC 7636)
|
|
602
|
+
* 32 random bytes, base64url-encoded (43 characters)
|
|
603
|
+
*/
|
|
604
|
+
export function generateCodeVerifier() {
|
|
605
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
606
|
+
const base64 = encodeBase64(bytes);
|
|
607
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Generate a PKCE code challenge from a verifier (RFC 7636, S256 method)
|
|
611
|
+
* SHA-256 hash of verifier, base64url-encoded (43 characters)
|
|
612
|
+
*/
|
|
613
|
+
export async function generateCodeChallenge(verifier) {
|
|
614
|
+
const encoder = new TextEncoder();
|
|
615
|
+
const hash = await crypto.subtle.digest("SHA-256", encoder.encode(verifier));
|
|
616
|
+
const hashArray = new Uint8Array(hash);
|
|
617
|
+
const base64 = encodeBase64(hashArray);
|
|
618
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
619
|
+
}
|
package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/functional-client/functional-client.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* FunctionalClient - A client that takes functions as config.
|
|
4
|
+
*
|
|
5
|
+
* Replaces createNode() for cases where you want to wire up
|
|
6
|
+
* custom behavior without class inheritance.
|
|
7
|
+
*/
|
|
8
|
+
import type { Output, ProtocolInterfaceNode, ReceiveResult, StatusResult } from "../types/types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for FunctionalClient.
|
|
11
|
+
* Each method is optional — missing methods return sensible defaults.
|
|
12
|
+
*/
|
|
13
|
+
export interface FunctionalClientConfig {
|
|
14
|
+
receive?: (msgs: Output[]) => Promise<ReceiveResult[]>;
|
|
15
|
+
read?: <T = unknown>(urls: string[]) => Promise<Output<T>[]>;
|
|
16
|
+
observe?: (urls: string[], signal: AbortSignal) => AsyncIterable<readonly string[]>;
|
|
17
|
+
status?: () => Promise<StatusResult>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A client that delegates each method to a config function.
|
|
21
|
+
*
|
|
22
|
+
* If a method is not provided, it returns a sensible default:
|
|
23
|
+
* - receive → { accepted: false, error: "not implemented" }
|
|
24
|
+
* - read → [{ success: false, error: "not implemented" }] per URI
|
|
25
|
+
* - status → { status: "healthy" }
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const client = new FunctionalClient({
|
|
30
|
+
* receive: async (msg) => backend.receive(msg),
|
|
31
|
+
* read: async (urls) => backend.read(urls),
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class FunctionalClient implements ProtocolInterfaceNode {
|
|
36
|
+
private config;
|
|
37
|
+
constructor(config: FunctionalClientConfig);
|
|
38
|
+
receive(msgs: Output[]): Promise<ReceiveResult[]>;
|
|
39
|
+
read<T = unknown>(urls: string[]): Promise<Output<T>[]>;
|
|
40
|
+
observe(urls: string[], signal: AbortSignal): AsyncIterable<readonly string[]>;
|
|
41
|
+
status(): Promise<StatusResult>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=functional-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"functional-client.d.ts","sourceRoot":"","sources":["../../../../../../../../src/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/functional-client/functional-client.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,MAAM,EACN,qBAAqB,EACrB,aAAa,EACb,YAAY,EACb,MAAM,mBAAmB,CAAC;AAE3B;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IACvD,IAAI,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC7D,OAAO,CAAC,EAAE,CACR,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,EAAE,WAAW,KAChB,aAAa,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC;CACtC;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,gBAAiB,YAAW,qBAAqB;IAC5D,OAAO,CAAC,MAAM,CAAyB;gBAE3B,MAAM,EAAE,sBAAsB;IAI1C,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IASjD,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAQhD,OAAO,CACZ,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,EAAE,WAAW,GAClB,aAAa,CAAC,SAAS,MAAM,EAAE,CAAC;IAOnC,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;CAMhC"}
|