feat(rsa): add fallback pure js fallback for webcrypto-ossl

This commit is contained in:
Friedel Ziegelmayer
2016-11-29 17:54:41 +01:00
parent 6d15450438
commit 148d16ab25
7 changed files with 326 additions and 230 deletions

View File

@ -8,13 +8,15 @@
"./src/crypto/webcrypto.js": "./src/crypto/webcrypto-browser.js", "./src/crypto/webcrypto.js": "./src/crypto/webcrypto-browser.js",
"./src/crypto/hmac.js": "./src/crypto/hmac-browser.js", "./src/crypto/hmac.js": "./src/crypto/hmac-browser.js",
"./src/crypto/ecdh.js": "./src/crypto/ecdh-browser.js", "./src/crypto/ecdh.js": "./src/crypto/ecdh-browser.js",
"./src/crypto/ciphers.js": "./src/crypto/ciphers-browser.js" "./src/crypto/ciphers.js": "./src/crypto/ciphers-browser.js",
"./src/crypto/rsa.js": "./src/crypto/rsa-browser.js"
}, },
"scripts": { "scripts": {
"lint": "aegir-lint", "lint": "aegir-lint",
"build": "aegir-build", "build": "aegir-build",
"test": "aegir-test", "test": "npm run test:node && npm run test:no-webcrypto && npm run test:browser",
"test:node": "aegir-test --env node", "test:node": "aegir-test --env node",
"test:no-webcrypto": "NO_WEBCRYPTO=true aegir-test --env node",
"test:browser": "aegir-test --env browser", "test:browser": "aegir-test --env browser",
"release": "aegir-release", "release": "aegir-release",
"release-minor": "aegir-release --type minor", "release-minor": "aegir-release --type minor",
@ -34,10 +36,12 @@
"asn1.js": "^4.8.1", "asn1.js": "^4.8.1",
"async": "^2.1.2", "async": "^2.1.2",
"browserify-aes": "^1.0.6", "browserify-aes": "^1.0.6",
"keypair": "^1.0.0",
"multihashing-async": "^0.2.0", "multihashing-async": "^0.2.0",
"node-webcrypto-ossl": "^1.0.13",
"nodeify": "^1.0.0", "nodeify": "^1.0.0",
"pem-jwk": "^1.5.1",
"protocol-buffers": "^3.2.1", "protocol-buffers": "^3.2.1",
"rsa-pem-to-jwk": "^1.1.3",
"webcrypto-shim": "github:dignifiedquire/webcrypto-shim#master" "webcrypto-shim": "github:dignifiedquire/webcrypto-shim#master"
}, },
"devDependencies": { "devDependencies": {
@ -46,6 +50,9 @@
"chai": "^3.5.0", "chai": "^3.5.0",
"pre-commit": "^1.1.3" "pre-commit": "^1.1.3"
}, },
"optionalDependencies": {
"node-webcrypto-ossl": "^1.0.13"
},
"pre-commit": [ "pre-commit": [
"lint", "lint",
"test" "test"

View File

@ -97,7 +97,7 @@ function marshalPublicKey (jwk) {
const byteLen = curveLengths[jwk.crv] const byteLen = curveLengths[jwk.crv]
return Buffer.concat([ return Buffer.concat([
Buffer([4]), // uncompressed point new Buffer([4]), // uncompressed point
toBn(jwk.x).toBuffer('be', byteLen), toBn(jwk.x).toBuffer('be', byteLen),
toBn(jwk.y).toBuffer('be', byteLen) toBn(jwk.y).toBuffer('be', byteLen)
], 1 + byteLen * 2) ], 1 + byteLen * 2)
@ -107,7 +107,7 @@ function marshalPublicKey (jwk) {
function unmarshalPublicKey (curve, key) { function unmarshalPublicKey (curve, key) {
const byteLen = curveLengths[curve] const byteLen = curveLengths[curve]
if (!key.slice(0, 1).equals(Buffer([4]))) { if (!key.slice(0, 1).equals(new Buffer([4]))) {
throw new Error('Invalid key format') throw new Error('Invalid key format')
} }
const x = new BN(key.slice(1, byteLen + 1)) const x = new BN(key.slice(1, byteLen + 1))

119
src/crypto/rsa-browser.js Normal file
View File

@ -0,0 +1,119 @@
'use strict'
const nodeify = require('nodeify')
const crypto = require('./webcrypto')()
exports.utils = require('./rsa-utils')
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) => ({
privateKey: keys[0],
publicKey: 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(key)
]).then((keys) => exportKey({
privateKey: keys[0],
publicKey: keys[1]
})).then((keys) => ({
privateKey: keys[0],
publicKey: keys[1]
})), callback)
}
exports.getRandomValues = function (arr) {
return Buffer.from(crypto.getRandomValues(arr))
}
exports.hashAndSign = function (key, msg, callback) {
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(msg)
)
}).then((sig) => Buffer.from(sig)), callback)
}
exports.hashAndVerify = function (key, sig, msg, callback) {
nodeify(crypto.subtle.importKey(
'jwk',
key,
{
name: 'RSASSA-PKCS1-v1_5',
hash: {name: 'SHA-256'}
},
false,
['verify']
).then((publicKey) => {
return crypto.subtle.verify(
{name: 'RSASSA-PKCS1-v1_5'},
publicKey,
sig,
msg
)
}), callback)
}
function exportKey (pair) {
return Promise.all([
crypto.subtle.exportKey('jwk', pair.privateKey),
crypto.subtle.exportKey('jwk', pair.publicKey)
])
}
function derivePublicFromPrivate (jwKey) {
return 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']
)
}

