feat: use webcrypto in favor of node-forge

BREAKING CHANGE: generateKeyPair is now async
This commit is contained in:
Friedel Ziegelmayer
2016-09-13 13:23:11 +02:00
parent 73a5258876
commit 08c5df5e79
32 changed files with 2728 additions and 334 deletions

7
src/crypto.js Normal file
View File

@ -0,0 +1,7 @@
'use strict'
exports.webcrypto = require('./crypto/webcrypto')()
exports.hmac = require('./crypto/hmac')
exports.ecdh = require('./crypto/ecdh')
exports.aes = require('./crypto/aes')
exports.rsa = require('./crypto/rsa')

View File

@ -1,13 +0,0 @@
enum KeyType {
RSA = 0;
}
message PublicKey {
required KeyType Type = 1;
required bytes Data = 2;
}
message PrivateKey {
required KeyType Type = 1;
required bytes Data = 2;
}

17
src/crypto.proto.js Normal file
View File

@ -0,0 +1,17 @@
'use strict'
module.exports = new Buffer(`
enum KeyType {
RSA = 0;
}
message PublicKey {
required KeyType Type = 1;
required bytes Data = 2;
}
message PrivateKey {
required KeyType Type = 1;
required bytes Data = 2;
}
`)

52
src/crypto/aes-browser.js Normal file
View File

@ -0,0 +1,52 @@
'use strict'
const nodeify = require('nodeify')
const crypto = require('./webcrypto')()
exports.create = function (key, iv, callback) {
nodeify(crypto.subtle.importKey(
'raw',
key,
{
name: 'AES-CTR'
},
false,
['encrypt', 'decrypt']
).then((key) => {
const counter = copy(iv)
return {
encrypt (data, cb) {
nodeify(crypto.subtle.encrypt(
{
name: 'AES-CTR',
counter: counter,
length: 128
},
key,
data
).then((raw) => Buffer.from(raw)), cb)
},
decrypt (data, cb) {
nodeify(crypto.subtle.decrypt(
{
name: 'AES-CTR',
counter: counter,
length: 128
},
key,
data
).then((raw) => Buffer.from(raw)), cb)
}
}
}), callback)
}
function copy (buf) {
const fresh = new Buffer(buf.length)
buf.copy(fresh)
return fresh
}

30
src/crypto/aes.js Normal file
View File

@ -0,0 +1,30 @@
'use strict'
const crypto = require('crypto')
const ciphers = {
16: 'aes-128-ctr',
32: 'aes-256-ctr'
}
exports.create = function (key, iv, callback) {
const name = ciphers[key.length]
if (!name) {
return callback(new Error('Invalid key length'))
}
const cipher = crypto.createCipheriv(name, key, iv)
const decipher = crypto.createDecipheriv(name, key, iv)
const res = {
encrypt (data, cb) {
cb(null, cipher.update(data))
},
decrypt (data, cb) {
cb(null, decipher.update(data))
}
}
callback(null, res)
}

58
src/crypto/ecdh.js Normal file
View File

@ -0,0 +1,58 @@
'use strict'
const crypto = require('./webcrypto')()
const nodeify = require('nodeify')
exports.generateEphmeralKeyPair = function (curve, callback) {
nodeify(crypto.subtle.generateKey(
{
name: 'ECDH',
namedCurve: curve
},
true,
['deriveBits']
).then((pair) => {
// forcePrivate is used for testing only
const genSharedKey = (theirPub, forcePrivate, cb) => {
if (typeof forcePrivate === 'function') {
cb = forcePrivate
forcePrivate = undefined
}
const privateKey = forcePrivate || pair.privateKey
nodeify(crypto.subtle.importKey(
'spki',
theirPub,
{
name: 'ECDH',
namedCurve: curve
},
false,
[]
).then((publicKey) => {
return crypto.subtle.deriveBits(
{
name: 'ECDH',
namedCurve: curve,
public: publicKey
},
privateKey,
256
)
}).then((bits) => {
// return p.derive(pub.getPublic()).toBuffer('be')
return Buffer.from(bits)
}), cb)
}
return crypto.subtle.exportKey(
'spki',
pair.publicKey
).then((publicKey) => {
return {
key: Buffer.from(publicKey),
genSharedKey
}
})
}), callback)
}

