@libp2p/keychain 5.1.4-b2124c2db → 5.1.4-d53ef170c

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/src/keychain.ts CHANGED
@@ -10,12 +10,13 @@ import { sha256 } from 'multiformats/hashes/sha2'
10
10
  import sanitize from 'sanitize-filename'
11
11
  import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
12
12
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
13
- import { exportPrivateKey } from './utils/export.js'
14
- import { importPrivateKey } from './utils/import.js'
15
- import type { KeychainComponents, KeychainInit, Keychain as KeychainInterface, KeyInfo } from './index.js'
13
+ import { exportPrivateKey, exporter } from './utils/export.js'
14
+ import { importPrivateKey, importer } from './utils/import.js'
15
+ import type { KeychainComponents, KeychainInit, Keychain as KeychainInterface, KeyInfo, X509Info } from './index.js'
16
16
  import type { Logger, PrivateKey } from '@libp2p/interface'
17
17
 
18
18
  const keyPrefix = '/pkcs8/'
19
+ const certPrefix = '/x509/'
19
20
  const infoPrefix = '/info/'
20
21
  const privates = new WeakMap<object, { dek: string }>()
21
22
 
@@ -63,8 +64,8 @@ async function randomDelay (): Promise<void> {
63
64
  /**
64
65
  * Converts a key name into a datastore name
65
66
  */
66
- function DsName (name: string): Key {
67
- return new Key(keyPrefix + name)
67
+ function DsName (prefix: string, name: string): Key {
68
+ return new Key(prefix + name)
68
69
  }
69
70
 
70
71
  /**
@@ -206,7 +207,7 @@ export class Keychain implements KeychainInterface {
206
207
  await randomDelay()
207
208
  throw new InvalidParametersError('Key is required')
208
209
  }
209
- const datastoreName = DsName(name)
210
+ const datastoreName = DsName(keyPrefix, name)
210
211
  const exists = await this.components.datastore.has(datastoreName)
211
212
  if (exists) {
212
213
  await randomDelay()
@@ -248,7 +249,7 @@ export class Keychain implements KeychainInterface {
248
249
  throw new InvalidParametersError(`Invalid key name '${name}'`)
249
250
  }
250
251
 
251
- const datastoreName = DsName(name)
252
+ const datastoreName = DsName(keyPrefix, name)
252
253
  try {
253
254
  const res = await this.components.datastore.get(datastoreName)
254
255
  const pem = uint8ArrayToString(res)
@@ -273,7 +274,7 @@ export class Keychain implements KeychainInterface {
273
274
  throw new InvalidParametersError(`Invalid key name '${name}'`)
274
275
  }
275
276
 
276
- const datastoreName = DsName(name)
277
+ const datastoreName = DsName(keyPrefix, name)
277
278
  const keyInfo = await this.findKeyByName(name)
278
279
  const batch = this.components.datastore.batch()
279
280
  batch.delete(datastoreName)
@@ -317,8 +318,8 @@ export class Keychain implements KeychainInterface {
317
318
  await randomDelay()
318
319
  throw new InvalidParametersError(`Invalid new key name '${newName}'`)
319
320
  }
320
- const oldDatastoreName = DsName(oldName)
321
- const newDatastoreName = DsName(newName)
321
+ const oldDatastoreName = DsName(keyPrefix, oldName)
322
+ const newDatastoreName = DsName(keyPrefix, newName)
322
323
  const oldInfoName = DsInfoName(oldName)
323
324
  const newInfoName = DsInfoName(newName)
324
325
 
@@ -347,6 +348,99 @@ export class Keychain implements KeychainInterface {
347
348
  }
348
349
  }
349
350
 
351
+ /**
352
+ * List all the certificates
353
+ */
354
+ async listX509 (): Promise<X509Info[]> {
355
+ const query = {
356
+ prefix: certPrefix
357
+ }
358
+
359
+ const info = []
360
+ for await (const value of this.components.datastore.query(query)) {
361
+ info.push({
362
+ name: value.key.toString().replace(certPrefix, '')
363
+ })
364
+ }
365
+
366
+ return info
367
+ }
368
+
369
+ async importX509 (name: string, pem: string): Promise<void> {
370
+ try {
371
+ if (!validateKeyName(name)) {
372
+ throw new InvalidParametersError(`Invalid certificate name '${name}'`)
373
+ }
374
+
375
+ if (pem == null) {
376
+ throw new InvalidParametersError('PEM is required')
377
+ }
378
+
379
+ if (!pem.includes('-----BEGIN CERTIFICATE-----') && !pem.includes('-----END CERTIFICATE-----')) {
380
+ throw new InvalidParametersError('PEM was invalid')
381
+ }
382
+
383
+ const datastoreName = DsName(certPrefix, name)
384
+
385
+ const exists = await this.components.datastore.has(datastoreName)
386
+ if (exists) {
387
+ throw new InvalidParametersError(`Certificate '${name}' already exists`)
388
+ }
389
+
390
+ const cached = privates.get(this)
391
+
392
+ if (cached == null) {
393
+ throw new InvalidParametersError('dek missing')
394
+ }
395
+
396
+ const dek = cached.dek
397
+ const dsPem = await exporter(uint8ArrayFromString(pem), dek)
398
+ await this.components.datastore.put(datastoreName, uint8ArrayFromString(dsPem))
399
+ } catch (err) {
400
+ await randomDelay()
401
+ throw err
402
+ }
403
+ }
404
+
405
+ async exportX509 (name: string): Promise<string> {
406
+ try {
407
+ if (!validateKeyName(name)) {
408
+ throw new InvalidParametersError(`Invalid key name '${name}'`)
409
+ }
410
+
411
+ const datastoreName = DsName(certPrefix, name)
412
+ const res = await this.components.datastore.get(datastoreName)
413
+ const encryptedPem = uint8ArrayToString(res)
414
+ const cached = privates.get(this)
415
+
416
+ if (cached == null) {
417
+ throw new InvalidParametersError('dek missing')
418
+ }
419
+
420
+ const dek = cached.dek
421
+ const buf = await importer(encryptedPem, dek)
422
+
423
+ return uint8ArrayToString(buf)
424
+ } catch (err: any) {
425
+ await randomDelay()
426
+ throw err
427
+ }
428
+ }
429
+
430
+ async removeX509 (name: string): Promise<void> {
431
+ try {
432
+ if (!validateKeyName(name) || name === this.self) {
433
+ throw new InvalidParametersError(`Invalid key name '${name}'`)
434
+ }
435
+
436
+ const datastoreName = DsName(certPrefix, name)
437
+ await this.components.datastore.delete(datastoreName)
438
+ } catch (err) {
439
+ await randomDelay()
440
+ throw err
441
+ }
442
+ }
443
+
350
444
  /**
351
445
  * Rotate keychain password and re-encrypt all associated keys
352
446
  */
@@ -381,24 +475,33 @@ export class Keychain implements KeychainInterface {
381
475
  this.init.dek?.hash)
382
476
  : ''
383
477
  privates.set(this, { dek: newDek })
384
- const keys = await this.listKeys()
385
- for (const key of keys) {
386
- const res = await this.components.datastore.get(DsName(key.name))
478
+
479
+ const batch = this.components.datastore.batch()
480
+
481
+ for (const key of await this.listKeys()) {
482
+ const res = await this.components.datastore.get(DsName(keyPrefix, key.name))
387
483
  const pem = uint8ArrayToString(res)
388
484
  const privateKey = await importPrivateKey(pem, oldDek)
389
485
  const password = newDek.toString()
390
486
  const keyAsPEM = await exportPrivateKey(privateKey, password, privateKey.type === 'RSA' ? 'pkcs-8' : 'libp2p-key')
391
487
 
392
- // Update stored key
393
- const batch = this.components.datastore.batch()
394
- const keyInfo = {
395
- name: key.name,
396
- id: key.id
397
- }
398
- batch.put(DsName(key.name), uint8ArrayFromString(keyAsPEM))
399
- batch.put(DsInfoName(key.name), uint8ArrayFromString(JSON.stringify(keyInfo)))
400
- await batch.commit()
488
+ // add to batch
489
+ batch.put(DsName(keyPrefix, key.name), uint8ArrayFromString(keyAsPEM))
401
490
  }
491
+
492
+ for (const key of await this.listX509()) {
493
+ // decrypt using old password and encrypt using new
494
+ const res = await this.components.datastore.get(DsName(certPrefix, key.name))
495
+ const pem = uint8ArrayToString(res)
496
+ const decrypted = await importer(pem, oldDek)
497
+ const encrypted = await exporter(decrypted, newDek)
498
+
499
+ // add to batch
500
+ batch.put(DsName(certPrefix, key.name), uint8ArrayFromString(encrypted))
501
+ }
502
+
503
+ await batch.commit()
504
+
402
505
  this.log('keychain reconstructed')
403
506
  }
404
507
  }
@@ -9,7 +9,7 @@ import * as asn1js from 'asn1js'
9
9
  import { base64 } from 'multiformats/bases/base64'
10
10
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
11
11
  import { ITERATIONS, KEY_SIZE, SALT_LENGTH } from './constants.js'
12
- import type { Ed25519PrivateKey, PrivateKey, RSAPrivateKey, Secp256k1PrivateKey } from '@libp2p/interface'
12
+ import type { ECDSAPrivateKey, Ed25519PrivateKey, PrivateKey, RSAPrivateKey, Secp256k1PrivateKey } from '@libp2p/interface'
13
13
  import type { Multibase } from 'multiformats/bases/interface'
14
14
 
15
15
  /**
@@ -43,6 +43,10 @@ export async function exportPrivateKey (key: PrivateKey, password: string, forma
43
43
  return exportSecp256k1PrivateKey(key, password, format)
44
44
  }
45
45
 
46
+ if (key.type === 'ECDSA') {
47
+ return exportECDSAPrivateKey(key, password, format)
48
+ }
49
+
46
50
  throw new UnsupportedKeyTypeError()
47
51
  }
48
52
 
@@ -68,6 +72,17 @@ export async function exportSecp256k1PrivateKey (key: Secp256k1PrivateKey, passw
68
72
  }
69
73
  }
70
74
 
75
+ /**
76
+ * Exports the key into a password protected `format`
77
+ */
78
+ export async function exportECDSAPrivateKey (key: ECDSAPrivateKey, password: string, format: ExportFormat = 'libp2p-key'): Promise<Multibase<'m'>> {
79
+ if (format === 'libp2p-key') {
80
+ return exporter(privateKeyToProtobuf(key), password)
81
+ } else {
82
+ throw new InvalidParametersError(`export format '${format}' is not supported`)
83
+ }
84
+ }
85
+
71
86
  /**
72
87
  * Exports the key as libp2p-key - a aes-gcm encrypted value with the key
73
88
  * derived from the password.