@@ -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
}
0 commit comments