Skip to content

Commit 478963a

Browse files
feat: keychain rotate passphrase (#944)
Co-authored-by: Vasco Santos <vasco.santos@ua.pt>
1 parent d22ad83 commit 478963a

File tree

2 files changed

+137
-2
lines changed

2 files changed

+137
-2
lines changed

src/keychain/index.js

+53-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/* eslint max-nested-callbacks: ["error", 5] */
22
'use strict'
3-
3+
const debug = require('debug')
4+
const log = Object.assign(debug('libp2p:keychain'), {
5+
error: debug('libp2p:keychain:err')
6+
})
47
const sanitize = require('sanitize-filename')
58
const mergeOptions = require('merge-options')
69
const crypto = require('libp2p-crypto')
@@ -503,6 +506,55 @@ class Keychain {
503506
return throwDelayed(errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND'))
504507
}
505508
}
509+
510+
/**
511+
* Rotate keychain password and re-encrypt all assosciated keys
512+
*
513+
* @param {string} oldPass - The old local keychain password
514+
* @param {string} newPass - The new local keychain password
515+
*/
516+
async rotateKeychainPass (oldPass, newPass) {
517+
if (typeof oldPass !== 'string') {
518+
return throwDelayed(errcode(new Error(`Invalid old pass type '${typeof oldPass}'`), 'ERR_INVALID_OLD_PASS_TYPE'))
519+
}
520+
if (typeof newPass !== 'string') {
521+
return throwDelayed(errcode(new Error(`Invalid new pass type '${typeof newPass}'`), 'ERR_INVALID_NEW_PASS_TYPE'))
522+
}
523+
if (newPass.length < 20) {
524+
return throwDelayed(errcode(new Error(`Invalid pass length ${newPass.length}`), 'ERR_INVALID_PASS_LENGTH'))
525+
}
526+
log('recreating keychain')
527+
const oldDek = privates.get(this).dek
528+
this.opts.pass = newPass
529+
const newDek = newPass
530+
? crypto.pbkdf2(
531+
newPass,
532+
this.opts.dek.salt,
533+
this.opts.dek.iterationCount,
534+
this.opts.dek.keyLength,
535+
this.opts.dek.hash)
536+
: ''
537+
privates.set(this, { dek: newDek })
538+
const keys = await this.listKeys()
539+
for (const key of keys) {
540+
const res = await this.store.get(DsName(key.name))
541+
const pem = uint8ArrayToString(res)
542+
const privateKey = await crypto.keys.import(pem, oldDek)
543+
const password = newDek.toString()
544+
const keyAsPEM = await privateKey.export(password)
545+
546+
// Update stored key
547+
const batch = this.store.batch()
548+
const keyInfo = {
549+
name: key.name,
550+
id: key.id
551+
}
552+
batch.put(DsName(key.name), uint8ArrayFromString(keyAsPEM))
553+
batch.put(DsInfoName(key.name), uint8ArrayFromString(JSON.stringify(keyInfo)))
554+
await batch.commit()
555+
}
556+
log('keychain reconstructed')
557+
}
506558
}
507559

508560
module.exports = Keychain

test/keychain/keychain.spec.js

+84-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ const uint8ArrayToString = require('uint8arrays/to-string')
99

1010
const peerUtils = require('../utils/creators/peer')
1111

12-
const { MemoryDatastore } = require('interface-datastore')
12+
const { MemoryDatastore, Key } = require('interface-datastore')
1313
const Keychain = require('../../src/keychain')
1414
const PeerId = require('peer-id')
15+
const crypto = require('libp2p-crypto')
1516

1617
describe('keychain', () => {
1718
const passPhrase = 'this is not a secure phrase'
@@ -492,6 +493,88 @@ describe('keychain', () => {
492493
expect(key).to.have.property('id', rsaKeyInfo.id)
493494
})
494495
})
496+
497+
describe('rotate keychain passphrase', () => {
498+
let oldPass
499+
let kc
500+
let options
501+
let ds
502+
before(async () => {
503+
ds = new MemoryDatastore()
504+
oldPass = `hello-${Date.now()}-${Date.now()}`
505+
options = {
506+
pass: oldPass,
507+
dek: {
508+
salt: '3Nd/Ya4ENB3bcByNKptb4IR',
509+
iterationCount: 10000,
510+
keyLength: 64,
511+
hash: 'sha2-512'
512+
}
513+
}
514+
kc = new Keychain(ds, options)
515+
await ds.open()
516+
})
517+
518+
it('should validate newPass is a string', async () => {
519+
try {
520+
await kc.rotateKeychainPass(oldPass, 1234567890)
521+
} catch (err) {
522+
expect(err).to.exist()
523+
}
524+
})
525+
526+
it('should validate oldPass is a string', async () => {
527+
try {
528+
await kc.rotateKeychainPass(1234, 'newInsecurePassword1')
529+
} catch (err) {
530+
expect(err).to.exist()
531+
}
532+
})
533+
534+
it('should validate newPass is at least 20 characters', async () => {
535+
try {
536+
await kc.rotateKeychainPass(oldPass, 'not20Chars')
537+
} catch (err) {
538+
expect(err).to.exist()
539+
}
540+
})
541+
542+
it('can rotate keychain passphrase', async () => {
543+
await kc.createKey('keyCreatedWithOldPassword', 'rsa', 2048)
544+
await kc.rotateKeychainPass(oldPass, 'newInsecurePassphrase')
545+
546+
// Get Key PEM from datastore
547+
const dsname = new Key('/pkcs8/' + 'keyCreatedWithOldPassword')
548+
const res = await ds.get(dsname)
549+
const pem = uint8ArrayToString(res)
550+
551+
const oldDek = options.pass
552+
? crypto.pbkdf2(
553+
options.pass,
554+
options.dek.salt,
555+
options.dek.iterationCount,
556+
options.dek.keyLength,
557+
options.dek.hash)
558+
: ''
559+
560+
// eslint-disable-next-line no-constant-condition
561+
const newDek = 'newInsecurePassphrase'
562+
? crypto.pbkdf2(
563+
'newInsecurePassphrase',
564+
options.dek.salt,
565+
options.dek.iterationCount,
566+
options.dek.keyLength,
567+
options.dek.hash)
568+
: ''
569+
570+
// Dek with old password should not work:
571+
await expect(kc.importKey('keyWhosePassChanged', pem, oldDek))
572+
.to.eventually.be.rejected()
573+
// Dek with new password should work:
574+
await expect(kc.importKey('keyWhosePasswordChanged', pem, newDek))
575+
.to.eventually.have.property('name', 'keyWhosePasswordChanged')
576+
}).timeout(10000)
577+
})
495578
})
496579

497580
describe('libp2p.keychain', () => {

0 commit comments

Comments
 (0)