Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: keychain rotate passphrase #944

Merged
merged 23 commits into from
May 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d04339e
changeKeychainPassword
testasdsdas May 13, 2021
f0220bb
Merge branch 'libp2p:master' into keychain-rotating-passphrase
zeim839 May 13, 2021
2591ca7
refactor: renamed changeKeychainPassword() to rotateKeychainPass()
zeim839 May 19, 2021
66eb066
refactor: renamed old/newPassword to old/newPass
testasdsdas May 19, 2021
c150749
fix: generates new options & operates on self key
testasdsdas May 19, 2021
b20e83b
doc: rotateKeychainPass
testasdsdas May 19, 2021
f8db3c9
test: rotate keychain passphrase
testasdsdas May 19, 2021
825d70c
test: rotate keychain passphrase
testasdsdas May 20, 2021
1744f8d
fix: throwDelayed error, export key with newDek
zeim839 May 21, 2021
9d591ff
lint: rotateKeychainPass
zeim839 May 21, 2021
28a2b4a
fix: rotateKeychainPass tests
zeim839 May 21, 2021
bc35ac3
fix: Uint8Array not assignable error
zeim839 May 21, 2021
87dcc5b
fix: typo
zeim839 May 22, 2021
1e06652
fix: lint
vasco-santos May 25, 2021
5e48dee
fix: lint in test
vasco-santos May 25, 2021
59e9871
fix: function returning before cycle finished
zeim839 May 26, 2021
59244c2
lint: removed parenthesis
zeim839 May 26, 2021
f855a2a
fix: await test to finish and expect to eventually be rejected
zeim839 May 26, 2021
dec59e2
fix: await test to finish and expect to eventually have property name
zeim839 May 26, 2021
0222ea7
fix: issue decrypting keys from previous tests
zeim839 May 26, 2021
ae68e1c
fix: rotateKeychainPass() test timing out
zeim839 May 26, 2021
00ed8ce
lint: fix typo
zeim839 May 26, 2021
7b66ea6
perf: removed redundant batch delete/commit
zeim839 May 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion src/keychain/index.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -503,6 +506,55 @@ class Keychain {
return throwDelayed(errcode(new Error(`Key '${name}' does not exist. ${err.message}`), 'ERR_KEY_NOT_FOUND'))
}
}

zeim839 marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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
vasco-santos marked this conversation as resolved.
Show resolved Hide resolved
? 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
85 changes: 84 additions & 1 deletion test/keychain/keychain.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 () => {
zeim839 marked this conversation as resolved.
Show resolved Hide resolved
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', () => {
Expand Down