@syncular/client-plugin-encryption 0.0.1-100
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/crypto-utils.d.ts +7 -0
- package/dist/crypto-utils.d.ts.map +1 -0
- package/dist/crypto-utils.js +110 -0
- package/dist/crypto-utils.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +639 -0
- package/dist/index.js.map +1 -0
- package/dist/key-sharing.d.ts +124 -0
- package/dist/key-sharing.d.ts.map +1 -0
- package/dist/key-sharing.js +332 -0
- package/dist/key-sharing.js.map +1 -0
- package/package.json +65 -0
- package/src/__tests__/field-encryption-keys.test.ts +68 -0
- package/src/__tests__/key-sharing.test.ts +225 -0
- package/src/__tests__/refresh-encrypted-fields.test.ts +182 -0
- package/src/__tests__/scope-resolution.test.ts +202 -0
- package/src/crypto-utils.test.ts +84 -0
- package/src/crypto-utils.ts +125 -0
- package/src/index.ts +939 -0
- package/src/key-sharing.ts +469 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key sharing utilities for human-friendly key exchange.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - BIP39 mnemonic phrases (24 words for 32-byte keys)
|
|
6
|
+
* - URL-safe encoding for QR codes
|
|
7
|
+
* - X25519 keypairs for asymmetric key wrapping
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { xchacha20poly1305 } from '@noble/ciphers/chacha.js';
|
|
11
|
+
import { x25519 } from '@noble/curves/ed25519.js';
|
|
12
|
+
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
13
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
14
|
+
import { entropyToMnemonic, mnemonicToEntropy } from '@scure/bip39';
|
|
15
|
+
import { wordlist } from '@scure/bip39/wordlists/english.js';
|
|
16
|
+
import { isRecord } from '@syncular/core';
|
|
17
|
+
import {
|
|
18
|
+
base64UrlToBytes,
|
|
19
|
+
bytesToBase64Url,
|
|
20
|
+
randomBytes,
|
|
21
|
+
} from './crypto-utils';
|
|
22
|
+
|
|
23
|
+
const WORD_SET = new Set(wordlist);
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Utility Functions
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
function concatBytes(...arrays: Uint8Array[]): Uint8Array {
|
|
30
|
+
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
31
|
+
const result = new Uint8Array(totalLength);
|
|
32
|
+
let offset = 0;
|
|
33
|
+
for (const arr of arrays) {
|
|
34
|
+
result.set(arr, offset);
|
|
35
|
+
offset += arr.length;
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isAllZero(bytes: Uint8Array): boolean {
|
|
41
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
42
|
+
if (bytes[i] !== 0) return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateSharedSecret(sharedSecret: Uint8Array): void {
|
|
48
|
+
// Reject all-zero shared secrets which indicate a low-order point attack.
|
|
49
|
+
// This can happen if a malicious party provides a small-order public key.
|
|
50
|
+
if (isAllZero(sharedSecret)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'X25519 shared secret is all zeros - possible low-order point attack'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Symmetric Key Utilities
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Generate a cryptographically secure 32-byte symmetric key.
|
|
63
|
+
*/
|
|
64
|
+
export function generateSymmetricKey(): Uint8Array {
|
|
65
|
+
return randomBytes(32);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Convert a 32-byte key to a 24-word BIP39 mnemonic phrase.
|
|
70
|
+
*
|
|
71
|
+
* The same key always produces the same words (deterministic).
|
|
72
|
+
*/
|
|
73
|
+
export function keyToMnemonic(key: Uint8Array): string {
|
|
74
|
+
if (key.length !== 32) {
|
|
75
|
+
throw new Error(`Key must be 32 bytes, got ${key.length}`);
|
|
76
|
+
}
|
|
77
|
+
return entropyToMnemonic(key, wordlist);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse a BIP39 mnemonic phrase back to key bytes.
|
|
82
|
+
*
|
|
83
|
+
* @throws If the mnemonic is invalid or has wrong word count
|
|
84
|
+
*/
|
|
85
|
+
export function normalizeMnemonicInput(phrase: string): string {
|
|
86
|
+
const normalized = phrase.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
87
|
+
if (!normalized) return normalized;
|
|
88
|
+
|
|
89
|
+
// Keep valid word tokens only so pasted numbered lists like
|
|
90
|
+
// "1 word 2 word ..." still recover cleanly.
|
|
91
|
+
const tokenizedWords = (normalized.match(/[a-z]+/g) ?? []).filter((word) =>
|
|
92
|
+
WORD_SET.has(word)
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (tokenizedWords.length === 24) return tokenizedWords.join(' ');
|
|
96
|
+
if (tokenizedWords.length > 24) {
|
|
97
|
+
for (let i = 0; i <= tokenizedWords.length - 24; i++) {
|
|
98
|
+
const candidate = tokenizedWords.slice(i, i + 24).join(' ');
|
|
99
|
+
try {
|
|
100
|
+
mnemonicToEntropy(candidate, wordlist);
|
|
101
|
+
return candidate;
|
|
102
|
+
} catch {
|
|
103
|
+
// Continue scanning for a valid 24-word window.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return normalized;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function mnemonicToKey(phrase: string): Uint8Array {
|
|
112
|
+
const normalized = normalizeMnemonicInput(phrase);
|
|
113
|
+
const entropy = mnemonicToEntropy(normalized, wordlist);
|
|
114
|
+
if (entropy.length !== 32) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Expected 24-word mnemonic (32 bytes), got ${entropy.length} bytes`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return entropy;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Encode a key as URL-safe base64 (for QR codes).
|
|
124
|
+
*/
|
|
125
|
+
export function keyToBase64Url(key: Uint8Array): string {
|
|
126
|
+
return bytesToBase64Url(key);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Decode URL-safe base64 back to key bytes.
|
|
131
|
+
*/
|
|
132
|
+
export function base64UrlToKey(encoded: string): Uint8Array {
|
|
133
|
+
const key = base64UrlToBytes(encoded);
|
|
134
|
+
if (key.length !== 32) {
|
|
135
|
+
throw new Error(`Invalid key length: expected 32 bytes, got ${key.length}`);
|
|
136
|
+
}
|
|
137
|
+
return key;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// X25519 Keypair Utilities
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
interface KeyPair {
|
|
145
|
+
publicKey: Uint8Array;
|
|
146
|
+
privateKey: Uint8Array;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Generate an X25519 keypair for key exchange.
|
|
151
|
+
*
|
|
152
|
+
* Store privateKey securely. Share publicKey freely.
|
|
153
|
+
*/
|
|
154
|
+
export function generateKeypair(): KeyPair {
|
|
155
|
+
const privateKey = randomBytes(32);
|
|
156
|
+
const publicKey = x25519.getPublicKey(privateKey);
|
|
157
|
+
return { publicKey, privateKey };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Convert a 32-byte public key to a 24-word mnemonic.
|
|
162
|
+
*/
|
|
163
|
+
export function publicKeyToMnemonic(publicKey: Uint8Array): string {
|
|
164
|
+
if (publicKey.length !== 32) {
|
|
165
|
+
throw new Error(`Public key must be 32 bytes, got ${publicKey.length}`);
|
|
166
|
+
}
|
|
167
|
+
return entropyToMnemonic(publicKey, wordlist);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parse a mnemonic back to a public key.
|
|
172
|
+
*/
|
|
173
|
+
export function mnemonicToPublicKey(phrase: string): Uint8Array {
|
|
174
|
+
return mnemonicToKey(phrase);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// Key Wrapping (Envelope Encryption)
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
interface WrappedKey {
|
|
182
|
+
/** Sender's ephemeral public key (32 bytes) */
|
|
183
|
+
ephemeralPublic: Uint8Array;
|
|
184
|
+
/** Encrypted symmetric key + auth tag (48 bytes) */
|
|
185
|
+
ciphertext: Uint8Array;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const HKDF_INFO = new TextEncoder().encode('syncular-key-wrap-v1');
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Wrap a symmetric key for a recipient using their public key.
|
|
192
|
+
*
|
|
193
|
+
* Uses X25519 ECDH + HKDF + XChaCha20-Poly1305.
|
|
194
|
+
*/
|
|
195
|
+
export function wrapKeyForRecipient(
|
|
196
|
+
recipientPublicKey: Uint8Array,
|
|
197
|
+
symmetricKey: Uint8Array
|
|
198
|
+
): WrappedKey {
|
|
199
|
+
if (recipientPublicKey.length !== 32) {
|
|
200
|
+
throw new Error('Recipient public key must be 32 bytes');
|
|
201
|
+
}
|
|
202
|
+
if (symmetricKey.length !== 32) {
|
|
203
|
+
throw new Error('Symmetric key must be 32 bytes');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Generate ephemeral keypair
|
|
207
|
+
const ephemeralPrivate = randomBytes(32);
|
|
208
|
+
const ephemeralPublic = x25519.getPublicKey(ephemeralPrivate);
|
|
209
|
+
|
|
210
|
+
// ECDH shared secret
|
|
211
|
+
const sharedSecret = x25519.getSharedSecret(
|
|
212
|
+
ephemeralPrivate,
|
|
213
|
+
recipientPublicKey
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Reject low-order points that would result in all-zero shared secret
|
|
217
|
+
validateSharedSecret(sharedSecret);
|
|
218
|
+
|
|
219
|
+
// Derive wrapping key using HKDF
|
|
220
|
+
const wrappingKey = hkdf(
|
|
221
|
+
sha256,
|
|
222
|
+
sharedSecret,
|
|
223
|
+
ephemeralPublic,
|
|
224
|
+
HKDF_INFO,
|
|
225
|
+
32
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Encrypt the symmetric key
|
|
229
|
+
const nonce = randomBytes(24);
|
|
230
|
+
const aead = xchacha20poly1305(wrappingKey, nonce);
|
|
231
|
+
const encrypted = aead.encrypt(symmetricKey);
|
|
232
|
+
|
|
233
|
+
// Ciphertext = nonce + encrypted (24 + 32 + 16 = 72 bytes)
|
|
234
|
+
const ciphertext = concatBytes(nonce, encrypted);
|
|
235
|
+
|
|
236
|
+
return { ephemeralPublic, ciphertext };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Unwrap a key using your private key.
|
|
241
|
+
*/
|
|
242
|
+
export function unwrapKey(
|
|
243
|
+
myPrivateKey: Uint8Array,
|
|
244
|
+
wrapped: WrappedKey
|
|
245
|
+
): Uint8Array {
|
|
246
|
+
if (myPrivateKey.length !== 32) {
|
|
247
|
+
throw new Error('Private key must be 32 bytes');
|
|
248
|
+
}
|
|
249
|
+
if (wrapped.ephemeralPublic.length !== 32) {
|
|
250
|
+
throw new Error('Ephemeral public key must be 32 bytes');
|
|
251
|
+
}
|
|
252
|
+
if (wrapped.ciphertext.length !== 72) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
'Ciphertext must be 72 bytes (nonce + encrypted key + tag)'
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ECDH shared secret
|
|
259
|
+
const sharedSecret = x25519.getSharedSecret(
|
|
260
|
+
myPrivateKey,
|
|
261
|
+
wrapped.ephemeralPublic
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Reject low-order points that would result in all-zero shared secret
|
|
265
|
+
validateSharedSecret(sharedSecret);
|
|
266
|
+
|
|
267
|
+
// Derive wrapping key using HKDF
|
|
268
|
+
const wrappingKey = hkdf(
|
|
269
|
+
sha256,
|
|
270
|
+
sharedSecret,
|
|
271
|
+
wrapped.ephemeralPublic,
|
|
272
|
+
HKDF_INFO,
|
|
273
|
+
32
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Extract nonce and encrypted data
|
|
277
|
+
const nonce = wrapped.ciphertext.slice(0, 24);
|
|
278
|
+
const encrypted = wrapped.ciphertext.slice(24);
|
|
279
|
+
|
|
280
|
+
// Decrypt
|
|
281
|
+
const aead = xchacha20poly1305(wrappingKey, nonce);
|
|
282
|
+
return aead.decrypt(encrypted);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Serialize a wrapped key for storage/transmission.
|
|
287
|
+
*/
|
|
288
|
+
export function encodeWrappedKey(wrapped: WrappedKey): string {
|
|
289
|
+
const combined = concatBytes(wrapped.ephemeralPublic, wrapped.ciphertext);
|
|
290
|
+
return bytesToBase64Url(combined);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Deserialize a wrapped key.
|
|
295
|
+
*/
|
|
296
|
+
export function decodeWrappedKey(encoded: string): WrappedKey {
|
|
297
|
+
const combined = base64UrlToBytes(encoded);
|
|
298
|
+
if (combined.length !== 104) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Invalid wrapped key length: expected 104 bytes, got ${combined.length}`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
ephemeralPublic: combined.slice(0, 32),
|
|
305
|
+
ciphertext: combined.slice(32),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================================================
|
|
310
|
+
// Share URL Format
|
|
311
|
+
// ============================================================================
|
|
312
|
+
|
|
313
|
+
const SHARE_URL_PREFIX = 'sync://';
|
|
314
|
+
|
|
315
|
+
interface SymmetricKeyShare {
|
|
316
|
+
type: 'symmetric';
|
|
317
|
+
key: Uint8Array;
|
|
318
|
+
kid?: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
interface PublicKeyShare {
|
|
322
|
+
type: 'publicKey';
|
|
323
|
+
publicKey: Uint8Array;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
type ParsedShare = SymmetricKeyShare | PublicKeyShare;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Encode a symmetric key as a shareable URL.
|
|
330
|
+
*
|
|
331
|
+
* Format: sync://k/1/<base64url>[/<kid>]
|
|
332
|
+
*/
|
|
333
|
+
export function keyToShareUrl(key: Uint8Array, kid?: string): string {
|
|
334
|
+
const encoded = bytesToBase64Url(key);
|
|
335
|
+
const kidPart = kid ? `/${encodeURIComponent(kid)}` : '';
|
|
336
|
+
return `${SHARE_URL_PREFIX}k/1/${encoded}${kidPart}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Encode a public key as a shareable URL.
|
|
341
|
+
*
|
|
342
|
+
* Format: sync://pk/1/<base64url>
|
|
343
|
+
*/
|
|
344
|
+
export function publicKeyToShareUrl(publicKey: Uint8Array): string {
|
|
345
|
+
const encoded = bytesToBase64Url(publicKey);
|
|
346
|
+
return `${SHARE_URL_PREFIX}pk/1/${encoded}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Parse a share URL back to typed result.
|
|
351
|
+
*/
|
|
352
|
+
export function parseShareUrl(url: string): ParsedShare {
|
|
353
|
+
if (!url.startsWith(SHARE_URL_PREFIX)) {
|
|
354
|
+
throw new Error(`Invalid share URL: must start with ${SHARE_URL_PREFIX}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const rest = url.slice(SHARE_URL_PREFIX.length);
|
|
358
|
+
const parts = rest.split('/');
|
|
359
|
+
|
|
360
|
+
if (parts.length < 3) {
|
|
361
|
+
throw new Error('Invalid share URL format');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const [type, version, encoded, kidEncoded] = parts;
|
|
365
|
+
|
|
366
|
+
if (version !== '1') {
|
|
367
|
+
throw new Error(`Unsupported share URL version: ${version}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!encoded) {
|
|
371
|
+
throw new Error('Invalid share URL: missing encoded key data');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (type === 'k') {
|
|
375
|
+
const key = base64UrlToKey(encoded);
|
|
376
|
+
const kid = kidEncoded ? decodeURIComponent(kidEncoded) : undefined;
|
|
377
|
+
return { type: 'symmetric', key, kid };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (type === 'pk') {
|
|
381
|
+
const publicKey = base64UrlToKey(encoded);
|
|
382
|
+
return { type: 'publicKey', publicKey };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
throw new Error(`Unknown share URL type: ${type}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ============================================================================
|
|
389
|
+
// JSON Format
|
|
390
|
+
// ============================================================================
|
|
391
|
+
|
|
392
|
+
interface SymmetricKeyJson {
|
|
393
|
+
type: 'symmetric';
|
|
394
|
+
kid?: string;
|
|
395
|
+
k: string;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
interface PublicKeyJson {
|
|
399
|
+
type: 'publicKey';
|
|
400
|
+
pk: string;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Encode a symmetric key as JSON.
|
|
405
|
+
*/
|
|
406
|
+
export function keyToJson(key: Uint8Array, kid?: string): SymmetricKeyJson {
|
|
407
|
+
return {
|
|
408
|
+
type: 'symmetric',
|
|
409
|
+
...(kid && { kid }),
|
|
410
|
+
k: bytesToBase64Url(key),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Encode a public key as JSON.
|
|
416
|
+
*/
|
|
417
|
+
export function publicKeyToJson(publicKey: Uint8Array): PublicKeyJson {
|
|
418
|
+
return {
|
|
419
|
+
type: 'publicKey',
|
|
420
|
+
pk: bytesToBase64Url(publicKey),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Parse a JSON key share string.
|
|
426
|
+
*/
|
|
427
|
+
export function parseKeyShareJson(json: string): ParsedShare {
|
|
428
|
+
let parsedValue: unknown;
|
|
429
|
+
try {
|
|
430
|
+
parsedValue = JSON.parse(json);
|
|
431
|
+
} catch {
|
|
432
|
+
throw new Error('Invalid key share JSON');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!isRecord(parsedValue)) {
|
|
436
|
+
throw new Error('Invalid key share JSON');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const parsedType = parsedValue.type;
|
|
440
|
+
|
|
441
|
+
if (parsedType === 'symmetric') {
|
|
442
|
+
const keyMaterial = parsedValue.k;
|
|
443
|
+
if (typeof keyMaterial !== 'string') {
|
|
444
|
+
throw new Error('Invalid symmetric key share JSON');
|
|
445
|
+
}
|
|
446
|
+
const kidValue = parsedValue.kid;
|
|
447
|
+
if (kidValue !== undefined && typeof kidValue !== 'string') {
|
|
448
|
+
throw new Error('Invalid symmetric key share JSON');
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
type: 'symmetric',
|
|
452
|
+
key: base64UrlToKey(keyMaterial),
|
|
453
|
+
kid: kidValue,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (parsedType === 'publicKey') {
|
|
458
|
+
const publicKeyMaterial = parsedValue.pk;
|
|
459
|
+
if (typeof publicKeyMaterial !== 'string') {
|
|
460
|
+
throw new Error('Invalid public key share JSON');
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
type: 'publicKey',
|
|
464
|
+
publicKey: base64UrlToKey(publicKeyMaterial),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
throw new Error(`Unknown key share type: ${String(parsedType)}`);
|
|
469
|
+
}
|