Skip to content
This repository was archived by the owner on Apr 8, 2020. It is now read-only.

Commit 841aee3

Browse files
committed
feat: adds key-composer to support import and export to pem
1 parent ce22cf1 commit 841aee3

File tree

7 files changed

+177
-16
lines changed

7 files changed

+177
-16
lines changed

.travis.yml

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
language: node_js
2+
3+
cache: npm
4+
5+
stages:
6+
- check
7+
- test
8+
- cov
9+
10+
node_js:
11+
- '10'
12+
13+
os:
14+
- linux
15+
- osx
16+
- windows
17+
18+
script: npx nyc -s npm run test:node -- --bail
19+
after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov
20+
21+
jobs:
22+
include:
23+
- stage: check
24+
script:
25+
- npx aegir commitlint --travis
26+
- npx aegir dep-check
27+
- npm run lint
28+
29+
- stage: test
30+
name: chrome
31+
addons:
32+
chrome: stable
33+
script:
34+
- npx aegir test -t browser
35+
36+
- stage: test
37+
name: firefox
38+
addons:
39+
firefox: latest
40+
script:
41+
- npx aegir test -t browser -- --browsers FirefoxHeadless
42+
43+
notifications:
44+
email: false

README.md

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
# js-libp2p-crypto-secp256k1
22

