mirror of
https://github.com/fluencelabs/js-libp2p
synced 2025-07-31 00:12:00 +00:00
feat: keychain rotate passphrase (#944)
Co-authored-by: Vasco Santos <vasco.santos@ua.pt>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
/* eslint max-nested-callbacks: ["error", 5] */
|
||||
'use strict'
|
||||
|
||||
const debug = require('debug')
|
||||
const log = Object.assign(debug('libp2p:keychain'), {
|
||||
error: debug('libp2p:keychain:err')
|
||||
})
|
||||
const sanitize = require('sanitize-filename')
|
||||
const mergeOptions = require('merge-options')
|
||||
const crypto = require('libp2p-crypto')
|
||||
@@ -503,6 +506,55 @@ class Keychain {
|
||||
return throwDelayed(errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate keychain password and re-encrypt all assosciated keys
|
||||
*
|
||||
* @param {string} oldPass - The old local keychain password
|
||||
* @param {string} newPass - The new local keychain password
|
||||
*/
|
||||
async rotateKeychainPass (oldPass, newPass) {
|
||||
if (typeof oldPass !== 'string') {
|
||||
return throwDelayed(errcode(new Error(`Invalid old pass type '${typeof oldPass}'`), 'ERR_INVALID_OLD_PASS_TYPE'))
|
||||
}
|
||||
if (typeof newPass !== 'string') {
|
||||
return throwDelayed(errcode(new Error(`Invalid new pass type '${typeof newPass}'`), 'ERR_INVALID_NEW_PASS_TYPE'))
|
||||
}
|
||||
if (newPass.length < 20) {
|
||||
return throwDelayed(errcode(new Error(`Invalid pass length ${newPass.length}`), 'ERR_INVALID_PASS_LENGTH'))
|
||||
}
|
||||
log('recreating keychain')
|
||||
const oldDek = privates.get(this).dek
|
||||
this.opts.pass = newPass
|
||||
const newDek = newPass
|
||||
? crypto.pbkdf2(
|
||||
newPass,
|
||||
this.opts.dek.salt,
|
||||
this.opts.dek.iterationCount,
|
||||
this.opts.dek.keyLength,
|
||||
this.opts.dek.hash)
|
||||
: ''
|
||||
privates.set(this, { dek: newDek })
|
||||
const keys = await this.listKeys()
|
||||
for (const key of keys) {
|
||||
const res = await this.store.get(DsName(key.name))
|
||||
const pem = uint8ArrayToString(res)
|
||||
const privateKey = await crypto.keys.import(pem, oldDek)
|
||||
const password = newDek.toString()
|
||||
const keyAsPEM = await privateKey.export(password)
|
||||
|
||||
// Update stored key
|
||||
const batch = this.store.batch()
|
||||
const keyInfo = {
|
||||
name: key.name,
|
||||
id: key.id
|
||||
}
|
||||
batch.put(DsName(key.name), uint8ArrayFromString(keyAsPEM))
|
||||
batch.put(DsInfoName(key.name), uint8ArrayFromString(JSON.stringify(keyInfo)))
|
||||
await batch.commit()
|
||||
}
|
||||
log('keychain reconstructed')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Keychain
|
||||
|
@@ -9,9 +9,10 @@ const uint8ArrayToString = require('uint8arrays/to-string')
|
||||
|
||||
const peerUtils = require('../utils/creators/peer')
|
||||
|
||||
const { MemoryDatastore } = require('interface-datastore')
|
||||
const { MemoryDatastore, Key } = require('interface-datastore')
|
||||
const Keychain = require('../../src/keychain')
|
||||
const PeerId = require('peer-id')
|
||||
const crypto = require('libp2p-crypto')
|
||||
|
||||
describe('keychain', () => {
|
||||
const passPhrase = 'this is not a secure phrase'
|
||||
@@ -492,6 +493,88 @@ describe('keychain', () => {
|
||||
expect(key).to.have.property('id', rsaKeyInfo.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rotate keychain passphrase', () => {
|
||||
let oldPass
|
||||
let kc
|
||||
let options
|
||||
let ds
|
||||
before(async () => {
|
||||
ds = new MemoryDatastore()
|
||||
oldPass = `hello-${Date.now()}-${Date.now()}`
|
||||
options = {
|
||||
pass: oldPass,
|
||||
dek: {
|
||||
salt: '3Nd/Ya4ENB3bcByNKptb4IR',
|
||||
iterationCount: 10000,
|
||||
keyLength: 64,
|
||||
hash: 'sha2-512'
|
||||
}
|
||||
}
|
||||
kc = new Keychain(ds, options)
|
||||
await ds.open()
|
||||
})
|
||||
|
||||
it('should validate newPass is a string', async () => {
|
||||
try {
|
||||
await kc.rotateKeychainPass(oldPass, 1234567890)
|
||||
} catch (err) {
|
||||
expect(err).to.exist()
|
||||
}
|
||||
})
|
||||
|
||||
it('should validate oldPass is a string', async () => {
|
||||
try {
|
||||
await kc.rotateKeychainPass(1234, 'newInsecurePassword1')
|
||||
} catch (err) {
|
||||
expect(err).to.exist()
|
||||
}
|
||||
})
|
||||
|
||||
it('should validate newPass is at least 20 characters', async () => {
|
||||
try {
|
||||
await kc.rotateKeychainPass(oldPass, 'not20Chars')
|
||||
} catch (err) {
|
||||
expect(err).to.exist()
|
||||
}
|
||||
})
|
||||
|
||||
it('can rotate keychain passphrase', async () => {
|
||||
await kc.createKey('keyCreatedWithOldPassword', 'rsa', 2048)
|
||||
await kc.rotateKeychainPass(oldPass, 'newInsecurePassphrase')
|
||||
|
||||
// Get Key PEM from datastore
|
||||
const dsname = new Key('/pkcs8/' + 'keyCreatedWithOldPassword')
|
||||
const res = await ds.get(dsname)
|
||||
const pem = uint8ArrayToString(res)
|
||||
|
||||
const oldDek = options.pass
|
||||
? crypto.pbkdf2(
|
||||
options.pass,
|
||||
options.dek.salt,
|
||||
options.dek.iterationCount,
|
||||
options.dek.keyLength,
|
||||
options.dek.hash)
|
||||
: ''
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
const newDek = 'newInsecurePassphrase'
|
||||
? crypto.pbkdf2(
|
||||
'newInsecurePassphrase',
|
||||
options.dek.salt,
|
||||
options.dek.iterationCount,
|
||||
options.dek.keyLength,
|
||||
options.dek.hash)
|
||||
: ''
|
||||
|
||||
// Dek with old password should not work:
|
||||
await expect(kc.importKey('keyWhosePassChanged', pem, oldDek))
|
||||
.to.eventually.be.rejected()
|
||||
// Dek with new password should work:
|
||||
await expect(kc.importKey('keyWhosePasswordChanged', pem, newDek))
|
||||
.to.eventually.have.property('name', 'keyWhosePasswordChanged')
|
||||
}).timeout(10000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('libp2p.keychain', () => {
|
||||
|
Reference in New Issue
Block a user