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

Commit 7273739

Browse files
authored
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
1 parent 609297b commit 7273739

16 files changed

+415
-46
lines changed

README.md

+13-4
Original file line numberDiff line numberDiff line change
@@ -262,14 +262,23 @@ Returns `Promise<RsaPrivateKey|Ed25519PrivateKey|Secp256k1PrivateKey>`
262262

263263
Converts a protobuf serialized private key into its representative object.
264264

265-
### `crypto.keys.import(pem, password)`
265+
### `crypto.keys.import(encryptedKey, password)`
266266

267-
- `pem: string`
267+
- `encryptedKey: string`
268268
- `password: string`
269269

270-
Returns `Promise<RsaPrivateKey>`
270+
Returns `Promise<PrivateKey>`
271271

272-
Converts a PEM password protected private key into its representative object.
272+
Converts an exported private key into its representative object. Supported formats are 'pem' (RSA only) and 'libp2p-key'.
273+
274+
### `privateKey.export(password, format)`
275+
276+
- `password: string`
277+
- `format: string` the format to export to: 'pem' (rsa only), 'libp2p-key'
278+
279+
Returns `string`
280+
281+
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).
273282

274283
### `crypto.randomBytes(number)`
275284

package.json

+10-8
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
"types": "src/index.d.ts",
77
"leadMaintainer": "Jacob Heun <[email protected]>",
88
"browser": {
9+
"./src/aes/ciphers.js": "./src/aes/ciphers-browser.js",
10+
"./src/ciphers/aes-gcm.js": "./src/ciphers/aes-gcm.browser.js",
911
"./src/hmac/index.js": "./src/hmac/index-browser.js",
1012
"./src/keys/ecdh.js": "./src/keys/ecdh-browser.js",
11-
"./src/aes/ciphers.js": "./src/aes/ciphers-browser.js",
1213
"./src/keys/rsa.js": "./src/keys/rsa-browser.js"
1314
},
1415
"files": [
@@ -43,21 +44,22 @@
4344
"is-typedarray": "^1.0.0",
4445
"iso-random-stream": "^1.1.0",
4546
"keypair": "^1.0.1",
46-
"multibase": "^0.7.0",
47+
"multibase": "^1.0.1",
48+
"multicodec": "^1.0.4",
4749
"multihashing-async": "^0.8.1",
4850
"node-forge": "^0.9.1",
4951
"pem-jwk": "^2.0.0",
50-
"protons": "^1.0.1",
52+
"protons": "^1.2.1",
5153
"secp256k1": "^4.0.0",
52-
"ursa-optional": "~0.10.1"
54+
"uint8arrays": "^1.0.0",
55+
"ursa-optional": "^0.10.1"
5356
},
5457
"devDependencies": {
55-
"@types/chai": "^4.2.11",
58+
"@types/chai": "^4.2.12",
5659
"@types/chai-string": "^1.4.2",
5760
"@types/dirty-chai": "^2.0.2",
58-
"@types/mocha": "^7.0.1",
59-
"@types/sinon": "^9.0.0",
60-
"aegir": "^22.0.0",
61+
"@types/mocha": "^8.0.1",
62+
"aegir": "^25.0.0",
6163
"benchmark": "^2.1.4",
6264
"chai": "^4.2.0",
6365
"chai-string": "^1.5.0",

src/ciphers/aes-gcm.browser.js

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
'use strict'
2+
3+
const concat = require('uint8arrays/concat')
4+
const fromString = require('uint8arrays/from-string')
5+
6+
const webcrypto = require('../webcrypto')
7+
8+
// Based off of code from https://github.com/luke-park/SecureCompatibleEncryptionExamples
9+
10+
/**
11+
*
12+
* @param {object} [options]
13+
* @param {string} [options.algorithm=AES-GCM]
14+
* @param {Number} [options.nonceLength=12]
15+
* @param {Number} [options.keyLength=16]
16+
* @param {string} [options.digest=sha256]
17+
* @param {Number} [options.saltLength=16]
18+
* @param {Number} [options.iterations=32767]
19+
* @returns {*}
20+
*/
21+
function create ({
22+
algorithm = 'AES-GCM',
23+
nonceLength = 12,
24+
keyLength = 16,
25+
digest = 'SHA-256',
26+
saltLength = 16,
27+
iterations = 32767
28+
} = {}) {
29+
const crypto = webcrypto.get()
30+
keyLength *= 8 // Browser crypto uses bits instead of bytes
31+
32+
/**
33+
* Uses the provided password to derive a pbkdf2 key. The key
34+
* will then be used to encrypt the data.
35+
*
36+
* @param {Uint8Array} data The data to decrypt
37+
* @param {string} password A plain password
38+
* @returns {Promise<Uint8Array>}
39+
*/
40+
async function encrypt (data, password) { // eslint-disable-line require-await
41+
const salt = crypto.getRandomValues(new Uint8Array(saltLength))
42+
const nonce = crypto.getRandomValues(new Uint8Array(nonceLength))
43+
const aesGcm = { name: algorithm, iv: nonce }
44+
45+
// Derive a key using PBKDF2.
46+
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
47+
const rawKey = await crypto.subtle.importKey('raw', fromString(password), { name: 'PBKDF2' }, false, ['deriveKey', 'deriveBits'])
48+
const cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['encrypt'])
49+
50+
// Encrypt the string.
51+
const ciphertext = await crypto.subtle.encrypt(aesGcm, cryptoKey, data)
52+
return concat([salt, aesGcm.iv, new Uint8Array(ciphertext)])
53+
}
54+
55+
/**
56+
* Uses the provided password to derive a pbkdf2 key. The key
57+
* will then be used to decrypt the data. The options used to create
58+
* this decryption cipher must be the same as those used to create
59+
* the encryption cipher.
60+
*
61+
* @param {Uint8Array} data The data to decrypt
62+
* @param {string} password A plain password
63+
* @returns {Promise<Uint8Array>}
64+
*/
65+
async function decrypt (data, password) {
66+
const salt = data.slice(0, saltLength)
67+
const nonce = data.slice(saltLength, saltLength + nonceLength)
68+
const ciphertext = data.slice(saltLength + nonceLength)
69+
const aesGcm = { name: algorithm, iv: nonce }
70+
71+
// Derive the key using PBKDF2.
72+
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
73+
const rawKey = await crypto.subtle.importKey('raw', fromString(password), { name: 'PBKDF2' }, false, ['deriveKey', 'deriveBits'])
74+
const cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['decrypt'])
75+
76+
// Decrypt the string.
77+
const plaintext = await crypto.subtle.decrypt(aesGcm, cryptoKey, ciphertext)
78+
return new Uint8Array(plaintext)
79+
}
80+
81+
return {
82+
encrypt,
83+
decrypt
84+
}
85+
}
86+
87+
module.exports = {
88+
create
89+
}

