Skip to content
This repository has been archived by the owner on Jul 21, 2023. It is now read-only.

feat: add exporting/importing of non rsa keys in libp2p-key format #179

Merged
merged 10 commits into from
Aug 5, 2020
Merged
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,14 +262,23 @@ Returns `Promise<RsaPrivateKey|Ed25519PrivateKey|Secp256k1PrivateKey>`

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`

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)`

Expand Down
18 changes: 10 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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",
Expand Down
89 changes: 89 additions & 0 deletions src/ciphers/aes-gcm.browser.js
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} [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 changes: 120 additions & 0 deletions src/ciphers/aes-gcm.js
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} [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 changes: 12 additions & 17 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ export interface PrivateKey {
* of the PKCS SubjectPublicKeyInfo.
*/
id(): Promise<string>;
/**
* Exports the password protected key in the format specified.
*/
export(password: string, format?: "pkcs-8" | string): Promise<string>;
}

export interface Keystretcher {
Expand Down Expand Up @@ -132,9 +136,6 @@ export namespace keys {
hash(): Promise<Buffer>;
}

// Type alias for export method
export type KeyInfo = any;

class RsaPrivateKey implements PrivateKey {
constructor(key: any, publicKey: Buffer);
readonly public: RsaPublicKey;
Expand All @@ -146,13 +147,7 @@ export namespace keys {
equals(key: PrivateKey): boolean;
hash(): Promise<Buffer>;
id(): 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;
export(password: string, format?: string): Promise<string>;
}
function unmarshalRsaPublicKey(buf: Buffer): RsaPublicKey;
function unmarshalRsaPrivateKey(buf: Buffer): Promise<RsaPrivateKey>;
Expand Down Expand Up @@ -180,6 +175,7 @@ export namespace keys {
equals(key: PrivateKey): boolean;
hash(): Promise<Buffer>;
id(): Promise<string>;
export(password: string, format?: string): Promise<string>;
}

function unmarshalEd25519PrivateKey(
Expand Down Expand Up @@ -212,6 +208,7 @@ export namespace keys {
equals(key: PrivateKey): boolean;
hash(): Promise<Buffer>;
id(): Promise<string>;
export(password: string, format?: string): Promise<string>;
}

function unmarshalSecp256k1PrivateKey(
Expand All @@ -234,16 +231,14 @@ export namespace keys {
bits: number
): Promise<PrivateKey>;
export function generateKeyPair(
type: "Ed25519",
bits: number
type: "Ed25519"
): Promise<keys.supportedKeys.ed25519.Ed25519PrivateKey>;
export function generateKeyPair(
export function generateKeyPair(
type: "RSA",
bits: number
): Promise<keys.supportedKeys.rsa.RsaPrivateKey>;
export function generateKeyPair(
type: "secp256k1",
bits: number
export function generateKeyPair(
type: "secp256k1"
): Promise<keys.supportedKeys.secp256k1.Secp256k1PrivateKey>;

/**
Expand Down Expand Up @@ -318,7 +313,7 @@ export namespace keys {
* @param pem Password protected private key in PEM format.
* @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 };
}

Expand Down
16 changes: 16 additions & 0 deletions src/keys/ed25519-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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=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')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a notice for this in the README

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It only has the import

}
}
}

function unmarshalEd25519PrivateKey (bytes) {
Expand Down
Loading