114
src/crypto/rsa-utils.js Normal file
View File

@ -0,0 +1,114 @@
'use strict'
const asn1 = require('asn1.js')
const util = require('./util')
const toBase64 = util.toBase64
const toBn = util.toBn
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()
)
})
const AlgorithmIdentifier = asn1.define('AlgorithmIdentifier', function () {
this.seq().obj(
this.key('algorithm').objid({
'1.2.840.113549.1.1.1': 'rsa'
}),
this.key('none').optional().null_(),
this.key('curve').optional().objid(),
this.key('params').optional().seq().obj(
this.key('p').int(),
this.key('q').int(),
this.key('g').int()
)
)
})
const PublicKey = asn1.define('RSAPublicKey', function () {
this.seq().obj(
this.key('algorithm').use(AlgorithmIdentifier),
this.key('subjectPublicKey').bitstr()
)
})
const RSAPublicKey = asn1.define('RSAPublicKey', function () {
this.seq().obj(
this.key('modulus').int(),
this.key('publicExponent').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'
}
}
// Convert a JWK key into PKCS#1 in ASN1 DER format
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 PKCIX in ASN1 DER format to a JWK key
exports.pkixToJwk = function (bytes) {
const ndata = PublicKey.decode(bytes, 'der')
const asn1 = RSAPublicKey.decode(ndata.subjectPublicKey.data, 'der')
return {
kty: 'RSA',
n: toBase64(asn1.modulus),
e: toBase64(asn1.publicExponent),
alg: 'RS256',
kid: '2011-04-29'
}
}
// Convert a JWK key to PKCIX in ASN1 DER format
exports.jwkToPkix = function (jwk) {
return PublicKey.encode({
algorithm: {
algorithm: 'rsa',
none: null
},
subjectPublicKey: {
data: RSAPublicKey.encode({
modulus: toBn(jwk.n),
publicExponent: toBn(jwk.e)
}, 'der')
}
}, 'der')
}

View File

@ -1,228 +1,80 @@
'use strict' 'use strict'
const nodeify = require('nodeify') // Node.js land
const asn1 = require('asn1.js') // First we look if node-webrypto-ossl is available
// otherwise we fall back to using keypair + node core
const util = require('./util') let webcrypto
const toBase64 = util.toBase64 try {
const toBn = util.toBn webcrypto = require('node-webcrypto-ossl')
const crypto = require('./webcrypto')() } catch (err) {
// not available, use the code below
}
if (webcrypto && !process.env.NO_WEBCRYPTO) {
module.exports = require('./rsa-browser')
} else {
const crypto = require('crypto')
const keypair = require('keypair')
const setImmediate = require('async/setImmediate')
const pemToJwk = require('pem-jwk').pem2jwk
const jwkToPem = require('pem-jwk').jwk2pem
exports.utils = require('./rsa-utils')
exports.generateKey = function (bits, callback) { exports.generateKey = function (bits, callback) {
nodeify(crypto.subtle.generateKey( const done = (err, res) => setImmediate(() => {
{ callback(err, res)
name: 'RSASSA-PKCS1-v1_5', })
modulusLength: bits,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), let key
hash: {name: 'SHA-256'} try {
}, key = keypair({
true, bits: bits
['sign', 'verify'] })
) } catch (err) {
.then(exportKey) done(err)
.then((keys) => ({ return
privateKey: keys[0], }
publicKey: keys[1]
})), callback) done(null, {
privateKey: pemToJwk(key.private),
publicKey: pemToJwk(key.public)
})
} }
// Takes a jwk key // Takes a jwk key
exports.unmarshalPrivateKey = function (key, callback) { exports.unmarshalPrivateKey = function (key, callback) {
const privateKey = crypto.subtle.importKey( callback(null, {
'jwk', privateKey: key,
key, publicKey: {
{ kty: key.kty,
name: 'RSASSA-PKCS1-v1_5', n: key.n,
hash: {name: 'SHA-256'} e: key.e
}, }
true, })
['sign']
)
nodeify(Promise.all([
privateKey,
derivePublicFromPrivate(key)
]).then((keys) => exportKey({
privateKey: keys[0],
publicKey: keys[1]
})).then((keys) => ({
privateKey: keys[0],
publicKey: keys[1]
})), callback)
} }
exports.getRandomValues = function (arr) { exports.getRandomValues = function (arr) {
return Buffer.from(crypto.getRandomValues(arr)) return crypto.randomBytes(arr.length)
} }
exports.hashAndSign = function (key, msg, callback) { exports.hashAndSign = function (key, msg, callback) {
nodeify(crypto.subtle.importKey( const sign = crypto.createSign('RSA-SHA256')
'jwk',
key, sign.update(msg)
{ setImmediate(() => {
name: 'RSASSA-PKCS1-v1_5', callback(null, sign.sign(jwkToPem(key)))
hash: {name: 'SHA-256'} })
},
false,
['sign']
).then((privateKey) => {
return crypto.subtle.sign(
{name: 'RSASSA-PKCS1-v1_5'},
privateKey,
Uint8Array.from(msg)
)
}).then((sig) => Buffer.from(sig)), callback)
} }
exports.hashAndVerify = function (key, sig, msg, callback) { exports.hashAndVerify = function (key, sig, msg, callback) {
nodeify(crypto.subtle.importKey( const verify = crypto.createVerify('RSA-SHA256')
'jwk',
key,
{
name: 'RSASSA-PKCS1-v1_5',
hash: {name: 'SHA-256'}
},
false,
['verify']
).then((publicKey) => {
return crypto.subtle.verify(
{name: 'RSASSA-PKCS1-v1_5'},
publicKey,
sig,
msg
)
}), callback)
}
function exportKey (pair) { verify.update(msg)
return Promise.all([
crypto.subtle.exportKey('jwk', pair.privateKey),
crypto.subtle.exportKey('jwk', pair.publicKey)
])
}
function derivePublicFromPrivate (jwKey) { setImmediate(() => {
return crypto.subtle.importKey( callback(null, verify.verify(jwkToPem(key), sig))
'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()
)
}) })
const AlgorithmIdentifier = asn1.define('AlgorithmIdentifier', function () {
this.seq().obj(
this.key('algorithm').objid({
'1.2.840.113549.1.1.1': 'rsa'
}),
this.key('none').optional().null_(),
this.key('curve').optional().objid(),
this.key('params').optional().seq().obj(
this.key('p').int(),
this.key('q').int(),
this.key('g').int()
)
)
})
const PublicKey = asn1.define('RSAPublicKey', function () {
this.seq().obj(
this.key('algorithm').use(AlgorithmIdentifier),
this.key('subjectPublicKey').bitstr()
)
})
const RSAPublicKey = asn1.define('RSAPublicKey', function () {
this.seq().obj(
this.key('modulus').int(),
this.key('publicExponent').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'
} }
} }
// Convert a JWK key into PKCS#1 in ASN1 DER format
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 PKCIX in ASN1 DER format to a JWK key
exports.pkixToJwk = function (bytes) {
const ndata = PublicKey.decode(bytes, 'der')
const asn1 = RSAPublicKey.decode(ndata.subjectPublicKey.data, 'der')
return {
kty: 'RSA',
n: toBase64(asn1.modulus),
e: toBase64(asn1.publicExponent),
alg: 'RS256',
kid: '2011-04-29'
}
}
// Convert a JWK key to PKCIX in ASN1 DER format
exports.jwkToPkix = function (jwk) {
return PublicKey.encode({
algorithm: {
algorithm: 'rsa',
none: null
},
subjectPublicKey: {
data: RSAPublicKey.encode({
modulus: toBn(jwk.n),
publicExponent: toBn(jwk.e)
}, 'der')
}
}, 'der')
}