src/ciphers/aes-gcm.js

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
'use strict'
2+
3+
const crypto = require('crypto')
4+
5+
// Based off of code from https://github.com/luke-park/SecureCompatibleEncryptionExamples
6+
7+
/**
8+
*
9+
* @param {object} [options]
10+
* @param {Number} [options.algorithmTagLength=16]
11+
* @param {Number} [options.nonceLength=12]
12+
* @param {Number} [options.keyLength=16]
13+
* @param {string} [options.digest=sha256]
14+
* @param {Number} [options.saltLength=16]
15+
* @param {Number} [options.iterations=32767]
16+
* @returns {*}
17+
*/
18+
function create ({
19+
algorithmTagLength = 16,
20+
nonceLength = 12,
21+
keyLength = 16,
22+
digest = 'sha256',
23+
saltLength = 16,
24+
iterations = 32767
25+
} = {}) {
26+
const algorithm = 'aes-128-gcm'
27+
/**
28+
*
29+
* @private
30+
* @param {Buffer} data
31+
* @param {Buffer} key
32+
* @returns {Promise<Buffer>}
33+
*/
34+
async function encryptWithKey (data, key) { // eslint-disable-line require-await
35+
const nonce = crypto.randomBytes(nonceLength)
36+
37+
// Create the cipher instance.
38+
const cipher = crypto.createCipheriv(algorithm, key, nonce)
39+
40+
// Encrypt and prepend nonce.
41+
const ciphertext = Buffer.concat([cipher.update(data), cipher.final()])
42+
43+
return Buffer.concat([nonce, ciphertext, cipher.getAuthTag()])
44+
}
45+
46+
/**
47+
* Uses the provided password to derive a pbkdf2 key. The key
48+
* will then be used to encrypt the data.
49+
*
50+
* @param {Buffer} data The data to decrypt
51+
* @param {string|Buffer} password A plain password
52+
* @returns {Promise<Buffer>}
53+
*/
54+
async function encrypt (data, password) { // eslint-disable-line require-await
55+
// Generate a 128-bit salt using a CSPRNG.
56+
const salt = crypto.randomBytes(saltLength)
57+
58+
// Derive a key using PBKDF2.
59+
const key = crypto.pbkdf2Sync(Buffer.from(password), salt, iterations, keyLength, digest)
60+
61+
// Encrypt and prepend salt.
62+
return Buffer.concat([salt, await encryptWithKey(Buffer.from(data), key)])
63+
}
64+
65+
/**
66+
* Decrypts the given cipher text with the provided key. The `key` should
67+
* be a cryptographically safe key and not a plaintext password. To use
68+
* a plaintext password, use `decrypt`. The options used to create
69+
* this decryption cipher must be the same as those used to create
70+
* the encryption cipher.
71+
*
72+
* @private
73+
* @param {Buffer} ciphertextAndNonce The data to decrypt
74+
* @param {Buffer} key
75+
* @returns {Promise<Buffer>}
76+
*/
77+
async function decryptWithKey (ciphertextAndNonce, key) { // eslint-disable-line require-await
78+
// Create buffers of nonce, ciphertext and tag.
79+
const nonce = ciphertextAndNonce.slice(0, nonceLength)
80+
const ciphertext = ciphertextAndNonce.slice(nonceLength, ciphertextAndNonce.length - algorithmTagLength)
81+
const tag = ciphertextAndNonce.slice(ciphertext.length + nonceLength)
82+
83+
// Create the cipher instance.
84+
const cipher = crypto.createDecipheriv(algorithm, key, nonce)
85+
86+
// Decrypt and return result.
87+
cipher.setAuthTag(tag)
88+
return Buffer.concat([cipher.update(ciphertext), cipher.final()])
89+
}
90+
91+
/**
92+
* Uses the provided password to derive a pbkdf2 key. The key
93+
* will then be used to decrypt the data. The options used to create
94+
* this decryption cipher must be the same as those used to create
95+
* the encryption cipher.
96+
*
97+
* @param {Buffer} data The data to decrypt
98+
* @param {string|Buffer} password A plain password
99+
*/
100+
async function decrypt (data, password) { // eslint-disable-line require-await
101+
// Create buffers of salt and ciphertextAndNonce.
102+
const salt = data.slice(0, saltLength)
103+
const ciphertextAndNonce = data.slice(saltLength)
104+
105+
// Derive the key using PBKDF2.
106+
const key = crypto.pbkdf2Sync(Buffer.from(password), salt, iterations, keyLength, digest)
107+
108+
// Decrypt and return result.
109+
return decryptWithKey(ciphertextAndNonce, key)
110+
}
111+
112+
return {
113+
encrypt,
114+
decrypt
115+
}
116+
}
117+
118+
module.exports = {
119+
create
120+
}

