@libp2p/keychain 4.1.6 → 5.0.0-18dd3cb26

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
@@ -1,17 +1,19 @@
1
1
  /* eslint max-nested-callbacks: ["error", 5] */
2
2
 
3
3
  import { pbkdf2, randomBytes } from '@libp2p/crypto'
4
- import { generateKeyPair, importKey, unmarshalPrivateKey } from '@libp2p/crypto/keys'
5
- import { CodeError, serviceCapabilities } from '@libp2p/interface'
6
- import { peerIdFromKeys } from '@libp2p/peer-id'
4
+ import { privateKeyToProtobuf } from '@libp2p/crypto/keys'
5
+ import { InvalidParametersError, NotFoundError, serviceCapabilities } from '@libp2p/interface'
7
6
  import { Key } from 'interface-datastore/key'
8
7
  import mergeOptions from 'merge-options'
8
+ import { base58btc } from 'multiformats/bases/base58'
9
+ import { sha256 } from 'multiformats/hashes/sha2'
9
10
  import sanitize from 'sanitize-filename'
10
11
  import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
11
12
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
12
- import { codes } from './errors.js'
13
- import type { KeychainComponents, KeychainInit, Keychain, KeyInfo } from './index.js'
14
- import type { Logger, KeyType, PeerId } from '@libp2p/interface'
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'
16
+ import type { Logger, PrivateKey } from '@libp2p/interface'
15
17
 
16
18
  const keyPrefix = '/pkcs8/'
17
19
  const infoPrefix = '/info/'
@@ -72,6 +74,13 @@ function DsInfoName (name: string): Key {
72
74
  return new Key(infoPrefix + name)
73
75
  }
74
76
 
77
+ export async function keyId (key: PrivateKey): Promise<string> {
78
+ const pb = privateKeyToProtobuf(key)
79
+ const hash = await sha256.digest(pb)
80
+
81
+ return base58btc.encode(hash.bytes).substring(1)
82
+ }
83
+
75
84
  /**
76
85
  * Manages the lifecycle of a key. Keys are encrypted at rest using PKCS #8.
77
86
  *
@@ -80,7 +89,7 @@ function DsInfoName (name: string): Key {
80
89
  * - '/pkcs8/*key-name*', contains the PKCS #8 for the key
81
90
  *
82
91
  */
