diff --git a/Gruntfile.js b/Gruntfile.js index 84481853..10af27be 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -34,10 +34,6 @@ module.exports = function(grunt) { browser_capabilities = JSON.parse(process.env.SELENIUM_BROWSER_CAPABILITIES); } - var getSauceKey = function getSaucekey () { - return '60ffb656-2346-4b77-81f3-bc435ff4c103'; - }; - // Project configuration. grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), @@ -53,8 +49,18 @@ module.exports = function(grunt) { external: [ 'crypto', 'buffer', 'node-localstorage', 'node-fetch' ], transform: [ ["babelify", { + plugins: ["transform-async-to-generator", + "syntax-async-functions", + "transform-regenerator", + "transform-runtime"], ignore: ['*.min.js'], - presets: ["es2015"] + presets: [ + ["babel-preset-env", { + "targets": { + "node": "current" + } + }] + ] }] ], plugin: [ 'browserify-derequire' ] @@ -72,8 +78,18 @@ module.exports = function(grunt) { external: [ 'crypto', 'buffer', 'node-localstorage', 'node-fetch' ], transform: [ ["babelify", { + plugins: ["transform-async-to-generator", + "syntax-async-functions", + "transform-regenerator", + "transform-runtime"], ignore: ['*.min.js'], - presets: ["es2015"] + presets: [ + ["babel-preset-env", { + "targets": { + "node": "current" + } + }] + ] }] ], plugin: [ 'browserify-derequire' ] @@ -226,7 +242,7 @@ module.exports = function(grunt) { all: { options: { username: 'openpgpjs', - key: getSauceKey, + key: '60ffb656-2346-4b77-81f3-bc435ff4c103', urls: ['http://127.0.0.1:3000/test/unittests.html'], build: process.env.TRAVIS_BUILD_ID, testname: 'Sauce Unit Test for openpgpjs', diff --git a/src/crypto/public_key/elliptic/curves.js b/src/crypto/public_key/elliptic/curves.js index 8c6bd280..b8dae437 100644 --- a/src/crypto/public_key/elliptic/curves.js +++ b/src/crypto/public_key/elliptic/curves.js @@ -30,53 +30,76 @@ import {ec as EC} from 'elliptic'; import {KeyPair} from './key.js'; import BigInteger from '../jsbn.js'; +import config from '../../../config'; import enums from '../../../enums.js'; import util from '../../../util.js'; +const webCrypto = util.getWebCrypto(); +const nodeCrypto = util.getNodeCrypto(); + +var webCurves = [], nodeCurves = []; +if (webCrypto && config.use_native) { + // see https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API/Supported_algorithms + webCurves = ['P-256', 'P-384', 'P-521']; +} else if (nodeCrypto && config.use_native) { + // FIXME make sure the name translations are correct + nodeCurves = nodeCrypto.getCurves(); +} + const curves = { p256: { oid: util.bin2str([0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]), - curveName: 'P-256', + namedCurve: 'P-256', + opensslCurve: 'prime256v1', hashName: 'SHA-256', hash: enums.hash.sha256, cipher: enums.symmetric.aes128, - nist: true + node: nodeCurves.includes('prime256v1'), + web: webCurves.includes('P-256') }, p384: { oid: util.bin2str([0x2B, 0x81, 0x04, 0x00, 0x22]), - curveName: 'P-384', + namedCurve: 'P-384', + opensslCurve: 'secp384r1', // FIXME hashName: 'SHA-384', hash: enums.hash.sha384, cipher: enums.symmetric.aes192, - nist: true + node: nodeCurves.includes('secp384r1'), // FIXME + web: webCurves.includes('P-384') }, p521: { oid: util.bin2str([0x2B, 0x81, 0x04, 0x00, 0x23]), - curveName: 'P-521', + namedCurve: 'P-521', + opensslCurve: 'secp521r1', // FIXME hashName: 'SHA-512', hash: enums.hash.sha512, cipher: enums.symmetric.aes256, - nist: true + node: nodeCurves.includes('secp521r1'), // FIXME + web: webCurves.includes('P-521') }, secp256k1: { oid: util.bin2str([0x2B, 0x81, 0x04, 0x00, 0x0A]), - curveName: 'SECP-256K1', + namedCurve: 'SECP-256K1', + opensslCurve: 'secp256k1', hashName: 'SHA-256', hash: enums.hash.sha256, cipher: enums.symmetric.aes128, - nist: false + node: nodeCurves.includes('secp256k1'), + web: false } }; -function Curve(name, {oid, hash, cipher, curveName, hashName, nist}) { +function Curve(name, {oid, hash, cipher, namedCurve, opensslCurve, hashName, node, web}) { this.curve = new EC(name); this.name = name; this.oid = oid; this.hash = hash; this.cipher = cipher; - this.curveName= curveName; + this.namedCurve= namedCurve; + this.opensslCurve = opensslCurve; this.hashName = hashName; - this.nist = nist; + this.node = node; + this.web = web; } Curve.prototype.keyFromPrivate = function (priv) { @@ -88,25 +111,26 @@ Curve.prototype.keyFromPublic = function (pub) { }; Curve.prototype.genKeyPair = function () { - var r = this.curve.genKeyPair(); - return new KeyPair(this.curve, { - pub: r.getPublic().encode(), - priv: r.getPrivate().toArray() - }); + var keyPair; + if (webCrypto && config.use_native && this.web) { + keyPair = webGenKeyPair(this.namedCurve); + } else if (nodeCrypto && config.use_native && this.node) { + keyPair = nodeGenKeyPair(this.opensslCurve); + } else { + var r = this.curve.genKeyPair(); + keyPair = { + pub: r.getPublic().encode(), + priv: r.getPrivate().toArray() + }; + } + return new KeyPair(this.curve, keyPair); }; function get(oid_or_name) { for (var name in curves) { if (curves[name].oid === oid_or_name || name === oid_or_name) { - return new Curve(name, { - oid: curves[name].oid, - hash: curves[name].hash, - cipher: curves[name].cipher, - curveName: curves[name].curveName, - hashName: curves[name].hashName, - nist: curves[name].nist - }); + return new Curve(name, curves[name]); } } throw new Error('Not valid curve'); @@ -131,3 +155,44 @@ module.exports = { generate: generate, get: get }; + + +////////////////////////// +// // +// Helper functions // +// // +////////////////////////// + + +function webGenKeyPair(namedCurve) { + return webCrypto.generateKey( + { + name: "ECDSA", +// FIXME +// name: "ECDH", + namedCurve: namedCurve, // "P-256", "P-384", or "P-521" + }, +// FIXME + false, // whether the key is extractable (i.e. can be used in exportKey) + ["sign", "verify"] // can be any combination of "sign" and "verify" +// FIXME +// ["deriveKey", "deriveBits"] // can be any combination of "deriveKey" and "deriveBits" + ).then(function(key){ + return { + pub: key.publicKey.encode(), // FIXME encoding + priv: key.privateKey.toArray() // FIXME encoding + }; + }).catch(function(err){ + throw new Error(err); + }); +} + +function nodeGenKeyPair(opensslCurve) { + // TODO turn this into a promise + var ecc = nodeCrypto.createECDH(opensslCurve); + ecc.generateKeys(); + return { + pub: ecc.getPrivateKey().toJSON().data, + priv: ecc.getPublicKey().toJSON().data + }; +} diff --git a/src/crypto/public_key/elliptic/ecdsa.js b/src/crypto/public_key/elliptic/ecdsa.js index 37cba51c..82df266f 100644 --- a/src/crypto/public_key/elliptic/ecdsa.js +++ b/src/crypto/public_key/elliptic/ecdsa.js @@ -25,22 +25,44 @@ 'use strict'; +import asn1 from 'asn1'; +import jwk2pem from 'jwk-to-pem'; + import curves from './curves.js'; import BigInteger from '../jsbn.js'; +import config from '../../../config'; +import enums from '../../../enums.js'; +import util from '../../../util.js'; + +const webCrypto = util.getWebCrypto(); +const nodeCrypto = util.getNodeCrypto(); + +var ECDSASignature = asn1.define('ecdsa-sig', function() { + return this.seq().obj( + this.key('r').int(), // FIXME int or BN? + this.key('s').int() // FIXME int or BN? + ); +}); /** * Sign a message using the provided key * @param {String} oid Elliptic curve for the key - * @param {BigInteger} Q Public key used to sign * @param {enums.hash} hash_algo Hash algorithm used to sign * @param {Uint8Array} m Message to sign -* @param {BigInteger} d Private key used to sign + * @param {BigInteger} d Private key used to sign * @return {{r: BigInteger, s: BigInteger}} Signature of the message */ function sign(oid, hash_algo, m, d) { + var signature; const curve = curves.get(oid); - const key = curve.keyFromPrivate(d.toByteArray()); - const signature = key.sign(m, hash_algo); + if (webCrypto && config.use_native && curve.web) { + signature = webSign(curve, hash_algo, m, d); + } else if (nodeCrypto && config.use_native && curve.node) { + signature = nodeSign(curve, hash_algo, m, d); + } else { + const key = curve.keyFromPrivate(d.toByteArray()); + signature = key.sign(m, hash_algo); + } return { r: new BigInteger(signature.r), s: new BigInteger(signature.s) @@ -53,15 +75,141 @@ function sign(oid, hash_algo, m, d) { * @param {enums.hash} hash_algo Hash algorithm used in the signature * @param {{r: BigInteger, s: BigInteger}} signature Signature to verify * @param {Uint8Array} m Message to verify - * @param {BigInteger} Q Public key used to verify the message + * @param {BigInteger} Q Public key used to verify the message + * @return {Boolean} */ function verify(oid, hash_algo, signature, m, Q) { const curve = curves.get(oid); - const key = curve.keyFromPublic(Q.toByteArray()); - return key.verify(m, {r: signature.r.toByteArray(), s: signature.s.toByteArray()}, hash_algo); + if (webCrypto && config.use_native && curve.web) { + return webVerify(curve, hash_algo, signature, m, Q); + } else if (nodeCrypto && config.use_native && curve.node) { + return nodeVerify(curve, hash_algo, signature, m, Q); + } else { + const key = curve.keyFromPublic(Q.toByteArray()); + return key.verify(m, {r: signature.r.toByteArray(), s: signature.s.toByteArray()}, hash_algo); + } } module.exports = { sign: sign, verify: verify }; + + +////////////////////////// +// // +// Helper functions // +// // +////////////////////////// + + +async function webSign(curve, hash_algo, m, d) { + const publicKey = curve.keyFromPrivate(d).getPublic(); + const privateKey = await webCrypto.importKey( + "jwk", + { + "kty": "EC", + "crv": curve.namedCurve, + "x": publicKey.getX().toBuffer().base64Slice(), + "y": publicKey.getY().toBuffer().base64Slice(), + "d": d.toBuffer().base64Slice(), + "use": "sig", + "kid": "ECDSA Private Key" + }, + { + "name": "ECDSA", + "namedCurve": curve.namedCurve, + "hash": { name: enums.read(enums.hash, hash_algo) } + }, + false, + ["sign"] + ); + + try { + return await webCrypto.sign( + { + "name": 'ECDSA', + "namedCurve": curve.namedCurve, + "hash": { name: enums.read(enums.hash, hash_algo) } + }, + privateKey, + m + ); + } catch(err) { + throw new Error(err); + } +} + +async function webVerify(curve, hash_algo, signature, m, Q) { + const publicKey = await webCrypto.importKey( + "jwk", + { + "kty": "EC", + "crv": curve.namedCurve, + "x": Q.getX().toBuffer().base64Slice(), + "y": Q.getY().toBuffer().base64Slice(), + "use": "sig", + "kid": "ECDSA Public Key" + }, + { + "name": "ECDSA", + "namedCurve": curve.namedCurve, + "hash": { name: enums.read(enums.hash, hash_algo) } + }, + false, + ["verify"] + ); + + try { + return await webCrypto.verify( + { + "name": 'ECDSA', + "namedCurve": curve.namedCurve, + "hash": { name: enums.read(enums.hash, hash_algo) } + }, + publicKey, + signature, + m + ); + } catch(err) { + throw new Error(err); + } +} + + +async function nodeSign(curve, hash_algo, m, d) { + const publicKey = curve.keyFromPrivate(d).getPublic(); + const privateKey = jwk2pem( + {"kty": "EC", + "crv": curve.namedCurve, + "x": publicKey.getX().toBuffer().base64Slice(), + "y": publicKey.getY().toBuffer().base64Slice(), + "d": d.toBuffer().base64Slice(), + "use": "sig", + "kid": "ECDSA Private Key"}, + {private: true} + ); + + const sign = nodeCrypto.createSign(enums.read(enums.hash, hash_algo)); + sign.write(m); + sign.end(); + const signature = await sign.sign(privateKey); + return ECDSASignature.decode(signature, 'der'); +} + +async function nodeVerify(curve, hash_algo, signature, m, Q) { + const publicKey = jwk2pem( + {"kty": "EC", + "crv": curve.namedCurve, + "x": Q.getX().toBuffer().base64Slice(), + "y": Q.getY().toBuffer().base64Slice(), + "use": "sig", + "kid": "ECDSA Public Key"}, + {private: false} + ); + + const verify = nodeCrypto.createVerify(enums.read(enums.hash, hash_algo)); + verify.write(m); + verify.end(); + return await verify.verify(publicKey, signature); +} diff --git a/src/openpgp.js b/src/openpgp.js index 3bad19b0..3d56d87c 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -300,9 +300,9 @@ export function sign({ data, privateKeys, armor=true, detached=false}) { } var result = {}; - return execute(() => { + const promise = async function(){ var message; - + if (util.isString(data)) { message = new cleartext.CleartextMessage(data); } else { @@ -310,14 +310,14 @@ export function sign({ data, privateKeys, armor=true, detached=false}) { } if (detached) { - var signature = message.signDetached(privateKeys); + var signature = await message.signDetached(privateKeys); if (armor) { result.signature = signature.armor(); } else { result.signature = signature; } } else { - message = message.sign(privateKeys); + message = await message.sign(privateKeys); if (armor) { result.data = message.armor(); } else { @@ -326,8 +326,8 @@ export function sign({ data, privateKeys, armor=true, detached=false}) { } return result; - - }, 'Error signing cleartext message'); + } + return promise().catch(onError.bind(null, 'Error signing cleartext message')); } /** @@ -348,7 +348,7 @@ export function verify({ message, publicKeys, signature=null }) { } var result = {}; - return execute(() => { + const promise = async function(){ if (cleartext.CleartextMessage.prototype.isPrototypeOf(message)) { result.data = message.getText(); } else { @@ -356,13 +356,14 @@ export function verify({ message, publicKeys, signature=null }) { } if (signature) { //detached signature - result.signatures = message.verifyDetached(signature, publicKeys); + result.signatures = await message.verifyDetached(signature, publicKeys); } else { - result.signatures = message.verify(publicKeys); + result.signatures = await message.verify(publicKeys); } return result; - }, 'Error verifying cleartext signed message'); + } + return promise().catch(onError.bind(null, 'Error verifying cleartext signed message')); }