View File

@ -1,7 +1,11 @@
'use strict' 'use strict'
module.exports = function getWebCrypto () { module.exports = function getWebCrypto () {
try {
const WebCrypto = require('node-webcrypto-ossl') const WebCrypto = require('node-webcrypto-ossl')
const webCrypto = new WebCrypto() const webCrypto = new WebCrypto()
return webCrypto return webCrypto
} catch (err) {
// fallback to other things
}
} }

View File

@ -17,7 +17,7 @@ class RsaPublicKey {
} }
marshal () { marshal () {
return crypto.jwkToPkix(this._key) return crypto.utils.jwkToPkix(this._key)
} }
get bytes () { get bytes () {
@ -71,7 +71,7 @@ class RsaPrivateKey {
} }
marshal () { marshal () {
return crypto.jwkToPkcs1(this._key) return crypto.utils.jwkToPkcs1(this._key)
} }
get bytes () { get bytes () {
@ -92,7 +92,7 @@ class RsaPrivateKey {
} }
function unmarshalRsaPrivateKey (bytes, callback) { function unmarshalRsaPrivateKey (bytes, callback) {
const jwk = crypto.pkcs1ToJwk(bytes) const jwk = crypto.utils.pkcs1ToJwk(bytes)
crypto.unmarshalPrivateKey(jwk, (err, keys) => { crypto.unmarshalPrivateKey(jwk, (err, keys) => {
if (err) { if (err) {
return callback(err) return callback(err)
@ -103,7 +103,7 @@ function unmarshalRsaPrivateKey (bytes, callback) {
} }
function unmarshalRsaPublicKey (bytes) { function unmarshalRsaPublicKey (bytes) {
const jwk = crypto.pkixToJwk(bytes) const jwk = crypto.utils.pkixToJwk(bytes)
return new RsaPublicKey(jwk) return new RsaPublicKey(jwk)
} }