View File

@ -0,0 +1,38 @@
'use strict'
const nodeify = require('nodeify')
const crypto = require('./webcrypto')()
const lengths = require('./hmac-lengths')
const hashTypes = {
SHA1: 'SHA-1',
SHA256: 'SHA-256',
SHA512: 'SHA-512'
}
exports.create = function (hashType, secret, callback) {
const hash = hashTypes[hashType]
nodeify(crypto.subtle.importKey(
'raw',
secret,
{
name: 'HMAC',
hash: {name: hash}
},
false,
['sign']
).then((key) => {
return {
digest (data, cb) {
nodeify(crypto.subtle.sign(
{name: 'HMAC'},
key,
data
).then((raw) => Buffer.from(raw)), cb)
},
length: lengths[hashType]
}
}), callback)
}

View File

@ -0,0 +1,7 @@
'use strict'
module.exports = {
SHA1: 20,
SHA256: 32,
SHA512: 64
}

24
src/crypto/hmac.js Normal file
View File

@ -0,0 +1,24 @@
'use strict'
const crypto = require('crypto')
const lengths = require('./hmac-lengths')
exports.create = function (hash, secret, callback) {
const res = {
digest (data, cb) {
const hmac = genFresh()
hmac.update(data)
setImmediate(() => {
cb(null, hmac.digest())
})
},
length: lengths[hash]
}
function genFresh () {
return crypto.createHmac(hash.toLowerCase(), secret)
}
callback(null, res)
}

205
src/crypto/rsa.js Normal file
View File