src/index.d.ts

+12-17
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ export interface PrivateKey {
9494
* of the PKCS SubjectPublicKeyInfo.
9595
*/
9696
id(): Promise<string>;
97+
/**
98+
* Exports the password protected key in the format specified.
99+
*/
100+
export(password: string, format?: "pkcs-8" | string): Promise<string>;
97101
}
98102

99103
export interface Keystretcher {
@@ -132,9 +136,6 @@ export namespace keys {
132136
hash(): Promise<Buffer>;
133137
}
134138

135-
// Type alias for export method
136-
export type KeyInfo = any;
137-
138139
class RsaPrivateKey implements PrivateKey {
139140
constructor(key: any, publicKey: Buffer);
140141
readonly public: RsaPublicKey;
@@ -146,13 +147,7 @@ export namespace keys {
146147
equals(key: PrivateKey): boolean;
147148
hash(): Promise<Buffer>;
148149
id(): Promise<string>;
149-
/**
150-
* Exports the key into a password protected PEM format
151-
*
152-
* @param password The password to read the encrypted PEM
153-
* @param format Defaults to 'pkcs-8'.
154-
*/
155-
export(password: string, format?: "pkcs-8" | string): KeyInfo;
150+
export(password: string, format?: string): Promise<string>;
156151
}
157152
function unmarshalRsaPublicKey(buf: Buffer): RsaPublicKey;
158153
function unmarshalRsaPrivateKey(buf: Buffer): Promise<RsaPrivateKey>;
@@ -180,6 +175,7 @@ export namespace keys {
180175
equals(key: PrivateKey): boolean;
181176
hash(): Promise<Buffer>;
182177
id(): Promise<string>;
178+
export(password: string, format?: string): Promise<string>;
183179
}
184180

185181
function unmarshalEd25519PrivateKey(
@@ -212,6 +208,7 @@ export namespace keys {
212208
equals(key: PrivateKey): boolean;
213209
hash(): Promise<Buffer>;
214210
id(): Promise<string>;
211+
export(password: string, format?: string): Promise<string>;
215212
}
216213

217214
function unmarshalSecp256k1PrivateKey(
@@ -234,16 +231,14 @@ export namespace keys {
234231
bits: number
235232
): Promise<PrivateKey>;
236233
export function generateKeyPair(
237-
type: "Ed25519",
238-
bits: number
234+
type: "Ed25519"
239235
): Promise<keys.supportedKeys.ed25519.Ed25519PrivateKey>;
240-
export function generateKeyPair(
236+
export function generateKeyPair(
241237
type: "RSA",
242238
bits: number
243239
): Promise<keys.supportedKeys.rsa.RsaPrivateKey>;
244-
export function generateKeyPair(
245-
type: "secp256k1",
246-
bits: number
240+
export function generateKeyPair(
241+
type: "secp256k1"
247242
): Promise<keys.supportedKeys.secp256k1.Secp256k1PrivateKey>;
248243

249244
/**
@@ -318,7 +313,7 @@ export namespace keys {
318313
* @param pem Password protected private key in PEM format.
319314
* @param password The password used to protect the key.
320315
*/
321-
function _import(pem: string, password: string): Promise<supportedKeys.rsa.RsaPrivateKey>;
316+
function _import(pem: string, password: string, format?: string): Promise<supportedKeys.rsa.RsaPrivateKey>;
322317
export { _import as import };
323318
}
324319

src/keys/ed25519-class.js

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const errcode = require('err-code')
88

99
const crypto = require('./ed25519')
1010
const pbm = protobuf(require('./keys.proto'))
11+
const exporter = require('./exporter')
1112

1213
class Ed25519PublicKey {
1314
constructor (key) {
@@ -86,6 +87,21 @@ class Ed25519PrivateKey {
8687
const hash = await this.public.hash()
8788
return multibase.encode('base58btc', hash).toString().slice(1)
8889
}
90+
91+
/**
92+
* Exports the key into a password protected `format`
93+
*
94+
* @param {string} password - The password to encrypt the key
95+
* @param {string} [format=libp2p-key] - The format in which to export as
96+
* @returns {Promise<Buffer>} The encrypted private key
97+
*/
98+
async export (password, format = 'libp2p-key') { // eslint-disable-line require-await
99+
if (format === 'libp2p-key') {
100+
return exporter.export(this.bytes, password)
101+
} else {
102+
throw errcode(new Error(`export format '${format}' is not supported`), 'ERR_INVALID_EXPORT_FORMAT')
103+
}
104+
}
89105
}
90106

91107
function unmarshalEd25519PrivateKey (bytes) {

0 commit comments

Comments
 (0)