mirror of
https://github.com/fluencelabs/js-libp2p-crypto
synced 2025-03-15 09:41:03 +00:00
feat: add exporting/importing of non rsa keys in libp2p-key format (#179)
* feat: add exporting/importing of ed25519 keys in libp2p-key format * feat: add libp2p-key export/import support for rsa and secp keys * chore: dep bumps * chore: update aegir * refactor: import and export base64 strings * refactor: simplify api for now * chore: fix lint * refactor: remove extraneous param * refactor: clean up * fix: review patches
This commit is contained in:
parent
609297be65
commit
7273739f04
17
README.md
17
README.md
@ -262,14 +262,23 @@ Returns `Promise<RsaPrivateKey|Ed25519PrivateKey|Secp256k1PrivateKey>`
|
|||||||
|
|
||||||
Converts a protobuf serialized private key into its representative object.
|
Converts a protobuf serialized private key into its representative object.
|
||||||
|
|
||||||
### `crypto.keys.import(pem, password)`
|
### `crypto.keys.import(encryptedKey, password)`
|
||||||
|
|
||||||
- `pem: string`
|
- `encryptedKey: string`
|
||||||
- `password: string`
|
- `password: string`
|
||||||
|
|
||||||
Returns `Promise<RsaPrivateKey>`
|
Returns `Promise<PrivateKey>`
|
||||||
|
|
||||||
Converts a PEM password protected private key into its representative object.
|
Converts an exported private key into its representative object. Supported formats are 'pem' (RSA only) and 'libp2p-key'.
|
||||||
|
|
||||||
|
### `privateKey.export(password, format)`
|
||||||
|
|
||||||
|
- `password: string`
|
||||||
|
- `format: string` the format to export to: 'pem' (rsa only), 'libp2p-key'
|
||||||
|
|
||||||
|
Returns `string`
|
||||||
|
|
||||||
|
Exports the password protected `PrivateKey`. RSA keys will be exported as password protected PEM by default. Ed25519 and Secp256k1 keys will be exported as password protected AES-GCM base64 encoded strings ('libp2p-key' format).
|
||||||
|
|
||||||
### `crypto.randomBytes(number)`
|
### `crypto.randomBytes(number)`
|
||||||
|
|
||||||
|
18
package.json
18
package.json
@ -6,9 +6,10 @@
|
|||||||
"types": "src/index.d.ts",
|
"types": "src/index.d.ts",
|
||||||
"leadMaintainer": "Jacob Heun <jacobheun@gmail.com>",
|
"leadMaintainer": "Jacob Heun <jacobheun@gmail.com>",
|
||||||
"browser": {
|
"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/hmac/index.js": "./src/hmac/index-browser.js",
|
||||||
"./src/keys/ecdh.js": "./src/keys/ecdh-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"
|
"./src/keys/rsa.js": "./src/keys/rsa-browser.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@ -43,21 +44,22 @@
|
|||||||
"is-typedarray": "^1.0.0",
|
"is-typedarray": "^1.0.0",
|
||||||
"iso-random-stream": "^1.1.0",
|
"iso-random-stream": "^1.1.0",
|
||||||
"keypair": "^1.0.1",
|
"keypair": "^1.0.1",
|
||||||
"multibase": "^0.7.0",
|
"multibase": "^1.0.1",
|
||||||
|
"multicodec": "^1.0.4",
|
||||||
"multihashing-async": "^0.8.1",
|
"multihashing-async": "^0.8.1",
|
||||||
"node-forge": "^0.9.1",
|
"node-forge": "^0.9.1",
|
||||||
"pem-jwk": "^2.0.0",
|
"pem-jwk": "^2.0.0",
|
||||||
"protons": "^1.0.1",
|
"protons": "^1.2.1",
|
||||||
"secp256k1": "^4.0.0",
|
"secp256k1": "^4.0.0",
|
||||||
"ursa-optional": "~0.10.1"
|
"uint8arrays": "^1.0.0",
|
||||||
|
"ursa-optional": "^0.10.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.2.11",
|
"@types/chai": "^4.2.12",
|
||||||
"@types/chai-string": "^1.4.2",
|
"@types/chai-string": "^1.4.2",
|
||||||
"@types/dirty-chai": "^2.0.2",
|
"@types/dirty-chai": "^2.0.2",
|
||||||
"@types/mocha": "^7.0.1",
|
"@types/mocha": "^8.0.1",
|
||||||
"@types/sinon": "^9.0.0",
|
"aegir": "^25.0.0",
|
||||||
"aegir": "^22.0.0",
|
|
||||||
"benchmark": "^2.1.4",
|
"benchmark": "^2.1.4",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"chai-string": "^1.5.0",
|
"chai-string": "^1.5.0",
|
||||||
|
89
src/ciphers/aes-gcm.browser.js
Normal file
89
src/ciphers/aes-gcm.browser.js
Normal file
@ -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} [options]
|
||||||
|
* @param {string} [options.algorithm=AES-GCM]
|
||||||
|
* @param {Number} [options.nonceLength=12]
|
||||||
|
* @param {Number} [options.keyLength=16]
|
||||||
|
* @param {string} [options.digest=sha256]
|
||||||
|
* @param {Number} [options.saltLength=16]
|
||||||
|
* @param {Number} [options.iterations=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
|
||||||
|
}
|
120
src/ciphers/aes-gcm.js
Normal file
120
src/ciphers/aes-gcm.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const crypto = require('crypto')
|
||||||
|
|
||||||
|
// Based off of code from https://github.com/luke-park/SecureCompatibleEncryptionExamples
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {Number} [options.algorithmTagLength=16]
|
||||||
|
* @param {Number} [options.nonceLength=12]
|
||||||
|
* @param {Number} [options.keyLength=16]
|
||||||
|
* @param {string} [options.digest=sha256]
|
||||||
|
* @param {Number} [options.saltLength=16]
|
||||||
|
* @param {Number} [options.iterations=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
|
||||||
|
}
|
29
src/index.d.ts
vendored
29
src/index.d.ts
vendored
@ -94,6 +94,10 @@ export interface PrivateKey {
|
|||||||
* of the PKCS SubjectPublicKeyInfo.
|
* of the PKCS SubjectPublicKeyInfo.
|
||||||
*/
|
*/
|
||||||
id(): Promise<string>;
|
id(): Promise<string>;
|
||||||
|
/**
|
||||||
|
* Exports the password protected key in the format specified.
|
||||||
|
*/
|
||||||
|
export(password: string, format?: "pkcs-8" | string): Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Keystretcher {
|
export interface Keystretcher {
|
||||||
@ -132,9 +136,6 @@ export namespace keys {
|
|||||||
hash(): Promise<Buffer>;
|
hash(): Promise<Buffer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type alias for export method
|
|
||||||
export type KeyInfo = any;
|
|
||||||
|
|
||||||
class RsaPrivateKey implements PrivateKey {
|
class RsaPrivateKey implements PrivateKey {
|
||||||
constructor(key: any, publicKey: Buffer);
|
constructor(key: any, publicKey: Buffer);
|
||||||
readonly public: RsaPublicKey;
|
readonly public: RsaPublicKey;
|
||||||
@ -146,13 +147,7 @@ export namespace keys {
|
|||||||
equals(key: PrivateKey): boolean;
|
equals(key: PrivateKey): boolean;
|
||||||
hash(): Promise<Buffer>;
|
hash(): Promise<Buffer>;
|
||||||
id(): Promise<string>;
|
id(): Promise<string>;
|
||||||
/**
|
export(password: string, format?: string): Promise<string>;
|
||||||
* Exports the key into a password protected PEM format
|
|
||||||
*
|
|
||||||
* @param password The password to read the encrypted PEM
|
|
||||||
* @param format Defaults to 'pkcs-8'.
|
|
||||||
*/
|
|
||||||
export(password: string, format?: "pkcs-8" | string): KeyInfo;
|
|
||||||
}
|
}
|
||||||
function unmarshalRsaPublicKey(buf: Buffer): RsaPublicKey;
|
function unmarshalRsaPublicKey(buf: Buffer): RsaPublicKey;
|
||||||
function unmarshalRsaPrivateKey(buf: Buffer): Promise<RsaPrivateKey>;
|
function unmarshalRsaPrivateKey(buf: Buffer): Promise<RsaPrivateKey>;
|
||||||
@ -180,6 +175,7 @@ export namespace keys {
|
|||||||
equals(key: PrivateKey): boolean;
|
equals(key: PrivateKey): boolean;
|
||||||
hash(): Promise<Buffer>;
|
hash(): Promise<Buffer>;
|
||||||
id(): Promise<string>;
|
id(): Promise<string>;
|
||||||
|
export(password: string, format?: string): Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function unmarshalEd25519PrivateKey(
|
function unmarshalEd25519PrivateKey(
|
||||||
@ -212,6 +208,7 @@ export namespace keys {
|
|||||||
equals(key: PrivateKey): boolean;
|
equals(key: PrivateKey): boolean;
|
||||||
hash(): Promise<Buffer>;
|
hash(): Promise<Buffer>;
|
||||||
id(): Promise<string>;
|
id(): Promise<string>;
|
||||||
|
export(password: string, format?: string): Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function unmarshalSecp256k1PrivateKey(
|
function unmarshalSecp256k1PrivateKey(
|
||||||
@ -234,16 +231,14 @@ export namespace keys {
|
|||||||
bits: number
|
bits: number
|
||||||
): Promise<PrivateKey>;
|
): Promise<PrivateKey>;
|
||||||
export function generateKeyPair(
|
export function generateKeyPair(
|
||||||
type: "Ed25519",
|
type: "Ed25519"
|
||||||
bits: number
|
|
||||||
): Promise<keys.supportedKeys.ed25519.Ed25519PrivateKey>;
|
): Promise<keys.supportedKeys.ed25519.Ed25519PrivateKey>;
|
||||||
export function generateKeyPair(
|
export function generateKeyPair(
|
||||||
type: "RSA",
|
type: "RSA",
|
||||||
bits: number
|
bits: number
|
||||||
): Promise<keys.supportedKeys.rsa.RsaPrivateKey>;
|
): Promise<keys.supportedKeys.rsa.RsaPrivateKey>;
|
||||||
export function generateKeyPair(
|
export function generateKeyPair(
|
||||||
type: "secp256k1",
|
type: "secp256k1"
|
||||||
bits: number
|
|
||||||
): Promise<keys.supportedKeys.secp256k1.Secp256k1PrivateKey>;
|
): Promise<keys.supportedKeys.secp256k1.Secp256k1PrivateKey>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -318,7 +313,7 @@ export namespace keys {
|
|||||||
* @param pem Password protected private key in PEM format.
|
* @param pem Password protected private key in PEM format.
|
||||||
* @param password The password used to protect the key.
|
* @param password The password used to protect the key.
|
||||||
*/
|
*/
|
||||||
function _import(pem: string, password: string): Promise<supportedKeys.rsa.RsaPrivateKey>;
|
function _import(pem: string, password: string, format?: string): Promise<supportedKeys.rsa.RsaPrivateKey>;
|
||||||
export { _import as import };
|
export { _import as import };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ const errcode = require('err-code')
|
|||||||
|
|
||||||
const crypto = require('./ed25519')
|
const crypto = require('./ed25519')
|
||||||
const pbm = protobuf(require('./keys.proto'))
|
const pbm = protobuf(require('./keys.proto'))
|
||||||
|
const exporter = require('./exporter')
|
||||||
|
|
||||||
class Ed25519PublicKey {
|
class Ed25519PublicKey {
|
||||||
constructor (key) {
|
constructor (key) {
|
||||||
@ -86,6 +87,21 @@ class Ed25519PrivateKey {
|
|||||||
const hash = await this.public.hash()
|
const hash = await this.public.hash()
|
||||||
return multibase.encode('base58btc', hash).toString().slice(1)
|
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=libp2p-key] - The format in which to export as
|
||||||
|
* @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')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function unmarshalEd25519PrivateKey (bytes) {
|
function unmarshalEd25519PrivateKey (bytes) {
|
||||||
|
22
src/keys/exporter.js
Normal file
22
src/keys/exporter.js
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
22
src/keys/importer.js
Normal file
22
src/keys/importer.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const multibase = require('multibase')
|
||||||
|
const ciphers = require('../ciphers/aes-gcm')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* Attempts to decrypt a base64 encoded PrivateKey string
|
||||||
|
* with the given password. The privateKey must have been exported
|
||||||
|
* using the same password and underlying cipher (aes-gcm)
|
||||||
|
*
|
||||||
|
* @param {string} privateKey A base64 encoded encrypted key
|
||||||
|
* @param {string} password
|
||||||
|
* @returns {Promise<Buffer>} The private key protobuf buffer
|
||||||
|
*/
|
||||||
|
import: async function (privateKey, password) {
|
||||||
|
const base64 = multibase.names.base64
|
||||||
|
const encryptedKey = base64.decode(privateKey)
|
||||||
|
const cipher = ciphers.create()
|
||||||
|
return await cipher.decrypt(encryptedKey, password)
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,8 @@ require('node-forge/lib/pbe')
|
|||||||
const forge = require('node-forge/lib/forge')
|
const forge = require('node-forge/lib/forge')
|
||||||
const errcode = require('err-code')
|
const errcode = require('err-code')
|
||||||
|
|
||||||
|
const importer = require('./importer')
|
||||||
|
|
||||||
exports = module.exports
|
exports = module.exports
|
||||||
|
|
||||||
const supportedKeys = {
|
const supportedKeys = {
|
||||||
@ -109,8 +111,21 @@ exports.marshalPrivateKey = (key, type) => {
|
|||||||
return key.bytes
|
return key.bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.import = async (pem, password) => { // eslint-disable-line require-await
|
/**
|
||||||
const key = forge.pki.decryptRsaPrivateKey(pem, password)
|
*
|
||||||
|
* @param {string} encryptedKey
|
||||||
|
* @param {string} password
|
||||||
|
*/
|
||||||
|
exports.import = async (encryptedKey, password) => { // eslint-disable-line require-await
|
||||||
|
try {
|
||||||
|
const key = await importer.import(encryptedKey, password)
|
||||||
|
return exports.unmarshalPrivateKey(key)
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore and try the old pem decrypt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only rsa supports pem right now
|
||||||
|
const key = forge.pki.decryptRsaPrivateKey(encryptedKey, password)
|
||||||
if (key === null) {
|
if (key === null) {
|
||||||
throw errcode(new Error('Cannot read the key, most likely the password is wrong or not a RSA key'), 'ERR_CANNOT_DECRYPT_PEM')
|
throw errcode(new Error('Cannot read the key, most likely the password is wrong or not a RSA key'), 'ERR_CANNOT_DECRYPT_PEM')
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,14 @@ const protobuf = require('protons')
|
|||||||
const multibase = require('multibase')
|
const multibase = require('multibase')
|
||||||
const errcode = require('err-code')
|
const errcode = require('err-code')
|
||||||
|
|
||||||
const crypto = require('./rsa')
|
|
||||||
const pbm = protobuf(require('./keys.proto'))
|
|
||||||
require('node-forge/lib/sha512')
|
require('node-forge/lib/sha512')
|
||||||
require('node-forge/lib/ed25519')
|
require('node-forge/lib/ed25519')
|
||||||
const forge = require('node-forge/lib/forge')
|
const forge = require('node-forge/lib/forge')
|
||||||
|
|
||||||
|
const crypto = require('./rsa')
|
||||||
|
const pbm = protobuf(require('./keys.proto'))
|
||||||
|
const exporter = require('./exporter')
|
||||||
|
|
||||||
class RsaPublicKey {
|
class RsaPublicKey {
|
||||||
constructor (key) {
|
constructor (key) {
|
||||||
this._key = key
|
this._key = key
|
||||||
@ -109,28 +111,26 @@ class RsaPrivateKey {
|
|||||||
* Exports the key into a password protected PEM format
|
* Exports the key into a password protected PEM format
|
||||||
*
|
*
|
||||||
* @param {string} password - The password to read the encrypted PEM
|
* @param {string} password - The password to read the encrypted PEM
|
||||||
* @param {string} [format] - Defaults to 'pkcs-8'.
|
* @param {string} [format=pkcs-8] - The format in which to export as
|
||||||
*/
|
*/
|
||||||
async export (password, format = 'pkcs-8') { // eslint-disable-line require-await
|
async export (password, format = 'pkcs-8') { // eslint-disable-line require-await
|
||||||
let pem = null
|
|
||||||
|
|
||||||
const buffer = new forge.util.ByteBuffer(this.marshal())
|
|
||||||
const asn1 = forge.asn1.fromDer(buffer)
|
|
||||||
const privateKey = forge.pki.privateKeyFromAsn1(asn1)
|
|
||||||
|
|
||||||
if (format === 'pkcs-8') {
|
if (format === 'pkcs-8') {
|
||||||
|
const buffer = new forge.util.ByteBuffer(this.marshal())
|
||||||
|
const asn1 = forge.asn1.fromDer(buffer)
|
||||||
|
const privateKey = forge.pki.privateKeyFromAsn1(asn1)
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
algorithm: 'aes256',
|
algorithm: 'aes256',
|
||||||
count: 10000,
|
count: 10000,
|
||||||
saltSize: 128 / 8,
|
saltSize: 128 / 8,
|
||||||
prfAlgorithm: 'sha512'
|
prfAlgorithm: 'sha512'
|
||||||
}
|
}
|
||||||
pem = forge.pki.encryptRsaPrivateKey(privateKey, password, options)
|
return forge.pki.encryptRsaPrivateKey(privateKey, password, options)
|
||||||
|
} else if (format === 'libp2p-key') {
|
||||||
|
return exporter.export(this.bytes, password)
|
||||||
} else {
|
} else {
|
||||||
throw errcode(new Error(`Unknown export format '${format}'. Must be pkcs-8`), 'ERR_INVALID_EXPORT_FORMAT')
|
throw errcode(new Error(`export format '${format}' is not supported`), 'ERR_INVALID_EXPORT_FORMAT')
|
||||||
}
|
}
|
||||||
|
|
||||||
return pem
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ const { bigIntegerToUintBase64url, base64urlToBigInteger } = require('./../util'
|
|||||||
|
|
||||||
// Convert a PKCS#1 in ASN1 DER format to a JWK key
|
// Convert a PKCS#1 in ASN1 DER format to a JWK key
|
||||||
exports.pkcs1ToJwk = function (bytes) {
|
exports.pkcs1ToJwk = function (bytes) {
|
||||||
|
bytes = Buffer.from(bytes) // convert Uint8Arrays
|
||||||
const asn1 = forge.asn1.fromDer(bytes.toString('binary'))
|
const asn1 = forge.asn1.fromDer(bytes.toString('binary'))
|
||||||
const privateKey = forge.pki.privateKeyFromAsn1(asn1)
|
const privateKey = forge.pki.privateKeyFromAsn1(asn1)
|
||||||
|
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
const multibase = require('multibase')
|
const multibase = require('multibase')
|
||||||
const sha = require('multihashing-async/src/sha')
|
const sha = require('multihashing-async/src/sha')
|
||||||
|
const errcode = require('err-code')
|
||||||
|
|
||||||
|
const exporter = require('./exporter')
|
||||||
|
|
||||||
module.exports = (keysProtobuf, randomBytes, crypto) => {
|
module.exports = (keysProtobuf, randomBytes, crypto) => {
|
||||||
crypto = crypto || require('./secp256k1')(randomBytes)
|
crypto = crypto || require('./secp256k1')(randomBytes)
|
||||||
@ -84,6 +87,21 @@ module.exports = (keysProtobuf, randomBytes, crypto) => {
|
|||||||
const hash = await this.public.hash()
|
const hash = await this.public.hash()
|
||||||
return multibase.encode('base58btc', hash).toString().slice(1)
|
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=libp2p-key] - The format in which to export as
|
||||||
|
* @returns {Promise<string>} 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function unmarshalSecp256k1PrivateKey (bytes) {
|
function unmarshalSecp256k1PrivateKey (bytes) {
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const chai = require('chai')
|
const chai = require('chai')
|
||||||
const dirtyChai = require('dirty-chai')
|
chai.use(require('dirty-chai'))
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
chai.use(dirtyChai)
|
|
||||||
|
const { Buffer } = require('buffer')
|
||||||
|
|
||||||
const crypto = require('../')
|
const crypto = require('../')
|
||||||
const webcrypto = require('../src/webcrypto')
|
const webcrypto = require('../src/webcrypto')
|
||||||
|
|
||||||
|
@ -85,6 +85,26 @@ describe('ed25519', function () {
|
|||||||
expect(id).to.be.a('string')
|
expect(id).to.be.a('string')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should export a password encrypted libp2p-key', async () => {
|
||||||
|
const key = await crypto.keys.generateKeyPair('Ed25519')
|
||||||
|
const encryptedKey = await key.export('my secret')
|
||||||
|
// Import the key
|
||||||
|
const importedKey = await crypto.keys.import(encryptedKey, 'my secret')
|
||||||
|
expect(key.equals(importedKey)).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail to import libp2p-key with wrong password', async () => {
|
||||||
|
const key = await crypto.keys.generateKeyPair('Ed25519')
|
||||||
|
const encryptedKey = await key.export('my secret', 'libp2p-key')
|
||||||
|
try {
|
||||||
|
await crypto.keys.import(encryptedKey, 'not my secret')
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.exist()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expect.fail('should have thrown')
|
||||||
|
})
|
||||||
|
|
||||||
describe('key equals', () => {
|
describe('key equals', () => {
|
||||||
it('equals itself', () => {
|
it('equals itself', () => {
|
||||||
expect(
|
expect(
|
||||||
|
@ -135,6 +135,24 @@ describe('RSA', function () {
|
|||||||
expect(key.equals(clone)).to.eql(true)
|
expect(key.equals(clone)).to.eql(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should export a password encrypted libp2p-key', async () => {
|
||||||
|
const encryptedKey = await key.export('my secret', 'libp2p-key')
|
||||||
|
// Import the key
|
||||||
|
const importedKey = await crypto.keys.import(encryptedKey, 'my secret')
|
||||||
|
expect(key.equals(importedKey)).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail to import libp2p-key with wrong password', async () => {
|
||||||
|
const encryptedKey = await key.export('my secret', 'libp2p-key')
|
||||||
|
try {
|
||||||
|
await crypto.keys.import(encryptedKey, 'not my secret')
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.exist()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expect.fail('should have thrown')
|
||||||
|
})
|
||||||
|
|
||||||
it('needs correct password', async () => {
|
it('needs correct password', async () => {
|
||||||
const pem = await key.export('another secret')
|
const pem = await key.export('another secret')
|
||||||
try {
|
try {
|
||||||
|
@ -63,6 +63,26 @@ describe('secp256k1 keys', () => {
|
|||||||
expect(id).to.be.a('string')
|
expect(id).to.be.a('string')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should export a password encrypted libp2p-key', async () => {
|
||||||
|
const key = await crypto.keys.generateKeyPair('secp256k1')
|
||||||
|
const encryptedKey = await key.export('my secret')
|
||||||
|
// Import the key
|
||||||
|
const importedKey = await crypto.keys.import(encryptedKey, 'my secret')
|
||||||
|
expect(key.equals(importedKey)).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail to import libp2p-key with wrong password', async () => {
|
||||||
|
const key = await crypto.keys.generateKeyPair('secp256k1')
|
||||||
|
const encryptedKey = await key.export('my secret', 'libp2p-key')
|
||||||
|
try {
|
||||||
|
await crypto.keys.import(encryptedKey, 'not my secret')
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.exist()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expect.fail('should have thrown')
|
||||||
|
})
|
||||||
|
|
||||||
describe('key equals', () => {
|
describe('key equals', () => {
|
||||||
it('equals itself', () => {
|
it('equals itself', () => {
|
||||||
expect(key.equals(key)).to.eql(true)
|
expect(key.equals(key)).to.eql(true)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user