@ -0,0 +1,205 @@
'use strict'
const multihashing = require('multihashing')
const nodeify = require('nodeify')
const BN = require('bn.js')
const asn1 = require('asn1.js')
const crypto = require('./webcrypto')()
const sha2256 = multihashing.createHash('sha2-256')
exports.generateKey = function (bits, callback) {
nodeify(crypto.subtle.generateKey(
{
name: 'RSASSA-PKCS1-v1_5',
modulusLength: bits,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: 'SHA-256'}
},
true,
['sign', 'verify']
)
.then(exportKey)
.then((keys) => {
return {
privateKey: keys[0],
publicKey: Buffer.from(keys[1])
}
}), callback)
}
// Takes a jwk key
exports.unmarshalPrivateKey = function (key, callback) {
const privateKey = crypto.subtle.importKey(
'jwk',
key,
{
name: 'RSASSA-PKCS1-v1_5',
hash: {name: 'SHA-256'}
},
true,
['sign']
)
nodeify(Promise.all([
privateKey,
derivePublicFromPrivate(privateKey)
]).then((keys) => {
return exportKey({
privateKey: keys[0],
publicKey: keys[1]
})
}).then((keys) => {
return {
privateKey: keys[0],
publicKey: Buffer.from(keys[1])
}
}), callback)
}
exports.getRandomValues = function (arr) {
return Buffer.from(crypto.getRandomValues(arr))
}
exports.hashAndSign = function (key, msg, callback) {
sha2256(msg, (err, digest) => {
if (err) {
return callback(err)
}
nodeify(crypto.subtle.importKey(
'jwk',
key,
{
name: 'RSASSA-PKCS1-v1_5',
hash: {name: 'SHA-256'}
},
false,
['sign']
).then((privateKey) => {
return crypto.subtle.sign(
{name: 'RSASSA-PKCS1-v1_5'},
privateKey,
Uint8Array.from(digest)
)
}).then((sig) => Buffer.from(sig)), callback)
})
}
exports.hashAndVerify = function (key, sig, msg, callback) {
sha2256(msg, (err, digest) => {
if (err) {
return callback(err)
}
nodeify(crypto.subtle.importKey(
'spki',
Uint8Array.from(key),
{
name: 'RSASSA-PKCS1-v1_5',
hash: {name: 'SHA-256'}
},
false,
['verify']
).then((publicKey) => {
return crypto.subtle.verify(
{name: 'RSASSA-PKCS1-v1_5'},
publicKey,
Uint8Array.from(sig),
Uint8Array.from(digest)
)
}), callback)
})
}
function exportKey (pair) {
return Promise.all([
crypto.subtle.exportKey('jwk', pair.privateKey),
crypto.subtle.exportKey('spki', pair.publicKey)
])
}
function derivePublicFromPrivate (privatePromise) {
return privatePromise.then((privateKey) => {
return crypto.subtle.exportKey('jwk', privateKey)
}).then((jwKey) => crypto.subtle.importKey(
'jwk',
{
kty: jwKey.kty,
n: jwKey.n,
e: jwKey.e,
alg: jwKey.alg,
kid: jwKey.kid
},
{
name: 'RSASSA-PKCS1-v1_5',
hash: {name: 'SHA-256'}
},
true,
['verify']
))
}
const RSAPrivateKey = asn1.define('RSAPrivateKey', function () {
this.seq().obj(
this.key('version').int(),
this.key('modulus').int(),
this.key('publicExponent').int(),
this.key('privateExponent').int(),
this.key('prime1').int(),
this.key('prime2').int(),
this.key('exponent1').int(),
this.key('exponent2').int(),
this.key('coefficient').int()
)
})
// Convert a PKCS#1 in ASN1 DER format to a JWK key
exports.pkcs1ToJwk = function (bytes) {
const asn1 = RSAPrivateKey.decode(bytes, 'der')
return {
kty: 'RSA',
n: toBase64(asn1.modulus),
e: toBase64(asn1.publicExponent),
d: toBase64(asn1.privateExponent),
p: toBase64(asn1.prime1),
q: toBase64(asn1.prime2),
dp: toBase64(asn1.exponent1),
dq: toBase64(asn1.exponent2),
qi: toBase64(asn1.coefficient),
alg: 'RS256',
kid: '2011-04-29'
}
}
exports.jwkToPkcs1 = function (jwk) {
return RSAPrivateKey.encode({
version: 0,
modulus: toBn(jwk.n),
publicExponent: toBn(jwk.e),
privateExponent: toBn(jwk.d),
prime1: toBn(jwk.p),
prime2: toBn(jwk.q),
exponent1: toBn(jwk.dp),
exponent2: toBn(jwk.dq),
coefficient: toBn(jwk.qi)
}, 'der')
}
// Convert a BN.js instance to a base64 encoded string without padding
// Adapted from https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#appendix-C
function toBase64 (bn) {
let s = bn.toBuffer('be').toString('base64')
return s
.replace(/(=*)$/, '') // Remove any trailing '='s
.replace(/\+/g, '-') // 62nd char of encoding
.replace(/\//g, '_') // 63rd char of encoding
}
// Convert a base64 encoded string to a BN.js instance
function toBn (str) {
return new BN(Buffer.from(str, 'base64'))
}

View File

@ -0,0 +1,13 @@
'use strict'
module.exports = function getWebCrypto () {
if (typeof window !== 'undefined') {
require('webcrypto-shim')
if (window.crypto) {
return window.crypto
}
}
throw new Error('Please use an environment with crypto support')
}

7
src/crypto/webcrypto.js Normal file
View File

@ -0,0 +1,7 @@
'use strict'
module.exports = function getWebCrypto () {
const WebCrypto = require('node-webcrypto-ossl')
const webCrypto = new WebCrypto()
return webCrypto
}

View File

@ -1,36 +1,11 @@
'use strict'
const EC = require('elliptic').ec
const curveMap = {
'P-256': 'p256',
'P-384': 'p384',
'P-521': 'p521'
}
const crypto = require('./crypto')
// Generates an ephemeral public key and returns a function that will compute
// the shared secret key.
//
// Focuses only on ECDH now, but can be made more general in the future.
module.exports = (curveName) => {
const curve = curveMap[curveName]
if (!curve) {
throw new Error('unsupported curve passed')
}
const ec = new EC(curve)
const priv = ec.genKeyPair()
// forcePrivate is used for testing only
const genSharedKey = (theirPub, forcePrivate) => {
const pub = ec.keyFromPublic(theirPub, 'hex')
const p = forcePrivate || priv
return p.derive(pub.getPublic()).toBuffer('be')
}
return {
key: new Buffer(priv.getPublic('hex'), 'hex'),
genSharedKey
}
module.exports = (curve, callback) => {
crypto.ecdh.generateEphmeralKeyPair(curve, callback)
}

View File

@ -1,24 +1,26 @@
'use strict'
const protobuf = require('protocol-buffers')
const fs = require('fs')
const path = require('path')
const pbm = protobuf(fs.readFileSync(path.join(__dirname, './crypto.proto')))
const pbm = protobuf(require('./crypto.proto'))
const c = require('./crypto')
exports.hmac = c.hmac
exports.aes = c.aes
exports.rsa = c.rsa
exports.webcrypto = c.webcrypto
exports.utils = require('./utils')
const keys = exports.keys = require('./keys')
exports.keyStretcher = require('./key-stretcher')
exports.generateEphemeralKeyPair = require('./ephemeral-keys')
// Generates a keypair of the given type and bitsize
exports.generateKeyPair = (type, bits) => {
exports.generateKeyPair = (type, bits, cb) => {
let key = keys[type.toLowerCase()]
if (!key) {
throw new Error('invalid or unsupported key type')
return cb(new Error('invalid or unsupported key type'))
}
return key.generateKeyPair(bits)
key.generateKeyPair(bits, cb)
}
// Converts a protobuf serialized public key into its
@ -43,22 +45,19 @@ exports.marshalPublicKey = (key, type) => {
throw new Error('invalid or unsupported key type')
}
return pbm.PublicKey.encode({
Type: pbm.KeyType.RSA,
Data: key.marshal()
})
return key.bytes
}
// Converts a protobuf serialized private key into its
// representative object
exports.unmarshalPrivateKey = (buf) => {
exports.unmarshalPrivateKey = (buf, callback) => {
const decoded = pbm.PrivateKey.decode(buf)
switch (decoded.Type) {
case pbm.KeyType.RSA:
return keys.rsa.unmarshalRsaPrivateKey(decoded.Data)
return keys.rsa.unmarshalRsaPrivateKey(decoded.Data, callback)
default:
throw new Error('invalid or unsupported key type')
callback(new Error('invalid or unsupported key type'))
}
}
@ -71,8 +70,5 @@ exports.marshalPrivateKey = (key, type) => {
throw new Error('invalid or unsupported key type')
}
return pbm.PrivateKey.encode({
Type: pbm.KeyType.RSA,
Data: key.marshal()
})
return key.bytes
}

View File

@ -1,7 +1,7 @@
'use strict'
const forge = require('node-forge')
const createBuffer = forge.util.createBuffer
const crypto = require('./crypto')
const whilst = require('async/whilst')
const cipherMap = {
'AES-128': {
@ -18,78 +18,91 @@ const cipherMap = {
}
}
const hashMap = {
SHA1: 'sha1',
SHA256: 'sha256',
// workaround for https://github.com/digitalbazaar/forge/issues/401
SHA512: forge.md.sha512.create()
}
// Generates a set of keys for each party by stretching the shared key.
// (myIV, theirIV, myCipherKey, theirCipherKey, myMACKey, theirMACKey)
module.exports = (cipherType, hashType, secret) => {
module.exports = (cipherType, hash, secret, callback) => {
const cipher = cipherMap[cipherType]
const hash = hashMap[hashType]
if (!cipher) {
throw new Error('unkown cipherType passed')
return callback(new Error('unkown cipherType passed'))
}
if (!hash) {
throw new Error('unkown hashType passed')
}
if (Buffer.isBuffer(secret)) {
secret = createBuffer(secret.toString('binary'))
return callback(new Error('unkown hashType passed'))
}
const cipherKeySize = cipher.keySize
const ivSize = cipher.ivSize
const hmacKeySize = 20
const seed = 'key expansion'
const seed = Buffer.from('key expansion')
const resultLength = 2 * (ivSize + cipherKeySize + hmacKeySize)
const m = forge.hmac.create()
m.start(hash, secret)
m.update(seed)
let a = m.digest().bytes()
const result = createBuffer()
let j = 0
for (; j < resultLength;) {
m.start(hash, secret)
m.update(a)
m.update(seed)
const b = createBuffer(m.digest(), 'raw')
let todo = b.length()
if (j + todo > resultLength) {
todo = resultLength - j
crypto.hmac.create(hash, secret, (err, m) => {
if (err) {
return callback(err)
}
result.putBytes(b.getBytes(todo))
m.digest(seed, (err, a) => {
if (err) {
return callback(err)
}
j += todo
let result = []
let j = 0
m.start(hash, secret)
m.update(a)
a = m.digest().bytes()
}
whilst(
() => j < resultLength,
stretch,
finish
)
const half = resultLength / 2
const r1 = createBuffer(result.getBytes(half))
const r2 = createBuffer(result.getBytes())
function stretch (cb) {
m.digest(Buffer.concat([a, seed]), (err, b) => {
if (err) {
return cb(err)
}
const createKey = (res) => ({
iv: new Buffer(res.getBytes(ivSize), 'binary'),
cipherKey: new Buffer(res.getBytes(cipherKeySize), 'binary'),
macKey: new Buffer(res.getBytes(), 'binary')
let todo = b.length
if (j + todo > resultLength) {
todo = resultLength - j
}
result.push(b)
j += todo
m.digest(a, (err, _a) => {
if (err) {
return cb(err)
}
a = _a
cb()
})
})
}
function finish (err) {
if (err) {
return callback(err)
}
const half = resultLength / 2
const resultBuffer = Buffer.concat(result)
const r1 = resultBuffer.slice(0, half)
const r2 = resultBuffer.slice(half, resultLength)
const createKey = (res) => ({
iv: res.slice(0, ivSize),
cipherKey: res.slice(ivSize, ivSize + cipherKeySize),
macKey: res.slice(ivSize + cipherKeySize)
})
callback(null, {
k1: createKey(r1),
k2: createKey(r2)
})
}
})
})
return {
k1: createKey(r1),
k2: createKey(r2)
}
}

View File

@ -1,35 +1,23 @@
'use strict'
const forge = require('node-forge')
const multihashing = require('multihashing')
const protobuf = require('protocol-buffers')
const fs = require('fs')
const path = require('path')
const utils = require('../utils')
const pki = forge.pki
const rsa = pki.rsa
const pbm = protobuf(fs.readFileSync(path.join(__dirname, '../crypto.proto')))
const crypto = require('../crypto').rsa
const pbm = protobuf(require('../crypto.proto'))
class RsaPublicKey {
constructor (k) {
this._key = k
constructor (key) {
this._key = key
}
verify (data, sig) {
const md = forge.md.sha256.create()
if (Buffer.isBuffer(data)) {
md.update(data.toString('binary'), 'binary')
} else {
md.update(data)
}
return this._key.verify(md.digest().bytes(), sig)
verify (data, sig, callback) {
ensure(callback)
crypto.hashAndVerify(this._key, sig, data, callback)
}
marshal () {
return new Buffer(forge.asn1.toDer(pki.publicKeyToAsn1(this._key)).bytes(), 'binary')
return this._key
}
get bytes () {
@ -47,34 +35,27 @@ class RsaPublicKey {
return this.bytes.equals(key.bytes)
}
hash () {
return utils.keyHash(this.bytes)
hash (callback) {
ensure(callback)
multihashing(this.bytes, 'sha2-256', callback)
}
}
class RsaPrivateKey {
constructor (privKey, pubKey) {
this._privateKey = privKey
if (pubKey) {
this._publicKey = pubKey
} else {
this._publicKey = forge.pki.setRsaPublicKey(privKey.n, privKey.e)
}
// key - Object of the jwk format
// publicKey - Buffer of the spki format
constructor (key, publicKey) {
this._key = key
this._publicKey = publicKey
}
genSecret () {
return forge.random.getBytesSync(16)
return crypto.getRandomValues(new Uint8Array(16))
}
sign (message) {
const md = forge.md.sha256.create()
if (Buffer.isBuffer(message)) {
md.update(message.toString('binary'), 'binary')
} else {
md.update(message)
}
const raw = this._privateKey.sign(md, 'RSASSA-PKCS1-V1_5')
return new Buffer(raw, 'binary')
sign (message, callback) {
ensure(callback)
crypto.hashAndSign(this._key, message, callback)
}
get public () {
@ -85,12 +66,12 @@ class RsaPrivateKey {
return new RsaPublicKey(this._publicKey)
}
decrypt (bytes) {
return this._privateKey.decrypt(bytes, 'RSAES-PKCS1-V1_5')
decrypt (msg, callback) {
crypto.decrypt(this._key, msg, callback)
}
marshal () {
return new Buffer(forge.asn1.toDer(pki.privateKeyToAsn1(this._privateKey)).bytes(), 'binary')
return crypto.jwkToPkcs1(this._key)
}
get bytes () {
@ -104,32 +85,41 @@ class RsaPrivateKey {
return this.bytes.equals(key.bytes)
}
hash () {
return utils.keyHash(this.bytes)
hash (callback) {
ensure(callback)
multihashing(this.bytes, 'sha2-256', callback)
}
}
function unmarshalRsaPrivateKey (bytes) {
if (Buffer.isBuffer(bytes)) {
bytes = forge.util.createBuffer(bytes.toString('binary'))
}
const key = pki.privateKeyFromAsn1(forge.asn1.fromDer(bytes))
function unmarshalRsaPrivateKey (bytes, callback) {
const jwk = crypto.pkcs1ToJwk(bytes)
crypto.unmarshalPrivateKey(jwk, (err, keys) => {
if (err) {
return callback(err)
}
return new RsaPrivateKey(key)
callback(null, new RsaPrivateKey(keys.privateKey, keys.publicKey))
})
}
function unmarshalRsaPublicKey (bytes) {
if (Buffer.isBuffer(bytes)) {
bytes = forge.util.createBuffer(bytes.toString('binary'))
}
const key = pki.publicKeyFromAsn1(forge.asn1.fromDer(bytes))
return new RsaPublicKey(key)
return new RsaPublicKey(bytes)
}
function generateKeyPair (bits) {
const p = rsa.generateKeyPair({bits})
return new RsaPrivateKey(p.privateKey, p.publicKey)
function generateKeyPair (bits, cb) {
crypto.generateKey(bits, (err, keys) => {
if (err) {
return cb(err)
}
cb(null, new RsaPrivateKey(keys.privateKey, keys.publicKey))
})
}
function ensure (cb) {
if (typeof cb !== 'function') {
throw new Error('callback is required')
}
}
module.exports = {

View File

@ -1,8 +0,0 @@
'use strict'
const multihashing = require('multihashing')
// Hashes a key
exports.keyHash = (bytes) => {
return multihashing(bytes, 'sha2-256')
}