Skip to content

Commit

Permalink
Add explicit key type parameter in openpgp.generateKey (#1179)
Browse files Browse the repository at this point in the history
- Changes `openpgp.generateKey` to accept an explicit `type` parameter,
  instead of inferring its value from the `curve` or `rsaBits` params
- Introduces `config.minRsaBits` to set minimum key size of RSA key generation
  • Loading branch information
larabr committed Jan 13, 2021
1 parent 57762b0 commit cd82f87
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 114 deletions.
6 changes: 4 additions & 2 deletions openpgp.d.ts
Expand Up @@ -310,6 +310,7 @@ export namespace config {
let ignoreMdcError: boolean;
let checksumRequired: boolean;
let rsaBlinding: boolean;
let minRsaBits: number;
let passwordCollisionCheck: boolean;
let revocationsExpire: boolean;
let useNative: boolean;
Expand Down Expand Up @@ -621,9 +622,10 @@ export type EllipticCurveName = 'ed25519' | 'curve25519' | 'p256' | 'p384' | 'p5
interface KeyOptions {
userIds: UserId[]; // generating a key with no user defined results in error
passphrase?: string;
numBits?: number;
keyExpirationTime?: number;
type?: 'ecc' | 'rsa';
curve?: EllipticCurveName;
rsaBits?: number;
keyExpirationTime?: number;
date?: Date;
subkeys?: KeyOptions[];
}
Expand Down
5 changes: 5 additions & 0 deletions src/config/config.js
Expand Up @@ -108,6 +108,11 @@ export default {
* @property {Boolean} rsaBlinding
*/
rsaBlinding: true,
/**
* @memberof module:config
* @property {Number} minRsaBits Minimum RSA key size allowed for key generation
*/
minRsaBits: 2048,
/**
* Work-around for rare GPG decryption bug when encrypting with multiple passwords.
* **Slower and slightly less secure**
Expand Down
41 changes: 16 additions & 25 deletions src/key/factory.js
Expand Up @@ -37,21 +37,16 @@ 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'}]
* sign parameter defaults to false, and indicates whether the subkey should sign rather than encrypt
* By default, primary and subkeys will be of same type.
* @param {ecc|rsa} options.type The primary key algorithm type: ECC or RSA
* @param {String} options.curve Elliptic curve for ECC keys
* @param {Integer} options.rsaBits Number of bits for RSA keys
* @param {Array<String|Object>} options.userIds User IDs as strings or objects: 'Jo Doe <info@jo.com>' or { name:'Jo Doe', email:'info@jo.com' }
* @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
* @static
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|Object>} options.userIds User IDs as strings or objects: 'Jo Doe <info@jo.com>' or { name:'Jo Doe', email:'info@jo.com' }
* @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
40 changes: 22 additions & 18 deletions src/key/helper.js
Expand Up @@ -327,6 +327,7 @@ export async function isAeadSupported(keys, date = new Date(), userIds = []) {
}

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

options.sign = options.sign || false;

if (options.curve) {
try {
options.curve = enums.write(enums.curve, options.curve);
} catch (e) {
throw new Error('Not valid 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;
}
} else if (options.rsaBits) {
options.algorithm = enums.publicKey.rsaEncryptSign;
} else {
throw new Error('Unrecognized key type');
switch (options.type) {
case 'ecc':
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':
options.algorithm = enums.publicKey.rsaEncryptSign;
break;
default:
throw new Error(`Unsupported key type ${options.type}`);
}
return options;
}
Expand Down
20 changes: 12 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 @@ -861,12 +862,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 {ecc|rsa} options.type 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 @@ -878,14 +879,17 @@ 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.minRsaBits) {
throw new Error(`rsaBits should be at least ${config.minRsaBits}, got: ${options.rsaBits}`);
}
const secretKeyPacket = this.primaryKey;
if (!secretKeyPacket.isDecrypted()) {
throw new Error("Key is not decrypted");
}
const defaultOptions = secretKeyPacket.getAlgorithmInfo();
defaultOptions.type = defaultOptions.curve ? 'ecc' : 'rsa'; // DSA keys default to RSA
defaultOptions.rsaBits = defaultOptions.bits || 4096;
defaultOptions.curve = defaultOptions.curve || 'curve25519';
options = helper.sanitizeKeyOptions(options, defaultOptions);
const keyPacket = await helper.generateSecretSubkey(options);
const bindingSignature = await helper.createBindingSignature(keyPacket, secretKeyPacket, options);
Expand Down
40 changes: 20 additions & 20 deletions src/openpgp.js
Expand Up @@ -62,28 +62,28 @@ 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 {ecc|rsa} type (optional) The primary key algorithm type: ECC (default) or RSA
* @param {Array<String|Object>} userIds User IDs as strings or objects: 'Jo Doe <info@jo.com>' or { name:'Jo Doe', email:'info@jo.com' }
* @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
* @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 = "", type = "ecc", rsaBits = 4096, 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);
const options = { userIds, passphrase, type, rsaBits, curve, keyExpirationTime, date, subkeys };
if (type === "rsa" && rsaBits < config.minRsaBits) {
throw new Error(`rsaBits should be at least ${config.minRsaBits}, got: ${rsaBits}`);
}

return generate(options).then(async key => {
Expand All @@ -103,10 +103,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<String|Object>} userIds User IDs as strings or objects: 'Jo Doe <info@jo.com>' or { name:'Jo Doe', email:'info@jo.com' }
* @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
1 change: 0 additions & 1 deletion src/packet/secret_key.js
Expand Up @@ -405,7 +405,6 @@ class SecretKeyPacket extends PublicKeyPacket {
}
}


async generate(bits, curve) {
const algo = enums.write(enums.publicKey, this.algorithm);
const { privateParams, publicParams } = await crypto.generateParams(algo, bits, curve);
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 @@ -244,7 +244,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({ type: '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 cd82f87

Please sign in to comment.