3-
[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io)
4-
[![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://ipfs.io/)
5-
[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs)
6-
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
3+
[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai)
4+
[![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/)
5+
[![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p)
6+
[![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io)
7+
[![](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-crypto-secp256k1.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-crypto-secp256k1)
8+
[![](https://img.shields.io/travis/libp2p/js-libp2p-crypto-secp256k1.svg?style=flat-square)](https://travis-ci.com/libp2p/js-libp2p-crypto-secp256k1)
9+
[![Dependency Status](https://david-dm.org/libp2p/js-libp2p-crypto-secp256k1.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-crypto-secp256k1)
710
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard)
811
![](https://img.shields.io/badge/npm-%3E%3D3.0.0-orange.svg?style=flat-square)
912
![](https://img.shields.io/badge/Node.js-%3E%3D4.0.0-orange.svg?style=flat-square)

ci/Jenkinsfile

-2
This file was deleted.

package.json

+10-8
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
"scripts": {
1111
"lint": "aegir lint",
1212
"build": "aegir build",
13-
"test": "npm run test:node && npm run test:browser",
14-
"test:node": "aegir test --env node",
15-
"test:browser": "aegir test --env browser",
13+
"test": "aegir test -t node -t browser",
14+
"test:node": "aegir test -t node",
15+
"test:browser": "aegir test -t browser",
1616
"release": "aegir release",
1717
"release-minor": "aegir release --type minor",
1818
"release-major": "aegir release --type major",
@@ -27,19 +27,21 @@
2727
],
2828
"license": "MIT",
2929
"dependencies": {
30-
"async": "^2.6.1",
30+
"async": "^2.6.2",
3131
"bs58": "^4.0.1",
32-
"multihashing-async": "~0.5.1",
32+
"crypto-key-composer": "~0.1.0",
33+
"multihashing-async": "~0.6.0",
3334
"nodeify": "^1.0.1",
3435
"safe-buffer": "^5.1.2",
35-
"secp256k1": "^3.6.1"
36+
"secp256k1": "^3.6.2"
3637
},
3738
"devDependencies": {
38-
"aegir": "^18.0.3",
39+
"aegir": "^18.2.2",
3940
"benchmark": "^2.1.4",
4041
"chai": "^4.2.0",
42+
"chai-string": "^1.5.0",
4143
"dirty-chai": "^2.0.1",
42-
"libp2p-crypto": "~0.16.0"
44+
"libp2p-crypto": "~0.16.1"
4345
},
4446
"engines": {
4547
"node": ">=6.0.0",

src/crypto.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ module.exports = (randomBytes) => {
1414

1515
let privateKey
1616
do {
17-
privateKey = randomBytes(32)
17+
privateKey = randomBytes(privateKeyLength)
1818
} while (!secp256k1.privateKeyVerify(privateKey))
1919

2020
done(null, privateKey)

src/index.js

+74-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const bs58 = require('bs58')
44
const multihashing = require('multihashing-async')
5+
const { composePrivateKey, decomposePrivateKey } = require('crypto-key-composer')
56

67
module.exports = (keysProtobuf, randomBytes, crypto) => {
78
crypto = crypto || require('./crypto')(randomBytes)
@@ -93,6 +94,63 @@ module.exports = (keysProtobuf, randomBytes, crypto) => {
9394
callback(null, bs58.encode(hash))
9495
})
9596
}
97+
98+
/**
99+
* Exports the key into a password protected PEM format
100+
*
101+
* @param {string} [format] - Defaults to 'pkcs-8'.
102+
* @param {string} password - The password to read the encrypted PEM
103+
* @param {function(Error, KeyInfo)} callback
104+
* @returns {undefined}
105+
*/
106+
export (format, password, callback) {
107+
if (typeof password === 'function') {
108+
callback = password
109+
password = format
110+
format = 'pkcs-8'
111+
}
112+
113+
ensure(callback)
114+
115+
let err = null
116+
let pem = null
117+
118+
const decompressedPublicKey = typedArrayToUint8Array(crypto.decompressPublicKey(this.public._key))
119+
try {
120+
if (format === 'pkcs-8') {
121+
pem = composePrivateKey({
122+
format: 'pkcs8-pem',
123+
keyAlgorithm: {
124+
id: 'ec-public-key',
125+
namedCurve: 'secp256k1'
126+
},
127+
keyData: {
128+
d: typedArrayToUint8Array(this.marshal()),
129+
// The public key concatenates the x and y values and adds an initial byte
130+
x: decompressedPublicKey.slice(1, 33),
131+
y: decompressedPublicKey.slice(33, 65)
132+
},
133+
encryptionAlgorithm: {
134+
keyDerivationFunc: {
135+
id: 'pbkdf2',
136+
iterationCount: 10000, // The number of iterations
137+
keyLength: 32, // Automatic, based on the `encryptionScheme`
138+
prf: 'hmac-with-sha512' // The pseudo-random function
139+
},
140+
encryptionScheme: {
141+
id: 'aes256-cbc'
142+
}
143+
}
144+
}, { password })
145+
} else {
146+
err = new Error(`Unknown export format '${format}'`)
147+
}
148+
} catch (_err) {
149+
err = _err
150+
}
151+
152+
callback(err, pem)
153+
}
96154
}
97155

98156
function unmarshalSecp256k1PrivateKey (bytes, callback) {
@@ -122,17 +180,32 @@ module.exports = (keysProtobuf, randomBytes, crypto) => {
122180
})
123181
}
124182

183+
function importPEM (pem, password, callback) {
184+
let privkey
185+
try {
186+
const decomposedPrivateKey = decomposePrivateKey(pem, { password })
187+
privkey = new Secp256k1PrivateKey(Buffer.from(decomposedPrivateKey.keyData.d))
188+
} catch (err) { return callback(err) }
189+
190+
callback(null, privkey)
191+
}
192+
125193
function ensure (callback) {
126194
if (typeof callback !== 'function') {
127195
throw new Error('callback is required')
128196
}
129197
}
130198

199+
function typedArrayToUint8Array (typedArray) {
200+
return new Uint8Array(typedArray.buffer.slice(typedArray.byteOffset, typedArray.byteOffset + typedArray.byteLength))
201+
}
202+
131203
return {
132204
Secp256k1PublicKey,
133205
Secp256k1PrivateKey,
134206
unmarshalSecp256k1PrivateKey,
135207
unmarshalSecp256k1PublicKey,
136-
generateKeyPair
208+
generateKeyPair,
209+
import: importPEM
137210
}
138211
}

test/secp256k1.spec.js

+41
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const chai = require('chai')
55
const dirtyChai = require('dirty-chai')
66
const expect = chai.expect
77
chai.use(dirtyChai)
8+
chai.use(require('chai-string'))
89

910
const Buffer = require('safe-buffer').Buffer
1011

@@ -139,6 +140,46 @@ describe('secp256k1 keys', () => {
139140
})
140141
})
141142
})
143+
144+
/* eslint-disable */
145+
describe('import and export', () => {
146+
it('password protected PKCS #8', (done) => {
147+
key.export('pkcs-8', 'my secret', (err, pem) => {
148+
expect(err).to.not.exist()
149+
expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----')
150+
secp256k1.import(pem, 'my secret', (err, clone) => {
151+
expect(err).to.not.exist()
152+
expect(clone).to.exist()
153+
expect(key.equals(clone)).to.eql(true)
154+
done()
155+
})
156+
})
157+
})
158+
159+
it('defaults to PKCS #8', (done) => {
160+
key.export('another secret', (err, pem) => {
161+
expect(err).to.not.exist()
162+
expect(pem).to.startsWith('-----BEGIN ENCRYPTED PRIVATE KEY-----')
163+
secp256k1.import(pem, 'another secret', (err, clone) => {
164+
expect(err).to.not.exist()
165+
expect(clone).to.exist()
166+
expect(key.equals(clone)).to.eql(true)
167+
done()
168+
})
169+
})
170+
})
171+
172+
it('needs correct password', (done) => {
173+
key.export('another secret', (err, pem) => {
174+
expect(err).to.not.exist()
175+
secp256k1.import(pem, 'not the secret', (err, clone) => {
176+
expect(err).to.exist()
177+
done()
178+
})
179+
})
180+
})
181+
})
182+
/* eslint-enable */
142183
})
143184

144185
describe('key generation error', () => {

0 commit comments

Comments
 (0)