Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v5] Explicit algorithm parameter in openpgp.generateKey #1179

Merged
merged 4 commits into from Jan 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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';
twiss marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -402,7 +402,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