diff --git a/src/cleartext.js b/src/cleartext.js index db2f282f..c4fea507 100644 --- a/src/cleartext.js +++ b/src/cleartext.js @@ -24,7 +24,7 @@ * @module cleartext */ -import armor from './encoding/armor'; +import { armor, unarmor } from './encoding/armor'; import enums from './enums'; import util from './util'; import { PacketList, LiteralDataPacket, SignaturePacket } from './packet'; @@ -140,7 +140,7 @@ export class CleartextMessage { text: this.text, data: this.signature.packets.write() }; - return armor.encode(enums.armor.signed, body); + return armor(enums.armor.signed, body); } /** @@ -162,7 +162,7 @@ export class CleartextMessage { * @static */ export async function readArmoredCleartextMessage(armoredText) { - const input = await armor.decode(armoredText); + const input = await unarmor(armoredText); if (input.type !== enums.armor.signed) { throw new Error('No cleartext signed message.'); } diff --git a/src/crypto/aes_kw.js b/src/crypto/aes_kw.js index 8588a0a1..669858b0 100644 --- a/src/crypto/aes_kw.js +++ b/src/crypto/aes_kw.js @@ -23,10 +23,17 @@ * @module crypto/aes_kw */ -import cipher from './cipher'; +import * as cipher from './cipher'; import util from '../util'; -function wrap(key, data) { +/** + * AES key wrap + * @function + * @param {String} key + * @param {String} data + * @returns {Uint8Array} + */ +export function wrap(key, data) { const aes = new cipher["aes" + (key.length * 8)](key); const IV = new Uint32Array([0xA6A6A6A6, 0xA6A6A6A6]); const P = unpack(data); @@ -58,7 +65,15 @@ function wrap(key, data) { return pack(A, R); } -function unwrap(key, data) { +/** + * AES key unwrap + * @function + * @param {String} key + * @param {String} data + * @returns {Uint8Array} + * @throws {Error} + */ +export function unwrap(key, data) { const aes = new cipher["aes" + (key.length * 8)](key); const IV = new Uint32Array([0xA6A6A6A6, 0xA6A6A6A6]); const C = unpack(data); @@ -131,23 +146,3 @@ function pack() { } return new Uint8Array(buffer); } - -export default { - /** - * AES key wrap - * @function - * @param {String} key - * @param {String} data - * @returns {Uint8Array} - */ - wrap, - /** - * AES key unwrap - * @function - * @param {String} key - * @param {String} data - * @returns {Uint8Array} - * @throws {Error} - */ - unwrap -}; diff --git a/src/crypto/cfb.js b/src/crypto/cfb.js index b5ddbf9f..440a04b6 100644 --- a/src/crypto/cfb.js +++ b/src/crypto/cfb.js @@ -27,7 +27,7 @@ import { AES_CFB } from 'asmcrypto.js/dist_es8/aes/cfb'; import stream from 'web-stream-tools'; -import cipher from './cipher'; +import * as cipher from './cipher'; import config from '../config'; import util from '../util'; @@ -38,7 +38,6 @@ const Buffer = util.getNodeBuffer(); const knownAlgos = nodeCrypto ? nodeCrypto.getCiphers() : []; const nodeAlgos = { idea: knownAlgos.includes('idea-cfb') ? 'idea-cfb' : undefined, /* Unused, not implemented */ - '3des': knownAlgos.includes('des-ede3-cfb') ? 'des-ede3-cfb' : undefined, tripledes: knownAlgos.includes('des-ede3-cfb') ? 'des-ede3-cfb' : undefined, cast5: knownAlgos.includes('cast5-cfb') ? 'cast5-cfb' : undefined, blowfish: knownAlgos.includes('bf-cfb') ? 'bf-cfb' : undefined, @@ -48,73 +47,71 @@ const nodeAlgos = { /* twofish is not implemented in OpenSSL */ }; -export default { - encrypt: function(algo, key, plaintext, iv) { - if (util.getNodeCrypto() && nodeAlgos[algo]) { // Node crypto library. - return nodeEncrypt(algo, key, plaintext, iv); - } - if (algo.substr(0, 3) === 'aes') { - return aesEncrypt(algo, key, plaintext, iv); - } - - const cipherfn = new cipher[algo](key); - const block_size = cipherfn.blockSize; - - const blockc = iv.slice(); - let pt = new Uint8Array(); - const process = chunk => { - if (chunk) { - pt = util.concatUint8Array([pt, chunk]); - } - const ciphertext = new Uint8Array(pt.length); - let i; - let j = 0; - while (chunk ? pt.length >= block_size : pt.length) { - const encblock = cipherfn.encrypt(blockc); - for (i = 0; i < block_size; i++) { - blockc[i] = pt[i] ^ encblock[i]; - ciphertext[j++] = blockc[i]; - } - pt = pt.subarray(block_size); - } - return ciphertext.subarray(0, j); - }; - return stream.transform(plaintext, process, process); - }, - - decrypt: async function(algo, key, ciphertext, iv) { - if (util.getNodeCrypto() && nodeAlgos[algo]) { // Node crypto library. - return nodeDecrypt(algo, key, ciphertext, iv); - } - if (algo.substr(0, 3) === 'aes') { - return aesDecrypt(algo, key, ciphertext, iv); - } - - const cipherfn = new cipher[algo](key); - const block_size = cipherfn.blockSize; - - let blockp = iv; - let ct = new Uint8Array(); - const process = chunk => { - if (chunk) { - ct = util.concatUint8Array([ct, chunk]); - } - const plaintext = new Uint8Array(ct.length); - let i; - let j = 0; - while (chunk ? ct.length >= block_size : ct.length) { - const decblock = cipherfn.encrypt(blockp); - blockp = ct; - for (i = 0; i < block_size; i++) { - plaintext[j++] = blockp[i] ^ decblock[i]; - } - ct = ct.subarray(block_size); - } - return plaintext.subarray(0, j); - }; - return stream.transform(ciphertext, process, process); +export async function encrypt(algo, key, plaintext, iv) { + if (util.getNodeCrypto() && nodeAlgos[algo]) { // Node crypto library. + return nodeEncrypt(algo, key, plaintext, iv); } -}; + if (algo.substr(0, 3) === 'aes') { + return aesEncrypt(algo, key, plaintext, iv); + } + + const cipherfn = new cipher[algo](key); + const block_size = cipherfn.blockSize; + + const blockc = iv.slice(); + let pt = new Uint8Array(); + const process = chunk => { + if (chunk) { + pt = util.concatUint8Array([pt, chunk]); + } + const ciphertext = new Uint8Array(pt.length); + let i; + let j = 0; + while (chunk ? pt.length >= block_size : pt.length) { + const encblock = cipherfn.encrypt(blockc); + for (i = 0; i < block_size; i++) { + blockc[i] = pt[i] ^ encblock[i]; + ciphertext[j++] = blockc[i]; + } + pt = pt.subarray(block_size); + } + return ciphertext.subarray(0, j); + }; + return stream.transform(plaintext, process, process); +} + +export async function decrypt(algo, key, ciphertext, iv) { + if (util.getNodeCrypto() && nodeAlgos[algo]) { // Node crypto library. + return nodeDecrypt(algo, key, ciphertext, iv); + } + if (algo.substr(0, 3) === 'aes') { + return aesDecrypt(algo, key, ciphertext, iv); + } + + const cipherfn = new cipher[algo](key); + const block_size = cipherfn.blockSize; + + let blockp = iv; + let ct = new Uint8Array(); + const process = chunk => { + if (chunk) { + ct = util.concatUint8Array([ct, chunk]); + } + const plaintext = new Uint8Array(ct.length); + let i; + let j = 0; + while (chunk ? ct.length >= block_size : ct.length) { + const decblock = cipherfn.encrypt(blockp); + blockp = ct; + for (i = 0; i < block_size; i++) { + plaintext[j++] = blockp[i] ^ decblock[i]; + } + ct = ct.subarray(block_size); + } + return plaintext.subarray(0, j); + }; + return stream.transform(ciphertext, process, process); +} function aesEncrypt(algo, key, pt, iv) { if ( diff --git a/src/crypto/cipher/des.js b/src/crypto/cipher/des.js index 2cd394e4..7c335d34 100644 --- a/src/crypto/cipher/des.js +++ b/src/crypto/cipher/des.js @@ -432,7 +432,7 @@ function des_removePadding(message, padding) { // added by Recurity Labs -function TripleDES(key) { +export function TripleDES(key) { this.key = []; for (let i = 0; i < 3; i++) { @@ -459,7 +459,7 @@ TripleDES.blockSize = TripleDES.prototype.blockSize = 8; // This is "original" DES -function DES(key) { +export function DES(key) { this.key = key; this.encrypt = function(block, padding) { @@ -472,5 +472,3 @@ function DES(key) { return des(keys, block, false, 0, null, padding); }; } - -export default { DES, TripleDES }; diff --git a/src/crypto/cipher/index.js b/src/crypto/cipher/index.js index 7ae8a06c..daf3f222 100644 --- a/src/crypto/cipher/index.js +++ b/src/crypto/cipher/index.js @@ -9,83 +9,80 @@ */ import aes from './aes'; -import des from './des.js'; -import cast5 from './cast5'; -import twofish from './twofish'; -import blowfish from './blowfish'; +import { DES, TripleDES } from './des.js'; +import Cast5 from './cast5'; +import TF from './twofish'; +import BF from './blowfish'; -export default { - /** - * AES-128 encryption and decryption (ID 7) - * @function - * @param {String} key 128-bit key - * @see {@link https://github.com/asmcrypto/asmcrypto.js|asmCrypto} - * @see {@link https://csrc.nist.gov/publications/fips/fips197/fips-197.pdf|NIST FIPS-197} - * @returns {Object} - * @requires asmcrypto.js - */ - aes128: aes(128), - /** - * AES-128 Block Cipher (ID 8) - * @function - * @param {String} key 192-bit key - * @see {@link https://github.com/asmcrypto/asmcrypto.js|asmCrypto} - * @see {@link https://csrc.nist.gov/publications/fips/fips197/fips-197.pdf|NIST FIPS-197} - * @returns {Object} - * @requires asmcrypto.js - */ - aes192: aes(192), - /** - * AES-128 Block Cipher (ID 9) - * @function - * @param {String} key 256-bit key - * @see {@link https://github.com/asmcrypto/asmcrypto.js|asmCrypto} - * @see {@link https://csrc.nist.gov/publications/fips/fips197/fips-197.pdf|NIST FIPS-197} - * @returns {Object} - * @requires asmcrypto.js - */ - aes256: aes(256), - // Not in OpenPGP specifications - des: des.DES, - /** - * Triple DES Block Cipher (ID 2) - * @function - * @param {String} key 192-bit key - * @see {@link https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-67r2.pdf|NIST SP 800-67} - * @returns {Object} - */ - tripledes: des.TripleDES, - '3des': des.TripleDES, - /** - * CAST-128 Block Cipher (ID 3) - * @function - * @param {String} key 128-bit key - * @see {@link https://tools.ietf.org/html/rfc2144|The CAST-128 Encryption Algorithm} - * @returns {Object} - */ - cast5: cast5, - /** - * Twofish Block Cipher (ID 10) - * @function - * @param {String} key 256-bit key - * @see {@link https://tools.ietf.org/html/rfc4880#ref-TWOFISH|TWOFISH} - * @returns {Object} - */ - twofish: twofish, - /** - * Blowfish Block Cipher (ID 4) - * @function - * @param {String} key 128-bit key - * @see {@link https://tools.ietf.org/html/rfc4880#ref-BLOWFISH|BLOWFISH} - * @returns {Object} - */ - blowfish: blowfish, - /** - * Not implemented - * @function - * @throws {Error} - */ - idea: function() { - throw new Error('IDEA symmetric-key algorithm not implemented'); - } +/** + * AES-128 encryption and decryption (ID 7) + * @function + * @param {String} key 128-bit key + * @see {@link https://github.com/asmcrypto/asmcrypto.js|asmCrypto} + * @see {@link https://csrc.nist.gov/publications/fips/fips197/fips-197.pdf|NIST FIPS-197} + * @returns {Object} + * @requires asmcrypto.js + */ +export const aes128 = aes(128); +/** + * AES-128 Block Cipher (ID 8) + * @function + * @param {String} key 192-bit key + * @see {@link https://github.com/asmcrypto/asmcrypto.js|asmCrypto} + * @see {@link https://csrc.nist.gov/publications/fips/fips197/fips-197.pdf|NIST FIPS-197} + * @returns {Object} + * @requires asmcrypto.js + */ +export const aes192 = aes(192); +/** + * AES-128 Block Cipher (ID 9) + * @function + * @param {String} key 256-bit key + * @see {@link https://github.com/asmcrypto/asmcrypto.js|asmCrypto} + * @see {@link https://csrc.nist.gov/publications/fips/fips197/fips-197.pdf|NIST FIPS-197} + * @returns {Object} + * @requires asmcrypto.js + */ +export const aes256 = aes(256); +// Not in OpenPGP specifications +export const des = DES; +/** + * Triple DES Block Cipher (ID 2) + * @function + * @param {String} key 192-bit key + * @see {@link https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-67r2.pdf|NIST SP 800-67} + * @returns {Object} + */ +export const tripledes = TripleDES; +/** + * CAST-128 Block Cipher (ID 3) + * @function + * @param {String} key 128-bit key + * @see {@link https://tools.ietf.org/html/rfc2144|The CAST-128 Encryption Algorithm} + * @returns {Object} + */ +export const cast5 = Cast5; +/** + * Twofish Block Cipher (ID 10) + * @function + * @param {String} key 256-bit key + * @see {@link https://tools.ietf.org/html/rfc4880#ref-TWOFISH|TWOFISH} + * @returns {Object} + */ +export const twofish = TF; +/** + * Blowfish Block Cipher (ID 4) + * @function + * @param {String} key 128-bit key + * @see {@link https://tools.ietf.org/html/rfc4880#ref-BLOWFISH|BLOWFISH} + * @returns {Object} + */ +export const blowfish = BF; +/** + * Not implemented + * @function + * @throws {Error} + */ +export const idea = function() { + throw new Error('IDEA symmetric-key algorithm not implemented'); }; diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index dff3ea7a..7c05467c 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -33,17 +33,17 @@ */ import publicKey from './public_key'; -import cipher from './cipher'; -import random from './random'; +import * as cipher from './cipher'; +import { getRandomBytes } from './random'; import type_ecdh_symkey from '../type/ecdh_symkey'; import KDFParams from '../type/kdf_params'; import type_mpi from '../type/mpi'; import enums from '../enums'; import util from '../util'; import OID from '../type/oid'; -import Curve from './public_key/elliptic/curves'; +import { Curve } from './public_key/elliptic/curves'; -function constructParams(types, data) { +export function constructParams(types, data) { return types.map(function(type, i) { if (data && data[i]) { return new type(data[i]); @@ -52,335 +52,331 @@ function constructParams(types, data) { }); } -export default { - /** - * Encrypts data using specified algorithm and public key parameters. - * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} for public key algorithms. - * @param {module:enums.publicKey} algo Public key algorithm - * @param {Object} pubParams Algorithm-specific public key parameters - * @param {Uint8Array} data Data to be encrypted - * @param {Uint8Array} fingerprint Recipient fingerprint - * @returns {Array} encrypted session key parameters - * @async - */ - publicKeyEncrypt: async function(algo, publicParams, data, fingerprint) { - const types = this.getEncSessionKeyParamTypes(algo); - switch (algo) { - case enums.publicKey.rsaEncrypt: - case enums.publicKey.rsaEncryptSign: { - const { n, e } = publicParams; - const res = await publicKey.rsa.encrypt(data, n, e); - return constructParams(types, [res]); - } - case enums.publicKey.elgamal: { - const { p, g, y } = publicParams; - const res = await publicKey.elgamal.encrypt(data, p, g, y); - return constructParams(types, [res.c1, res.c2]); - } - case enums.publicKey.ecdh: { - const { oid, Q, kdfParams } = publicParams; - const { publicKey: V, wrappedKey: C } = await publicKey.elliptic.ecdh.encrypt( - oid, kdfParams, data, Q, fingerprint); - return constructParams(types, [V, C]); - } - default: - return []; +/** + * Encrypts data using specified algorithm and public key parameters. + * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} for public key algorithms. + * @param {module:enums.publicKey} algo Public key algorithm + * @param {Object} pubParams Algorithm-specific public key parameters + * @param {Uint8Array} data Data to be encrypted + * @param {Uint8Array} fingerprint Recipient fingerprint + * @returns {Array} encrypted session key parameters + * @async + */ +export async function publicKeyEncrypt(algo, publicParams, data, fingerprint) { + const types = getEncSessionKeyParamTypes(algo); + switch (algo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: { + const { n, e } = publicParams; + const res = await publicKey.rsa.encrypt(data, n, e); + return constructParams(types, [res]); } - }, - - /** - * Decrypts data using specified algorithm and private key parameters. - * See {@link https://tools.ietf.org/html/rfc4880#section-5.5.3|RFC 4880 5.5.3} - * @param {module:enums.publicKey} algo Public key algorithm - * @param {Object} publicKeyParams Algorithm-specific public key parameters - * @param {Object} privateKeyParams Algorithm-specific private key parameters - * @param {Array} - data_params encrypted session key parameters - * @param {Uint8Array} fingerprint Recipient fingerprint - * @returns {Uint8Array} decrypted data - * @async - */ - publicKeyDecrypt: async function(algo, publicKeyParams, privateKeyParams, data_params, fingerprint) { - switch (algo) { - case enums.publicKey.rsaEncryptSign: - case enums.publicKey.rsaEncrypt: { - const c = data_params[0].toUint8Array(); - const { n, e } = publicKeyParams; - const { d, p, q, u } = privateKeyParams; - return publicKey.rsa.decrypt(c, n, e, d, p, q, u); - } - case enums.publicKey.elgamal: { - const c1 = data_params[0].toUint8Array(); - const c2 = data_params[1].toUint8Array(); - const p = publicKeyParams.p; - const x = privateKeyParams.x; - return publicKey.elgamal.decrypt(c1, c2, p, x); - } - case enums.publicKey.ecdh: { - const { oid, Q, kdfParams } = publicKeyParams; - const { d } = privateKeyParams; - const V = data_params[0].toUint8Array(); - const C = data_params[1].data; - return publicKey.elliptic.ecdh.decrypt( - oid, kdfParams, V, C, Q, d, fingerprint); - } - default: - throw new Error('Invalid public key encryption algorithm.'); + case enums.publicKey.elgamal: { + const { p, g, y } = publicParams; + const res = await publicKey.elgamal.encrypt(data, p, g, y); + return constructParams(types, [res.c1, res.c2]); } - }, - - /** - * Parse public key material in binary form to get the key parameters - * @param {module:enums.publicKey} algo The key algorithm - * @param {Uint8Array} bytes The key material to parse - * @returns {Object} key parameters referenced by name - * @returns { read: Number, publicParams: Object } number of read bytes plus key parameters referenced by name - */ - parsePublicKeyParams: function(algo, bytes) { - let read = 0; - switch (algo) { - case enums.publicKey.rsaEncrypt: - case enums.publicKey.rsaEncryptSign: - case enums.publicKey.rsaSign: { - let read = 0; - const n = util.readMPI(bytes.subarray(read)); read += n.length + 2; - const e = util.readMPI(bytes.subarray(read)); read += e.length + 2; - return { read, publicParams: { n, e } }; - } - case enums.publicKey.dsa: { - const p = util.readMPI(bytes.subarray(read)); read += p.length + 2; - const q = util.readMPI(bytes.subarray(read)); read += q.length + 2; - const g = util.readMPI(bytes.subarray(read)); read += g.length + 2; - const y = util.readMPI(bytes.subarray(read)); read += y.length + 2; - return { read, publicParams: { p, q, g, y } }; - } - case enums.publicKey.elgamal: { - const p = util.readMPI(bytes.subarray(read)); read += p.length + 2; - const g = util.readMPI(bytes.subarray(read)); read += g.length + 2; - const y = util.readMPI(bytes.subarray(read)); read += y.length + 2; - return { read, publicParams: { p, g, y } }; - } - case enums.publicKey.ecdsa: { - const oid = new OID(); read += oid.read(bytes); - const Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2; - return { read: read, publicParams: { oid, Q } }; - } - case enums.publicKey.eddsa: { - const oid = new OID(); read += oid.read(bytes); - let Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2; - Q = util.padToLength(Q, 33); - return { read: read, publicParams: { oid, Q } }; - } - case enums.publicKey.ecdh: { - const oid = new OID(); read += oid.read(bytes); - const Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2; - const kdfParams = new KDFParams(); read += kdfParams.read(bytes.subarray(read)); - return { read: read, publicParams: { oid, Q, kdfParams } }; - } - default: - throw new Error('Invalid public key encryption algorithm.'); + case enums.publicKey.ecdh: { + const { oid, Q, kdfParams } = publicParams; + const { publicKey: V, wrappedKey: C } = await publicKey.elliptic.ecdh.encrypt( + oid, kdfParams, data, Q, fingerprint); + return constructParams(types, [V, C]); } - }, + default: + return []; + } +} - /** - * Parse private key material in binary form to get the key parameters - * @param {module:enums.publicKey} algo The key algorithm - * @param {Uint8Array} bytes The key material to parse - * @param {Object} publicParams (ECC only) public params, needed to format some private params - * @returns { read: Number, privateParams: Object } number of read bytes plus the key parameters referenced by name - */ - parsePrivateKeyParams: function(algo, bytes, publicParams) { - let read = 0; - switch (algo) { - case enums.publicKey.rsaEncrypt: - case enums.publicKey.rsaEncryptSign: - case enums.publicKey.rsaSign: { - const d = util.readMPI(bytes.subarray(read)); read += d.length + 2; - const p = util.readMPI(bytes.subarray(read)); read += p.length + 2; - const q = util.readMPI(bytes.subarray(read)); read += q.length + 2; - const u = util.readMPI(bytes.subarray(read)); read += u.length + 2; - return { read, privateParams: { d, p, q, u } }; - } - case enums.publicKey.dsa: - case enums.publicKey.elgamal: { - const x = util.readMPI(bytes.subarray(read)); read += x.length + 2; - return { read, privateParams: { x } }; - } - case enums.publicKey.ecdsa: - case enums.publicKey.ecdh: { - const curve = new Curve(publicParams.oid); - let d = util.readMPI(bytes.subarray(read)); read += d.length + 2; - d = util.padToLength(d, curve.payloadSize); - return { read, privateParams: { d } }; - } - case enums.publicKey.eddsa: { - let seed = util.readMPI(bytes.subarray(read)); read += seed.length + 2; - seed = util.padToLength(seed, 32); - return { read, privateParams: { seed } }; - } - default: - throw new Error('Invalid public key encryption algorithm.'); +/** + * Decrypts data using specified algorithm and private key parameters. + * See {@link https://tools.ietf.org/html/rfc4880#section-5.5.3|RFC 4880 5.5.3} + * @param {module:enums.publicKey} algo Public key algorithm + * @param {Object} publicKeyParams Algorithm-specific public key parameters + * @param {Object} privateKeyParams Algorithm-specific private key parameters + * @param {Array} + data_params encrypted session key parameters + * @param {Uint8Array} fingerprint Recipient fingerprint + * @returns {Uint8Array} decrypted data + * @async + */ +export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, data_params, fingerprint) { + switch (algo) { + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaEncrypt: { + const c = data_params[0].toUint8Array(); + const { n, e } = publicKeyParams; + const { d, p, q, u } = privateKeyParams; + return publicKey.rsa.decrypt(c, n, e, d, p, q, u); } - }, - - /** Returns the types comprising the encrypted session key of an algorithm - * @param {module:enums.publicKey} algo The public key algorithm - * @returns {Array} The array of types - */ - getEncSessionKeyParamTypes: function(algo) { - switch (algo) { - // Algorithm-Specific Fields for RSA encrypted session keys: - // - MPI of RSA encrypted value m**e mod n. - case enums.publicKey.rsaEncrypt: - case enums.publicKey.rsaEncryptSign: - return [type_mpi]; - - // Algorithm-Specific Fields for Elgamal encrypted session keys: - // - MPI of Elgamal value g**k mod p - // - MPI of Elgamal value m * y**k mod p - case enums.publicKey.elgamal: - return [type_mpi, type_mpi]; - // Algorithm-Specific Fields for ECDH encrypted session keys: - // - MPI containing the ephemeral key used to establish the shared secret - // - ECDH Symmetric Key - case enums.publicKey.ecdh: - return [type_mpi, type_ecdh_symkey]; - default: - throw new Error('Invalid public key encryption algorithm.'); + case enums.publicKey.elgamal: { + const c1 = data_params[0].toUint8Array(); + const c2 = data_params[1].toUint8Array(); + const p = publicKeyParams.p; + const x = privateKeyParams.x; + return publicKey.elgamal.decrypt(c1, c2, p, x); } - }, - - /** - * Convert params to MPI and serializes them in the proper order - * @param {module:enums.publicKey} algo The public key algorithm - * @param {Object} params The key parameters indexed by name - * @returns {Uint8Array} The array containing the MPIs - */ - serializeKeyParams: function(algo, params) { - const orderedParams = Object.keys(params).map(name => { - const param = params[name]; - return util.isUint8Array(param) ? util.uint8ArrayToMpi(param) : param.write(); - }); - return util.concatUint8Array(orderedParams); - }, - - /** - * Generate algorithm-specific key parameters - * @param {module:enums.publicKey} algo The public key algorithm - * @param {Integer} bits Bit length for RSA keys - * @param {module:type/oid} oid Object identifier for ECC keys - * @returns { publicParams, privateParams: {Object} } The parameters referenced by name - * @async - */ - generateParams: function(algo, bits, oid) { - switch (algo) { - case enums.publicKey.rsaEncrypt: - case enums.publicKey.rsaEncryptSign: - case enums.publicKey.rsaSign: { - return publicKey.rsa.generate(bits, 65537).then(({ n, e, d, p, q, u }) => ({ - privateParams: { d, p, q, u }, - publicParams: { n, e } - })); - } - case enums.publicKey.ecdsa: - return publicKey.elliptic.generate(oid).then(({ oid, Q, secret }) => ({ - privateParams: { d: secret }, - publicParams: { oid: new OID(oid), Q } - })); - case enums.publicKey.eddsa: - return publicKey.elliptic.generate(oid).then(({ oid, Q, secret }) => ({ - privateParams: { seed: secret }, - publicParams: { oid: new OID(oid), Q } - })); - case enums.publicKey.ecdh: - return publicKey.elliptic.generate(oid).then(({ oid, Q, secret, hash, cipher }) => ({ - privateParams: { d: secret }, - publicParams: { - oid: new OID(oid), - Q, - kdfParams: new KDFParams({ hash, cipher }) - } - })); - case enums.publicKey.dsa: - case enums.publicKey.elgamal: - throw new Error('Unsupported algorithm for key generation.'); - default: - throw new Error('Invalid public key algorithm.'); + case enums.publicKey.ecdh: { + const { oid, Q, kdfParams } = publicKeyParams; + const { d } = privateKeyParams; + const V = data_params[0].toUint8Array(); + const C = data_params[1].data; + return publicKey.elliptic.ecdh.decrypt( + oid, kdfParams, V, C, Q, d, fingerprint); } - }, + default: + throw new Error('Invalid public key encryption algorithm.'); + } +} - /** - * Validate algorithm-specific key parameters - * @param {module:enums.publicKey} algo The public key algorithm - * @param {Object} publicParams Algorithm-specific public key parameters - * @param {Object} privateParams Algorithm-specific private key parameters - * @returns {Promise} whether the parameters are valid - * @async - */ - validateParams: async function(algo, publicParams, privateParams) { - if (!publicParams || !privateParams) { - throw new Error('Missing key parameters'); +/** + * Parse public key material in binary form to get the key parameters + * @param {module:enums.publicKey} algo The key algorithm + * @param {Uint8Array} bytes The key material to parse + * @returns {Object} key parameters referenced by name + * @returns { read: Number, publicParams: Object } number of read bytes plus key parameters referenced by name + */ +export function parsePublicKeyParams(algo, bytes) { + let read = 0; + switch (algo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaSign: { + let read = 0; + const n = util.readMPI(bytes.subarray(read)); read += n.length + 2; + const e = util.readMPI(bytes.subarray(read)); read += e.length + 2; + return { read, publicParams: { n, e } }; } - switch (algo) { - case enums.publicKey.rsaEncrypt: - case enums.publicKey.rsaEncryptSign: - case enums.publicKey.rsaSign: { - const { n, e } = publicParams; - const { d, p, q, u } = privateParams; - return publicKey.rsa.validateParams(n, e, d, p, q, u); - } - case enums.publicKey.dsa: { - const { p, q, g, y } = publicParams; - const { x } = privateParams; - return publicKey.dsa.validateParams(p, q, g, y, x); - } - case enums.publicKey.elgamal: { - const { p, g, y } = publicParams; - const { x } = privateParams; - return publicKey.elgamal.validateParams(p, g, y, x); - } - case enums.publicKey.ecdsa: - case enums.publicKey.ecdh: { - const algoModule = publicKey.elliptic[enums.read(enums.publicKey, algo)]; - const { oid, Q } = publicParams; - const { d } = privateParams; - return algoModule.validateParams(oid, Q, d); - } - case enums.publicKey.eddsa: { - const { oid, Q } = publicParams; - const { seed } = privateParams; - return publicKey.elliptic.eddsa.validateParams(oid, Q, seed); - } - default: - throw new Error('Invalid public key algorithm.'); + case enums.publicKey.dsa: { + const p = util.readMPI(bytes.subarray(read)); read += p.length + 2; + const q = util.readMPI(bytes.subarray(read)); read += q.length + 2; + const g = util.readMPI(bytes.subarray(read)); read += g.length + 2; + const y = util.readMPI(bytes.subarray(read)); read += y.length + 2; + return { read, publicParams: { p, q, g, y } }; } - }, + case enums.publicKey.elgamal: { + const p = util.readMPI(bytes.subarray(read)); read += p.length + 2; + const g = util.readMPI(bytes.subarray(read)); read += g.length + 2; + const y = util.readMPI(bytes.subarray(read)); read += y.length + 2; + return { read, publicParams: { p, g, y } }; + } + case enums.publicKey.ecdsa: { + const oid = new OID(); read += oid.read(bytes); + const Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2; + return { read: read, publicParams: { oid, Q } }; + } + case enums.publicKey.eddsa: { + const oid = new OID(); read += oid.read(bytes); + let Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2; + Q = util.padToLength(Q, 33); + return { read: read, publicParams: { oid, Q } }; + } + case enums.publicKey.ecdh: { + const oid = new OID(); read += oid.read(bytes); + const Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2; + const kdfParams = new KDFParams(); read += kdfParams.read(bytes.subarray(read)); + return { read: read, publicParams: { oid, Q, kdfParams } }; + } + default: + throw new Error('Invalid public key encryption algorithm.'); + } +} - /** - * Generates a random byte prefix for the specified algorithm - * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. - * @param {module:enums.symmetric} algo Symmetric encryption algorithm - * @returns {Uint8Array} Random bytes with length equal to the block size of the cipher, plus the last two bytes repeated. - * @async - */ - getPrefixRandom: async function(algo) { - const prefixrandom = await random.getRandomBytes(cipher[algo].blockSize); - const repeat = new Uint8Array([prefixrandom[prefixrandom.length - 2], prefixrandom[prefixrandom.length - 1]]); - return util.concat([prefixrandom, repeat]); - }, +/** + * Parse private key material in binary form to get the key parameters + * @param {module:enums.publicKey} algo The key algorithm + * @param {Uint8Array} bytes The key material to parse + * @param {Object} publicParams (ECC only) public params, needed to format some private params + * @returns { read: Number, privateParams: Object } number of read bytes plus the key parameters referenced by name + */ +export function parsePrivateKeyParams(algo, bytes, publicParams) { + let read = 0; + switch (algo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaSign: { + const d = util.readMPI(bytes.subarray(read)); read += d.length + 2; + const p = util.readMPI(bytes.subarray(read)); read += p.length + 2; + const q = util.readMPI(bytes.subarray(read)); read += q.length + 2; + const u = util.readMPI(bytes.subarray(read)); read += u.length + 2; + return { read, privateParams: { d, p, q, u } }; + } + case enums.publicKey.dsa: + case enums.publicKey.elgamal: { + const x = util.readMPI(bytes.subarray(read)); read += x.length + 2; + return { read, privateParams: { x } }; + } + case enums.publicKey.ecdsa: + case enums.publicKey.ecdh: { + const curve = new Curve(publicParams.oid); + let d = util.readMPI(bytes.subarray(read)); read += d.length + 2; + d = util.padToLength(d, curve.payloadSize); + return { read, privateParams: { d } }; + } + case enums.publicKey.eddsa: { + let seed = util.readMPI(bytes.subarray(read)); read += seed.length + 2; + seed = util.padToLength(seed, 32); + return { read, privateParams: { seed } }; + } + default: + throw new Error('Invalid public key encryption algorithm.'); + } +} - /** - * Generating a session key for the specified symmetric algorithm - * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. - * @param {module:enums.symmetric} algo Symmetric encryption algorithm - * @returns {Uint8Array} Random bytes as a string to be used as a key - * @async - */ - generateSessionKey: function(algo) { - return random.getRandomBytes(cipher[algo].keySize); - }, +/** Returns the types comprising the encrypted session key of an algorithm + * @param {module:enums.publicKey} algo The public key algorithm + * @returns {Array} The array of types + */ +export function getEncSessionKeyParamTypes(algo) { + switch (algo) { + // Algorithm-Specific Fields for RSA encrypted session keys: + // - MPI of RSA encrypted value m**e mod n. + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + return [type_mpi]; - constructParams: constructParams -}; + // Algorithm-Specific Fields for Elgamal encrypted session keys: + // - MPI of Elgamal value g**k mod p + // - MPI of Elgamal value m * y**k mod p + case enums.publicKey.elgamal: + return [type_mpi, type_mpi]; + // Algorithm-Specific Fields for ECDH encrypted session keys: + // - MPI containing the ephemeral key used to establish the shared secret + // - ECDH Symmetric Key + case enums.publicKey.ecdh: + return [type_mpi, type_ecdh_symkey]; + default: + throw new Error('Invalid public key encryption algorithm.'); + } +} + +/** + * Convert params to MPI and serializes them in the proper order + * @param {module:enums.publicKey} algo The public key algorithm + * @param {Object} params The key parameters indexed by name + * @returns {Uint8Array} The array containing the MPIs + */ +export function serializeKeyParams(algo, params) { + const orderedParams = Object.keys(params).map(name => { + const param = params[name]; + return util.isUint8Array(param) ? util.uint8ArrayToMpi(param) : param.write(); + }); + return util.concatUint8Array(orderedParams); +} + +/** + * Generate algorithm-specific key parameters + * @param {module:enums.publicKey} algo The public key algorithm + * @param {Integer} bits Bit length for RSA keys + * @param {module:type/oid} oid Object identifier for ECC keys + * @returns { publicParams, privateParams: {Object} } The parameters referenced by name + * @async + */ +export function generateParams(algo, bits, oid) { + switch (algo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaSign: { + return publicKey.rsa.generate(bits, 65537).then(({ n, e, d, p, q, u }) => ({ + privateParams: { d, p, q, u }, + publicParams: { n, e } + })); + } + case enums.publicKey.ecdsa: + return publicKey.elliptic.generate(oid).then(({ oid, Q, secret }) => ({ + privateParams: { d: secret }, + publicParams: { oid: new OID(oid), Q } + })); + case enums.publicKey.eddsa: + return publicKey.elliptic.generate(oid).then(({ oid, Q, secret }) => ({ + privateParams: { seed: secret }, + publicParams: { oid: new OID(oid), Q } + })); + case enums.publicKey.ecdh: + return publicKey.elliptic.generate(oid).then(({ oid, Q, secret, hash, cipher }) => ({ + privateParams: { d: secret }, + publicParams: { + oid: new OID(oid), + Q, + kdfParams: new KDFParams({ hash, cipher }) + } + })); + case enums.publicKey.dsa: + case enums.publicKey.elgamal: + throw new Error('Unsupported algorithm for key generation.'); + default: + throw new Error('Invalid public key algorithm.'); + } +} + +/** + * Validate algorithm-specific key parameters + * @param {module:enums.publicKey} algo The public key algorithm + * @param {Object} publicParams Algorithm-specific public key parameters + * @param {Object} privateParams Algorithm-specific private key parameters + * @returns {Promise} whether the parameters are valid + * @async + */ +export async function validateParams(algo, publicParams, privateParams) { + if (!publicParams || !privateParams) { + throw new Error('Missing key parameters'); + } + switch (algo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaSign: { + const { n, e } = publicParams; + const { d, p, q, u } = privateParams; + return publicKey.rsa.validateParams(n, e, d, p, q, u); + } + case enums.publicKey.dsa: { + const { p, q, g, y } = publicParams; + const { x } = privateParams; + return publicKey.dsa.validateParams(p, q, g, y, x); + } + case enums.publicKey.elgamal: { + const { p, g, y } = publicParams; + const { x } = privateParams; + return publicKey.elgamal.validateParams(p, g, y, x); + } + case enums.publicKey.ecdsa: + case enums.publicKey.ecdh: { + const algoModule = publicKey.elliptic[enums.read(enums.publicKey, algo)]; + const { oid, Q } = publicParams; + const { d } = privateParams; + return algoModule.validateParams(oid, Q, d); + } + case enums.publicKey.eddsa: { + const { oid, Q } = publicParams; + const { seed } = privateParams; + return publicKey.elliptic.eddsa.validateParams(oid, Q, seed); + } + default: + throw new Error('Invalid public key algorithm.'); + } +} + +/** + * Generates a random byte prefix for the specified algorithm + * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. + * @param {module:enums.symmetric} algo Symmetric encryption algorithm + * @returns {Uint8Array} Random bytes with length equal to the block size of the cipher, plus the last two bytes repeated. + * @async + */ +export async function getPrefixRandom(algo) { + const prefixrandom = await getRandomBytes(cipher[algo].blockSize); + const repeat = new Uint8Array([prefixrandom[prefixrandom.length - 2], prefixrandom[prefixrandom.length - 1]]); + return util.concat([prefixrandom, repeat]); +} + +/** + * Generating a session key for the specified symmetric algorithm + * See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms. + * @param {module:enums.symmetric} algo Symmetric encryption algorithm + * @returns {Uint8Array} Random bytes as a string to be used as a key + * @async + */ +export function generateSessionKey(algo) { + return getRandomBytes(cipher[algo].keySize); +} diff --git a/src/crypto/index.js b/src/crypto/index.js index 398006a2..ac08ed0f 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -9,19 +9,19 @@ * @module crypto */ -import cipher from './cipher'; +import * as cipher from './cipher'; import hash from './hash'; -import cfb from './cfb'; +import * as cfb from './cfb'; import gcm from './gcm'; import eax from './eax'; import ocb from './ocb'; import publicKey from './public_key'; -import signature from './signature'; -import random from './random'; -import pkcs1 from './pkcs1'; -import pkcs5 from './pkcs5'; -import crypto from './crypto'; -import aes_kw from './aes_kw'; +import * as signature from './signature'; +import * as random from './random'; +import * as pkcs1 from './pkcs1'; +import * as pkcs5 from './pkcs5'; +import * as crypto from './crypto'; +import * as aes_kw from './aes_kw'; // TODO move cfb and gcm to cipher const mod = { diff --git a/src/crypto/ocb.js b/src/crypto/ocb.js index f193e2af..8547e27e 100644 --- a/src/crypto/ocb.js +++ b/src/crypto/ocb.js @@ -22,7 +22,7 @@ * @module crypto/ocb */ -import ciphers from './cipher'; +import * as ciphers from './cipher'; import util from '../util'; diff --git a/src/crypto/pkcs1.js b/src/crypto/pkcs1.js index 03dc400a..d91f234e 100644 --- a/src/crypto/pkcs1.js +++ b/src/crypto/pkcs1.js @@ -25,14 +25,9 @@ * @module crypto/pkcs1 */ -import random from './random'; +import { getRandomBytes } from './random'; import hash from './hash'; -/** @namespace */ -const eme = {}; -/** @namespace */ -const emsa = {}; - /** * ASN1 object identifiers for hashes * @see {@link https://tools.ietf.org/html/rfc4880#section-5.2.2} @@ -62,7 +57,7 @@ async function getPkcs1Padding(length) { const result = new Uint8Array(length); let count = 0; while (count < length) { - const randomBytes = await random.getRandomBytes(length - count); + const randomBytes = await getRandomBytes(length - count); for (let i = 0; i < randomBytes.length; i++) { if (randomBytes[i] !== 0) { result[count++] = randomBytes[i]; @@ -80,7 +75,7 @@ async function getPkcs1Padding(length) { * @returns {Promise} EME-PKCS1 padded message * @async */ -eme.encode = async function(message, keyLength) { +export async function emeEncode(message, keyLength) { const mLength = message.length; // length checking if (mLength > keyLength - 11) { @@ -98,7 +93,7 @@ eme.encode = async function(message, keyLength) { // 0x00 bytes encoded.set(message, keyLength - mLength); return encoded; -}; +} /** * Decode a EME-PKCS1-v1_5 padded message @@ -106,7 +101,7 @@ eme.encode = async function(message, keyLength) { * @param {Uint8Array} encoded encoded message bytes * @returns {Uint8Array} message */ -eme.decode = function(encoded) { +export function emeDecode(encoded) { let i = 2; while (encoded[i] !== 0 && i < encoded.length) { i++; @@ -117,7 +112,7 @@ eme.decode = function(encoded) { return encoded.subarray(i); } throw new Error('Decryption error'); -}; +} /** * Create a EMSA-PKCS1-v1_5 padded message @@ -127,7 +122,7 @@ eme.decode = function(encoded) { * @param {Integer} emLen intended length in octets of the encoded message * @returns {Uint8Array} encoded message */ -emsa.encode = async function(algo, hashed, emLen) { +export async function emsaEncode(algo, hashed, emLen) { let i; if (hashed.length !== hash.getHashByteLength(algo)) { throw new Error('Invalid hash length'); @@ -155,6 +150,4 @@ emsa.encode = async function(algo, hashed, emLen) { EM.set(hashPrefix, emLen - tLen); EM.set(hashed, emLen - hashed.length); return EM; -}; - -export default { eme, emsa }; +} diff --git a/src/crypto/pkcs5.js b/src/crypto/pkcs5.js index bfa5c0df..acb1577b 100644 --- a/src/crypto/pkcs5.js +++ b/src/crypto/pkcs5.js @@ -28,7 +28,7 @@ import util from '../util'; * @param {Uint8Array} message message to pad * @returns {Uint8Array} padded message */ -function encode(message) { +export function encode(message) { const c = 8 - (message.length % 8); const padded = new Uint8Array(message.length + c).fill(c); padded.set(message); @@ -40,7 +40,7 @@ function encode(message) { * @param {Uint8Array} message message to remove padding from * @returns {Uint8Array} message without padding */ -function decode(message) { +export function decode(message) { const len = message.length; if (len > 0) { const c = message[len - 1]; @@ -54,5 +54,3 @@ function decode(message) { } throw new Error('Invalid padding'); } - -export default { encode, decode }; diff --git a/src/crypto/public_key/dsa.js b/src/crypto/public_key/dsa.js index 83dca363..a501eb39 100644 --- a/src/crypto/public_key/dsa.js +++ b/src/crypto/public_key/dsa.js @@ -21,9 +21,9 @@ * @requires util * @module crypto/public_key/dsa */ -import random from '../random'; +import { getRandomBigInteger } from '../random'; import util from '../../util'; -import prime from './prime'; +import { isProbablePrime } from './prime'; /* TODO regarding the hash function, read: @@ -31,170 +31,168 @@ import prime from './prime'; https://tools.ietf.org/html/rfc4880#section-14 */ -export default { - /** - * DSA Sign function - * @param {Integer} hash_algo - * @param {Uint8Array} hashed - * @param {Uint8Array} g - * @param {Uint8Array} p - * @param {Uint8Array} q - * @param {Uint8Array} x - * @returns {{ r: Uint8Array, s: Uint8Array }} - * @async - */ - sign: async function(hash_algo, hashed, g, p, q, x) { - const BigInteger = await util.getBigInteger(); - const one = new BigInteger(1); - p = new BigInteger(p); - q = new BigInteger(q); - g = new BigInteger(g); - x = new BigInteger(x); +/** + * DSA Sign function + * @param {Integer} hash_algo + * @param {Uint8Array} hashed + * @param {Uint8Array} g + * @param {Uint8Array} p + * @param {Uint8Array} q + * @param {Uint8Array} x + * @returns {{ r: Uint8Array, s: Uint8Array }} + * @async + */ +export async function sign(hash_algo, hashed, g, p, q, x) { + const BigInteger = await util.getBigInteger(); + const one = new BigInteger(1); + p = new BigInteger(p); + q = new BigInteger(q); + g = new BigInteger(g); + x = new BigInteger(x); - let k; - let r; - let s; - let t; - g = g.mod(p); - x = x.mod(q); - // If the output size of the chosen hash is larger than the number of - // bits of q, the hash result is truncated to fit by taking the number - // of leftmost bits equal to the number of bits of q. This (possibly - // truncated) hash function result is treated as a number and used - // directly in the DSA signature algorithm. - const h = new BigInteger(hashed.subarray(0, q.byteLength())).mod(q); - // FIPS-186-4, section 4.6: - // The values of r and s shall be checked to determine if r = 0 or s = 0. - // If either r = 0 or s = 0, a new value of k shall be generated, and the - // signature shall be recalculated. It is extremely unlikely that r = 0 - // or s = 0 if signatures are generated properly. - while (true) { - // See Appendix B here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf - k = await random.getRandomBigInteger(one, q); // returns in [1, q-1] - r = g.modExp(k, p).imod(q); // (g**k mod p) mod q - if (r.isZero()) { - continue; - } - const xr = x.mul(r).imod(q); - t = h.add(xr).imod(q); // H(m) + x*r mod q - s = k.modInv(q).imul(t).imod(q); // k**-1 * (H(m) + x*r) mod q - if (s.isZero()) { - continue; - } - break; + let k; + let r; + let s; + let t; + g = g.mod(p); + x = x.mod(q); + // If the output size of the chosen hash is larger than the number of + // bits of q, the hash result is truncated to fit by taking the number + // of leftmost bits equal to the number of bits of q. This (possibly + // truncated) hash function result is treated as a number and used + // directly in the DSA signature algorithm. + const h = new BigInteger(hashed.subarray(0, q.byteLength())).mod(q); + // FIPS-186-4, section 4.6: + // The values of r and s shall be checked to determine if r = 0 or s = 0. + // If either r = 0 or s = 0, a new value of k shall be generated, and the + // signature shall be recalculated. It is extremely unlikely that r = 0 + // or s = 0 if signatures are generated properly. + while (true) { + // See Appendix B here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf + k = await getRandomBigInteger(one, q); // returns in [1, q-1] + r = g.modExp(k, p).imod(q); // (g**k mod p) mod q + if (r.isZero()) { + continue; } - return { - r: r.toUint8Array('be', q.byteLength()), - s: s.toUint8Array('be', q.byteLength()) - }; - }, - - /** - * DSA Verify function - * @param {Integer} hash_algo - * @param {Uint8Array} r - * @param {Uint8Array} s - * @param {Uint8Array} hashed - * @param {Uint8Array} g - * @param {Uint8Array} p - * @param {Uint8Array} q - * @param {Uint8Array} y - * @returns {boolean} - * @async - */ - verify: async function(hash_algo, r, s, hashed, g, p, q, y) { - const BigInteger = await util.getBigInteger(); - const zero = new BigInteger(0); - r = new BigInteger(r); - s = new BigInteger(s); - - p = new BigInteger(p); - q = new BigInteger(q); - g = new BigInteger(g); - y = new BigInteger(y); - - if (r.lte(zero) || r.gte(q) || - s.lte(zero) || s.gte(q)) { - util.printDebug("invalid DSA Signature"); - return false; + const xr = x.mul(r).imod(q); + t = h.add(xr).imod(q); // H(m) + x*r mod q + s = k.modInv(q).imul(t).imod(q); // k**-1 * (H(m) + x*r) mod q + if (s.isZero()) { + continue; } - const h = new BigInteger(hashed.subarray(0, q.byteLength())).imod(q); - const w = s.modInv(q); // s**-1 mod q - if (w.isZero()) { - util.printDebug("invalid DSA Signature"); - return false; - } - - g = g.mod(p); - y = y.mod(p); - const u1 = h.mul(w).imod(q); // H(m) * w mod q - const u2 = r.mul(w).imod(q); // r * w mod q - const t1 = g.modExp(u1, p); // g**u1 mod p - const t2 = y.modExp(u2, p); // y**u2 mod p - const v = t1.mul(t2).imod(p).imod(q); // (g**u1 * y**u2 mod p) mod q - return v.equal(r); - }, - - /** - * Validate DSA parameters - * @param {Uint8Array} p DSA prime - * @param {Uint8Array} q DSA group order - * @param {Uint8Array} g DSA sub-group generator - * @param {Uint8Array} y DSA public key - * @param {Uint8Array} x DSA private key - * @returns {Promise} whether params are valid - * @async - */ - validateParams: async function (p, q, g, y, x) { - const BigInteger = await util.getBigInteger(); - p = new BigInteger(p); - q = new BigInteger(q); - g = new BigInteger(g); - y = new BigInteger(y); - const one = new BigInteger(1); - // Check that 1 < g < p - if (g.lte(one) || g.gte(p)) { - return false; - } - - /** - * Check that subgroup order q divides p-1 - */ - if (!p.dec().mod(q).isZero()) { - return false; - } - - /** - * g has order q - * Check that g ** q = 1 mod p - */ - if (!g.modExp(q, p).isOne()) { - return false; - } - - /** - * Check q is large and probably prime (we mainly want to avoid small factors) - */ - const qSize = new BigInteger(q.bitLength()); - const n150 = new BigInteger(150); - if (qSize.lt(n150) || !(await prime.isProbablePrime(q, null, 32))) { - return false; - } - - /** - * Re-derive public key y' = g ** x mod p - * Expect y == y' - * - * Blinded exponentiation computes g**{rq + x} to compare to y - */ - x = new BigInteger(x); - const two = new BigInteger(2); - const r = await random.getRandomBigInteger(two.leftShift(qSize.dec()), two.leftShift(qSize)); // draw r of same size as q - const rqx = q.mul(r).add(x); - if (!y.equal(g.modExp(rqx, p))) { - return false; - } - - return true; + break; } -}; + return { + r: r.toUint8Array('be', q.byteLength()), + s: s.toUint8Array('be', q.byteLength()) + }; +} + +/** + * DSA Verify function + * @param {Integer} hash_algo + * @param {Uint8Array} r + * @param {Uint8Array} s + * @param {Uint8Array} hashed + * @param {Uint8Array} g + * @param {Uint8Array} p + * @param {Uint8Array} q + * @param {Uint8Array} y + * @returns {boolean} + * @async + */ +export async function verify(hash_algo, r, s, hashed, g, p, q, y) { + const BigInteger = await util.getBigInteger(); + const zero = new BigInteger(0); + r = new BigInteger(r); + s = new BigInteger(s); + + p = new BigInteger(p); + q = new BigInteger(q); + g = new BigInteger(g); + y = new BigInteger(y); + + if (r.lte(zero) || r.gte(q) || + s.lte(zero) || s.gte(q)) { + util.printDebug("invalid DSA Signature"); + return false; + } + const h = new BigInteger(hashed.subarray(0, q.byteLength())).imod(q); + const w = s.modInv(q); // s**-1 mod q + if (w.isZero()) { + util.printDebug("invalid DSA Signature"); + return false; + } + + g = g.mod(p); + y = y.mod(p); + const u1 = h.mul(w).imod(q); // H(m) * w mod q + const u2 = r.mul(w).imod(q); // r * w mod q + const t1 = g.modExp(u1, p); // g**u1 mod p + const t2 = y.modExp(u2, p); // y**u2 mod p + const v = t1.mul(t2).imod(p).imod(q); // (g**u1 * y**u2 mod p) mod q + return v.equal(r); +} + +/** + * Validate DSA parameters + * @param {Uint8Array} p DSA prime + * @param {Uint8Array} q DSA group order + * @param {Uint8Array} g DSA sub-group generator + * @param {Uint8Array} y DSA public key + * @param {Uint8Array} x DSA private key + * @returns {Promise} whether params are valid + * @async + */ +export async function validateParams(p, q, g, y, x) { + const BigInteger = await util.getBigInteger(); + p = new BigInteger(p); + q = new BigInteger(q); + g = new BigInteger(g); + y = new BigInteger(y); + const one = new BigInteger(1); + // Check that 1 < g < p + if (g.lte(one) || g.gte(p)) { + return false; + } + + /** + * Check that subgroup order q divides p-1 + */ + if (!p.dec().mod(q).isZero()) { + return false; + } + + /** + * g has order q + * Check that g ** q = 1 mod p + */ + if (!g.modExp(q, p).isOne()) { + return false; + } + + /** + * Check q is large and probably prime (we mainly want to avoid small factors) + */ + const qSize = new BigInteger(q.bitLength()); + const n150 = new BigInteger(150); + if (qSize.lt(n150) || !(await isProbablePrime(q, null, 32))) { + return false; + } + + /** + * Re-derive public key y' = g ** x mod p + * Expect y == y' + * + * Blinded exponentiation computes g**{rq + x} to compare to y + */ + x = new BigInteger(x); + const two = new BigInteger(2); + const r = await getRandomBigInteger(two.leftShift(qSize.dec()), two.leftShift(qSize)); // draw r of same size as q + const rqx = q.mul(r).add(x); + if (!y.equal(g.modExp(rqx, p))) { + return false; + } + + return true; +} diff --git a/src/crypto/public_key/elgamal.js b/src/crypto/public_key/elgamal.js index 9e611cc0..81fa505b 100644 --- a/src/crypto/public_key/elgamal.js +++ b/src/crypto/public_key/elgamal.js @@ -23,125 +23,123 @@ */ import util from '../../util'; -import random from '../random'; -import pkcs1 from '../pkcs1'; +import { getRandomBigInteger } from '../random'; +import { emeEncode, emeDecode } from '../pkcs1'; -export default { - /** - * ElGamal Encryption function - * Note that in OpenPGP, the message needs to be padded with PKCS#1 (same as RSA) - * @param {Uint8Array} data to be padded and encrypted - * @param {Uint8Array} p - * @param {Uint8Array} g - * @param {Uint8Array} y - * @returns {{ c1: Uint8Array, c2: Uint8Array }} - * @async - */ - encrypt: async function(data, p, g, y) { - const BigInteger = await util.getBigInteger(); - p = new BigInteger(p); - g = new BigInteger(g); - y = new BigInteger(y); +/** + * ElGamal Encryption function + * Note that in OpenPGP, the message needs to be padded with PKCS#1 (same as RSA) + * @param {Uint8Array} data to be padded and encrypted + * @param {Uint8Array} p + * @param {Uint8Array} g + * @param {Uint8Array} y + * @returns {{ c1: Uint8Array, c2: Uint8Array }} + * @async + */ +export async function encrypt(data, p, g, y) { + const BigInteger = await util.getBigInteger(); + p = new BigInteger(p); + g = new BigInteger(g); + y = new BigInteger(y); - const padded = await pkcs1.eme.encode(data, p.byteLength()); - const m = new BigInteger(padded); + const padded = await emeEncode(data, p.byteLength()); + const m = new BigInteger(padded); - // OpenPGP uses a "special" version of ElGamal where g is generator of the full group Z/pZ* - // hence g has order p-1, and to avoid that k = 0 mod p-1, we need to pick k in [1, p-2] - const k = await random.getRandomBigInteger(new BigInteger(1), p.dec()); - return { - c1: g.modExp(k, p).toUint8Array(), - c2: y.modExp(k, p).imul(m).imod(p).toUint8Array() - }; - }, + // OpenPGP uses a "special" version of ElGamal where g is generator of the full group Z/pZ* + // hence g has order p-1, and to avoid that k = 0 mod p-1, we need to pick k in [1, p-2] + const k = await getRandomBigInteger(new BigInteger(1), p.dec()); + return { + c1: g.modExp(k, p).toUint8Array(), + c2: y.modExp(k, p).imul(m).imod(p).toUint8Array() + }; +} - /** - * ElGamal Encryption function - * @param {Uint8Array} c1 - * @param {Uint8Array} c2 - * @param {Uint8Array} p - * @param {Uint8Array} x - * @returns {Uint8Array} unpadded message - * @async - */ - decrypt: async function(c1, c2, p, x) { - const BigInteger = await util.getBigInteger(); - c1 = new BigInteger(c1); - c2 = new BigInteger(c2); - p = new BigInteger(p); - x = new BigInteger(x); +/** + * ElGamal Encryption function + * @param {Uint8Array} c1 + * @param {Uint8Array} c2 + * @param {Uint8Array} p + * @param {Uint8Array} x + * @returns {Uint8Array} unpadded message + * @async + */ +export async function decrypt(c1, c2, p, x) { + const BigInteger = await util.getBigInteger(); + c1 = new BigInteger(c1); + c2 = new BigInteger(c2); + p = new BigInteger(p); + x = new BigInteger(x); - const padded = c1.modExp(x, p).modInv(p).imul(c2).imod(p); - return pkcs1.eme.decode(padded.toUint8Array('be', p.byteLength())); - }, + const padded = c1.modExp(x, p).modInv(p).imul(c2).imod(p); + return emeDecode(padded.toUint8Array('be', p.byteLength())); +} - /** - * Validate ElGamal parameters - * @param {Uint8Array} p ElGamal prime - * @param {Uint8Array} g ElGamal group generator - * @param {Uint8Array} y ElGamal public key - * @param {Uint8Array} x ElGamal private exponent - * @returns {Promise} whether params are valid - * @async - */ - validateParams: async function (p, g, y, x) { - const BigInteger = await util.getBigInteger(); - p = new BigInteger(p); - g = new BigInteger(g); - y = new BigInteger(y); +/** + * Validate ElGamal parameters + * @param {Uint8Array} p ElGamal prime + * @param {Uint8Array} g ElGamal group generator + * @param {Uint8Array} y ElGamal public key + * @param {Uint8Array} x ElGamal private exponent + * @returns {Promise} whether params are valid + * @async + */ +export async function validateParams(p, g, y, x) { + const BigInteger = await util.getBigInteger(); + p = new BigInteger(p); + g = new BigInteger(g); + y = new BigInteger(y); - const one = new BigInteger(1); - // Check that 1 < g < p - if (g.lte(one) || g.gte(p)) { - return false; - } - - // Expect p-1 to be large - const pSize = new BigInteger(p.bitLength()); - const n1023 = new BigInteger(1023); - if (pSize.lt(n1023)) { - return false; - } - - /** - * g should have order p-1 - * Check that g ** (p-1) = 1 mod p - */ - if (!g.modExp(p.dec(), p).isOne()) { - return false; - } - - /** - * Since p-1 is not prime, g might have a smaller order that divides p-1 - * We want to make sure that the order is large enough to hinder a small subgroup attack - * - * We just check g**i != 1 for all i up to a threshold - */ - let res = g; - const i = new BigInteger(1); - const threshold = new BigInteger(2).leftShift(new BigInteger(17)); // we want order > threshold - while (i.lt(threshold)) { - res = res.mul(g).imod(p); - if (res.isOne()) { - return false; - } - i.iinc(); - } - - /** - * Re-derive public key y' = g ** x mod p - * Expect y == y' - * - * Blinded exponentiation computes g**{r(p-1) + x} to compare to y - */ - x = new BigInteger(x); - const two = new BigInteger(2); - const r = await random.getRandomBigInteger(two.leftShift(pSize.dec()), two.leftShift(pSize)); // draw r of same size as p-1 - const rqx = p.dec().imul(r).iadd(x); - if (!y.equal(g.modExp(rqx, p))) { - return false; - } - - return true; + const one = new BigInteger(1); + // Check that 1 < g < p + if (g.lte(one) || g.gte(p)) { + return false; } -}; + + // Expect p-1 to be large + const pSize = new BigInteger(p.bitLength()); + const n1023 = new BigInteger(1023); + if (pSize.lt(n1023)) { + return false; + } + + /** + * g should have order p-1 + * Check that g ** (p-1) = 1 mod p + */ + if (!g.modExp(p.dec(), p).isOne()) { + return false; + } + + /** + * Since p-1 is not prime, g might have a smaller order that divides p-1 + * We want to make sure that the order is large enough to hinder a small subgroup attack + * + * We just check g**i != 1 for all i up to a threshold + */ + let res = g; + const i = new BigInteger(1); + const threshold = new BigInteger(2).leftShift(new BigInteger(17)); // we want order > threshold + while (i.lt(threshold)) { + res = res.mul(g).imod(p); + if (res.isOne()) { + return false; + } + i.iinc(); + } + + /** + * Re-derive public key y' = g ** x mod p + * Expect y == y' + * + * Blinded exponentiation computes g**{r(p-1) + x} to compare to y + */ + x = new BigInteger(x); + const two = new BigInteger(2); + const r = await getRandomBigInteger(two.leftShift(pSize.dec()), two.leftShift(pSize)); // draw r of same size as p-1 + const rqx = p.dec().imul(r).iadd(x); + if (!y.equal(g.modExp(rqx, p))) { + return false; + } + + return true; +} diff --git a/src/crypto/public_key/elliptic/curves.js b/src/crypto/public_key/elliptic/curves.js index f99c9a6a..53d69fa1 100644 --- a/src/crypto/public_key/elliptic/curves.js +++ b/src/crypto/public_key/elliptic/curves.js @@ -28,7 +28,7 @@ */ import nacl from 'tweetnacl/nacl-fast-light.js'; -import random from '../../random'; +import { getRandomBytes } from '../../random'; import enums from '../../../enums'; import util from '../../../util'; import OID from '../../../type/oid'; @@ -186,7 +186,7 @@ class Curve { case 'node': return nodeGenKeyPair(this.name); case 'curve25519': { - const privateKey = await random.getRandomBytes(32); + const privateKey = await getRandomBytes(32); privateKey[0] = (privateKey[0] & 127) | 64; privateKey[31] &= 248; const secretKey = privateKey.slice().reverse(); @@ -195,7 +195,7 @@ class Curve { return { publicKey, privateKey }; } case 'ed25519': { - const privateKey = await random.getRandomBytes(32); + const privateKey = await getRandomBytes(32); const keyPair = nacl.sign.keyPair.fromSeed(privateKey); const publicKey = util.concatUint8Array([new Uint8Array([0x40]), keyPair.publicKey]); return { publicKey, privateKey }; @@ -203,7 +203,7 @@ class Curve { } const indutnyCurve = await getIndutnyCurve(this.name); keyPair = await indutnyCurve.genKeyPair({ - entropy: util.uint8ArrayToStr(await random.getRandomBytes(32)) + entropy: util.uint8ArrayToStr(await getRandomBytes(32)) }); return { publicKey: new Uint8Array(keyPair.getPublic('array', false)), privateKey: keyPair.getPrivate().toArrayLike(Uint8Array) }; } @@ -291,10 +291,8 @@ async function validateStandardParams(algo, oid, Q, d) { return true; } -export default Curve; - export { - curves, webCurves, nodeCurves, generate, getPreferredHashAlgo, jwkToRawPublic, rawPublicToJwk, privateToJwk, validateStandardParams + Curve, curves, webCurves, nodeCurves, generate, getPreferredHashAlgo, jwkToRawPublic, rawPublicToJwk, privateToJwk, validateStandardParams }; ////////////////////////// diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 3f4a2be0..53a4852d 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -30,14 +30,14 @@ */ import nacl from 'tweetnacl/nacl-fast-light.js'; -import Curve, { jwkToRawPublic, rawPublicToJwk, privateToJwk, validateStandardParams } from './curves'; -import aes_kw from '../../aes_kw'; -import cipher from '../../cipher'; -import random from '../../random'; +import { Curve, jwkToRawPublic, rawPublicToJwk, privateToJwk, validateStandardParams } from './curves'; +import * as aes_kw from '../../aes_kw'; +import * as cipher from '../../cipher'; +import { getRandomBytes } from '../../random'; import hash from '../../hash'; import enums from '../../../enums'; import util from '../../../util'; -import pkcs5 from '../../pkcs5'; +import * as pkcs5 from '../../pkcs5'; import MPI from '../../../type/mpi'; import { keyFromPublic, keyFromPrivate, getIndutnyCurve } from './indutnyKey'; @@ -52,7 +52,7 @@ const nodeCrypto = util.getNodeCrypto(); * @returns {Promise} whether params are valid * @async */ -async function validateParams(oid, Q, d) { +export async function validateParams(oid, Q, d) { return validateStandardParams(enums.publicKey.ecdh, oid, Q, d); } @@ -102,7 +102,7 @@ async function kdf(hash_algo, X, length, param, stripLeading = false, stripTrail async function genPublicEphemeralKey(curve, Q) { switch (curve.type) { case 'curve25519': { - const d = await random.getRandomBytes(32); + const d = await getRandomBytes(32); const { secretKey, sharedKey } = await genPrivateEphemeralKey(curve, Q, null, d); let { publicKey } = nacl.box.keyPair.fromSecretKey(secretKey); publicKey = util.concatUint8Array([new Uint8Array([0x40]), publicKey]); @@ -134,7 +134,7 @@ async function genPublicEphemeralKey(curve, Q) { * @returns {Promise<{publicKey: Uint8Array, wrappedKey: Uint8Array}>} * @async */ -async function encrypt(oid, kdfParams, data, Q, fingerprint) { +export async function encrypt(oid, kdfParams, data, Q, fingerprint) { const m = new MPI(pkcs5.encode(data)); const curve = new Curve(oid); @@ -196,7 +196,7 @@ async function genPrivateEphemeralKey(curve, V, Q, d) { * @returns {Promise} Value derived from session key * @async */ -async function decrypt(oid, kdfParams, V, C, Q, d, fingerprint) { +export async function decrypt(oid, kdfParams, V, C, Q, d, fingerprint) { const curve = new Curve(oid); const { sharedKey } = await genPrivateEphemeralKey(curve, V, Q, d); const param = buildEcdhParam(enums.publicKey.ecdh, oid, kdfParams, fingerprint); @@ -387,5 +387,3 @@ async function nodePublicEphemeralKey(curve, Q) { const publicKey = new Uint8Array(sender.getPublicKey()); return { publicKey, sharedKey }; } - -export default { encrypt, decrypt, genPublicEphemeralKey, genPrivateEphemeralKey, buildEcdhParam, kdf, webPublicEphemeralKey, webPrivateEphemeralKey, ellipticPublicEphemeralKey, ellipticPrivateEphemeralKey, nodePublicEphemeralKey, nodePrivateEphemeralKey, validateParams }; diff --git a/src/crypto/public_key/elliptic/ecdsa.js b/src/crypto/public_key/elliptic/ecdsa.js index 6efeebc8..063bbd5e 100644 --- a/src/crypto/public_key/elliptic/ecdsa.js +++ b/src/crypto/public_key/elliptic/ecdsa.js @@ -26,9 +26,9 @@ import enums from '../../../enums'; import util from '../../../util'; -import random from '../../random'; +import { getRandomBytes } from '../../random'; import hash from '../../hash'; -import Curve, { webCurves, privateToJwk, rawPublicToJwk, validateStandardParams } from './curves'; +import { Curve, webCurves, privateToJwk, rawPublicToJwk, validateStandardParams } from './curves'; import { getIndutnyCurve, keyFromPrivate, keyFromPublic } from './indutnyKey'; const webCrypto = util.getWebCrypto(); @@ -46,7 +46,7 @@ const nodeCrypto = util.getNodeCrypto(); * s: Uint8Array}} Signature of the message * @async */ -async function sign(oid, hash_algo, message, publicKey, privateKey, hashed) { +export async function sign(oid, hash_algo, message, publicKey, privateKey, hashed) { const curve = new Curve(oid); if (message && !util.isStream(message)) { const keyPair = { publicKey, privateKey }; @@ -91,7 +91,7 @@ async function sign(oid, hash_algo, message, publicKey, privateKey, hashed) { * @returns {Boolean} * @async */ -async function verify(oid, hash_algo, signature, message, publicKey, hashed) { +export async function verify(oid, hash_algo, signature, message, publicKey, hashed) { const curve = new Curve(oid); if (message && !util.isStream(message)) { switch (curve.type) { @@ -125,7 +125,7 @@ async function verify(oid, hash_algo, signature, message, publicKey, hashed) { * @returns {Promise} whether params are valid * @async */ -async function validateParams(oid, Q, d) { +export async function validateParams(oid, Q, d) { const curve = new Curve(oid); // Reject curves x25519 and ed25519 if (curve.keyType !== enums.publicKey.ecdsa) { @@ -137,7 +137,7 @@ async function validateParams(oid, Q, d) { switch (curve.type) { case 'web': case 'node': { - const message = await random.getRandomBytes(8); + const message = await getRandomBytes(8); const hashAlgo = enums.hash.sha256; const hashed = await hash.digest(hashAlgo, message); try { @@ -152,8 +152,6 @@ async function validateParams(oid, Q, d) { } } -export default { sign, verify, ellipticVerify, ellipticSign, validateParams }; - ////////////////////////// // // diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index 01cde9ee..9c2e99a4 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -42,7 +42,7 @@ nacl.hash = bytes => new Uint8Array(sha512().update(bytes).digest()); * S: Uint8Array}} Signature of the message * @async */ -async function sign(oid, hash_algo, message, publicKey, privateKey, hashed) { +export async function sign(oid, hash_algo, message, publicKey, privateKey, hashed) { const secretKey = util.concatUint8Array([privateKey, publicKey.subarray(1)]); const signature = nacl.sign.detached(hashed, secretKey); // EdDSA signature params are returned in little-endian format @@ -64,11 +64,10 @@ async function sign(oid, hash_algo, message, publicKey, privateKey, hashed) { * @returns {Boolean} * @async */ -async function verify(oid, hash_algo, { R, S }, m, publicKey, hashed) { +export async function verify(oid, hash_algo, { R, S }, m, publicKey, hashed) { const signature = util.concatUint8Array([R, S]); return nacl.sign.detached.verify(hashed, signature, publicKey.subarray(1)); } - /** * Validate EdDSA parameters * @param {module:type/oid} oid Elliptic curve object identifier @@ -77,7 +76,7 @@ async function verify(oid, hash_algo, { R, S }, m, publicKey, hashed) { * @returns {Promise} whether params are valid * @async */ -async function validateParams(oid, Q, k) { +export async function validateParams(oid, Q, k) { // Check whether the given curve is supported if (oid.getName() !== 'ed25519') { return false; @@ -91,5 +90,3 @@ async function validateParams(oid, Q, k) { const dG = new Uint8Array([0x40, ...publicKey]); // Add public key prefix return util.equalsUint8Array(Q, dG); } - -export default { sign, verify, validateParams }; diff --git a/src/crypto/public_key/elliptic/index.js b/src/crypto/public_key/elliptic/index.js index aa271fff..4fac26fd 100644 --- a/src/crypto/public_key/elliptic/index.js +++ b/src/crypto/public_key/elliptic/index.js @@ -24,11 +24,11 @@ * @module crypto/public_key/elliptic */ -import Curve, { generate, getPreferredHashAlgo } from './curves'; -import ecdsa from './ecdsa'; -import eddsa from './eddsa'; -import ecdh from './ecdh'; +import { Curve, generate, getPreferredHashAlgo } from './curves'; +import * as ecdsa from './ecdsa'; +import * as eddsa from './eddsa'; +import * as ecdh from './ecdh'; -export default { +export { Curve, ecdh, ecdsa, eddsa, generate, getPreferredHashAlgo }; diff --git a/src/crypto/public_key/index.js b/src/crypto/public_key/index.js index 4e2edbcf..e9a81025 100644 --- a/src/crypto/public_key/index.js +++ b/src/crypto/public_key/index.js @@ -9,10 +9,10 @@ */ import nacl from 'tweetnacl/nacl-fast-light.js'; -import rsa from './rsa'; -import elgamal from './elgamal'; -import elliptic from './elliptic'; -import dsa from './dsa'; +import * as rsa from './rsa'; +import * as elgamal from './elgamal'; +import * as elliptic from './elliptic'; +import * as dsa from './dsa'; export default { /** @see module:crypto/public_key/rsa */ diff --git a/src/crypto/public_key/prime.js b/src/crypto/public_key/prime.js index 595e0793..d2f39a4a 100644 --- a/src/crypto/public_key/prime.js +++ b/src/crypto/public_key/prime.js @@ -22,11 +22,7 @@ */ import util from '../../util'; -import random from '../random'; - -export default { - randomProbablePrime, isProbablePrime, fermat, millerRabin, divisionTest -}; +import { getRandomBigInteger } from '../random'; /** * Probabilistic random number generator @@ -36,7 +32,7 @@ export default { * @returns BigInteger * @async */ -async function randomProbablePrime(bits, e, k) { +export async function randomProbablePrime(bits, e, k) { const BigInteger = await util.getBigInteger(); const one = new BigInteger(1); const min = one.leftShift(new BigInteger(bits - 1)); @@ -49,7 +45,7 @@ async function randomProbablePrime(bits, e, k) { */ const adds = [1, 6, 5, 4, 3, 2, 1, 4, 3, 2, 1, 2, 1, 4, 3, 2, 1, 2, 1, 4, 3, 2, 1, 6, 5, 4, 3, 2, 1, 2]; - const n = await random.getRandomBigInteger(min, min.leftShift(one)); + const n = await getRandomBigInteger(min, min.leftShift(one)); let i = n.mod(thirty).toNumber(); do { @@ -72,7 +68,7 @@ async function randomProbablePrime(bits, e, k) { * @returns {boolean} * @async */ -async function isProbablePrime(n, e, k) { +export async function isProbablePrime(n, e, k) { if (e && !n.dec().gcd(e).isOne()) { return false; } @@ -97,13 +93,13 @@ async function isProbablePrime(n, e, k) { * @param {BigInteger} b Optional Fermat test base * @returns {boolean} */ -async function fermat(n, b) { +export async function fermat(n, b) { const BigInteger = await util.getBigInteger(); b = b || new BigInteger(2); return b.modExp(n.dec(), n).isOne(); } -async function divisionTest(n) { +export async function divisionTest(n) { const BigInteger = await util.getBigInteger(); return smallPrimes.every(m => { return n.mod(new BigInteger(m)) !== 0; @@ -232,7 +228,7 @@ const smallPrimes = [ * @returns {boolean} * @async */ -async function millerRabin(n, k, rand) { +export async function millerRabin(n, k, rand) { const BigInteger = await util.getBigInteger(); const len = n.bitLength(); @@ -248,7 +244,7 @@ async function millerRabin(n, k, rand) { const d = n.rightShift(new BigInteger(s)); for (; k > 0; k--) { - const a = rand ? rand() : await random.getRandomBigInteger(new BigInteger(2), n1); + const a = rand ? rand() : await getRandomBigInteger(new BigInteger(2), n1); let x = a.modExp(d, n); if (x.isOne() || x.equal(n1)) { diff --git a/src/crypto/public_key/rsa.js b/src/crypto/public_key/rsa.js index daa55cf9..412bc52d 100644 --- a/src/crypto/public_key/rsa.js +++ b/src/crypto/public_key/rsa.js @@ -24,11 +24,11 @@ * @module crypto/public_key/rsa */ -import prime from './prime'; -import random from '../random'; +import { randomProbablePrime } from './prime'; +import { getRandomBigInteger } from '../random'; import config from '../../config'; import util from '../../util'; -import pkcs1 from '../pkcs1'; +import { emsaEncode, emeEncode, emeDecode } from '../pkcs1'; import enums from '../../enums'; const webCrypto = util.getWebCrypto(); @@ -73,484 +73,480 @@ const RSAPublicKey = util.detectNode() ? asn1.define('RSAPubliceKey', function ( }) : undefined; /* eslint-enable no-invalid-this */ -export default { - /** Create signature - * @param {module:enums.hash} hash_algo Hash algorithm - * @param {Uint8Array} data message - * @param {Uint8Array} n RSA public modulus - * @param {Uint8Array} e RSA public exponent - * @param {Uint8Array} d RSA private exponent - * @param {Uint8Array} p RSA private prime p - * @param {Uint8Array} q RSA private prime q - * @param {Uint8Array} u RSA private coefficient - * @param {Uint8Array} hashed hashed message - * @returns {Uint8Array} RSA Signature - * @async - */ - sign: async function(hash_algo, data, n, e, d, p, q, u, hashed) { - if (data && !util.isStream(data)) { - if (util.getWebCrypto()) { - try { - return await this.webSign(enums.read(enums.webHash, hash_algo), data, n, e, d, p, q, u); - } catch (err) { - util.printDebugError(err); - } - } else if (util.getNodeCrypto()) { - return this.nodeSign(hash_algo, data, n, e, d, p, q, u); - } - } - return this.bnSign(hash_algo, n, d, hashed); - }, - - /** - * Verify signature - * @param {module:enums.hash} hash_algo Hash algorithm - * @param {Uint8Array} data message - * @param {Uint8Array} s signature - * @param {Uint8Array} n RSA public modulus - * @param {Uint8Array} e RSA public exponent - * @param {Uint8Array} hashed hashed message - * @returns {Boolean} - * @async - */ - verify: async function(hash_algo, data, s, n, e, hashed) { - if (data && !util.isStream(data)) { - if (util.getWebCrypto()) { - try { - return await this.webVerify(enums.read(enums.webHash, hash_algo), data, s, n, e); - } catch (err) { - util.printDebugError(err); - } - } else if (util.getNodeCrypto()) { - return this.nodeVerify(hash_algo, data, s, n, e); - } - } - return this.bnVerify(hash_algo, s, n, e, hashed); - }, - - /** - * Encrypt message - * @param {Uint8Array} data message - * @param {Uint8Array} n RSA public modulus - * @param {Uint8Array} e RSA public exponent - * @returns {Uint8Array} RSA Ciphertext - * @async - */ - encrypt: async function(data, n, e) { - if (util.getNodeCrypto()) { - return this.nodeEncrypt(data, n, e); - } - return this.bnEncrypt(data, n, e); - }, - - /** - * Decrypt RSA message - * @param {Uint8Array} m message - * @param {Uint8Array} n RSA public modulus - * @param {Uint8Array} e RSA public exponent - * @param {Uint8Array} d RSA private exponent - * @param {Uint8Array} p RSA private prime p - * @param {Uint8Array} q RSA private prime q - * @param {Uint8Array} u RSA private coefficient - * @returns {String} RSA Plaintext - * @async - */ - decrypt: async function(data, n, e, d, p, q, u) { - if (util.getNodeCrypto()) { - return this.nodeDecrypt(data, n, e, d, p, q, u); - } - return this.bnDecrypt(data, n, e, d, p, q, u); - }, - - /** - * Generate a new random private key B bits long with public exponent E. - * - * When possible, webCrypto or nodeCrypto is used. Otherwise, primes are generated using - * 40 rounds of the Miller-Rabin probabilistic random prime generation algorithm. - * @see module:crypto/public_key/prime - * @param {Integer} bits RSA bit length - * @param {Integer} e RSA public exponent - * @returns {{n, e, d, - * p, q ,u: Uint8Array}} RSA public modulus, RSA public exponent, RSA private exponent, - * RSA private prime p, RSA private prime q, u = p ** -1 mod q - * @async - */ - generate: async function(bits, e) { - const BigInteger = await util.getBigInteger(); - - e = new BigInteger(e); - - // Native RSA keygen using Web Crypto +/** Create signature + * @param {module:enums.hash} hash_algo Hash algorithm + * @param {Uint8Array} data message + * @param {Uint8Array} n RSA public modulus + * @param {Uint8Array} e RSA public exponent + * @param {Uint8Array} d RSA private exponent + * @param {Uint8Array} p RSA private prime p + * @param {Uint8Array} q RSA private prime q + * @param {Uint8Array} u RSA private coefficient + * @param {Uint8Array} hashed hashed message + * @returns {Uint8Array} RSA Signature + * @async + */ +export async function sign(hash_algo, data, n, e, d, p, q, u, hashed) { + if (data && !util.isStream(data)) { if (util.getWebCrypto()) { - let keyPair; - let keyGenOpt; - if ((globalThis.crypto && globalThis.crypto.subtle) || globalThis.msCrypto) { - // current standard spec - keyGenOpt = { - name: 'RSASSA-PKCS1-v1_5', - modulusLength: bits, // the specified keysize in bits - publicExponent: e.toUint8Array(), // take three bytes (max 65537) for exponent - hash: { - name: 'SHA-1' // not required for actual RSA keys, but for crypto api 'sign' and 'verify' - } - }; - keyPair = webCrypto.generateKey(keyGenOpt, true, ['sign', 'verify']); - keyPair = await promisifyIE11Op(keyPair, 'Error generating RSA key pair.'); - } else if (globalThis.crypto && globalThis.crypto.webkitSubtle) { - // outdated spec implemented by old Webkit - keyGenOpt = { - name: 'RSA-OAEP', - modulusLength: bits, // the specified keysize in bits - publicExponent: e.toUint8Array(), // take three bytes (max 65537) for exponent - hash: { - name: 'SHA-1' // not required for actual RSA keys, but for crypto api 'sign' and 'verify' - } - }; - keyPair = await webCrypto.generateKey(keyGenOpt, true, ['encrypt', 'decrypt']); - } else { - throw new Error('Unknown WebCrypto implementation'); + try { + return await webSign(enums.read(enums.webHash, hash_algo), data, n, e, d, p, q, u); + } catch (err) { + util.printDebugError(err); } + } else if (util.getNodeCrypto()) { + return nodeSign(hash_algo, data, n, e, d, p, q, u); + } + } + return bnSign(hash_algo, n, d, hashed); +} - // export the generated keys as JsonWebKey (JWK) - // https://tools.ietf.org/html/draft-ietf-jose-json-web-key-33 - let jwk = webCrypto.exportKey('jwk', keyPair.privateKey); - jwk = await promisifyIE11Op(jwk, 'Error exporting RSA key pair.'); - - // parse raw ArrayBuffer bytes to jwk/json (WebKit/Safari/IE11 quirk) - if (jwk instanceof ArrayBuffer) { - jwk = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(jwk))); +/** + * Verify signature + * @param {module:enums.hash} hash_algo Hash algorithm + * @param {Uint8Array} data message + * @param {Uint8Array} s signature + * @param {Uint8Array} n RSA public modulus + * @param {Uint8Array} e RSA public exponent + * @param {Uint8Array} hashed hashed message + * @returns {Boolean} + * @async + */ +export async function verify(hash_algo, data, s, n, e, hashed) { + if (data && !util.isStream(data)) { + if (util.getWebCrypto()) { + try { + return await webVerify(enums.read(enums.webHash, hash_algo), data, s, n, e); + } catch (err) { + util.printDebugError(err); } - // map JWK parameters to corresponding OpenPGP names - return { - n: util.b64ToUint8Array(jwk.n), - e: e.toUint8Array(), - d: util.b64ToUint8Array(jwk.d), - // switch p and q - p: util.b64ToUint8Array(jwk.q), - q: util.b64ToUint8Array(jwk.p), - // Since p and q are switched in places, u is the inverse of jwk.q - u: util.b64ToUint8Array(jwk.qi) - }; - } else if (util.getNodeCrypto() && nodeCrypto.generateKeyPair && RSAPrivateKey) { - const opts = { - modulusLength: bits, - publicExponent: e.toNumber(), - publicKeyEncoding: { type: 'pkcs1', format: 'der' }, - privateKeyEncoding: { type: 'pkcs1', format: 'der' } - }; - const prv = await new Promise((resolve, reject) => nodeCrypto.generateKeyPair('rsa', opts, (err, _, der) => { - if (err) { - reject(err); - } else { - resolve(RSAPrivateKey.decode(der, 'der')); + } else if (util.getNodeCrypto()) { + return nodeVerify(hash_algo, data, s, n, e); + } + } + return bnVerify(hash_algo, s, n, e, hashed); +} + +/** + * Encrypt message + * @param {Uint8Array} data message + * @param {Uint8Array} n RSA public modulus + * @param {Uint8Array} e RSA public exponent + * @returns {Uint8Array} RSA Ciphertext + * @async + */ +export async function encrypt(data, n, e) { + if (util.getNodeCrypto()) { + return nodeEncrypt(data, n, e); + } + return bnEncrypt(data, n, e); +} + +/** + * Decrypt RSA message + * @param {Uint8Array} m message + * @param {Uint8Array} n RSA public modulus + * @param {Uint8Array} e RSA public exponent + * @param {Uint8Array} d RSA private exponent + * @param {Uint8Array} p RSA private prime p + * @param {Uint8Array} q RSA private prime q + * @param {Uint8Array} u RSA private coefficient + * @returns {String} RSA Plaintext + * @async + */ +export async function decrypt(data, n, e, d, p, q, u) { + if (util.getNodeCrypto()) { + return nodeDecrypt(data, n, e, d, p, q, u); + } + return bnDecrypt(data, n, e, d, p, q, u); +} + +/** + * Generate a new random private key B bits long with public exponent E. + * + * When possible, webCrypto or nodeCrypto is used. Otherwise, primes are generated using + * 40 rounds of the Miller-Rabin probabilistic random prime generation algorithm. + * @see module:crypto/public_key/prime + * @param {Integer} bits RSA bit length + * @param {Integer} e RSA public exponent + * @returns {{n, e, d, + * p, q ,u: Uint8Array}} RSA public modulus, RSA public exponent, RSA private exponent, + * RSA private prime p, RSA private prime q, u = p ** -1 mod q + * @async + */ +export async function generate(bits, e) { + const BigInteger = await util.getBigInteger(); + + e = new BigInteger(e); + + // Native RSA keygen using Web Crypto + if (util.getWebCrypto()) { + let keyPair; + let keyGenOpt; + if ((globalThis.crypto && globalThis.crypto.subtle) || globalThis.msCrypto) { + // current standard spec + keyGenOpt = { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: bits, // the specified keysize in bits + publicExponent: e.toUint8Array(), // take three bytes (max 65537) for exponent + hash: { + name: 'SHA-1' // not required for actual RSA keys, but for crypto api 'sign' and 'verify' } - })); - /** - * OpenPGP spec differs from DER spec, DER: `u = (inverse of q) mod p`, OpenPGP: `u = (inverse of p) mod q`. - * @link https://tools.ietf.org/html/rfc3447#section-3.2 - * @link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-08#section-5.6.1 - */ - return { - n: prv.modulus.toArrayLike(Uint8Array), - e: prv.publicExponent.toArrayLike(Uint8Array), - d: prv.privateExponent.toArrayLike(Uint8Array), - // switch p and q - p: prv.prime2.toArrayLike(Uint8Array), - q: prv.prime1.toArrayLike(Uint8Array), - // Since p and q are switched in places, we can keep u as defined by DER - u: prv.coefficient.toArrayLike(Uint8Array) }; + keyPair = webCrypto.generateKey(keyGenOpt, true, ['sign', 'verify']); + keyPair = await promisifyIE11Op(keyPair, 'Error generating RSA key pair.'); + } else if (globalThis.crypto && globalThis.crypto.webkitSubtle) { + // outdated spec implemented by old Webkit + keyGenOpt = { + name: 'RSA-OAEP', + modulusLength: bits, // the specified keysize in bits + publicExponent: e.toUint8Array(), // take three bytes (max 65537) for exponent + hash: { + name: 'SHA-1' // not required for actual RSA keys, but for crypto api 'sign' and 'verify' + } + }; + keyPair = await webCrypto.generateKey(keyGenOpt, true, ['encrypt', 'decrypt']); + } else { + throw new Error('Unknown WebCrypto implementation'); } - // RSA keygen fallback using 40 iterations of the Miller-Rabin test - // See https://stackoverflow.com/a/6330138 for justification - // Also see section C.3 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST - let q = await prime.randomProbablePrime(bits - (bits >> 1), e, 40); - let p = await prime.randomProbablePrime(bits >> 1, e, 40); + // export the generated keys as JsonWebKey (JWK) + // https://tools.ietf.org/html/draft-ietf-jose-json-web-key-33 + let jwk = webCrypto.exportKey('jwk', keyPair.privateKey); + jwk = await promisifyIE11Op(jwk, 'Error exporting RSA key pair.'); - if (q.lt(p)) { - [p, q] = [q, p]; + // parse raw ArrayBuffer bytes to jwk/json (WebKit/Safari/IE11 quirk) + if (jwk instanceof ArrayBuffer) { + jwk = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(jwk))); } - const phi = p.dec().imul(q.dec()); + // map JWK parameters to corresponding OpenPGP names return { - n: p.mul(q).toUint8Array(), + n: util.b64ToUint8Array(jwk.n), e: e.toUint8Array(), - d: e.modInv(phi).toUint8Array(), - p: p.toUint8Array(), - q: q.toUint8Array(), - // dp: d.mod(p.subn(1)), - // dq: d.mod(q.subn(1)), - u: p.modInv(q).toUint8Array() - }; - }, - - /** - * Validate RSA parameters - * @param {Uint8Array} n RSA public modulus - * @param {Uint8Array} e RSA public exponent - * @param {Uint8Array} d RSA private exponent - * @param {Uint8Array} p RSA private prime p - * @param {Uint8Array} q RSA private prime q - * @param {Uint8Array} u RSA inverse of p w.r.t. q - * @returns {Promise} whether params are valid - * @async - */ - validateParams: async function (n, e, d, p, q, u) { - const BigInteger = await util.getBigInteger(); - n = new BigInteger(n); - p = new BigInteger(p); - q = new BigInteger(q); - - // expect pq = n - if (!p.mul(q).equal(n)) { - return false; - } - - const two = new BigInteger(2); - // expect p*u = 1 mod q - u = new BigInteger(u); - if (!p.mul(u).mod(q).isOne()) { - return false; - } - - e = new BigInteger(e); - d = new BigInteger(d); - /** - * In RSA pkcs#1 the exponents (d, e) are inverses modulo lcm(p-1, q-1) - * We check that [de = 1 mod (p-1)] and [de = 1 mod (q-1)] - * By CRT on coprime factors of (p-1, q-1) it follows that [de = 1 mod lcm(p-1, q-1)] - * - * We blind the multiplication with r, and check that rde = r mod lcm(p-1, q-1) - */ - const nSizeOver3 = new BigInteger(Math.floor(n.bitLength() / 3)); - const r = await random.getRandomBigInteger(two, two.leftShift(nSizeOver3)); // r in [ 2, 2^{|n|/3} ) < p and q - const rde = r.mul(d).mul(e); - - const areInverses = rde.mod(p.dec()).equal(r) && rde.mod(q.dec()).equal(r); - if (!areInverses) { - return false; - } - - return true; - }, - - bnSign: async function (hash_algo, n, d, hashed) { - const BigInteger = await util.getBigInteger(); - n = new BigInteger(n); - const m = new BigInteger(await pkcs1.emsa.encode(hash_algo, hashed, n.byteLength())); - d = new BigInteger(d); - if (m.gte(n)) { - throw new Error('Message size cannot exceed modulus size'); - } - return m.modExp(d, n).toUint8Array('be', n.byteLength()); - }, - - webSign: async function (hash_name, data, n, e, d, p, q, u) { - /** OpenPGP keys require that p < q, and Safari Web Crypto requires that p > q. - * We swap them in privateToJwk, so it usually works out, but nevertheless, - * not all OpenPGP keys are compatible with this requirement. - * OpenPGP.js used to generate RSA keys the wrong way around (p > q), and still - * does if the underlying Web Crypto does so (e.g. old MS Edge 50% of the time). - */ - const jwk = await privateToJwk(n, e, d, p, q, u); - const algo = { - name: "RSASSA-PKCS1-v1_5", - hash: { name: hash_name } - }; - const key = await webCrypto.importKey("jwk", jwk, algo, false, ["sign"]); - // add hash field for ms edge support - return new Uint8Array(await webCrypto.sign({ "name": "RSASSA-PKCS1-v1_5", "hash": hash_name }, key, data)); - }, - - nodeSign: async function (hash_algo, data, n, e, d, p, q, u) { - const { default: BN } = await import('bn.js'); - const pBNum = new BN(p); - const qBNum = new BN(q); - const dBNum = new BN(d); - const dq = dBNum.mod(qBNum.subn(1)); // d mod (q-1) - const dp = dBNum.mod(pBNum.subn(1)); // d mod (p-1) - const sign = nodeCrypto.createSign(enums.read(enums.hash, hash_algo)); - sign.write(data); - sign.end(); - const keyObject = { - version: 0, - modulus: new BN(n), - publicExponent: new BN(e), - privateExponent: new BN(d), + d: util.b64ToUint8Array(jwk.d), // switch p and q - prime1: new BN(q), - prime2: new BN(p), - // switch dp and dq - exponent1: dq, - exponent2: dp, - coefficient: new BN(u) + p: util.b64ToUint8Array(jwk.q), + q: util.b64ToUint8Array(jwk.p), + // Since p and q are switched in places, u is the inverse of jwk.q + u: util.b64ToUint8Array(jwk.qi) }; - if (typeof nodeCrypto.createPrivateKey !== 'undefined') { //from version 11.6.0 Node supports der encoded key objects - const der = RSAPrivateKey.encode(keyObject, 'der'); - return new Uint8Array(sign.sign({ key: der, format: 'der', type: 'pkcs1' })); - } + } else if (util.getNodeCrypto() && nodeCrypto.generateKeyPair && RSAPrivateKey) { + const opts = { + modulusLength: bits, + publicExponent: e.toNumber(), + publicKeyEncoding: { type: 'pkcs1', format: 'der' }, + privateKeyEncoding: { type: 'pkcs1', format: 'der' } + }; + const prv = await new Promise((resolve, reject) => nodeCrypto.generateKeyPair('rsa', opts, (err, _, der) => { + if (err) { + reject(err); + } else { + resolve(RSAPrivateKey.decode(der, 'der')); + } + })); + /** + * OpenPGP spec differs from DER spec, DER: `u = (inverse of q) mod p`, OpenPGP: `u = (inverse of p) mod q`. + * @link https://tools.ietf.org/html/rfc3447#section-3.2 + * @link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-08#section-5.6.1 + */ + return { + n: prv.modulus.toArrayLike(Uint8Array), + e: prv.publicExponent.toArrayLike(Uint8Array), + d: prv.privateExponent.toArrayLike(Uint8Array), + // switch p and q + p: prv.prime2.toArrayLike(Uint8Array), + q: prv.prime1.toArrayLike(Uint8Array), + // Since p and q are switched in places, we can keep u as defined by DER + u: prv.coefficient.toArrayLike(Uint8Array) + }; + } + + // RSA keygen fallback using 40 iterations of the Miller-Rabin test + // See https://stackoverflow.com/a/6330138 for justification + // Also see section C.3 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST + let q = await randomProbablePrime(bits - (bits >> 1), e, 40); + let p = await randomProbablePrime(bits >> 1, e, 40); + + if (q.lt(p)) { + [p, q] = [q, p]; + } + const phi = p.dec().imul(q.dec()); + return { + n: p.mul(q).toUint8Array(), + e: e.toUint8Array(), + d: e.modInv(phi).toUint8Array(), + p: p.toUint8Array(), + q: q.toUint8Array(), + // dp: d.mod(p.subn(1)), + // dq: d.mod(q.subn(1)), + u: p.modInv(q).toUint8Array() + }; +} + +/** + * Validate RSA parameters + * @param {Uint8Array} n RSA public modulus + * @param {Uint8Array} e RSA public exponent + * @param {Uint8Array} d RSA private exponent + * @param {Uint8Array} p RSA private prime p + * @param {Uint8Array} q RSA private prime q + * @param {Uint8Array} u RSA inverse of p w.r.t. q + * @returns {Promise} whether params are valid + * @async + */ +export async function validateParams(n, e, d, p, q, u) { + const BigInteger = await util.getBigInteger(); + n = new BigInteger(n); + p = new BigInteger(p); + q = new BigInteger(q); + + // expect pq = n + if (!p.mul(q).equal(n)) { + return false; + } + + const two = new BigInteger(2); + // expect p*u = 1 mod q + u = new BigInteger(u); + if (!p.mul(u).mod(q).isOne()) { + return false; + } + + e = new BigInteger(e); + d = new BigInteger(d); + /** + * In RSA pkcs#1 the exponents (d, e) are inverses modulo lcm(p-1, q-1) + * We check that [de = 1 mod (p-1)] and [de = 1 mod (q-1)] + * By CRT on coprime factors of (p-1, q-1) it follows that [de = 1 mod lcm(p-1, q-1)] + * + * We blind the multiplication with r, and check that rde = r mod lcm(p-1, q-1) + */ + const nSizeOver3 = new BigInteger(Math.floor(n.bitLength() / 3)); + const r = await getRandomBigInteger(two, two.leftShift(nSizeOver3)); // r in [ 2, 2^{|n|/3} ) < p and q + const rde = r.mul(d).mul(e); + + const areInverses = rde.mod(p.dec()).equal(r) && rde.mod(q.dec()).equal(r); + if (!areInverses) { + return false; + } + + return true; +} + +async function bnSign(hash_algo, n, d, hashed) { + const BigInteger = await util.getBigInteger(); + n = new BigInteger(n); + const m = new BigInteger(await emsaEncode(hash_algo, hashed, n.byteLength())); + d = new BigInteger(d); + if (m.gte(n)) { + throw new Error('Message size cannot exceed modulus size'); + } + return m.modExp(d, n).toUint8Array('be', n.byteLength()); +} + +async function webSign(hash_name, data, n, e, d, p, q, u) { + /** OpenPGP keys require that p < q, and Safari Web Crypto requires that p > q. + * We swap them in privateToJwk, so it usually works out, but nevertheless, + * not all OpenPGP keys are compatible with this requirement. + * OpenPGP.js used to generate RSA keys the wrong way around (p > q), and still + * does if the underlying Web Crypto does so (e.g. old MS Edge 50% of the time). + */ + const jwk = await privateToJwk(n, e, d, p, q, u); + const algo = { + name: "RSASSA-PKCS1-v1_5", + hash: { name: hash_name } + }; + const key = await webCrypto.importKey("jwk", jwk, algo, false, ["sign"]); + // add hash field for ms edge support + return new Uint8Array(await webCrypto.sign({ "name": "RSASSA-PKCS1-v1_5", "hash": hash_name }, key, data)); +} + +async function nodeSign(hash_algo, data, n, e, d, p, q, u) { + const { default: BN } = await import('bn.js'); + const pBNum = new BN(p); + const qBNum = new BN(q); + const dBNum = new BN(d); + const dq = dBNum.mod(qBNum.subn(1)); // d mod (q-1) + const dp = dBNum.mod(pBNum.subn(1)); // d mod (p-1) + const sign = nodeCrypto.createSign(enums.read(enums.hash, hash_algo)); + sign.write(data); + sign.end(); + const keyObject = { + version: 0, + modulus: new BN(n), + publicExponent: new BN(e), + privateExponent: new BN(d), + // switch p and q + prime1: new BN(q), + prime2: new BN(p), + // switch dp and dq + exponent1: dq, + exponent2: dp, + coefficient: new BN(u) + }; + if (typeof nodeCrypto.createPrivateKey !== 'undefined') { //from version 11.6.0 Node supports der encoded key objects + const der = RSAPrivateKey.encode(keyObject, 'der'); + return new Uint8Array(sign.sign({ key: der, format: 'der', type: 'pkcs1' })); + } + const pem = RSAPrivateKey.encode(keyObject, 'pem', { + label: 'RSA PRIVATE KEY' + }); + return new Uint8Array(sign.sign(pem)); +} + +async function bnVerify(hash_algo, s, n, e, hashed) { + const BigInteger = await util.getBigInteger(); + n = new BigInteger(n); + s = new BigInteger(s); + e = new BigInteger(e); + if (s.gte(n)) { + throw new Error('Signature size cannot exceed modulus size'); + } + const EM1 = s.modExp(e, n).toUint8Array('be', n.byteLength()); + const EM2 = await emsaEncode(hash_algo, hashed, n.byteLength()); + return util.equalsUint8Array(EM1, EM2); +} + +async function webVerify(hash_name, data, s, n, e) { + const jwk = publicToJwk(n, e); + const key = await webCrypto.importKey("jwk", jwk, { + name: "RSASSA-PKCS1-v1_5", + hash: { name: hash_name } + }, false, ["verify"]); + // add hash field for ms edge support + return webCrypto.verify({ "name": "RSASSA-PKCS1-v1_5", "hash": hash_name }, key, s, data); +} + +async function nodeVerify(hash_algo, data, s, n, e) { + const { default: BN } = await import('bn.js'); + + const verify = nodeCrypto.createVerify(enums.read(enums.hash, hash_algo)); + verify.write(data); + verify.end(); + const keyObject = { + modulus: new BN(n), + publicExponent: new BN(e) + }; + let key; + if (typeof nodeCrypto.createPrivateKey !== 'undefined') { //from version 11.6.0 Node supports der encoded key objects + const der = RSAPublicKey.encode(keyObject, 'der'); + key = { key: der, format: 'der', type: 'pkcs1' }; + } else { + key = RSAPublicKey.encode(keyObject, 'pem', { + label: 'RSA PUBLIC KEY' + }); + } + try { + return await verify.verify(key, s); + } catch (err) { + return false; + } +} + +async function nodeEncrypt(data, n, e) { + const { default: BN } = await import('bn.js'); + + const keyObject = { + modulus: new BN(n), + publicExponent: new BN(e) + }; + let key; + if (typeof nodeCrypto.createPrivateKey !== 'undefined') { + const der = RSAPublicKey.encode(keyObject, 'der'); + key = { key: der, format: 'der', type: 'pkcs1', padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; + } else { + const pem = RSAPublicKey.encode(keyObject, 'pem', { + label: 'RSA PUBLIC KEY' + }); + key = { key: pem, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; + } + return new Uint8Array(nodeCrypto.publicEncrypt(key, data)); +} + +async function bnEncrypt(data, n, e) { + const BigInteger = await util.getBigInteger(); + n = new BigInteger(n); + data = new BigInteger(await emeEncode(data, n.byteLength())); + e = new BigInteger(e); + if (data.gte(n)) { + throw new Error('Message size cannot exceed modulus size'); + } + return data.modExp(e, n).toUint8Array('be', n.byteLength()); +} + +async function nodeDecrypt(data, n, e, d, p, q, u) { + const { default: BN } = await import('bn.js'); + + const pBNum = new BN(p); + const qBNum = new BN(q); + const dBNum = new BN(d); + const dq = dBNum.mod(qBNum.subn(1)); // d mod (q-1) + const dp = dBNum.mod(pBNum.subn(1)); // d mod (p-1) + const keyObject = { + version: 0, + modulus: new BN(n), + publicExponent: new BN(e), + privateExponent: new BN(d), + // switch p and q + prime1: new BN(q), + prime2: new BN(p), + // switch dp and dq + exponent1: dq, + exponent2: dp, + coefficient: new BN(u) + }; + let key; + if (typeof nodeCrypto.createPrivateKey !== 'undefined') { + const der = RSAPrivateKey.encode(keyObject, 'der'); + key = { key: der, format: 'der' , type: 'pkcs1', padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; + } else { const pem = RSAPrivateKey.encode(keyObject, 'pem', { label: 'RSA PRIVATE KEY' }); - return new Uint8Array(sign.sign(pem)); - }, + key = { key: pem, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; + } + try { + return new Uint8Array(nodeCrypto.privateDecrypt(key, data)); + } catch (err) { + throw new Error('Decryption error'); + } +} - bnVerify: async function (hash_algo, s, n, e, hashed) { - const BigInteger = await util.getBigInteger(); - n = new BigInteger(n); - s = new BigInteger(s); - e = new BigInteger(e); - if (s.gte(n)) { - throw new Error('Signature size cannot exceed modulus size'); - } - const EM1 = s.modExp(e, n).toUint8Array('be', n.byteLength()); - const EM2 = await pkcs1.emsa.encode(hash_algo, hashed, n.byteLength()); - return util.equalsUint8Array(EM1, EM2); - }, +async function bnDecrypt(data, n, e, d, p, q, u) { + const BigInteger = await util.getBigInteger(); + data = new BigInteger(data); + n = new BigInteger(n); + e = new BigInteger(e); + d = new BigInteger(d); + p = new BigInteger(p); + q = new BigInteger(q); + u = new BigInteger(u); + if (data.gte(n)) { + throw new Error('Data too large.'); + } + const dq = d.mod(q.dec()); // d mod (q-1) + const dp = d.mod(p.dec()); // d mod (p-1) - webVerify: async function (hash_name, data, s, n, e) { - const jwk = publicToJwk(n, e); - const key = await webCrypto.importKey("jwk", jwk, { - name: "RSASSA-PKCS1-v1_5", - hash: { name: hash_name } - }, false, ["verify"]); - // add hash field for ms edge support - return webCrypto.verify({ "name": "RSASSA-PKCS1-v1_5", "hash": hash_name }, key, s, data); - }, + let blinder; + let unblinder; + if (config.rsaBlinding) { + unblinder = (await getRandomBigInteger(new BigInteger(2), n)).mod(n); + blinder = unblinder.modInv(n).modExp(e, n); + data = data.mul(blinder).mod(n); + } - nodeVerify: async function (hash_algo, data, s, n, e) { - const { default: BN } = await import('bn.js'); + const mp = data.modExp(dp, p); // data**{d mod (q-1)} mod p + const mq = data.modExp(dq, q); // data**{d mod (p-1)} mod q + const h = u.mul(mq.sub(mp)).mod(q); // u * (mq-mp) mod q (operands already < q) - const verify = nodeCrypto.createVerify(enums.read(enums.hash, hash_algo)); - verify.write(data); - verify.end(); - const keyObject = { - modulus: new BN(n), - publicExponent: new BN(e) - }; - let key; - if (typeof nodeCrypto.createPrivateKey !== 'undefined') { //from version 11.6.0 Node supports der encoded key objects - const der = RSAPublicKey.encode(keyObject, 'der'); - key = { key: der, format: 'der', type: 'pkcs1' }; - } else { - key = RSAPublicKey.encode(keyObject, 'pem', { - label: 'RSA PUBLIC KEY' - }); - } - try { - return await verify.verify(key, s); - } catch (err) { - return false; - } - }, + let result = h.mul(p).add(mp); // result < n due to relations above - nodeEncrypt: async function (data, n, e) { - const { default: BN } = await import('bn.js'); + if (config.rsaBlinding) { + result = result.mul(unblinder).mod(n); + } - const keyObject = { - modulus: new BN(n), - publicExponent: new BN(e) - }; - let key; - if (typeof nodeCrypto.createPrivateKey !== 'undefined') { - const der = RSAPublicKey.encode(keyObject, 'der'); - key = { key: der, format: 'der', type: 'pkcs1', padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; - } else { - const pem = RSAPublicKey.encode(keyObject, 'pem', { - label: 'RSA PUBLIC KEY' - }); - key = { key: pem, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; - } - return new Uint8Array(nodeCrypto.publicEncrypt(key, data)); - }, - - bnEncrypt: async function (data, n, e) { - const BigInteger = await util.getBigInteger(); - n = new BigInteger(n); - data = new BigInteger(await pkcs1.eme.encode(data, n.byteLength())); - e = new BigInteger(e); - if (data.gte(n)) { - throw new Error('Message size cannot exceed modulus size'); - } - return data.modExp(e, n).toUint8Array('be', n.byteLength()); - }, - - nodeDecrypt: async function (data, n, e, d, p, q, u) { - const { default: BN } = await import('bn.js'); - - const pBNum = new BN(p); - const qBNum = new BN(q); - const dBNum = new BN(d); - const dq = dBNum.mod(qBNum.subn(1)); // d mod (q-1) - const dp = dBNum.mod(pBNum.subn(1)); // d mod (p-1) - const keyObject = { - version: 0, - modulus: new BN(n), - publicExponent: new BN(e), - privateExponent: new BN(d), - // switch p and q - prime1: new BN(q), - prime2: new BN(p), - // switch dp and dq - exponent1: dq, - exponent2: dp, - coefficient: new BN(u) - }; - let key; - if (typeof nodeCrypto.createPrivateKey !== 'undefined') { - const der = RSAPrivateKey.encode(keyObject, 'der'); - key = { key: der, format: 'der' , type: 'pkcs1', padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; - } else { - const pem = RSAPrivateKey.encode(keyObject, 'pem', { - label: 'RSA PRIVATE KEY' - }); - key = { key: pem, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }; - } - try { - return new Uint8Array(nodeCrypto.privateDecrypt(key, data)); - } catch (err) { - throw new Error('Decryption error'); - } - }, - - bnDecrypt: async function(data, n, e, d, p, q, u) { - const BigInteger = await util.getBigInteger(); - data = new BigInteger(data); - n = new BigInteger(n); - e = new BigInteger(e); - d = new BigInteger(d); - p = new BigInteger(p); - q = new BigInteger(q); - u = new BigInteger(u); - if (data.gte(n)) { - throw new Error('Data too large.'); - } - const dq = d.mod(q.dec()); // d mod (q-1) - const dp = d.mod(p.dec()); // d mod (p-1) - - let blinder; - let unblinder; - if (config.rsaBlinding) { - unblinder = (await random.getRandomBigInteger(new BigInteger(2), n)).mod(n); - blinder = unblinder.modInv(n).modExp(e, n); - data = data.mul(blinder).mod(n); - } - - const mp = data.modExp(dp, p); // data**{d mod (q-1)} mod p - const mq = data.modExp(dq, q); // data**{d mod (p-1)} mod q - const h = u.mul(mq.sub(mp)).mod(q); // u * (mq-mp) mod q (operands already < q) - - let result = h.mul(p).add(mp); // result < n due to relations above - - if (config.rsaBlinding) { - result = result.mul(unblinder).mod(n); - } - - return pkcs1.eme.decode(result.toUint8Array('be', n.byteLength())); - }, - - prime: prime -}; + return emeDecode(result.toUint8Array('be', n.byteLength())); +} /** Convert Openpgp private key params to jwk key according to * @link https://tools.ietf.org/html/rfc7517 diff --git a/src/crypto/random.js b/src/crypto/random.js index ae34ca96..64d5e042 100644 --- a/src/crypto/random.js +++ b/src/crypto/random.js @@ -94,53 +94,51 @@ class RandomBuffer { } } -export default { - /** - * Retrieve secure random byte array of the specified length - * @param {Integer} length Length in bytes to generate - * @returns {Uint8Array} Random byte array - * @async - */ - getRandomBytes: async function(length) { - const buf = new Uint8Array(length); - if (typeof crypto !== 'undefined' && crypto.getRandomValues) { - crypto.getRandomValues(buf); - } else if (typeof globalThis !== 'undefined' && typeof globalThis.msCrypto === 'object' && typeof globalThis.msCrypto.getRandomValues === 'function') { - globalThis.msCrypto.getRandomValues(buf); - } else if (nodeCrypto) { - const bytes = nodeCrypto.randomBytes(buf.length); - buf.set(bytes); - } else if (this.randomBuffer.buffer) { - await this.randomBuffer.get(buf); - } else { - throw new Error('No secure random number generator available.'); - } - return buf; - }, +/** + * Retrieve secure random byte array of the specified length + * @param {Integer} length Length in bytes to generate + * @returns {Uint8Array} Random byte array + * @async + */ +export async function getRandomBytes(length) { + const buf = new Uint8Array(length); + if (typeof crypto !== 'undefined' && crypto.getRandomValues) { + crypto.getRandomValues(buf); + } else if (typeof globalThis !== 'undefined' && typeof globalThis.msCrypto === 'object' && typeof globalThis.msCrypto.getRandomValues === 'function') { + globalThis.msCrypto.getRandomValues(buf); + } else if (nodeCrypto) { + const bytes = nodeCrypto.randomBytes(buf.length); + buf.set(bytes); + } else if (randomBuffer.buffer) { + await randomBuffer.get(buf); + } else { + throw new Error('No secure random number generator available.'); + } + return buf; +} - /** - * Create a secure random BigInteger that is greater than or equal to min and less than max. - * @param {module:BigInteger} min Lower bound, included - * @param {module:BigInteger} max Upper bound, excluded - * @returns {module:BigInteger} Random BigInteger - * @async - */ - getRandomBigInteger: async function(min, max) { - const BigInteger = await util.getBigInteger(); +/** + * Create a secure random BigInteger that is greater than or equal to min and less than max. + * @param {module:BigInteger} min Lower bound, included + * @param {module:BigInteger} max Upper bound, excluded + * @returns {module:BigInteger} Random BigInteger + * @async + */ +export async function getRandomBigInteger(min, max) { + const BigInteger = await util.getBigInteger(); - if (max.lt(min)) { - throw new Error('Illegal parameter value: max <= min'); - } + if (max.lt(min)) { + throw new Error('Illegal parameter value: max <= min'); + } - const modulus = max.sub(min); - const bytes = modulus.byteLength(); + const modulus = max.sub(min); + const bytes = modulus.byteLength(); - // Using a while loop is necessary to avoid bias introduced by the mod operation. - // However, we request 64 extra random bits so that the bias is negligible. - // Section B.1.1 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf - const r = new BigInteger(await this.getRandomBytes(bytes + 8)); - return r.mod(modulus).add(min); - }, + // Using a while loop is necessary to avoid bias introduced by the mod operation. + // However, we request 64 extra random bits so that the bias is negligible. + // Section B.1.1 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf + const r = new BigInteger(await getRandomBytes(bytes + 8)); + return r.mod(modulus).add(min); +} - randomBuffer: new RandomBuffer() -}; +export const randomBuffer = new RandomBuffer(); diff --git a/src/crypto/signature.js b/src/crypto/signature.js index 1681c1a2..2adda547 100644 --- a/src/crypto/signature.js +++ b/src/crypto/signature.js @@ -1,125 +1,121 @@ /** * @fileoverview Provides functions for asymmetric signing and signature verification - * @requires crypto/crypto * @requires crypto/public_key * @requires enums * @requires util * @module crypto/signature */ -import crypto from './crypto'; import publicKey from './public_key'; import enums from '../enums'; import util from '../util'; -export default { - /** - * Verifies the signature provided for data using specified algorithms and public key parameters. - * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} - * and {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC 4880 9.4} - * for public key and hash algorithms. - * @param {module:enums.publicKey} algo Public key algorithm - * @param {module:enums.hash} hash_algo Hash algorithm - * @param {Array} msg_MPIs Algorithm-specific signature parameters - * @param {Object} publicParams Algorithm-specific public key parameters - * @param {Uint8Array} data Data for which the signature was created - * @param {Uint8Array} hashed The hashed data - * @returns {Boolean} True if signature is valid - * @async - */ - verify: async function(algo, hash_algo, msg_MPIs, publicParams, data, hashed) { - switch (algo) { - case enums.publicKey.rsaEncryptSign: - case enums.publicKey.rsaEncrypt: - case enums.publicKey.rsaSign: { - const { n, e } = publicParams; - const m = msg_MPIs[0].toUint8Array('be', n.length); - return publicKey.rsa.verify(hash_algo, data, m, n, e, hashed); - } - case enums.publicKey.dsa: { - const r = await msg_MPIs[0].toUint8Array(); - const s = await msg_MPIs[1].toUint8Array(); - const { g, p, q, y } = publicParams; - return publicKey.dsa.verify(hash_algo, r, s, hashed, g, p, q, y); - } - case enums.publicKey.ecdsa: { - const { oid, Q } = publicParams; - const signature = { r: msg_MPIs[0].toUint8Array(), s: msg_MPIs[1].toUint8Array() }; - return publicKey.elliptic.ecdsa.verify(oid, hash_algo, signature, data, Q, hashed); - } - case enums.publicKey.eddsa: { - const { oid, Q } = publicParams; - // EdDSA signature params are expected in little-endian format - const signature = { - R: msg_MPIs[0].toUint8Array('le', 32), - S: msg_MPIs[1].toUint8Array('le', 32) - }; - return publicKey.elliptic.eddsa.verify(oid, hash_algo, signature, data, Q, hashed); - } - default: - throw new Error('Invalid signature algorithm.'); +/** + * Verifies the signature provided for data using specified algorithms and public key parameters. + * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} + * and {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC 4880 9.4} + * for public key and hash algorithms. + * @param {module:enums.publicKey} algo Public key algorithm + * @param {module:enums.hash} hash_algo Hash algorithm + * @param {Array} msg_MPIs Algorithm-specific signature parameters + * @param {Object} publicParams Algorithm-specific public key parameters + * @param {Uint8Array} data Data for which the signature was created + * @param {Uint8Array} hashed The hashed data + * @returns {Boolean} True if signature is valid + * @async + */ +export async function verify(algo, hash_algo, msg_MPIs, publicParams, data, hashed) { + switch (algo) { + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaSign: { + const { n, e } = publicParams; + const m = msg_MPIs[0].toUint8Array('be', n.length); + return publicKey.rsa.verify(hash_algo, data, m, n, e, hashed); } - }, - - /** - * Creates a signature on data using specified algorithms and private key parameters. - * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} - * and {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC 4880 9.4} - * for public key and hash algorithms. - * @param {module:enums.publicKey} algo Public key algorithm - * @param {module:enums.hash} hash_algo Hash algorithm - * @param {Object} publicKeyParams Algorithm-specific public and private key parameters - * @param {Object} privateKeyParams Algorithm-specific public and private key parameters - * @param {Uint8Array} data Data to be signed - * @param {Uint8Array} hashed The hashed data - * @returns {Uint8Array} Signature - * @async - */ - sign: async function(algo, hash_algo, publicKeyParams, privateKeyParams, data, hashed) { - if (!publicKeyParams || !privateKeyParams) { - throw new Error('Missing key parameters'); + case enums.publicKey.dsa: { + const r = await msg_MPIs[0].toUint8Array(); + const s = await msg_MPIs[1].toUint8Array(); + const { g, p, q, y } = publicParams; + return publicKey.dsa.verify(hash_algo, r, s, hashed, g, p, q, y); } - switch (algo) { - case enums.publicKey.rsaEncryptSign: - case enums.publicKey.rsaEncrypt: - case enums.publicKey.rsaSign: { - const { n, e } = publicKeyParams; - const { d, p, q, u } = privateKeyParams; - const signature = await publicKey.rsa.sign(hash_algo, data, n, e, d, p, q, u, hashed); - return util.uint8ArrayToMpi(signature); - } - case enums.publicKey.dsa: { - const { g, p, q } = publicKeyParams; - const { x } = privateKeyParams; - const signature = await publicKey.dsa.sign(hash_algo, hashed, g, p, q, x); - return util.concatUint8Array([ - util.uint8ArrayToMpi(signature.r), - util.uint8ArrayToMpi(signature.s) - ]); - } - case enums.publicKey.elgamal: { - throw new Error('Signing with Elgamal is not defined in the OpenPGP standard.'); - } - case enums.publicKey.ecdsa: { - const { oid, Q } = publicKeyParams; - const { d } = privateKeyParams; - const signature = await publicKey.elliptic.ecdsa.sign(oid, hash_algo, data, Q, d, hashed); - return util.concatUint8Array([ - util.uint8ArrayToMpi(signature.r), - util.uint8ArrayToMpi(signature.s) - ]); - } - case enums.publicKey.eddsa: { - const { oid, Q } = publicKeyParams; - const { seed } = privateKeyParams; - const signature = await publicKey.elliptic.eddsa.sign(oid, hash_algo, data, Q, seed, hashed); - return util.concatUint8Array([ - util.uint8ArrayToMpi(signature.R), - util.uint8ArrayToMpi(signature.S) - ]); - } - default: - throw new Error('Invalid signature algorithm.'); + case enums.publicKey.ecdsa: { + const { oid, Q } = publicParams; + const signature = { r: msg_MPIs[0].toUint8Array(), s: msg_MPIs[1].toUint8Array() }; + return publicKey.elliptic.ecdsa.verify(oid, hash_algo, signature, data, Q, hashed); } + case enums.publicKey.eddsa: { + const { oid, Q } = publicParams; + // EdDSA signature params are expected in little-endian format + const signature = { + R: msg_MPIs[0].toUint8Array('le', 32), + S: msg_MPIs[1].toUint8Array('le', 32) + }; + return publicKey.elliptic.eddsa.verify(oid, hash_algo, signature, data, Q, hashed); + } + default: + throw new Error('Invalid signature algorithm.'); } -}; +} + +/** + * Creates a signature on data using specified algorithms and private key parameters. + * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} + * and {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC 4880 9.4} + * for public key and hash algorithms. + * @param {module:enums.publicKey} algo Public key algorithm + * @param {module:enums.hash} hash_algo Hash algorithm + * @param {Object} publicKeyParams Algorithm-specific public and private key parameters + * @param {Object} privateKeyParams Algorithm-specific public and private key parameters + * @param {Uint8Array} data Data to be signed + * @param {Uint8Array} hashed The hashed data + * @returns {Uint8Array} Signature + * @async + */ +export async function sign(algo, hash_algo, publicKeyParams, privateKeyParams, data, hashed) { + if (!publicKeyParams || !privateKeyParams) { + throw new Error('Missing key parameters'); + } + switch (algo) { + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaSign: { + const { n, e } = publicKeyParams; + const { d, p, q, u } = privateKeyParams; + const signature = await publicKey.rsa.sign(hash_algo, data, n, e, d, p, q, u, hashed); + return util.uint8ArrayToMpi(signature); + } + case enums.publicKey.dsa: { + const { g, p, q } = publicKeyParams; + const { x } = privateKeyParams; + const signature = await publicKey.dsa.sign(hash_algo, hashed, g, p, q, x); + return util.concatUint8Array([ + util.uint8ArrayToMpi(signature.r), + util.uint8ArrayToMpi(signature.s) + ]); + } + case enums.publicKey.elgamal: { + throw new Error('Signing with Elgamal is not defined in the OpenPGP standard.'); + } + case enums.publicKey.ecdsa: { + const { oid, Q } = publicKeyParams; + const { d } = privateKeyParams; + const signature = await publicKey.elliptic.ecdsa.sign(oid, hash_algo, data, Q, d, hashed); + return util.concatUint8Array([ + util.uint8ArrayToMpi(signature.r), + util.uint8ArrayToMpi(signature.s) + ]); + } + case enums.publicKey.eddsa: { + const { oid, Q } = publicKeyParams; + const { seed } = privateKeyParams; + const signature = await publicKey.elliptic.eddsa.sign(oid, hash_algo, data, Q, seed, hashed); + return util.concatUint8Array([ + util.uint8ArrayToMpi(signature.R), + util.uint8ArrayToMpi(signature.S) + ]); + } + default: + throw new Error('Invalid signature algorithm.'); + } +} diff --git a/src/encoding/armor.js b/src/encoding/armor.js index c7957b93..a10bdf1e 100644 --- a/src/encoding/armor.js +++ b/src/encoding/armor.js @@ -25,7 +25,7 @@ */ import stream from 'web-stream-tools'; -import base64 from './base64.js'; +import * as base64 from './base64.js'; import enums from '../enums.js'; import config from '../config'; import util from '../util'; @@ -233,7 +233,7 @@ function splitChecksum(text) { * @async * @static */ -function dearmor(input) { +export function unarmor(input) { return new Promise(async (resolve, reject) => { try { const reSplit = /^-----[^-]+-----$/m; @@ -359,7 +359,7 @@ function dearmor(input) { * @returns {String | ReadableStream} Armored text * @static */ -function armor(messagetype, body, partindex, parttotal, customComment) { +export function armor(messagetype, body, partindex, parttotal, customComment) { let text; let hash; if (messagetype === enums.armor.signed) { @@ -426,8 +426,3 @@ function armor(messagetype, body, partindex, parttotal, customComment) { return util.concat(result); } - -export default { - encode: armor, - decode: dearmor -}; diff --git a/src/encoding/base64.js b/src/encoding/base64.js index 5f534b48..c3727ffc 100644 --- a/src/encoding/base64.js +++ b/src/encoding/base64.js @@ -41,7 +41,7 @@ if (Buffer) { * @returns {String | ReadableStream} radix-64 version of input string * @static */ -function encode(data) { +export function encode(data) { let buf = new Uint8Array(); return stream.transform(data, value => { buf = util.concatUint8Array([buf, value]); @@ -65,7 +65,7 @@ function encode(data) { * @returns {Uint8Array | ReadableStream} binary array version of input string * @static */ -function decode(data) { +export function decode(data) { let buf = ''; return stream.transform(data, value => { buf += value; @@ -92,5 +92,3 @@ function decode(data) { return decoded; }, () => decodeChunk(buf)); } - -export default { encode, decode }; diff --git a/src/enums.js b/src/enums.js index 530a8383..88fe6ba1 100644 --- a/src/enums.js +++ b/src/enums.js @@ -126,7 +126,6 @@ export default { plaintext: 0, /** Not implemented! */ idea: 1, - '3des': 2, tripledes: 2, cast5: 3, blowfish: 4, diff --git a/src/index.js b/src/index.js index dee6ba34..b1efb6a8 100644 --- a/src/index.js +++ b/src/index.js @@ -98,7 +98,7 @@ export { default as stream } from 'web-stream-tools'; * @see module:encoding/armor * @name module:openpgp.armor */ -export { default as armor } from './encoding/armor'; +export * from './encoding/armor'; /** * @see module:enums diff --git a/src/key/factory.js b/src/key/factory.js index 4e73ab67..e9f40bfd 100644 --- a/src/key/factory.js +++ b/src/key/factory.js @@ -33,7 +33,7 @@ import * as helper from './helper'; import enums from '../enums'; import util from '../util'; import config from '../config'; -import armor from '../encoding/armor'; +import { unarmor } from '../encoding/armor'; /** * Generates a new OpenPGP key. Supports RSA and ECC keys. @@ -283,7 +283,7 @@ export async function readKey(data) { * @static */ export async function readArmoredKey(armoredKey) { - const input = await armor.decode(armoredKey); + const input = await unarmor(armoredKey); if (!(input.type === enums.armor.publicKey || input.type === enums.armor.privateKey)) { throw new Error('Armored text not of type key'); } @@ -321,7 +321,7 @@ export async function readKeys(data) { * @static */ export async function readArmoredKeys(armoredKey) { - const input = await armor.decode(armoredKey); + const input = await unarmor(armoredKey); if (!(input.type === enums.armor.publicKey || input.type === enums.armor.privateKey)) { throw new Error('Armored text not of type key'); } diff --git a/src/key/key.js b/src/key/key.js index 91b59aa3..2e33937b 100644 --- a/src/key/key.js +++ b/src/key/key.js @@ -25,7 +25,7 @@ * @module key/Key */ -import armor from '../encoding/armor'; +import { armor, unarmor } from '../encoding/armor'; import { PacketList, PublicKeyPacket, @@ -280,7 +280,7 @@ class Key { */ armor() { const type = this.isPublic() ? enums.armor.publicKey : enums.armor.privateKey; - return armor.encode(type, this.toPacketlist().write()); + return armor(type, this.toPacketlist().write()); } /** @@ -749,7 +749,7 @@ class Key { const revocationSignature = await helper.getLatestValidSignature(this.revocationSignatures, this.keyPacket, enums.signature.keyRevocation, dataToVerify, date); const packetlist = new PacketList(); packetlist.push(revocationSignature); - return armor.encode(enums.armor.publicKey, packetlist.write(), null, null, 'This is a revocation certificate'); + return armor(enums.armor.publicKey, packetlist.write(), null, null, 'This is a revocation certificate'); } /** @@ -761,7 +761,7 @@ class Key { * @async */ async applyRevocationCertificate(revocationCertificate) { - const input = await armor.decode(revocationCertificate); + const input = await unarmor(revocationCertificate); const packetlist = new PacketList(); await packetlist.read(input.data, { SignaturePacket }); const revocationSignature = packetlist.findPacket(enums.packet.signature); diff --git a/src/message.js b/src/message.js index 23657eb6..97be573b 100644 --- a/src/message.js +++ b/src/message.js @@ -30,7 +30,7 @@ */ import stream from 'web-stream-tools'; -import armor from './encoding/armor'; +import { armor, unarmor } from './encoding/armor'; import type_keyid from './type/keyid'; import config from './config'; import crypto from './crypto'; @@ -607,7 +607,7 @@ export class Message { * @param {String|Uint8Array} detachedSignature The detached ASCII-armored or Uint8Array PGP signature */ async appendSignature(detachedSignature) { - await this.packets.read(util.isUint8Array(detachedSignature) ? detachedSignature : (await armor.decode(detachedSignature)).data, { SignaturePacket }); + await this.packets.read(util.isUint8Array(detachedSignature) ? detachedSignature : (await unarmor(detachedSignature)).data, { SignaturePacket }); } /** @@ -623,7 +623,7 @@ export class Message { * @returns {ReadableStream} ASCII armor */ armor() { - return armor.encode(enums.armor.message, this.write()); + return armor(enums.armor.message, this.write()); } /** @@ -816,7 +816,7 @@ export async function readArmoredMessage(armoredText) { if (streamType === 'node') { armoredText = stream.nodeToWeb(armoredText); } - const input = await armor.decode(armoredText); + const input = await unarmor(armoredText); return readMessage(input.data, streamType); } diff --git a/src/packet/packet.js b/src/packet/packet.js index 45c329bc..912df433 100644 --- a/src/packet/packet.js +++ b/src/packet/packet.js @@ -29,268 +29,266 @@ import stream from 'web-stream-tools'; import enums from '../enums'; import util from '../util'; -export default { - readSimpleLength: function(bytes) { - let len = 0; - let offset; - const type = bytes[0]; +export function readSimpleLength(bytes) { + let len = 0; + let offset; + const type = bytes[0]; - if (type < 192) { - [len] = bytes; - offset = 1; - } else if (type < 255) { - len = ((bytes[0] - 192) << 8) + (bytes[1]) + 192; - offset = 2; - } else if (type === 255) { - len = util.readNumber(bytes.subarray(1, 1 + 4)); - offset = 5; + if (type < 192) { + [len] = bytes; + offset = 1; + } else if (type < 255) { + len = ((bytes[0] - 192) << 8) + (bytes[1]) + 192; + offset = 2; + } else if (type === 255) { + len = util.readNumber(bytes.subarray(1, 1 + 4)); + offset = 5; + } + + return { + len: len, + offset: offset + }; +} + +/** + * Encodes a given integer of length to the openpgp length specifier to a + * string + * + * @param {Integer} length The length to encode + * @returns {Uint8Array} String with openpgp length representation + */ +export function writeSimpleLength(length) { + if (length < 192) { + return new Uint8Array([length]); + } else if (length > 191 && length < 8384) { + /* + * let a = (total data packet length) - 192 let bc = two octet + * representation of a let d = b + 192 + */ + return new Uint8Array([((length - 192) >> 8) + 192, (length - 192) & 0xFF]); + } + return util.concatUint8Array([new Uint8Array([255]), util.writeNumber(length, 4)]); +} + +export function writePartialLength(power) { + if (power < 0 || power > 30) { + throw new Error('Partial Length power must be between 1 and 30'); + } + return new Uint8Array([224 + power]); +} + +export function writeTag(tag_type) { + /* we're only generating v4 packet headers here */ + return new Uint8Array([0xC0 | tag_type]); +} + +/** + * Writes a packet header version 4 with the given tag_type and length to a + * string + * + * @param {Integer} tag_type Tag type + * @param {Integer} length Length of the payload + * @returns {String} String of the header + */ +export function writeHeader(tag_type, length) { + /* we're only generating v4 packet headers here */ + return util.concatUint8Array([writeTag(tag_type), writeSimpleLength(length)]); +} + +/** + * Whether the packet type supports partial lengths per RFC4880 + * @param {Integer} tag_type Tag type + * @returns {Boolean} String of the header + */ +export function supportsStreaming(tag_type) { + return [ + enums.packet.literalData, + enums.packet.compressedData, + enums.packet.symmetricallyEncryptedData, + enums.packet.symEncryptedIntegrityProtectedData, + enums.packet.AEADEncryptedData + ].includes(tag_type); +} + +/** + * Generic static Packet Parser function + * + * @param {Uint8Array | ReadableStream} input Input stream as string + * @param {Function} callback Function to call with the parsed packet + * @returns {Boolean} Returns false if the stream was empty and parsing is done, and true otherwise. + */ +export async function readPackets(input, streaming, callback) { + const reader = stream.getReader(input); + let writer; + let callbackReturned; + try { + const peekedBytes = await reader.peekBytes(2); + // some sanity checks + if (!peekedBytes || peekedBytes.length < 2 || (peekedBytes[0] & 0x80) === 0) { + throw new Error("Error during parsing. This message / key probably does not conform to a valid OpenPGP format."); + } + const headerByte = await reader.readByte(); + let tag = -1; + let format = -1; + let packet_length; + + format = 0; // 0 = old format; 1 = new format + if ((headerByte & 0x40) !== 0) { + format = 1; } - return { - len: len, - offset: offset - }; - }, - - /** - * Encodes a given integer of length to the openpgp length specifier to a - * string - * - * @param {Integer} length The length to encode - * @returns {Uint8Array} String with openpgp length representation - */ - writeSimpleLength: function(length) { - if (length < 192) { - return new Uint8Array([length]); - } else if (length > 191 && length < 8384) { - /* - * let a = (total data packet length) - 192 let bc = two octet - * representation of a let d = b + 192 - */ - return new Uint8Array([((length - 192) >> 8) + 192, (length - 192) & 0xFF]); + let packet_length_type; + if (format) { + // new format header + tag = headerByte & 0x3F; // bit 5-0 + } else { + // old format header + tag = (headerByte & 0x3F) >> 2; // bit 5-2 + packet_length_type = headerByte & 0x03; // bit 1-0 } - return util.concatUint8Array([new Uint8Array([255]), util.writeNumber(length, 4)]); - }, - writePartialLength: function(power) { - if (power < 0 || power > 30) { - throw new Error('Partial Length power must be between 1 and 30'); + const packetSupportsStreaming = supportsStreaming(tag); + let packet = null; + if (streaming && packetSupportsStreaming) { + const transform = new stream.TransformStream(); + writer = stream.getWriter(transform.writable); + packet = transform.readable; + callbackReturned = callback({ tag, packet }); + } else { + packet = []; } - return new Uint8Array([224 + power]); - }, - writeTag: function(tag_type) { - /* we're only generating v4 packet headers here */ - return new Uint8Array([0xC0 | tag_type]); - }, - - /** - * Writes a packet header version 4 with the given tag_type and length to a - * string - * - * @param {Integer} tag_type Tag type - * @param {Integer} length Length of the payload - * @returns {String} String of the header - */ - writeHeader: function(tag_type, length) { - /* we're only generating v4 packet headers here */ - return util.concatUint8Array([this.writeTag(tag_type), this.writeSimpleLength(length)]); - }, - - /** - * Whether the packet type supports partial lengths per RFC4880 - * @param {Integer} tag_type Tag type - * @returns {Boolean} String of the header - */ - supportsStreaming: function(tag_type) { - return [ - enums.packet.literalData, - enums.packet.compressedData, - enums.packet.symmetricallyEncryptedData, - enums.packet.symEncryptedIntegrityProtectedData, - enums.packet.AEADEncryptedData - ].includes(tag_type); - }, - - /** - * Generic static Packet Parser function - * - * @param {Uint8Array | ReadableStream} input Input stream as string - * @param {Function} callback Function to call with the parsed packet - * @returns {Boolean} Returns false if the stream was empty and parsing is done, and true otherwise. - */ - read: async function(input, streaming, callback) { - const reader = stream.getReader(input); - let writer; - let callbackReturned; - try { - const peekedBytes = await reader.peekBytes(2); - // some sanity checks - if (!peekedBytes || peekedBytes.length < 2 || (peekedBytes[0] & 0x80) === 0) { - throw new Error("Error during parsing. This message / key probably does not conform to a valid OpenPGP format."); - } - const headerByte = await reader.readByte(); - let tag = -1; - let format = -1; - let packet_length; - - format = 0; // 0 = old format; 1 = new format - if ((headerByte & 0x40) !== 0) { - format = 1; - } - - let packet_length_type; - if (format) { - // new format header - tag = headerByte & 0x3F; // bit 5-0 - } else { - // old format header - tag = (headerByte & 0x3F) >> 2; // bit 5-2 - packet_length_type = headerByte & 0x03; // bit 1-0 - } - - const supportsStreaming = this.supportsStreaming(tag); - let packet = null; - if (streaming && supportsStreaming) { - const transform = new stream.TransformStream(); - writer = stream.getWriter(transform.writable); - packet = transform.readable; - callbackReturned = callback({ tag, packet }); - } else { - packet = []; - } - - let wasPartialLength; - do { - if (!format) { - // 4.2.1. Old Format Packet Lengths - switch (packet_length_type) { - case 0: - // The packet has a one-octet length. The header is 2 octets - // long. - packet_length = await reader.readByte(); - break; - case 1: - // The packet has a two-octet length. The header is 3 octets - // long. - packet_length = (await reader.readByte() << 8) | await reader.readByte(); - break; - case 2: - // The packet has a four-octet length. The header is 5 - // octets long. - packet_length = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() << - 8) | await reader.readByte(); - break; - default: - // 3 - The packet is of indeterminate length. The header is 1 - // octet long, and the implementation must determine how long - // the packet is. If the packet is in a file, this means that - // the packet extends until the end of the file. In general, - // an implementation SHOULD NOT use indeterminate-length - // packets except where the end of the data will be clear - // from the context, and even then it is better to use a - // definite length, or a new format header. The new format - // headers described below have a mechanism for precisely - // encoding data of indeterminate length. - packet_length = Infinity; - break; - } - } else { // 4.2.2. New Format Packet Lengths - // 4.2.2.1. One-Octet Lengths - const lengthByte = await reader.readByte(); - wasPartialLength = false; - if (lengthByte < 192) { - packet_length = lengthByte; - // 4.2.2.2. Two-Octet Lengths - } else if (lengthByte >= 192 && lengthByte < 224) { - packet_length = ((lengthByte - 192) << 8) + (await reader.readByte()) + 192; - // 4.2.2.4. Partial Body Lengths - } else if (lengthByte > 223 && lengthByte < 255) { - packet_length = 1 << (lengthByte & 0x1F); - wasPartialLength = true; - if (!supportsStreaming) { - throw new TypeError('This packet type does not support partial lengths.'); - } - // 4.2.2.3. Five-Octet Lengths - } else { + let wasPartialLength; + do { + if (!format) { + // 4.2.1. Old Format Packet Lengths + switch (packet_length_type) { + case 0: + // The packet has a one-octet length. The header is 2 octets + // long. + packet_length = await reader.readByte(); + break; + case 1: + // The packet has a two-octet length. The header is 3 octets + // long. + packet_length = (await reader.readByte() << 8) | await reader.readByte(); + break; + case 2: + // The packet has a four-octet length. The header is 5 + // octets long. packet_length = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() << 8) | await reader.readByte(); + break; + default: + // 3 - The packet is of indeterminate length. The header is 1 + // octet long, and the implementation must determine how long + // the packet is. If the packet is in a file, this means that + // the packet extends until the end of the file. In general, + // an implementation SHOULD NOT use indeterminate-length + // packets except where the end of the data will be clear + // from the context, and even then it is better to use a + // definite length, or a new format header. The new format + // headers described below have a mechanism for precisely + // encoding data of indeterminate length. + packet_length = Infinity; + break; + } + } else { // 4.2.2. New Format Packet Lengths + // 4.2.2.1. One-Octet Lengths + const lengthByte = await reader.readByte(); + wasPartialLength = false; + if (lengthByte < 192) { + packet_length = lengthByte; + // 4.2.2.2. Two-Octet Lengths + } else if (lengthByte >= 192 && lengthByte < 224) { + packet_length = ((lengthByte - 192) << 8) + (await reader.readByte()) + 192; + // 4.2.2.4. Partial Body Lengths + } else if (lengthByte > 223 && lengthByte < 255) { + packet_length = 1 << (lengthByte & 0x1F); + wasPartialLength = true; + if (!packetSupportsStreaming) { + throw new TypeError('This packet type does not support partial lengths.'); + } + // 4.2.2.3. Five-Octet Lengths + } else { + packet_length = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() << + 8) | await reader.readByte(); + } + } + if (packet_length > 0) { + let bytesRead = 0; + while (true) { + if (writer) await writer.ready; + const { done, value } = await reader.read(); + if (done) { + if (packet_length === Infinity) break; + throw new Error('Unexpected end of packet'); + } + const chunk = packet_length === Infinity ? value : value.subarray(0, packet_length - bytesRead); + if (writer) await writer.write(chunk); + else packet.push(chunk); + bytesRead += value.length; + if (bytesRead >= packet_length) { + reader.unshift(value.subarray(packet_length - bytesRead + value.length)); + break; } } - if (packet_length > 0) { - let bytesRead = 0; - while (true) { - if (writer) await writer.ready; - const { done, value } = await reader.read(); - if (done) { - if (packet_length === Infinity) break; - throw new Error('Unexpected end of packet'); - } - const chunk = packet_length === Infinity ? value : value.subarray(0, packet_length - bytesRead); - if (writer) await writer.write(chunk); - else packet.push(chunk); - bytesRead += value.length; - if (bytesRead >= packet_length) { - reader.unshift(value.subarray(packet_length - bytesRead + value.length)); - break; - } - } - } - } while (wasPartialLength); + } + } while (wasPartialLength); - // If this was not a packet that "supports streaming", we peek to check - // whether it is the last packet in the message. We peek 2 bytes instead - // of 1 because the beginning of this function also peeks 2 bytes, and we - // want to cut a `subarray` of the correct length into `web-stream-tools`' - // `externalBuffer` as a tiny optimization here. - // - // If it *was* a streaming packet (i.e. the data packets), we peek at the - // entire remainder of the stream, in order to forward errors in the - // remainder of the stream to the packet data. (Note that this means we - // read/peek at all signature packets before closing the literal data - // packet, for example.) This forwards MDC errors to the literal data - // stream, for example, so that they don't get lost / forgotten on - // decryptedMessage.packets.stream, which we never look at. - // - // An example of what we do when stream-parsing a message containing - // [ one-pass signature packet, literal data packet, signature packet ]: - // 1. Read the one-pass signature packet - // 2. Peek 2 bytes of the literal data packet - // 3. Parse the one-pass signature packet - // - // 4. Read the literal data packet, simultaneously stream-parsing it - // 5. Peek until the end of the message - // 6. Finish parsing the literal data packet - // - // 7. Read the signature packet again (we already peeked at it in step 5) - // 8. Peek at the end of the stream again (`peekBytes` returns undefined) - // 9. Parse the signature packet - // - // Note that this means that if there's an error in the very end of the - // stream, such as an MDC error, we throw in step 5 instead of in step 8 - // (or never), which is the point of this exercise. - const nextPacket = await reader.peekBytes(supportsStreaming ? Infinity : 2); - if (writer) { - await writer.ready; - await writer.close(); - } else { - packet = util.concatUint8Array(packet); - await callback({ tag, packet }); - } - return !nextPacket || !nextPacket.length; - } catch (e) { - if (writer) { - await writer.abort(e); - return true; - } else { - throw e; - } - } finally { - if (writer) { - await callbackReturned; - } - reader.releaseLock(); + // If this was not a packet that "supports streaming", we peek to check + // whether it is the last packet in the message. We peek 2 bytes instead + // of 1 because the beginning of this function also peeks 2 bytes, and we + // want to cut a `subarray` of the correct length into `web-stream-tools`' + // `externalBuffer` as a tiny optimization here. + // + // If it *was* a streaming packet (i.e. the data packets), we peek at the + // entire remainder of the stream, in order to forward errors in the + // remainder of the stream to the packet data. (Note that this means we + // read/peek at all signature packets before closing the literal data + // packet, for example.) This forwards MDC errors to the literal data + // stream, for example, so that they don't get lost / forgotten on + // decryptedMessage.packets.stream, which we never look at. + // + // An example of what we do when stream-parsing a message containing + // [ one-pass signature packet, literal data packet, signature packet ]: + // 1. Read the one-pass signature packet + // 2. Peek 2 bytes of the literal data packet + // 3. Parse the one-pass signature packet + // + // 4. Read the literal data packet, simultaneously stream-parsing it + // 5. Peek until the end of the message + // 6. Finish parsing the literal data packet + // + // 7. Read the signature packet again (we already peeked at it in step 5) + // 8. Peek at the end of the stream again (`peekBytes` returns undefined) + // 9. Parse the signature packet + // + // Note that this means that if there's an error in the very end of the + // stream, such as an MDC error, we throw in step 5 instead of in step 8 + // (or never), which is the point of this exercise. + const nextPacket = await reader.peekBytes(packetSupportsStreaming ? Infinity : 2); + if (writer) { + await writer.ready; + await writer.close(); + } else { + packet = util.concatUint8Array(packet); + await callback({ tag, packet }); } + return !nextPacket || !nextPacket.length; + } catch (e) { + if (writer) { + await writer.abort(e); + return true; + } else { + throw e; + } + } finally { + if (writer) { + await callbackReturned; + } + reader.releaseLock(); } -}; +} diff --git a/src/packet/packetlist.js b/src/packet/packetlist.js index 10306d26..7b51f684 100644 --- a/src/packet/packetlist.js +++ b/src/packet/packetlist.js @@ -10,7 +10,11 @@ import stream from 'web-stream-tools'; import * as packets from './all_packets'; -import packetParser from './packet'; +import { + readPackets, supportsStreaming, + writeTag, writeHeader, + writePartialLength, writeSimpleLength +} from './packet'; import config from '../config'; import enums from '../enums'; import util from '../util'; @@ -33,7 +37,7 @@ class PacketList extends Array { try { while (true) { await writer.ready; - const done = await packetParser.read(readable, streaming, async parsed => { + const done = await readPackets(readable, streaming, async parsed => { try { const tag = enums.read(enums.packet, parsed.tag); const packet = packets.newPacketFromTag(tag, allowedPackets); @@ -42,7 +46,7 @@ class PacketList extends Array { await packet.read(parsed.packet, streaming); await writer.write(packet); } catch (e) { - if (!config.tolerant || packetParser.supportsStreaming(parsed.tag)) { + if (!config.tolerant || supportsStreaming(parsed.tag)) { // The packets that support streaming are the ones that contain // message data. Those are also the ones we want to be more strict // about and throw on parse errors for. @@ -71,7 +75,7 @@ class PacketList extends Array { } else { this.stream = null; } - if (done || packetParser.supportsStreaming(value.tag)) { + if (done || supportsStreaming(value.tag)) { break; } } @@ -88,31 +92,31 @@ class PacketList extends Array { for (let i = 0; i < this.length; i++) { const packetbytes = this[i].write(); - if (util.isStream(packetbytes) && packetParser.supportsStreaming(this[i].tag)) { + if (util.isStream(packetbytes) && supportsStreaming(this[i].tag)) { let buffer = []; let bufferLength = 0; const minLength = 512; - arr.push(packetParser.writeTag(this[i].tag)); + arr.push(writeTag(this[i].tag)); arr.push(stream.transform(packetbytes, value => { buffer.push(value); bufferLength += value.length; if (bufferLength >= minLength) { const powerOf2 = Math.min(Math.log(bufferLength) / Math.LN2 | 0, 30); const chunkSize = 2 ** powerOf2; - const bufferConcat = util.concat([packetParser.writePartialLength(powerOf2)].concat(buffer)); + const bufferConcat = util.concat([writePartialLength(powerOf2)].concat(buffer)); buffer = [bufferConcat.subarray(1 + chunkSize)]; bufferLength = buffer[0].length; return bufferConcat.subarray(0, 1 + chunkSize); } - }, () => util.concat([packetParser.writeSimpleLength(bufferLength)].concat(buffer)))); + }, () => util.concat([writeSimpleLength(bufferLength)].concat(buffer)))); } else { if (util.isStream(packetbytes)) { let length = 0; arr.push(stream.transform(stream.clone(packetbytes), value => { length += value.length; - }, () => packetParser.writeHeader(this[i].tag, length))); + }, () => writeHeader(this[i].tag, length))); } else { - arr.push(packetParser.writeHeader(this[i].tag, packetbytes.length)); + arr.push(writeHeader(this[i].tag, packetbytes.length)); } arr.push(packetbytes); } diff --git a/src/packet/signature.js b/src/packet/signature.js index 7ca56314..275e7c7e 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -26,7 +26,7 @@ */ import stream from 'web-stream-tools'; -import packet from './packet'; +import { readSimpleLength, writeSimpleLength } from './packet'; import type_keyid from '../type/keyid.js'; import type_mpi from '../type/mpi.js'; import crypto from '../crypto'; @@ -319,7 +319,7 @@ class SignaturePacket { arr.push(write_sub_packet(sub.issuerFingerprint, bytes)); } this.unhashedSubpackets.forEach(data => { - arr.push(packet.writeSimpleLength(data.length)); + arr.push(writeSimpleLength(data.length)); arr.push(data); }); @@ -535,7 +535,7 @@ class SignaturePacket { // subpacket data set (zero or more subpackets) while (i < 2 + subpacket_length) { - const len = packet.readSimpleLength(bytes.subarray(i, bytes.length)); + const len = readSimpleLength(bytes.subarray(i, bytes.length)); i += len.offset; this.read_sub_packet(bytes.subarray(i, i + len.len), trusted); @@ -767,7 +767,7 @@ class SignaturePacket { */ function write_sub_packet(type, data) { const arr = []; - arr.push(packet.writeSimpleLength(data.length + 1)); + arr.push(writeSimpleLength(data.length + 1)); arr.push(new Uint8Array([type])); arr.push(data); return util.concat(arr); diff --git a/src/packet/user_attribute.js b/src/packet/user_attribute.js index d73a7803..cba957d7 100644 --- a/src/packet/user_attribute.js +++ b/src/packet/user_attribute.js @@ -21,7 +21,7 @@ * @requires util */ -import packet from './packet'; +import { readSimpleLength, writeSimpleLength } from './packet'; import enums from '../enums'; import util from '../util'; @@ -56,7 +56,7 @@ class UserAttributePacket { read(bytes) { let i = 0; while (i < bytes.length) { - const len = packet.readSimpleLength(bytes.subarray(i, bytes.length)); + const len = readSimpleLength(bytes.subarray(i, bytes.length)); i += len.offset; this.attributes.push(util.uint8ArrayToStr(bytes.subarray(i, i + len.len))); @@ -71,7 +71,7 @@ class UserAttributePacket { write() { const arr = []; for (let i = 0; i < this.attributes.length; i++) { - arr.push(packet.writeSimpleLength(this.attributes[i].length)); + arr.push(writeSimpleLength(this.attributes[i].length)); arr.push(util.strToUint8Array(this.attributes[i])); } return util.concatUint8Array(arr); diff --git a/src/signature.js b/src/signature.js index 541b3115..39e0280d 100644 --- a/src/signature.js +++ b/src/signature.js @@ -22,7 +22,7 @@ * @module signature */ -import armor from './encoding/armor'; +import { armor, unarmor } from './encoding/armor'; import { PacketList, SignaturePacket } from './packet'; import enums from './enums'; @@ -50,7 +50,7 @@ export class Signature { * @returns {ReadableStream} ASCII armor */ armor() { - return armor.encode(enums.armor.signature, this.write()); + return armor(enums.armor.signature, this.write()); } } @@ -62,7 +62,7 @@ export class Signature { * @static */ export async function readArmoredSignature(armoredText) { - const input = await armor.decode(armoredText); + const input = await unarmor(armoredText); return readSignature(input.data); } diff --git a/src/util.js b/src/util.js index ffb4fcfe..431d4245 100644 --- a/src/util.js +++ b/src/util.js @@ -30,7 +30,7 @@ import emailAddresses from 'email-addresses'; import stream from 'web-stream-tools'; import config from './config'; import util from './util'; // re-import module to access util functions -import b64 from './encoding/base64'; +import * as b64 from './encoding/base64'; import { getBigInteger } from './biginteger'; export default { diff --git a/test/crypto/elliptic.js b/test/crypto/elliptic.js index 041837e4..47929612 100644 --- a/test/crypto/elliptic.js +++ b/test/crypto/elliptic.js @@ -186,17 +186,17 @@ module.exports = () => describe('Elliptic Curve Cryptography @lightweight', func ]); } }); - it('Invalid point', function () { + it('Invalid point', async function () { if (!openpgp.config.useIndutnyElliptic && !openpgp.util.getNodeCrypto()) { this.skip(); } if (openpgp.util.getNodeCrypto()) { - expect(verify_signature( + await expect(verify_signature( 'secp256k1', 8, [], [], [], secp256k1_invalid_point )).to.eventually.be.false; } if (openpgp.config.useIndutnyElliptic) { - expect(verify_signature_elliptic( + await expect(verify_signature_elliptic( 'secp256k1', 8, [], [], [], secp256k1_invalid_point )).to.be.rejectedWith(Error, /Invalid elliptic public key/); } diff --git a/test/general/armor.js b/test/general/armor.js index bb154f4e..9d976dd3 100644 --- a/test/general/armor.js +++ b/test/general/armor.js @@ -377,8 +377,8 @@ NJCB6+LWtabSoVIjNVgKwyKqyTLaESNwC2ogZwkdE8qPGiDFEHo4Gg9zuRof -----END PGP PUBLIC KEY BLOCK----- `; - const { type, data } = await openpgp.armor.decode(pubKey); - const armor = await openpgp.stream.readToEnd(openpgp.armor.encode(type, data)); + const { type, data } = await openpgp.unarmor(pubKey); + const armor = await openpgp.stream.readToEnd(openpgp.armor(type, data)); expect( armor .replace(/^(Version|Comment): .*$\r\n/mg, '') diff --git a/test/general/key.js b/test/general/key.js index a3803be2..0034026f 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -2584,35 +2584,9 @@ function versionSpecificTests() { } module.exports = () => describe('Key', function() { - async function deepCopyKeyParams(params) { - const paramsCopy = {}; - Object.keys(params).forEach(name => { - const param = params[name]; - const copy = new Uint8Array(param.length); - copy.set(param); - paramsCopy[name] = copy; - }); - return paramsCopy; - } - let rsaGenStub; let v5KeysVal; let aeadProtectVal; - const rsaGenValue = { - 512: openpgp.crypto.publicKey.rsa.generate(512, 65537), - 1024: openpgp.crypto.publicKey.rsa.generate(1024, 65537), - 2048: openpgp.crypto.publicKey.rsa.generate(2048, 65537) - }; - - beforeEach(function() { - // We fake the generation function to speed up the tests - rsaGenStub = stub(openpgp.crypto.publicKey.rsa, 'generate'); - rsaGenStub.callsFake(async N => deepCopyKeyParams(await rsaGenValue[N])); - }); - - afterEach(function() { - rsaGenStub.restore(); - }); tryTests('V4', versionSpecificTests, { if: !openpgp.config.ci, @@ -2706,7 +2680,7 @@ module.exports = () => describe('Key', function() { const packetlist = new openpgp.PacketList(); - await packetlist.read((await openpgp.armor.decode(pub_sig_test)).data, openpgp); + await packetlist.read((await openpgp.unarmor(pub_sig_test)).data, openpgp); const subkeys = pubKey.getSubkeys(); expect(subkeys).to.exist; @@ -3132,10 +3106,10 @@ module.exports = () => describe('Key', function() { const revKey = await openpgp.readArmoredKey(revoked_key_arm4); const revocationCertificate = await revKey.getRevocationCertificate(); - const input = await openpgp.armor.decode(revocation_certificate_arm4); + const input = await openpgp.unarmor(revocation_certificate_arm4); const packetlist = new openpgp.PacketList(); await packetlist.read(input.data, { SignaturePacket: openpgp.SignaturePacket }); - const armored = openpgp.armor.encode(openpgp.enums.armor.publicKey, packetlist.write()); + const armored = openpgp.armor(openpgp.enums.armor.publicKey, packetlist.write()); expect(revocationCertificate.replace(/^Comment: .*$\r\n/mg, '')).to.equal(armored.replace(/^Comment: .*$\r\n/mg, '')); }); diff --git a/test/general/openpgp.js b/test/general/openpgp.js index f7f4e942..4c73436b 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -549,29 +549,6 @@ function withCompression(tests) { } module.exports = () => describe('OpenPGP.js public api tests', function() { - async function deepCopyKeyParams(params) { - const paramsCopy = {}; - Object.keys(params).forEach(name => { - const param = params[name]; - const copy = new Uint8Array(param.length); - copy.set(param); - paramsCopy[name] = copy; - }); - return paramsCopy; - } - - let rsaGenStub; - const rsaGenValue = openpgp.crypto.publicKey.rsa.generate(openpgp.util.getWebCryptoAll() ? 2048 : 512, 65537); - - beforeEach(function() { - // We fake the generation function to speed up the tests - rsaGenStub = stub(openpgp.crypto.publicKey.rsa, 'generate'); - rsaGenStub.returns(async () => deepCopyKeyParams(await rsaGenValue())); - }); - - afterEach(function() { - rsaGenStub.restore(); - }); describe('generateKey - validate user ids', function() { it('should fail for invalid user name', async function() { diff --git a/test/general/packet.js b/test/general/packet.js index df6066e6..0547a14b 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -243,6 +243,9 @@ module.exports = () => describe("Packet", function() { it('Sym. encrypted AEAD protected packet test vector (AEAD)', async function() { // From https://gitlab.com/openpgp-wg/rfc4880bis/commit/00b20923e6233fb6ff1666ecd5acfefceb32907d + const nodeCrypto = openpgp.util.getNodeCrypto(); + if (!nodeCrypto) return; + let packetBytes = openpgp.util.hexToUint8Array(` d4 4a 01 07 01 0e b7 32 37 9f 73 c4 92 8d e2 5f ac fe 65 17 ec 10 5d c1 1a 81 dc 0c b8 a2 f6 f3 @@ -271,8 +274,8 @@ module.exports = () => describe("Packet", function() { const msg2 = new openpgp.PacketList(); - let randomBytesStub = stub(openpgp.crypto.random, 'getRandomBytes'); - randomBytesStub.returns(resolves(iv)); + let randomBytesStub = stub(nodeCrypto, 'randomBytes'); + randomBytesStub.returns(iv); try { await enc.encrypt(algo, key); @@ -298,7 +301,7 @@ module.exports = () => describe("Packet", function() { '=VZ0/\n' + '-----END PGP MESSAGE-----'; - const msgbytes = (await openpgp.armor.decode(msg)).data; + const msgbytes = (await openpgp.unarmor(msg)).data; const parsed = new openpgp.PacketList(); await parsed.read(msgbytes, openpgp); @@ -365,7 +368,7 @@ module.exports = () => describe("Packet", function() { '-----END PGP PRIVATE KEY BLOCK-----'; let key = new openpgp.PacketList(); - await key.read((await openpgp.armor.decode(armored_key)).data, openpgp); + await key.read((await openpgp.unarmor(armored_key)).data, openpgp); key = key[0]; const enc = new openpgp.PublicKeyEncryptedSessionKeyPacket(); @@ -432,11 +435,11 @@ module.exports = () => describe("Packet", function() { '-----END PGP MESSAGE-----'; let key = new openpgp.PacketList(); - await key.read((await openpgp.armor.decode(armored_key)).data, openpgp); + await key.read((await openpgp.unarmor(armored_key)).data, openpgp); key = key[3]; const msg = new openpgp.PacketList(); - await msg.read((await openpgp.armor.decode(armored_msg)).data, openpgp); + await msg.read((await openpgp.unarmor(armored_msg)).data, openpgp); return msg[0].decrypt(key).then(async () => { await msg[1].decrypt(msg[0].sessionKeyAlgorithm, msg[0].sessionKey); @@ -521,6 +524,9 @@ module.exports = () => describe("Packet", function() { it('Sym. encrypted session key reading/writing test vector (EAX, AEAD)', async function() { // From https://gitlab.com/openpgp-wg/rfc4880bis/blob/00b20923/back.mkd#sample-aead-eax-encryption-and-decryption + const nodeCrypto = openpgp.util.getNodeCrypto(); + if (!nodeCrypto) return; + let aeadProtectVal = openpgp.config.aeadProtect; let aeadChunkSizeByteVal = openpgp.config.aeadChunkSizeByte; let s2kIterationCountByteVal = openpgp.config.s2kIterationCountByte; @@ -533,11 +539,11 @@ module.exports = () => describe("Packet", function() { let sessionIV = openpgp.util.hexToUint8Array(`bc 66 9e 34 e5 00 dc ae dc 5b 32 aa 2d ab 02 35`.replace(/\s+/g, '')); let dataIV = openpgp.util.hexToUint8Array(`b7 32 37 9f 73 c4 92 8d e2 5f ac fe 65 17 ec 10`.replace(/\s+/g, '')); - let randomBytesStub = stub(openpgp.crypto.random, 'getRandomBytes'); - randomBytesStub.onCall(0).returns(resolves(salt)); - randomBytesStub.onCall(1).returns(resolves(sessionKey)); - randomBytesStub.onCall(2).returns(resolves(sessionIV)); - randomBytesStub.onCall(3).returns(resolves(dataIV)); + let randomBytesStub = stub(nodeCrypto, 'randomBytes'); + randomBytesStub.onCall(0).returns(salt); + randomBytesStub.onCall(1).returns(sessionKey); + randomBytesStub.onCall(2).returns(sessionIV); + randomBytesStub.onCall(3).returns(dataIV); let packetBytes = openpgp.util.hexToUint8Array(` c3 3e 05 07 01 03 08 cd 5a 9f 70 fb e0 bc 65 90 @@ -596,6 +602,9 @@ module.exports = () => describe("Packet", function() { it('Sym. encrypted session key reading/writing test vector (AEAD, OCB)', async function() { // From https://gitlab.com/openpgp-wg/rfc4880bis/blob/00b20923/back.mkd#sample-aead-ocb-encryption-and-decryption + const nodeCrypto = openpgp.util.getNodeCrypto(); + if (!nodeCrypto) return; + let aeadProtectVal = openpgp.config.aeadProtect; let aeadChunkSizeByteVal = openpgp.config.aeadChunkSizeByte; let s2kIterationCountByteVal = openpgp.config.s2kIterationCountByte; @@ -608,11 +617,11 @@ module.exports = () => describe("Packet", function() { let sessionIV = openpgp.util.hexToUint8Array(`99 e3 26 e5 40 0a 90 93 6c ef b4 e8 eb a0 8c`.replace(/\s+/g, '')); let dataIV = openpgp.util.hexToUint8Array(`5e d2 bc 1e 47 0a be 8f 1d 64 4c 7a 6c 8a 56`.replace(/\s+/g, '')); - let randomBytesStub = stub(openpgp.crypto.random, 'getRandomBytes'); - randomBytesStub.onCall(0).returns(resolves(salt)); - randomBytesStub.onCall(1).returns(resolves(sessionKey)); - randomBytesStub.onCall(2).returns(resolves(sessionIV)); - randomBytesStub.onCall(3).returns(resolves(dataIV)); + let randomBytesStub = stub(nodeCrypto, 'randomBytes'); + randomBytesStub.onCall(0).returns(salt); + randomBytesStub.onCall(1).returns(sessionKey); + randomBytesStub.onCall(2).returns(sessionIV); + randomBytesStub.onCall(3).returns(dataIV); let packetBytes = openpgp.util.hexToUint8Array(` c3 3d 05 07 02 03 08 9f 0b 7d a3 e5 ea 64 77 90 @@ -683,12 +692,12 @@ module.exports = () => describe("Packet", function() { '-----END PGP MESSAGE-----'; let key = new openpgp.PacketList(); - await key.read((await openpgp.armor.decode(armored_key)).data, openpgp); + await key.read((await openpgp.unarmor(armored_key)).data, openpgp); key = key[3]; await key.decrypt('test'); const msg = new openpgp.PacketList(); - await msg.read((await openpgp.armor.decode(armored_msg)).data, openpgp); + await msg.read((await openpgp.unarmor(armored_msg)).data, openpgp); return msg[0].decrypt(key).then(async () => { await msg[1].decrypt(msg[0].sessionKeyAlgorithm, msg[0].sessionKey); @@ -701,7 +710,7 @@ module.exports = () => describe("Packet", function() { it('Secret key reading with signature verification.', async function() { const key = new openpgp.PacketList(); - await key.read((await openpgp.armor.decode(armored_key)).data, openpgp); + await key.read((await openpgp.unarmor(armored_key)).data, openpgp); return Promise.all([ expect(key[2].verify(key[0], openpgp.enums.signature.certGeneric, @@ -736,11 +745,11 @@ module.exports = () => describe("Packet", function() { '-----END PGP MESSAGE-----'; const key = new openpgp.PacketList(); - await key.read((await openpgp.armor.decode(armored_key)).data, openpgp); + await key.read((await openpgp.unarmor(armored_key)).data, openpgp); await key[3].decrypt('test'); const msg = new openpgp.PacketList(); - await msg.read((await openpgp.armor.decode(armored_msg)).data, openpgp); + await msg.read((await openpgp.unarmor(armored_msg)).data, openpgp); return msg[0].decrypt(key[3]).then(async () => { await msg[1].decrypt(msg[0].sessionKeyAlgorithm, msg[0].sessionKey);