This repository has been archived by the owner on Jul 21, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 52
feat: add exporting/importing of non rsa keys in libp2p-key format #179
Merged
Merged
Changes from 9 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
926b74d
feat: add exporting/importing of ed25519 keys in libp2p-key format
jacobheun 1c7fff8
feat: add libp2p-key export/import support for rsa and secp keys
jacobheun d7cb0bd
chore: dep bumps
jacobheun 8cd671b
chore: update aegir
jacobheun 2ae1d2b
refactor: import and export base64 strings
jacobheun be4b372
refactor: simplify api for now
jacobheun ad26c60
chore: fix lint
jacobheun d02ab5c
refactor: remove extraneous param
jacobheun d391509
refactor: clean up
jacobheun 7fa1a16
fix: review patches
jacobheun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,9 +6,10 @@ | |
"types": "src/index.d.ts", | ||
"leadMaintainer": "Jacob Heun <[email protected]>", | ||
"browser": { | ||
"./src/aes/ciphers.js": "./src/aes/ciphers-browser.js", | ||
"./src/ciphers/aes-gcm.js": "./src/ciphers/aes-gcm.browser.js", | ||
"./src/hmac/index.js": "./src/hmac/index-browser.js", | ||
"./src/keys/ecdh.js": "./src/keys/ecdh-browser.js", | ||
"./src/aes/ciphers.js": "./src/aes/ciphers-browser.js", | ||
"./src/keys/rsa.js": "./src/keys/rsa-browser.js" | ||
}, | ||
"files": [ | ||
|
@@ -43,21 +44,22 @@ | |
"is-typedarray": "^1.0.0", | ||
"iso-random-stream": "^1.1.0", | ||
"keypair": "^1.0.1", | ||
"multibase": "^0.7.0", | ||
"multibase": "^1.0.1", | ||
"multicodec": "^1.0.4", | ||
"multihashing-async": "^0.8.1", | ||
"node-forge": "^0.9.1", | ||
"pem-jwk": "^2.0.0", | ||
"protons": "^1.0.1", | ||
"protons": "^1.2.1", | ||
"secp256k1": "^4.0.0", | ||
"ursa-optional": "~0.10.1" | ||
"uint8arrays": "^1.0.0", | ||
"ursa-optional": "^0.10.1" | ||
}, | ||
"devDependencies": { | ||
"@types/chai": "^4.2.11", | ||
"@types/chai": "^4.2.12", | ||
"@types/chai-string": "^1.4.2", | ||
"@types/dirty-chai": "^2.0.2", | ||
"@types/mocha": "^7.0.1", | ||
"@types/sinon": "^9.0.0", | ||
"aegir": "^22.0.0", | ||
"@types/mocha": "^8.0.1", | ||
"aegir": "^25.0.0", | ||
"benchmark": "^2.1.4", | ||
"chai": "^4.2.0", | ||
"chai-string": "^1.5.0", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
'use strict' | ||
|
||
const concat = require('uint8arrays/concat') | ||
const fromString = require('uint8arrays/from-string') | ||
|
||
const webcrypto = require('../webcrypto') | ||
|
||
// Based off of code from https://github.com/luke-park/SecureCompatibleEncryptionExamples | ||
|
||
/** | ||
* | ||
* @param {object} param0 | ||
* @param {string} [param0.algorithm] Defaults to 'aes-128-gcm' | ||
* @param {Number} [param0.nonceLength] Defaults to 12 (96-bit) | ||
* @param {Number} [param0.keyLength] Defaults to 16 | ||
* @param {string} [param0.digest] Defaults to 'sha256' | ||
* @param {Number} [param0.saltLength] Defaults to 16 | ||
* @param {Number} [param0.iterations] Defaults to 32767 | ||
* @returns {*} | ||
*/ | ||
function create ({ | ||
algorithm = 'AES-GCM', | ||
nonceLength = 12, | ||
keyLength = 16, | ||
digest = 'SHA-256', | ||
saltLength = 16, | ||
iterations = 32767 | ||
} = {}) { | ||
const crypto = webcrypto.get() | ||
keyLength *= 8 // Browser crypto uses bits instead of bytes | ||
|
||
/** | ||
* Uses the provided password to derive a pbkdf2 key. The key | ||
* will then be used to encrypt the data. | ||
* | ||
* @param {Uint8Array} data The data to decrypt | ||
* @param {string} password A plain password | ||
* @returns {Promise<Uint8Array>} | ||
*/ | ||
async function encrypt (data, password) { // eslint-disable-line require-await | ||
const salt = crypto.getRandomValues(new Uint8Array(saltLength)) | ||
const nonce = crypto.getRandomValues(new Uint8Array(nonceLength)) | ||
const aesGcm = { name: algorithm, iv: nonce } | ||
|
||
// Derive a key using PBKDF2. | ||
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } } | ||
const rawKey = await crypto.subtle.importKey('raw', fromString(password), { name: 'PBKDF2' }, false, ['deriveKey', 'deriveBits']) | ||
const cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['encrypt']) | ||
|
||
// Encrypt the string. | ||
const ciphertext = await crypto.subtle.encrypt(aesGcm, cryptoKey, data) | ||
return concat([salt, aesGcm.iv, new Uint8Array(ciphertext)]) | ||
} | ||
|
||
/** | ||
* Uses the provided password to derive a pbkdf2 key. The key | ||
* will then be used to decrypt the data. The options used to create | ||
* this decryption cipher must be the same as those used to create | ||
* the encryption cipher. | ||
* | ||
* @param {Uint8Array} data The data to decrypt | ||
* @param {string} password A plain password | ||
* @returns {Promise<Uint8Array>} | ||
*/ | ||
async function decrypt (data, password) { | ||
const salt = data.slice(0, saltLength) | ||
const nonce = data.slice(saltLength, saltLength + nonceLength) | ||
const ciphertext = data.slice(saltLength + nonceLength) | ||
const aesGcm = { name: algorithm, iv: nonce } | ||
|
||
// Derive the key using PBKDF2. | ||
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } } | ||
const rawKey = await crypto.subtle.importKey('raw', fromString(password), { name: 'PBKDF2' }, false, ['deriveKey', 'deriveBits']) | ||
const cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['decrypt']) | ||
|
||
// Decrypt the string. | ||
const plaintext = await crypto.subtle.decrypt(aesGcm, cryptoKey, ciphertext) | ||
return new Uint8Array(plaintext) | ||
} | ||
|
||
return { | ||
encrypt, | ||
decrypt | ||
} | ||
} | ||
|
||
module.exports = { | ||
create | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
'use strict' | ||
|
||
const crypto = require('crypto') | ||
|
||
// Based off of code from https://github.com/luke-park/SecureCompatibleEncryptionExamples | ||
|
||
/** | ||
* | ||
* @param {object} param0 | ||
* @param {Number} [param0.algorithmTagLength] Defaults to 16 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as below |
||
* @param {Number} [param0.nonceLength] Defaults to 12 (96-bit) | ||
* @param {Number} [param0.keyLength] Defaults to 16 | ||
* @param {string} [param0.digest] Defaults to 'sha256' | ||
* @param {Number} [param0.saltLength] Defaults to 16 | ||
* @param {Number} [param0.iterations] Defaults to 32767 | ||
* @returns {*} | ||
*/ | ||
function create ({ | ||
algorithmTagLength = 16, | ||
nonceLength = 12, | ||
keyLength = 16, | ||
digest = 'sha256', | ||
saltLength = 16, | ||
iterations = 32767 | ||
} = {}) { | ||
const algorithm = 'aes-128-gcm' | ||
/** | ||
* | ||
* @private | ||
* @param {Buffer} data | ||
* @param {Buffer} key | ||
* @returns {Promise<Buffer>} | ||
*/ | ||
async function encryptWithKey (data, key) { // eslint-disable-line require-await | ||
const nonce = crypto.randomBytes(nonceLength) | ||
|
||
// Create the cipher instance. | ||
const cipher = crypto.createCipheriv(algorithm, key, nonce) | ||
|
||
// Encrypt and prepend nonce. | ||
const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]) | ||
|
||
return Buffer.concat([nonce, ciphertext, cipher.getAuthTag()]) | ||
} | ||
|
||
/** | ||
* Uses the provided password to derive a pbkdf2 key. The key | ||
* will then be used to encrypt the data. | ||
* | ||
* @param {Buffer} data The data to decrypt | ||
* @param {string|Buffer} password A plain password | ||
* @returns {Promise<Buffer>} | ||
*/ | ||
async function encrypt (data, password) { // eslint-disable-line require-await | ||
// Generate a 128-bit salt using a CSPRNG. | ||
const salt = crypto.randomBytes(saltLength) | ||
|
||
// Derive a key using PBKDF2. | ||
const key = crypto.pbkdf2Sync(Buffer.from(password), salt, iterations, keyLength, digest) | ||
|
||
// Encrypt and prepend salt. | ||
return Buffer.concat([salt, await encryptWithKey(Buffer.from(data), key)]) | ||
} | ||
|
||
/** | ||
* Decrypts the given cipher text with the provided key. The `key` should | ||
* be a cryptographically safe key and not a plaintext password. To use | ||
* a plaintext password, use `decrypt`. The options used to create | ||
* this decryption cipher must be the same as those used to create | ||
* the encryption cipher. | ||
* | ||
* @private | ||
* @param {Buffer} ciphertextAndNonce The data to decrypt | ||
* @param {Buffer} key | ||
* @returns {Promise<Buffer>} | ||
*/ | ||
async function decryptWithKey (ciphertextAndNonce, key) { // eslint-disable-line require-await | ||
// Create buffers of nonce, ciphertext and tag. | ||
const nonce = ciphertextAndNonce.slice(0, nonceLength) | ||
const ciphertext = ciphertextAndNonce.slice(nonceLength, ciphertextAndNonce.length - algorithmTagLength) | ||
const tag = ciphertextAndNonce.slice(ciphertext.length + nonceLength) | ||
|
||
// Create the cipher instance. | ||
const cipher = crypto.createDecipheriv(algorithm, key, nonce) | ||
|
||
// Decrypt and return result. | ||
cipher.setAuthTag(tag) | ||
return Buffer.concat([cipher.update(ciphertext), cipher.final()]) | ||
} | ||
|
||
/** | ||
* Uses the provided password to derive a pbkdf2 key. The key | ||
* will then be used to decrypt the data. The options used to create | ||
* this decryption cipher must be the same as those used to create | ||
* the encryption cipher. | ||
* | ||
* @param {Buffer} data The data to decrypt | ||
* @param {string|Buffer} password A plain password | ||
*/ | ||
async function decrypt (data, password) { // eslint-disable-line require-await | ||
// Create buffers of salt and ciphertextAndNonce. | ||
const salt = data.slice(0, saltLength) | ||
const ciphertextAndNonce = data.slice(saltLength) | ||
|
||
// Derive the key using PBKDF2. | ||
const key = crypto.pbkdf2Sync(Buffer.from(password), salt, iterations, keyLength, digest) | ||
|
||
// Decrypt and return result. | ||
return decryptWithKey(ciphertextAndNonce, key) | ||
} | ||
|
||
return { | ||
encrypt, | ||
decrypt | ||
} | ||
} | ||
|
||
module.exports = { | ||
create | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ const errcode = require('err-code') | |
|
||
const crypto = require('./ed25519') | ||
const pbm = protobuf(require('./keys.proto')) | ||
const exporter = require('./exporter') | ||
|
||
class Ed25519PublicKey { | ||
constructor (key) { | ||
|
@@ -86,6 +87,21 @@ class Ed25519PrivateKey { | |
const hash = await this.public.hash() | ||
return multibase.encode('base58btc', hash).toString().slice(1) | ||
} | ||
|
||
/** | ||
* Exports the key into a password protected `format` | ||
* | ||
* @param {string} password - The password to encrypt the key | ||
* @param {string} [format] - Defaults to 'libp2p-key'. | ||
* @returns {Promise<Buffer>} The encrypted private key | ||
*/ | ||
async export (password, format = 'libp2p-key') { // eslint-disable-line require-await | ||
if (format === 'libp2p-key') { | ||
return exporter.export(this.bytes, password) | ||
} else { | ||
throw errcode(new Error(`export format '${format}' is not supported`), 'ERR_INVALID_EXPORT_FORMAT') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add a notice for this in the README There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It only has the import |
||
} | ||
} | ||
} | ||
|
||
function unmarshalEd25519PrivateKey (bytes) { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
'use strict' | ||
|
||
const multibase = require('multibase') | ||
const ciphers = require('../ciphers/aes-gcm') | ||
|
||
module.exports = { | ||
/** | ||
* Exports the given PrivateKey as a base64 encoded string. | ||
* The PrivateKey is encrypted via a password derived PBKDF2 key | ||
* leveraging the aes-gcm cipher algorithm. | ||
* | ||
* @param {Buffer} privateKey The PrivateKey protobuf buffer | ||
* @param {string} password | ||
* @returns {Promise<string>} A base64 encoded string | ||
*/ | ||
export: async function (privateKey, password) { | ||
const cipher = ciphers.create() | ||
const encryptedKey = await cipher.encrypt(privateKey, password) | ||
const base64 = multibase.names.base64 | ||
return base64.encode(encryptedKey) | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add the defaults in the params, according to the jsdoc docs? This might be useful when we auto generate from jsdoc