83
- export class DefaultKeychain implements Keychain {
92
+ export class Keychain implements KeychainInterface {
84
93
  private readonly components: KeychainComponents
85
94
  private readonly init: KeychainInit
86
95
  private readonly log: Logger
@@ -147,151 +156,149 @@ export class DefaultKeychain implements Keychain {
147
156
  return defaultOptions
148
157
  }
149
158
 
150
- /**
151
- * Create a new key.
152
- *
153
- * @param {string} name - The local key name; cannot already exist.
154
- * @param {string} type - One of the key types; 'rsa'.
155
- * @param {number} [size = 2048] - The key size in bits. Used for rsa keys only
156
- */
157
- async createKey (name: string, type: KeyType, size = 2048): Promise<KeyInfo> {
158
- if (!validateKeyName(name) || name === 'self') {
159
+ async findKeyByName (name: string): Promise<KeyInfo> {
160
+ if (!validateKeyName(name)) {
159
161
  await randomDelay()
160
- throw new CodeError('Invalid key name', codes.ERR_INVALID_KEY_NAME)
162
+ throw new InvalidParametersError(`Invalid key name '${name}'`)
161
163
  }
162
164
 
163
- if (typeof type !== 'string') {
164
- await randomDelay()
165
- throw new CodeError('Invalid key type', codes.ERR_INVALID_KEY_TYPE)
166
- }
165
+ const dsname = DsInfoName(name)
167
166
 
168
- const dsname = DsName(name)
169
- const exists = await this.components.datastore.has(dsname)
170
- if (exists) {
167
+ try {
168
+ const res = await this.components.datastore.get(dsname)
169
+ return JSON.parse(uint8ArrayToString(res))
170
+ } catch (err: any) {
171
171
  await randomDelay()
172
- throw new CodeError('Key name already exists', codes.ERR_KEY_ALREADY_EXISTS)
173
- }
174
-
175
- switch (type.toLowerCase()) {
176
- case 'rsa':
177
- if (!Number.isSafeInteger(size) || size < 2048) {
178
- await randomDelay()
179
- throw new CodeError('Invalid RSA key size', codes.ERR_INVALID_KEY_SIZE)
180
- }
181
- break
182
- default:
183
- break
172
+ this.log.error(err)
173
+ throw new NotFoundError(`Key '${name}' does not exist.`)
184
174
  }
175
+ }
185
176
 
186
- let keyInfo
177
+ async findKeyById (id: string): Promise<KeyInfo> {
187
178
  try {
188
- const keypair = await generateKeyPair(type, size)
189
- const kid = await keypair.id()
190
- const cached = privates.get(this)
191
-
192
- if (cached == null) {
193
- throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS)
179
+ const query = {
180
+ prefix: infoPrefix
194
181
  }
195
182
 
196
- const dek = cached.dek
197
- const pem = await keypair.export(dek)
198
- keyInfo = {
199
- name,
200
- id: kid
183
+ for await (const value of this.components.datastore.query(query)) {
184
+ const key = JSON.parse(uint8ArrayToString(value.value))
185
+
186
+ if (key.id === id) {
187
+ return key
188
+ }
201
189
  }
202
- const batch = this.components.datastore.batch()
203
- batch.put(dsname, uint8ArrayFromString(pem))
204
- batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo)))
205
190
 
206
- await batch.commit()
191
+ throw new InvalidParametersError(`Key with id '${id}' does not exist.`)
207
192
  } catch (err: any) {
208
193
  await randomDelay()
209
194
  throw err
210
195
  }
211
-
212
- return keyInfo
213
196
  }
214
197
 
215
- /**
216
- * List all the keys.
217
- *
218
- * @returns {Promise<KeyInfo[]>}
219
- */
220
- async listKeys (): Promise<KeyInfo[]> {
221
- const query = {
222
- prefix: infoPrefix
198
+ async importKey (name: string, key: PrivateKey): Promise<KeyInfo> {
199
+ if (!validateKeyName(name) || name === 'self') {
200
+ await randomDelay()
201
+ throw new InvalidParametersError(`Invalid key name '${name}'`)
223
202
  }
224
-
225
- const info = []
226
- for await (const value of this.components.datastore.query(query)) {
227
- info.push(JSON.parse(uint8ArrayToString(value.value)))
203
+ if (key == null) {
204
+ await randomDelay()
205
+ throw new InvalidParametersError('Key is required')
206
+ }
207
+ const dsname = DsName(name)
208
+ const exists = await this.components.datastore.has(dsname)
209
+ if (exists) {
210
+ await randomDelay()
211
+ throw new InvalidParametersError(`Key '${name}' already exists`)
228
212
  }
229
213
 
230
- return info
231
- }
232
-
233
- /**
234
- * Find a key by it's id
235
- */
236
- async findKeyById (id: string): Promise<KeyInfo> {
214
+ let kid: string
215
+ let pem: string
237
216
  try {
238
- const keys = await this.listKeys()
239
- const key = keys.find((k) => k.id === id)
217
+ kid = await keyId(key)
218
+ const cached = privates.get(this)
240
219
 
241
- if (key == null) {
242
- throw new CodeError(`Key with id '${id}' does not exist.`, codes.ERR_KEY_NOT_FOUND)
220
+ if (cached == null) {
221
+ throw new InvalidParametersError('dek missing')
243
222
  }
244
223
 
245
- return key
224
+ const dek = cached.dek
225
+ pem = await exportPrivateKey(key, dek, key.type === 'RSA' ? 'pkcs-8' : 'libp2p-key')
246
226
  } catch (err: any) {
247
227
  await randomDelay()
248
228
  throw err
249
229
  }
230
+
231
+ const keyInfo = {
232
+ name,
233
+ id: kid
234
+ }
235
+ const batch = this.components.datastore.batch()
236
+ batch.put(dsname, uint8ArrayFromString(pem))
237
+ batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo)))
238
+ await batch.commit()
239
+
240
+ return keyInfo
250
241
  }
251
242
 
252
- /**
253
- * Find a key by it's name.
254
- *
255
- * @param {string} name - The local key name.
256
- * @returns {Promise<KeyInfo>}
257
- */
258
- async findKeyByName (name: string): Promise<KeyInfo> {
243
+ async exportKey (name: string): Promise<PrivateKey> {
259
244
  if (!validateKeyName(name)) {
260
245
  await randomDelay()
261
- throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME)
246
+ throw new InvalidParametersError(`Invalid key name '${name}'`)
262
247
  }
263
248
 
264
- const dsname = DsInfoName(name)
249
+ const dsname = DsName(name)
265
250
  try {
266
251
  const res = await this.components.datastore.get(dsname)
267
- return JSON.parse(uint8ArrayToString(res))
252
+ const pem = uint8ArrayToString(res)
253
+ const cached = privates.get(this)
254
+
255
+ if (cached == null) {
256
+ throw new InvalidParametersError('dek missing')
257
+ }
258
+
259
+ const dek = cached.dek
260
+
261
+ return await importPrivateKey(pem, dek)
268
262
  } catch (err: any) {
269
263
  await randomDelay()
270
- this.log.error(err)
271
- throw new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND)
264
+ throw err
272
265
  }
273
266
  }
274
267
 
275
- /**
276
- * Remove an existing key.
277
- *
278
- * @param {string} name - The local key name; must already exist.
279
- * @returns {Promise<KeyInfo>}
280
- */
281
268
  async removeKey (name: string): Promise<KeyInfo> {
282
269
  if (!validateKeyName(name) || name === 'self') {
283
270
  await randomDelay()
284
- throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME)
271
+ throw new InvalidParametersError(`Invalid key name '${name}'`)
285
272
  }
273
+
286
274
  const dsname = DsName(name)
287
275
  const keyInfo = await this.findKeyByName(name)
288
276
  const batch = this.components.datastore.batch()
289
277
  batch.delete(dsname)
290
278
  batch.delete(DsInfoName(name))
291
279
  await batch.commit()
280
+
292
281
  return keyInfo
293
282
  }
294
283
 
284
+ /**
285
+ * List all the keys.
286
+ *
287
+ * @returns {Promise<KeyInfo[]>}
288
+ */
289
+ async listKeys (): Promise<KeyInfo[]> {
290
+ const query = {
291
+ prefix: infoPrefix
292
+ }
293
+
294
+ const info = []
295
+ for await (const value of this.components.datastore.query(query)) {
296
+ info.push(JSON.parse(uint8ArrayToString(value.value)))
297
+ }
298
+
299
+ return info
300
+ }
301
+
295
302
  /**
296
303
  * Rename a key
297
304
  *
@@ -302,11 +309,11 @@ export class DefaultKeychain implements Keychain {
302
309
  async renameKey (oldName: string, newName: string): Promise<KeyInfo> {
303
310
  if (!validateKeyName(oldName) || oldName === 'self') {
304
311
  await randomDelay()
305
- throw new CodeError(`Invalid old key name '${oldName}'`, codes.ERR_OLD_KEY_NAME_INVALID)
312
+ throw new InvalidParametersError(`Invalid old key name '${oldName}'`)
306
313
  }
307
314
  if (!validateKeyName(newName) || newName === 'self') {
308
315
  await randomDelay()
309
- throw new CodeError(`Invalid new key name '${newName}'`, codes.ERR_NEW_KEY_NAME_INVALID)
316
+ throw new InvalidParametersError(`Invalid new key name '${newName}'`)
310
317
  }
311
318
  const oldDsname = DsName(oldName)
312
319
  const newDsname = DsName(newName)
@@ -316,7 +323,7 @@ export class DefaultKeychain implements Keychain {
316
323
  const exists = await this.components.datastore.has(newDsname)
317
324
  if (exists) {
318
325
  await randomDelay()
319
- throw new CodeError(`Key '${newName}' already exists`, codes.ERR_KEY_ALREADY_EXISTS)
326
+ throw new InvalidParametersError(`Key '${newName}' already exists`)
320
327
  }
321
328
 
322
329
  try {
@@ -338,199 +345,27 @@ export class DefaultKeychain implements Keychain {
338
345
  }
339
346
  }
340
347
 
341
- /**
342
- * Export an existing key as a PEM encrypted PKCS #8 string
343
- */
344
- async exportKey (name: string, password: string): Promise<string> {
345
- if (!validateKeyName(name)) {
346
- await randomDelay()
347
- throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME)
348
- }
349
- if (password == null) {
350
- await randomDelay()
351
- throw new CodeError('Password is required', codes.ERR_PASSWORD_REQUIRED)
352
- }
353
-
354
- const dsname = DsName(name)
355
- try {
356
- const res = await this.components.datastore.get(dsname)
357
- const pem = uint8ArrayToString(res)
358
- const cached = privates.get(this)
359
-
360
- if (cached == null) {
361
- throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS)
362
- }
363
-
364
- const dek = cached.dek
365
- const privateKey = await importKey(pem, dek)
366
- const keyString = await privateKey.export(password)
367
-
368
- return keyString
369
- } catch (err: any) {
370
- await randomDelay()
371
- throw err
372
- }
373
- }
374
-
375
- /**
376
- * Export an existing key as a PeerId
377
- */
378
- async exportPeerId (name: string): Promise<PeerId> {
379
- const password = 'temporary-password'
380
- const pem = await this.exportKey(name, password)
381
- const privateKey = await importKey(pem, password)
382
-
383
- return peerIdFromKeys(privateKey.public.bytes, privateKey.bytes)
384
- }
385
-
386
- /**
387
- * Import a new key from a PEM encoded PKCS #8 string
388
- *
389
- * @param {string} name - The local key name; must not already exist.
390
- * @param {string} pem - The PEM encoded PKCS #8 string
391
- * @param {string} password - The password.
392
- * @returns {Promise<KeyInfo>}
393
- */
394
- async importKey (name: string, pem: string, password: string): Promise<KeyInfo> {
395
- if (!validateKeyName(name) || name === 'self') {
396
- await randomDelay()
397
- throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME)
398
- }
399
- if (pem == null) {
400
- await randomDelay()
401
- throw new CodeError('PEM encoded key is required', codes.ERR_PEM_REQUIRED)
402
- }
403
- const dsname = DsName(name)
404
- const exists = await this.components.datastore.has(dsname)
405
- if (exists) {
406
- await randomDelay()
407
- throw new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS)
408
- }
409
-
410
- let privateKey
411
- try {
412
- privateKey = await importKey(pem, password)
413
- } catch (err: any) {
414
- await randomDelay()
415
- throw new CodeError('Cannot read the key, most likely the password is wrong', codes.ERR_CANNOT_READ_KEY)
416
- }
417
-
418
- let kid
419
- try {
420
- kid = await privateKey.id()
421
- const cached = privates.get(this)
422
-
423
- if (cached == null) {
424
- throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS)
425
- }
426
-
427
- const dek = cached.dek
428
- pem = await privateKey.export(dek)
429
- } catch (err: any) {
430
- await randomDelay()
431
- throw err
432
- }
433
-
434
- const keyInfo = {
435
- name,
436
- id: kid
437
- }
438
- const batch = this.components.datastore.batch()
439
- batch.put(dsname, uint8ArrayFromString(pem))
440
- batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo)))
441
- await batch.commit()
442
-
443
- return keyInfo
444
- }
445
-
446
- /**
447
- * Import a peer key
448
- */
449
- async importPeer (name: string, peer: PeerId): Promise<KeyInfo> {
450
- try {
451
- if (!validateKeyName(name)) {
452
- throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME)
453
- }
454
- if (peer == null) {
455
- throw new CodeError('PeerId is required', codes.ERR_MISSING_PRIVATE_KEY)
456
- }
457
- if (peer.privateKey == null) {
458
- throw new CodeError('PeerId.privKey is required', codes.ERR_MISSING_PRIVATE_KEY)
459
- }
460
-
461
- const privateKey = await unmarshalPrivateKey(peer.privateKey)
462
-
463
- const dsname = DsName(name)
464
- const exists = await this.components.datastore.has(dsname)
465
- if (exists) {
466
- await randomDelay()
467
- throw new CodeError(`Key '${name}' already exists`, codes.ERR_KEY_ALREADY_EXISTS)
468
- }
469
-
470
- const cached = privates.get(this)
471
-
472
- if (cached == null) {
473
- throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS)
474
- }
475
-
476
- const dek = cached.dek
477
- const pem = await privateKey.export(dek)
478
- const keyInfo: KeyInfo = {
479
- name,
480
- id: peer.toString()
481
- }
482
- const batch = this.components.datastore.batch()
483
- batch.put(dsname, uint8ArrayFromString(pem))
484
- batch.put(DsInfoName(name), uint8ArrayFromString(JSON.stringify(keyInfo)))
485
- await batch.commit()
486
- return keyInfo
487
- } catch (err: any) {
488
- await randomDelay()
489
- throw err
490
- }
491
- }
492
-
493
- /**
494
- * Gets the private key as PEM encoded PKCS #8 string
495
- */
496
- async getPrivateKey (name: string): Promise<string> {
497
- if (!validateKeyName(name)) {
498
- await randomDelay()
499
- throw new CodeError(`Invalid key name '${name}'`, codes.ERR_INVALID_KEY_NAME)
500
- }
501
-
502
- try {
503
- const dsname = DsName(name)
504
- const res = await this.components.datastore.get(dsname)
505
- return uint8ArrayToString(res)
506
- } catch (err: any) {
507
- await randomDelay()
508
- this.log.error(err)
509
- throw new CodeError(`Key '${name}' does not exist.`, codes.ERR_KEY_NOT_FOUND)
510
- }
511
- }
512
-
513
348
  /**
514
349
  * Rotate keychain password and re-encrypt all associated keys
515
350
  */
