@mblaney/holster 1.0.35 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/dist/holster.js +4 -0
- package/dist/holster.js.map +7 -0
- package/package.json +22 -5
- package/src/dup.js +4 -5
- package/src/get.js +10 -19
- package/src/ham.js +22 -49
- package/src/holster.js +172 -58
- package/src/index.js +1 -1
- package/src/radisk.js +89 -86
- package/src/sea-utils.js +1 -1
- package/src/sea.js +0 -52
- package/src/store.js +8 -8
- package/src/user.js +0 -4
- package/src/wire.js +209 -79
- package/ts/src/array.ts +78 -0
- package/ts/src/buffer.ts +441 -0
- package/ts/src/dup.ts +38 -0
- package/ts/src/get.ts +83 -0
- package/ts/src/ham.ts +258 -0
- package/ts/src/holster.ts +968 -0
- package/ts/src/index.ts +35 -0
- package/ts/src/radisk.ts +591 -0
- package/ts/src/radix.ts +157 -0
- package/ts/src/schemas.ts +515 -0
- package/ts/src/sea-utils.ts +103 -0
- package/ts/src/sea.ts +388 -0
- package/ts/src/store.ts +321 -0
- package/ts/src/user.ts +377 -0
- package/ts/src/utils.ts +232 -0
- package/ts/src/wire.ts +961 -0
- package/.dockerignore +0 -9
- package/.github/workflows/node.js.yml +0 -31
- package/.github/workflows/npm-publish.yml +0 -33
- package/Dockerfile +0 -20
- package/examples/index.html +0 -33
- package/examples/index.js +0 -49
- package/examples/user.html +0 -31
- package/examples/user.js +0 -93
- package/examples/wire.html +0 -27
- package/examples/wire.js +0 -55
- package/test/holster.get.test.js +0 -94
- package/test/holster.lex.test.js +0 -196
- package/test/holster.off.test.js +0 -53
- package/test/holster.on.test.js +0 -264
- package/test/holster.put.test.js +0 -358
- package/test/holster.secure.test.js +0 -202
- package/test/holster.user.get.test.js +0 -114
- package/test/holster.user.lex.test.js +0 -149
- package/test/holster.user.off.test.js +0 -73
- package/test/holster.user.on.test.js +0 -300
- package/test/holster.user.put.test.js +0 -463
- package/test/kitty/! +0 -9
- package/test/names.js +0 -225
- package/test/radisk.cache.test.js +0 -213
- package/test/radisk.test.js +0 -331
- package/test/radix.test.js +0 -117
- package/test/sea.test.js +0 -284
- package/test/split.test.js +0 -317
- package/test/store.test.js +0 -316
- package/test/system/basic.test.js +0 -29
- package/test/system/chained-on.test.js +0 -41
- package/test/system/concurrent-listeners.test.js +0 -125
- package/test/system/immediate-read.test.js +0 -30
- package/test/system/listener-before-data.test.js +0 -51
- package/test/system/promise-wrapper.test.js +0 -47
- package/test/system/user-chained-on.test.js +0 -60
- package/test/system/user-concurrent-listeners.test.js +0 -142
- package/test/system/user-listener-before-data.test.js +0 -66
- package/test/user.test.js +0 -173
- package/test/wire.test.js +0 -158
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEA Utilities - Cryptographic utility functions
|
|
3
|
+
* Provides helpers for encryption, hashing, and key management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import SafeBuffer from "./buffer.ts"
|
|
7
|
+
import type { JWK } from "./schemas.ts"
|
|
8
|
+
|
|
9
|
+
const isNode = typeof process !== "undefined" && process.versions?.node != null
|
|
10
|
+
|
|
11
|
+
// Get crypto based on environment - dynamic import for Node.js compatibility
|
|
12
|
+
const crypto: Crypto = isNode
|
|
13
|
+
? ((await import(/*webpackIgnore: true*/ "node:crypto")).webcrypto as Crypto)
|
|
14
|
+
: globalThis.crypto
|
|
15
|
+
|
|
16
|
+
export const subtle = crypto.subtle
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convert data to string format for cryptographic operations
|
|
20
|
+
*/
|
|
21
|
+
export const stringify = (data: unknown): string => {
|
|
22
|
+
return typeof data === "string" ? data : JSON.stringify(data)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse JSON text safely
|
|
27
|
+
*/
|
|
28
|
+
export const parse = (text: string): unknown => {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(text)
|
|
31
|
+
} catch {
|
|
32
|
+
return text
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate cryptographically secure random bytes
|
|
38
|
+
*/
|
|
39
|
+
export const random = (length: number): SeaArray => {
|
|
40
|
+
const array = new Uint8Array(length)
|
|
41
|
+
const filled = crypto.getRandomValues(array)
|
|
42
|
+
return SafeBuffer.from(filled)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create JWK from public and optional private key components
|
|
47
|
+
*/
|
|
48
|
+
export const jwk = (pub: string, priv?: string): JWK => {
|
|
49
|
+
const [x, y] = pub.split(".")
|
|
50
|
+
const ops = priv ? ["sign"] : ["verify"]
|
|
51
|
+
return {
|
|
52
|
+
kty: "EC",
|
|
53
|
+
crv: "P-256",
|
|
54
|
+
x: x!,
|
|
55
|
+
y: y!,
|
|
56
|
+
d: priv,
|
|
57
|
+
ext: true,
|
|
58
|
+
key_ops: ops,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* SHA-256 hash of data
|
|
64
|
+
*/
|
|
65
|
+
export const sha256 = async (data: unknown): Promise<SeaArray> => {
|
|
66
|
+
const hash = await subtle.digest(
|
|
67
|
+
{ name: "SHA-256" },
|
|
68
|
+
new TextEncoder().encode(stringify(data))
|
|
69
|
+
)
|
|
70
|
+
return SafeBuffer.from(new Uint8Array(hash))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Derive AES key from password and salt
|
|
75
|
+
*/
|
|
76
|
+
export const aeskey = async (
|
|
77
|
+
key: string,
|
|
78
|
+
salt: SeaArray
|
|
79
|
+
) => {
|
|
80
|
+
const combined = key + salt.toString("utf8")
|
|
81
|
+
const hashResult = await sha256(combined)
|
|
82
|
+
const jwkKey = keyToJwk(hashResult)
|
|
83
|
+
return await subtle.importKey("jwk", jwkKey, { name: "AES-GCM" }, false, [
|
|
84
|
+
"encrypt",
|
|
85
|
+
"decrypt",
|
|
86
|
+
])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Convert key buffer to JWK format for AES
|
|
91
|
+
*/
|
|
92
|
+
const keyToJwk = (key: SeaArray): JWK => {
|
|
93
|
+
const k = key
|
|
94
|
+
.toString("base64")
|
|
95
|
+
.replace(/\+/g, "-")
|
|
96
|
+
.replace(/\//g, "_")
|
|
97
|
+
.replace(/=/g, "")
|
|
98
|
+
return { kty: "oct", k: k, ext: false, alg: "A256GCM" }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Re-export SeaArray type for convenience
|
|
102
|
+
import type SeaArray from "./array.ts"
|
|
103
|
+
|
package/ts/src/sea.ts
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEA - Security, Encryption, and Authorization
|
|
3
|
+
* Cryptographic functions for signing, verification, encryption, and decryption
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { userPublicKey, userSignature } from "./utils.ts"
|
|
7
|
+
import * as utils from "./sea-utils.ts"
|
|
8
|
+
import SafeBuffer from "./buffer.ts"
|
|
9
|
+
import type {
|
|
10
|
+
UserPair,
|
|
11
|
+
EncryptedData,
|
|
12
|
+
SignedData,
|
|
13
|
+
} from "./schemas.ts"
|
|
14
|
+
|
|
15
|
+
type Callback<T> = (result: T) => void
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* SEA cryptographic operations
|
|
19
|
+
*/
|
|
20
|
+
const SEA = {
|
|
21
|
+
/**
|
|
22
|
+
* Generate a new ECDSA + ECDH key pair
|
|
23
|
+
*/
|
|
24
|
+
pair: async (cb?: Callback<UserPair>): Promise<UserPair> => {
|
|
25
|
+
// ECDSA keys for signing/verifying
|
|
26
|
+
const ecdsa = await utils.subtle
|
|
27
|
+
.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [
|
|
28
|
+
"sign",
|
|
29
|
+
"verify",
|
|
30
|
+
])
|
|
31
|
+
.then(async keys => {
|
|
32
|
+
const pub = await utils.subtle.exportKey("jwk", keys.publicKey)
|
|
33
|
+
return {
|
|
34
|
+
priv: (await utils.subtle.exportKey("jwk", keys.privateKey)).d!,
|
|
35
|
+
pub: pub.x! + "." + pub.y!,
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// ECDH keys for encryption/decryption
|
|
40
|
+
const ecdh = await utils.subtle
|
|
41
|
+
.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveKey"])
|
|
42
|
+
.then(async keys => {
|
|
43
|
+
const pub = await utils.subtle.exportKey("jwk", keys.publicKey)
|
|
44
|
+
return {
|
|
45
|
+
epriv: (await utils.subtle.exportKey("jwk", keys.privateKey)).d!,
|
|
46
|
+
epub: pub.x! + "." + pub.y!,
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const pair: UserPair = {
|
|
51
|
+
pub: ecdsa.pub,
|
|
52
|
+
priv: ecdsa.priv,
|
|
53
|
+
epub: ecdh.epub,
|
|
54
|
+
epriv: ecdh.epriv,
|
|
55
|
+
}
|
|
56
|
+
if (cb) cb(pair)
|
|
57
|
+
return pair
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Encrypt data with a key pair
|
|
62
|
+
*/
|
|
63
|
+
encrypt: async (
|
|
64
|
+
data: unknown,
|
|
65
|
+
pair: Partial<UserPair> | null,
|
|
66
|
+
cb?: Callback<EncryptedData | null>
|
|
67
|
+
): Promise<EncryptedData | null> => {
|
|
68
|
+
if (!pair || !pair.epriv) {
|
|
69
|
+
if (cb) cb(null)
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const rand = { s: utils.random(9), iv: utils.random(15) }
|
|
74
|
+
const ct = await utils.aeskey(pair.epriv, rand.s).then(aes => {
|
|
75
|
+
return utils.subtle.encrypt(
|
|
76
|
+
{
|
|
77
|
+
name: "AES-GCM",
|
|
78
|
+
iv: new Uint8Array(rand.iv),
|
|
79
|
+
},
|
|
80
|
+
aes,
|
|
81
|
+
new TextEncoder().encode(utils.stringify(data))
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
const enc: EncryptedData = {
|
|
85
|
+
ct: SafeBuffer.from(ct, "binary").toString("base64"),
|
|
86
|
+
iv: rand.iv.toString("base64"),
|
|
87
|
+
s: rand.s.toString("base64"),
|
|
88
|
+
}
|
|
89
|
+
if (cb) cb(enc)
|
|
90
|
+
return enc
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Decrypt data with a key pair
|
|
95
|
+
*/
|
|
96
|
+
decrypt: async (
|
|
97
|
+
enc: Partial<EncryptedData> | null,
|
|
98
|
+
pair: Partial<UserPair> | null,
|
|
99
|
+
cb?: Callback<unknown | null>
|
|
100
|
+
): Promise<unknown | null> => {
|
|
101
|
+
if (!enc || !enc.ct || !enc.iv || !enc.s || !pair || !pair.epriv) {
|
|
102
|
+
if (cb) cb(null)
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const data = {
|
|
107
|
+
ct: SafeBuffer.from(enc.ct, "base64"),
|
|
108
|
+
iv: SafeBuffer.from(enc.iv, "base64"),
|
|
109
|
+
s: SafeBuffer.from(enc.s, "base64"),
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const ct = await utils.aeskey(pair.epriv, data.s).then(aes => {
|
|
113
|
+
return utils.subtle.decrypt(
|
|
114
|
+
{
|
|
115
|
+
name: "AES-GCM",
|
|
116
|
+
iv: new Uint8Array(data.iv),
|
|
117
|
+
tagLength: 128,
|
|
118
|
+
},
|
|
119
|
+
aes,
|
|
120
|
+
new Uint8Array(data.ct)
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
const dec = utils.parse(new TextDecoder("utf8").decode(ct))
|
|
124
|
+
if (cb) cb(dec)
|
|
125
|
+
return dec
|
|
126
|
+
} catch (err) {
|
|
127
|
+
// An error will be thrown if the wrong key is used
|
|
128
|
+
if (cb) cb(null)
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Verify a signed message
|
|
135
|
+
*/
|
|
136
|
+
verify: async (
|
|
137
|
+
data: string | SignedData,
|
|
138
|
+
pair: Partial<UserPair> | null,
|
|
139
|
+
cb?: Callback<unknown | null>
|
|
140
|
+
): Promise<unknown | null> => {
|
|
141
|
+
if (!pair || !pair.pub) {
|
|
142
|
+
if (cb) cb(null)
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const signed = (
|
|
147
|
+
typeof data === "string" ? utils.parse(data) : data
|
|
148
|
+
) as SignedData
|
|
149
|
+
const key = await utils.subtle.importKey(
|
|
150
|
+
"jwk",
|
|
151
|
+
utils.jwk(pair.pub),
|
|
152
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
153
|
+
false,
|
|
154
|
+
["verify"]
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
let msg: string | Record<string, unknown> = {}
|
|
158
|
+
if (typeof signed.m === "string") {
|
|
159
|
+
msg = signed.m
|
|
160
|
+
} else {
|
|
161
|
+
// Allow data to be passed in with graph meta data,
|
|
162
|
+
// (which should not be part of signature, so not verified)
|
|
163
|
+
for (const k of Object.keys(signed.m).sort()) {
|
|
164
|
+
if (k !== "_" && k !== userPublicKey && k !== userSignature) {
|
|
165
|
+
(msg as Record<string, unknown>)[k] = signed.m[k]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const hash = await utils.sha256(msg)
|
|
170
|
+
const sig = new Uint8Array(SafeBuffer.from(signed.s, "base64"))
|
|
171
|
+
const alg = { name: "ECDSA", hash: { name: "SHA-256" } }
|
|
172
|
+
if (await utils.subtle.verify(alg, key, sig, new Uint8Array(hash))) {
|
|
173
|
+
const verified = utils.parse(signed.m as string)
|
|
174
|
+
if (cb) cb(verified)
|
|
175
|
+
return verified
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (cb) cb(null)
|
|
179
|
+
return null
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Sign data with a key pair
|
|
184
|
+
*/
|
|
185
|
+
sign: async (
|
|
186
|
+
data: string | Record<string, unknown>,
|
|
187
|
+
pair: Partial<UserPair> | null,
|
|
188
|
+
cb?: Callback<SignedData | null>
|
|
189
|
+
): Promise<SignedData | null> => {
|
|
190
|
+
if (!pair || !pair.pub || !pair.priv) {
|
|
191
|
+
if (cb) cb(null)
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let msg: string | Record<string, unknown> = {}
|
|
196
|
+
if (typeof data === "string") {
|
|
197
|
+
msg = data
|
|
198
|
+
} else {
|
|
199
|
+
const check = utils.parse(data as never) as Record<string, unknown>
|
|
200
|
+
for (const k of Object.keys(check).sort()) {
|
|
201
|
+
if (k !== "_" && k !== userPublicKey && k !== userSignature) {
|
|
202
|
+
(msg as Record<string, unknown>)[k] = check[k]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const hash = await utils.sha256(msg)
|
|
207
|
+
const jwk = utils.jwk(pair.pub, pair.priv)
|
|
208
|
+
const alg = { name: "ECDSA", namedCurve: "P-256" }
|
|
209
|
+
const sig = await utils.subtle
|
|
210
|
+
.importKey("jwk", jwk, alg, false, ["sign"])
|
|
211
|
+
.then(key =>
|
|
212
|
+
utils.subtle.sign(
|
|
213
|
+
{ name: "ECDSA", hash: { name: "SHA-256" } },
|
|
214
|
+
key,
|
|
215
|
+
new Uint8Array(hash)
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
const signed: SignedData = {
|
|
219
|
+
m: msg,
|
|
220
|
+
s: SafeBuffer.from(sig, "binary").toString("base64"),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (cb) cb(signed)
|
|
224
|
+
return signed
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Sign a timestamp to prove ownership of an update
|
|
229
|
+
*/
|
|
230
|
+
signTimestamp: async (
|
|
231
|
+
timestamp: number,
|
|
232
|
+
pair: Partial<UserPair> | null,
|
|
233
|
+
cb?: Callback<string | null>
|
|
234
|
+
): Promise<string | null> => {
|
|
235
|
+
if (!pair || !pair.pub || !pair.priv) {
|
|
236
|
+
if (cb) cb(null)
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Convert timestamp to string for consistent hashing
|
|
241
|
+
const timestampStr = timestamp.toString()
|
|
242
|
+
const hash = await utils.sha256(timestampStr)
|
|
243
|
+
const jwk = utils.jwk(pair.pub, pair.priv)
|
|
244
|
+
const alg = { name: "ECDSA", namedCurve: "P-256" }
|
|
245
|
+
|
|
246
|
+
const sig = await utils.subtle
|
|
247
|
+
.importKey("jwk", jwk, alg, false, ["sign"])
|
|
248
|
+
.then(key =>
|
|
249
|
+
utils.subtle.sign(
|
|
250
|
+
{ name: "ECDSA", hash: { name: "SHA-256" } },
|
|
251
|
+
key,
|
|
252
|
+
new Uint8Array(hash)
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const signature = SafeBuffer.from(sig, "binary").toString("base64")
|
|
257
|
+
|
|
258
|
+
if (cb) cb(signature)
|
|
259
|
+
return signature
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Verify a timestamp signature to confirm ownership of update
|
|
264
|
+
*/
|
|
265
|
+
verifyTimestamp: async (
|
|
266
|
+
timestamp: number,
|
|
267
|
+
signature: string,
|
|
268
|
+
publicKey: string,
|
|
269
|
+
cb?: Callback<boolean>
|
|
270
|
+
): Promise<boolean> => {
|
|
271
|
+
if (!publicKey || !timestamp || !signature) {
|
|
272
|
+
if (cb) cb(false)
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
// Convert timestamp to string for consistent hashing
|
|
278
|
+
const timestampStr = timestamp.toString()
|
|
279
|
+
const hash = await utils.sha256(timestampStr)
|
|
280
|
+
|
|
281
|
+
const key = await utils.subtle.importKey(
|
|
282
|
+
"jwk",
|
|
283
|
+
utils.jwk(publicKey),
|
|
284
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
285
|
+
false,
|
|
286
|
+
["verify"]
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
const sig = new Uint8Array(SafeBuffer.from(signature, "base64"))
|
|
290
|
+
const alg = { name: "ECDSA", hash: { name: "SHA-256" } }
|
|
291
|
+
|
|
292
|
+
const isValid = await utils.subtle.verify(
|
|
293
|
+
alg,
|
|
294
|
+
key,
|
|
295
|
+
sig,
|
|
296
|
+
new Uint8Array(hash)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if (cb) cb(isValid)
|
|
300
|
+
return isValid
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.log(`error verifying timestamp signature: ${(err as Error).message}`)
|
|
303
|
+
if (cb) cb(false)
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Key derivation using PBKDF2
|
|
310
|
+
*/
|
|
311
|
+
work: async (
|
|
312
|
+
data: unknown,
|
|
313
|
+
salt?: unknown,
|
|
314
|
+
cb?: Callback<{ epriv: string }>
|
|
315
|
+
): Promise<{ epriv: string }> => {
|
|
316
|
+
let _cb = cb
|
|
317
|
+
let _salt = salt
|
|
318
|
+
|
|
319
|
+
if (typeof salt === "function") {
|
|
320
|
+
_cb = salt as Callback<{ epriv: string }>
|
|
321
|
+
_salt = undefined
|
|
322
|
+
}
|
|
323
|
+
if (typeof _salt === "undefined") _salt = utils.random(9)
|
|
324
|
+
|
|
325
|
+
const key = await utils.subtle.importKey(
|
|
326
|
+
"raw",
|
|
327
|
+
new TextEncoder().encode(utils.stringify(data)),
|
|
328
|
+
{ name: "PBKDF2" },
|
|
329
|
+
false,
|
|
330
|
+
["deriveBits"]
|
|
331
|
+
)
|
|
332
|
+
const alg = {
|
|
333
|
+
name: "PBKDF2",
|
|
334
|
+
iterations: 100000,
|
|
335
|
+
salt: new TextEncoder().encode(_salt as string),
|
|
336
|
+
hash: { name: "SHA-256" },
|
|
337
|
+
}
|
|
338
|
+
const work = await utils.subtle.deriveBits(alg, key, 512)
|
|
339
|
+
// Use "pair" format so that work can be used as epriv by decrypt
|
|
340
|
+
const pair = { epriv: SafeBuffer.from(work, "binary").toString("base64") }
|
|
341
|
+
if (_cb) _cb(pair)
|
|
342
|
+
return pair
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Derive shared secret using ECDH
|
|
347
|
+
*/
|
|
348
|
+
secret: async (
|
|
349
|
+
to: Partial<UserPair> | null,
|
|
350
|
+
from: Partial<UserPair> | null,
|
|
351
|
+
cb?: Callback<{ epriv: string } | null>
|
|
352
|
+
): Promise<{ epriv: string } | null> => {
|
|
353
|
+
if (!to || !to.epub || !from || !from.epub || !from.epriv) {
|
|
354
|
+
if (cb) cb(null)
|
|
355
|
+
return null
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const alg = { name: "ECDH", namedCurve: "P-256" }
|
|
359
|
+
const pub = utils.jwk(to.epub)
|
|
360
|
+
const pubKey = await utils.subtle.importKey("jwk", pub, alg, true, [])
|
|
361
|
+
const priv = utils.jwk(from.epub, from.epriv)
|
|
362
|
+
// utils.jwk provides default key_ops but it shouldn't be used here
|
|
363
|
+
delete (priv as { key_ops?: unknown }).key_ops
|
|
364
|
+
const derived = await utils.subtle
|
|
365
|
+
.importKey("jwk", priv, alg, false, ["deriveBits"])
|
|
366
|
+
.then(async key => {
|
|
367
|
+
const derivedBits = await utils.subtle.deriveBits(
|
|
368
|
+
{ public: pubKey, name: "ECDH", namedCurve: "P-256" } as never,
|
|
369
|
+
key,
|
|
370
|
+
256
|
|
371
|
+
)
|
|
372
|
+
const derivedKey = await utils.subtle.importKey(
|
|
373
|
+
"raw",
|
|
374
|
+
new Uint8Array(derivedBits),
|
|
375
|
+
{ name: "AES-GCM", length: 256 },
|
|
376
|
+
true,
|
|
377
|
+
["encrypt", "decrypt"]
|
|
378
|
+
)
|
|
379
|
+
return utils.subtle.exportKey("jwk", derivedKey).then(({ k }) => k!)
|
|
380
|
+
})
|
|
381
|
+
// Use "pair" format so that secret can be used as epriv by encrypt
|
|
382
|
+
if (cb) cb({ epriv: derived })
|
|
383
|
+
return { epriv: derived }
|
|
384
|
+
},
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export default SEA
|
|
388
|
+
|