Skip to content

Commit

Permalink
Add explicit algorithm param in generateKey and add config.rsaBitsMin
Browse files Browse the repository at this point in the history
  • Loading branch information
larabr committed Nov 19, 2020
1 parent 2d69fda commit a54c160
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 107 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -5,3 +5,4 @@ test/lib/
dist/
openpgp.store/
.nyc_output/
browserify-cache*.json
5 changes: 5 additions & 0 deletions src/config/config.js
Expand Up @@ -106,6 +106,11 @@ export default {
* @property {Boolean} rsaBlinding
*/
rsaBlinding: true,
/**
* @memberof module:config
* @property {Number} rsaBitsMin Minimum RSA key size allowed for key generation
*/
rsaBitsMin: 2048,
/**
* Work-around for rare GPG decryption bug when encrypting with multiple passwords.
* **Slower and slightly less secure**
Expand Down
39 changes: 15 additions & 24 deletions src/key/factory.js
Expand Up @@ -37,20 +37,15 @@ import { unarmor } from '../encoding/armor';

/**
* Generates a new OpenPGP key. Supports RSA and ECC keys.
* Primary and subkey will be of same type.
* @param {module:enums.publicKey} [options.keyType=module:enums.publicKey.rsaEncryptSign]
* To indicate what type of key to make.
* RSA is 1. See {@link https://tools.ietf.org/html/rfc4880#section-9.1}
* @param {Integer} options.rsaBits number of bits for the key creation.
* @param {String|Array<String>} options.userIds
* Assumes already in form of "User Name <username@email.com>"
* If array is used, the first userId is set as primary user Id
* @param {String} options.passphrase The passphrase used to encrypt the resulting private key
* @param {Number} [options.keyExpirationTime=0]
* The number of seconds after the key creation time that the key expires
* @param {String} options.curve (optional) elliptic curve for ECC keys
* @param {Date} options.date Override the creation date of the key and the key signatures
* @param {Array<Object>} options.subkeys (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}]
* By default, primary and subkeys will be of same type.
* @param {String} options.algorithm The primary key algorithm: ECC (default) or RSA
* @param {Integer} options.rsaBits Number of bits for RSA keys
* @param {String} options.curve Elliptic curve for ECC keys
* @param {Array<String>} options.userIds Array of user IDs e.g. [{ name:'Phil Zimmermann', email:'phil@openpgp.org' }]
* @param {String} options.passphrase Passphrase used to encrypt the resulting private key
* @param {Number} options.keyExpirationTime (optional) Number of seconds from the key creation time after which the key expires
* @param {Date} options.date Creation date of the key and the key signatures
* @param {Array<Object>} options.subkeys (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}]
* sign parameter defaults to false, and indicates whether the subkey should sign rather than encrypt
* @returns {Promise<module:key.Key>}
* @async
Expand All @@ -68,16 +63,12 @@ export async function generate(options) {

/**
* Reformats and signs an OpenPGP key with a given User ID. Currently only supports RSA keys.
* @param {module:key.Key} options.privateKey The private key to reformat
* @param {module:enums.publicKey} [options.keyType=module:enums.publicKey.rsaEncryptSign]
* @param {String|Array<String>} options.userIds
* Assumes already in form of "User Name <username@email.com>"
* If array is used, the first userId is set as primary user Id
* @param {String} options.passphrase The passphrase used to encrypt the resulting private key
* @param {Number} [options.keyExpirationTime=0]
* The number of seconds after the key creation time that the key expires
* @param {Date} options.date Override the creation date of the key and the key signatures
* @param {Array<Object>} options.subkeys (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}]
* @param {module:key.Key} options.privateKey The private key to reformat
* @param {Array<String>} options.userIds Array of user IDs e.g. [{ name:'Phil Zimmermann', email:'phil@openpgp.org' }]
* @param {String} options.passphrase Passphrase used to encrypt the resulting private key
* @param {Number} options.keyExpirationTime Number of seconds from the key creation time after which the key expires
* @param {Date} options.date Override the creation date of the key and the key signatures
* @param {Array<Object>} options.subkeys (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}]
*
* @returns {Promise<module:key.Key>}
* @async
Expand Down
47 changes: 32 additions & 15 deletions src/key/helper.js
Expand Up @@ -326,6 +326,7 @@ export async function isAeadSupported(keys, date = new Date(), userIds = []) {
}

export function sanitizeKeyOptions(options, subkeyDefaults = {}) {
options.algorithm = options.algorithm || subkeyDefaults.algorithm;
options.curve = options.curve || subkeyDefaults.curve;
options.rsaBits = options.rsaBits || subkeyDefaults.rsaBits;
options.keyExpirationTime = options.keyExpirationTime !== undefined ? options.keyExpirationTime : subkeyDefaults.keyExpirationTime;
Expand All @@ -334,24 +335,40 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) {

options.sign = options.sign || false;

if (options.curve) {
if (!util.isString(options.algorithm)) {
try {
options.curve = enums.write(enums.curve, options.curve);
options.algorithm = enums.read(enums.publicKey, options.algorithm);
} catch (e) {
throw new Error('Not valid curve.');
throw new Error('Invalid key algorithm');
}
if (options.curve === enums.curve.ed25519 || options.curve === enums.curve.curve25519) {
options.curve = options.sign ? enums.curve.ed25519 : enums.curve.curve25519;
}
if (options.sign) {
options.algorithm = options.curve === enums.curve.ed25519 ? enums.publicKey.eddsa : enums.publicKey.ecdsa;
} else {
options.algorithm = enums.publicKey.ecdh;
}
} else if (options.rsaBits) {
options.algorithm = enums.publicKey.rsaEncryptSign;
} else {
throw new Error('Unrecognized key type');
}
switch (options.algorithm) {
case "ecc":
case "eddsa":
case "ecdsa":
case "ecdh":
try {
options.curve = enums.write(enums.curve, options.curve);
} catch (e) {
throw new Error('Invalid curve');
}
if (options.curve === enums.curve.ed25519 || options.curve === enums.curve.curve25519) {
options.curve = options.sign ? enums.curve.ed25519 : enums.curve.curve25519;
}
if (options.sign) {
options.algorithm = options.curve === enums.curve.ed25519 ? enums.publicKey.eddsa : enums.publicKey.ecdsa;
} else {
options.algorithm = enums.publicKey.ecdh;
}
break;
case "rsa":
case "rsaEncryptSign":
case "rsaEncrypt":
case "rsaSign":
options.algorithm = enums.publicKey.rsaEncryptSign;
break;
default:
throw new Error(`Unsupported key algorithm ${options.algorithm}`);
}
return options;
}
Expand Down
22 changes: 14 additions & 8 deletions src/key/key.js
Expand Up @@ -32,6 +32,7 @@ import {
PublicSubkeyPacket,
SignaturePacket
} from '../packet';
import config from '../config';
import enums from '../enums';
import util from '../util';
import User from './user';
Expand Down Expand Up @@ -863,12 +864,12 @@ class Key {

/**
* Generates a new OpenPGP subkey, and returns a clone of the Key object with the new subkey added.
* Supports RSA and ECC keys. Defaults to the algorithm and bit size/curve of the primary key.
* @param {Integer} options.rsaBits number of bits for the key creation.
* @param {Number} [options.keyExpirationTime=0]
* The number of seconds after the key creation time that the key expires
* @param {String} options.curve (optional) Elliptic curve for ECC keys
* @param {Date} options.date (optional) Override the creation date of the key and the key signatures
* Supports RSA and ECC keys. Defaults to the algorithm and bit size/curve of the primary key. DSA primary keys default to RSA subkeys.
* @param {String} options.algorithm The subkey algorithm: ECC or RSA
* @param {String} options.curve (optional) Elliptic curve for ECC keys
* @param {Integer} options.rsaBits (optional) Number of bits for RSA subkeys
* @param {Number} options.keyExpirationTime (optional) Number of seconds from the key creation time after which the key expires
* @param {Date} options.date (optional) Override the creation date of the key and the key signatures
* @param {Boolean} options.sign (optional) Indicates whether the subkey should sign rather than encrypt. Defaults to false
* @returns {Promise<module:key.Key>}
* @async
Expand All @@ -880,14 +881,19 @@ class Key {
if (options.passphrase) {
throw new Error("Subkey could not be encrypted here, please encrypt whole key");
}
if (util.getWebCryptoAll() && options.rsaBits < 2048) {
throw new Error('When using webCrypto rsaBits should be 2048 or 4096, found: ' + options.rsaBits);
if (options.rsaBits < config.rsaBitsMin) {
throw new Error(`rsaBits should be at least ${config.rsaBitsMin}, got: ${options.rsaBits}`);
}
const secretKeyPacket = this.primaryKey;
if (!secretKeyPacket.isDecrypted()) {
throw new Error("Key is not decrypted");
}
const defaultOptions = secretKeyPacket.getAlgorithmInfo();
defaultOptions.rsaBits = Math.max(defaultOptions.bits || 4096, config.rsaBitsMin);
defaultOptions.curve = defaultOptions.curve || "curve25519";
if (defaultOptions.algorithm === "dsa") {
defaultOptions.algorithm = "rsa";
}
options = helper.sanitizeKeyOptions(options, defaultOptions);
const keyPacket = await helper.generateSecretSubkey(options);
const bindingSignature = await helper.createBindingSignature(keyPacket, secretKeyPacket, options);
Expand Down
42 changes: 22 additions & 20 deletions src/openpgp.js
Expand Up @@ -62,28 +62,30 @@ if (globalThis.ReadableStream) {


/**
* Generates a new OpenPGP key pair. Supports RSA and ECC keys. Primary and subkey will be of same type.
* @param {Array<Object>} userIds array of user IDs e.g. [{ name:'Phil Zimmermann', email:'phil@openpgp.org' }]
* @param {String} passphrase (optional) The passphrase used to encrypt the resulting private key
* @param {Number} rsaBits (optional) number of bits for RSA keys: 2048 or 4096.
* @param {Number} keyExpirationTime (optional) The number of seconds after the key creation time that the key expires
* @param {String} curve (optional) elliptic curve for ECC keys:
* curve25519, p256, p384, p521, secp256k1,
* brainpoolP256r1, brainpoolP384r1, or brainpoolP512r1.
* @param {Date} date (optional) override the creation date of the key and the key signatures
* @param {Array<Object>} subkeys (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}]
* sign parameter defaults to false, and indicates whether the subkey should sign rather than encrypt
* Generates a new OpenPGP key pair. Supports RSA and ECC keys. By default, primary and subkeys will be of same type.
* @param {String} algorithm (optional) The primary key algorithm: ECC (default) or RSA
* @param {Array<Object>} userIds Array of user IDs e.g. [{ name:'Phil Zimmermann', email:'phil@openpgp.org' }]
* @param {String} passphrase (optional) The passphrase used to encrypt the resulting private key
* @param {Number} rsaBits (optional) Number of bits for RSA keys, defaults to 4096, or a higher value equal to config.rsaBitsMin
* @param {String} curve (optional) Elliptic curve for ECC keys:
* curve25519 (default), p256, p384, p521, secp256k1,
* brainpoolP256r1, brainpoolP384r1, or brainpoolP512r1
* @param {Date} date (optional) Override the creation date of the key and the key signatures
* @param {Number} keyExpirationTime (optional) Number of seconds from the key creation time after which the key expires
* @param {Array<Object>} subkeys (optional) Options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}]
* sign parameter defaults to false, and indicates whether the subkey should sign rather than encrypt
* @returns {Promise<Object>} The generated key object in the form:
* { key:Key, privateKeyArmored:String, publicKeyArmored:String, revocationCertificate:String }
* @async
* @static
*/
export function generateKey({ userIds = [], passphrase = "", rsaBits = null, keyExpirationTime = 0, curve = "curve25519", date = new Date(), subkeys = [{}] }) {
export function generateKey({ userIds = [], passphrase = "", algorithm = "ecc", rsaBits, curve = "curve25519", keyExpirationTime = 0, date = new Date(), subkeys = [{}] }) {
userIds = toArray(userIds);
curve = rsaBits ? "" : curve;
const options = { userIds, passphrase, rsaBits, keyExpirationTime, curve, date, subkeys };
if (util.getWebCryptoAll() && rsaBits && rsaBits < 2048) {
throw new Error('rsaBits should be 2048 or 4096, found: ' + rsaBits);
rsaBits = rsaBits || Math.max(4096, config.rsaBitsMin);
algorithm = algorithm.toLowerCase();
const options = { userIds, passphrase, algorithm, rsaBits, curve, keyExpirationTime, date, subkeys };
if (algorithm === "rsa" && rsaBits < config.rsaBitsMin) {
throw new Error(`rsaBits should be at least ${config.rsaBitsMin}, got: ${rsaBits}`);
}

return generate(options).then(async key => {
Expand All @@ -103,10 +105,10 @@ export function generateKey({ userIds = [], passphrase = "", rsaBits = null, key

/**
* Reformats signature packets for a key and rewraps key object.
* @param {Key} privateKey private key to reformat
* @param {Array<Object>} userIds array of user IDs e.g. [{ name:'Phil Zimmermann', email:'phil@openpgp.org' }]
* @param {String} passphrase (optional) The passphrase used to encrypt the resulting private key
* @param {Number} keyExpirationTime (optional) The number of seconds after the key creation time that the key expires
* @param {Key} privateKey Private key to reformat
* @param {Array<Object>} userIds Array of user IDs e.g. [{ name:'Phil Zimmermann', email:'phil@openpgp.org' }]
* @param {String} passphrase (optional) The passphrase used to encrypt the resulting private key
* @param {Number} keyExpirationTime (optional) Number of seconds from the key creation time after which the key expires
* @returns {Promise<Object>} The generated key object in the form:
* { key:Key, privateKeyArmored:String, publicKeyArmored:String, revocationCertificate:String }
* @async
Expand Down
9 changes: 5 additions & 4 deletions src/packet/public_key.js
Expand Up @@ -231,14 +231,15 @@ class PublicKeyPacket {

/**
* Returns algorithm information
* @returns {Object} An object of the form {algorithm: String, rsaBits:int, curve:String}
* @returns {Object} An object of the form {algorithm: String, bits:int, curve:String}
*/
getAlgorithmInfo() {
const result = {};
result.algorithm = this.algorithm;
if (this.publicParams.n) {
result.rsaBits = this.publicParams.n.length * 8;
result.bits = result.rsaBits; // Deprecated.
// RSA, DSA or ElGamal public modulo
const modulo = this.publicParams.n || this.publicParams.p;
if (modulo) {
result.bits = modulo.length * 8;
} else {
result.curve = this.publicParams.oid.getName();
}
Expand Down
10 changes: 5 additions & 5 deletions test/crypto/rsa.js
Expand Up @@ -14,7 +14,7 @@ const expect = chai.expect;
const native = util.getWebCrypto() || util.getNodeCrypto();
module.exports = () => (!native ? describe.skip : describe)('basic RSA cryptography with native crypto', function () {
it('generate rsa key', async function() {
const bits = util.getWebCryptoAll() ? 2048 : 1024;
const bits = 1024;
const keyObject = await crypto.publicKey.rsa.generate(bits, 65537);
expect(keyObject.n).to.exist;
expect(keyObject.e).to.exist;
Expand All @@ -25,7 +25,7 @@ module.exports = () => (!native ? describe.skip : describe)('basic RSA cryptogra
});

it('sign and verify using generated key params', async function() {
const bits = util.getWebCryptoAll() ? 2048 : 1024;
const bits = 1024;
const { publicParams, privateParams } = await crypto.generateParams(openpgp.enums.publicKey.rsaSign, bits);
const message = await random.getRandomBytes(64);
const hash_algo = openpgp.enums.write(openpgp.enums.hash, 'sha256');
Expand All @@ -38,7 +38,7 @@ module.exports = () => (!native ? describe.skip : describe)('basic RSA cryptogra
});

it('encrypt and decrypt using generated key params', async function() {
const bits = util.getWebCryptoAll() ? 2048 : 1024;
const bits = 1024;
const { publicParams, privateParams } = await crypto.generateParams(openpgp.enums.publicKey.rsaSign, bits);
const { n, e, d, p, q, u } = { ...publicParams, ...privateParams };
const message = await crypto.generateSessionKey('aes256');
Expand Down Expand Up @@ -72,7 +72,7 @@ module.exports = () => (!native ? describe.skip : describe)('basic RSA cryptogra
});

it('compare native crypto and bn math sign', async function() {
const bits = util.getWebCrypto() ? 2048 : 1024;
const bits = 1024;
const { publicParams, privateParams } = await crypto.generateParams(openpgp.enums.publicKey.rsaSign, bits);
const { n, e, d, p, q, u } = { ...publicParams, ...privateParams };
const message = await random.getRandomBytes(64);
Expand All @@ -98,7 +98,7 @@ module.exports = () => (!native ? describe.skip : describe)('basic RSA cryptogra
});

it('compare native crypto and bn math verify', async function() {
const bits = util.getWebCrypto() ? 2048 : 1024;
const bits = 1024;
const { publicParams, privateParams } = await crypto.generateParams(openpgp.enums.publicKey.rsaSign, bits);
const { n, e, d, p, q, u } = { ...publicParams, ...privateParams };
const message = await random.getRandomBytes(64);
Expand Down
2 changes: 1 addition & 1 deletion test/crypto/validate.js
Expand Up @@ -238,7 +238,7 @@ module.exports = () => {
describe('RSA parameter validation', function() {
let rsaKey;
before(async () => {
rsaKey = (await openpgp.generateKey({ rsaBits: 2048, userIds: [{ name: 'Test', email: 'test@test.com' }] })).key;
rsaKey = (await openpgp.generateKey({ algorithm: "rsa", rsaBits: 2048, userIds: [{ name: 'Test', email: 'test@test.com' }] })).key;
});

it('generated RSA params are valid', async function() {
Expand Down

0 comments on commit a54c160

Please sign in to comment.