516
351
  async rotateKeychainPass (oldPass: string, newPass: string): Promise<void> {
517
352
  if (typeof oldPass !== 'string') {
518
353
  await randomDelay()
519
- throw new CodeError(`Invalid old pass type '${typeof oldPass}'`, codes.ERR_INVALID_OLD_PASS_TYPE)
354
+ throw new InvalidParametersError(`Invalid old pass type '${typeof oldPass}'`)
520
355
  }
521
356
  if (typeof newPass !== 'string') {
522
357
  await randomDelay()
523
- throw new CodeError(`Invalid new pass type '${typeof newPass}'`, codes.ERR_INVALID_NEW_PASS_TYPE)
358
+ throw new InvalidParametersError(`Invalid new pass type '${typeof newPass}'`)
524
359
  }
525
360
  if (newPass.length < 20) {
526
361
  await randomDelay()
527
- throw new CodeError(`Invalid pass length ${newPass.length}`, codes.ERR_INVALID_PASS_LENGTH)
362
+ throw new InvalidParametersError(`Invalid pass length ${newPass.length}`)
528
363
  }
529
364
  this.log('recreating keychain')
530
365
  const cached = privates.get(this)
531
366
 
532
367
  if (cached == null) {
533
- throw new CodeError('dek missing', codes.ERR_INVALID_PARAMETERS)
368
+ throw new InvalidParametersError('dek missing')
534
369
  }
535
370
 
536
371
  const oldDek = cached.dek
@@ -548,9 +383,9 @@ export class DefaultKeychain implements Keychain {
548
383
  for (const key of keys) {
549
384
  const res = await this.components.datastore.get(DsName(key.name))
550
385
  const pem = uint8ArrayToString(res)
551
- const privateKey = await importKey(pem, oldDek)
386
+ const privateKey = await importPrivateKey(pem, oldDek)
552
387
  const password = newDek.toString()
553
- const keyAsPEM = await privateKey.export(password)
388
+ const keyAsPEM = await exportPrivateKey(privateKey, password, privateKey.type === 'RSA' ? 'pkcs-8' : 'libp2p-key')
554
389
 
555
390
  // Update stored key
556
391
  const batch = this.components.datastore.batch()
@@ -0,0 +1,3 @@
1
+ export const SALT_LENGTH = 16
2
+ export const KEY_SIZE = 32
3
+ export const ITERATIONS = 10000