Skip to content

V5 Changelog

Daniel Huigens edited this page Jan 10, 2023 · 20 revisions

Security improvements

  • Improve the security of newly generated keys:
    • Generate ECC keys by default (#1065)
    • RSA keys can still be generated with the new type parameter of generateKey: (#1179)
      import { generateKey } from 'openpgp';
      const { privateKey } = await generateKey({
        userIDs: [{ name: 'Test', email: 'test@email' }],
        type: 'rsa'
      });
    • Newly generated RSA keys are now 4096-bits by default
    • Remove SHA-1 from the default preferred hash algorithms (#1067)
    • Remove 3DES and CAST5 from the default preferred symmetric algorithms (#1068)
  • Reject certain insecure keys by default:
    • Add config.rejectPublicKeyAlgorithms, and default to disallowing the use of ElGamal and DSA for encrypting new messages and signing and verifying signatures, respectively (#1264)
    • Reject RSA keys with fewer than config.minRSABits bits (defaulting to 2048) when encrypting new messages and signing and verifying signatures, not just on key generation (#1264)
    • If you want to make an exception for a certain key or algorithm, rather than adjusting the global openpgp.config, you can now pass a config to a single function call, perhaps after warning the user / confirming that they want to allow this; for example:
      import { createMessage, encrypt } from 'openpgp';
      const message = await createMessage('Hello world!');
      try {
        await encrypt({
          message,
          encryptionKeys: publicKeys
        });
      } catch (err) {
        if (err.message.includes('...') && confirm('Warning: keys are insecure. Use anyway?')) {
          await encrypt({
            message,
            encryptionKeys: publicKeys,
            config: { minRSABits: 1024, rejectPublicKeyAlgorithms: new Set() }
          });
        } else {
          throw err;
        }
      }
      Of course, if at all possible, it's better to (ask users to) generate new keys, instead.
  • … and a few smaller configuration changes

Library size reductions

  • openpgp.HKP has been moved to a separate package: openpgpjs/hkp-client
  • openpgp.WKD has been moved to a separate package: openpgpjs/wkd-client
  • openpgp.Keyring and LocalStore have been removed, because keyring handling and storage should be handled in the application, as localStorage may not meet the durability requirements of the application.
  • The built-in Web Worker and openpgp.createWorker have been removed (for the rationale, please see #964)
  • The "lightweight build" has been greatly reduced in size. The lightweight build loads certain dependencies on demand only when needed:
    • The indutny/elliptic library is loaded only when using or generating an ECC key with a brainpool or secp256k1 curve (or the NIST curves if Web Crypto is not supported). When using Curve25519/Ed25519, "elliptic" is never loaded.
    • The indutny/bn.js library is only loaded when the native BigInt API is not supported (see caniuse.com/bigint), and/or when the elliptic library is loaded (as it depends on bn.js).
    • The web-streams-polyfill and web-streams-adapter libraries are only loaded when streaming is used, and only if TransformStreams aren't natively supported (see caniuse.com/transformstream).
    • To use the lightweight build:
      import * as openpgp from 'openpgp/lightweight';
  • OpenPGP.js has been refactored to be more amenable to dead-code elimination, such that it's now more beneficial to only import the functions you need. For example, if you only import generateKey, support for encrypting and compressing messages is not needed, reducing the size of the build when using a build system such as webpack:
    import { generateKey } from 'openpgp/lightweight';

High-level API changes

(Examples below)

  • Replace openpgp.key.read/readArmored with openpgp.readKey
    • openpgp.readKey now takes an options object (either { armoredKey } or { binaryKey })
    • It now only returns a single key object, rather than a { keys: [key...], err } object
    • It now throws an error if the key failed to parse or if the key block contains multiple keys
  • Add openpgp.readKeys
    • It takes an options object (either { armoredKeys } or { binaryKeys })
    • It returns an array of key objects
    • It throws if any of the keys in the key block failed to parse
  • Add openpgp.readPrivateKey and openpgp.readPrivateKeys
    • These behave identically to readKey and readKeys, but additionally guarantee that a private key is returned
    • They throw an error if the parsed key is a public key
  • Remove Key.prototype.encrypt/decrypt in favor of openpgp.encryptKey/decryptKey
    • These functions take an option object ({ privateKey, passphrase })
    • They return a clone of the key object, rather than mutating the key
    • This eliminates the possibility of ending up in an inconsistent state where Key.prototype.decrypt would throw when decrypting one subkey after already decrypting (and mutating) others
  • Replace openpgp.message.read/readArmored with openpgp.readMessage, openpgp.signature.read/readArmored with openpgp.readSignature, and openpgp.cleartext.readArmored with openpgp.readCleartextMessage
    • openpgp.readMessage now takes an options object (either { armoredMessage } or { binaryMessage })
    • openpgp.readSignature now takes an options object (either { armoredSignature } or { binarySignature })
    • openpgp.readCleartextMessage now takes an options object ({ cleartextMessage })
  • Replace openpgp.message.fromText/fromBinary with openpgp.createMessage, and openpgp.cleartext.fromText with openpgp.createCleartextMessage
    • openpgp.createMessage now takes an options object (either { text } or { binary })
    • openpgp.createCleartextMessage now takes an options object ({ text })
    • Both of these functions are now async, and need to be awaited, like all the other top-level functions
  • In openpgp.generateKey, reformatKey, and revokeKey, return a simplified "key pair" object:
    • The returned object now contains privateKey and publicKey properties, and no longer contains key, privateKeyArmored, and publicKeyArmored
    • These functions now take the option format: 'armored' | 'binary' | 'object' to determine the format of privateKey and privateKey (defaulting to 'armored')
    • The object returned from generateKey and reformatKey still contains a revocationCertificate property, which is not affected by the format option
  • In openpgp.encrypt, sign, encryptSessionKey, encryptKey and decryptKey, return the result directly without wrapping it in a "results" object
  • In all top-level functions, rename public/privateKeys to encryption/decryption/signing/verificationKeys depending on their use
    • In openpgp.encrypt, encryptSessionKey, and generateSessionKey (see below), publicKeys is now called encryptionKeys
    • In openpgp.encrypt and sign, privateKeys is now called signingKeys
    • In openpgp.decrypt and decryptSessionKeys, privateKeys is now called decryptionKeys
    • In openpgp.decrypt and verify, publicKeys is now called verificationKeys
    • Similarly, rename toUserIDs to encryptionUserIDs and fromUserIDs to signingUserIDs
  • Remove the detached option of openpgp.encrypt. You can separately call openpgp.sign({ message, privateKeys, detached: true }) instead (don't forget to remove the privateKeys option from openpgp.encrypt as well if you do so, if you don't want the message to be signed inline). However, note that storing detached signatures of plaintext data together with the encrypted data is not secure
  • Add a new openpgp.generateSessionKey function
  • Remove the returnSessionKey option of openpgp.encrypt. You can separately call openpgp.generateSessionKey({ encryptionKeys: publicKeys }) instead and call openpgp.encrypt({ sessionKey }) with the result.
  • Remove the streaming option of openpgp.encrypt, decrypt, sign and verify. These functions now only use streaming when a ReadableStream or Node.js Readable stream is passed.
  • Replace the armor option in openpgp.encrypt and sign with a format option accepting the values 'armored' (default), 'binary' or 'object':
    • format: 'armored' is equivalent to armor: true
    • format: 'object' is equivalent to armor: false
    • format: 'binary' returns the message serialized as a Uint8Array (this was not supported before)
  • Add an expectSigned option to openpgp.encrypt and verify, which causes these functions to throw if there was no valid signature in the message.
  • Throughout the API, userId has been changed to userID, and keyId has been changed to keyID. This affects the userIDs option of openpgp.generateKey, the fromUserIDs and toUserIDs options of openpgp.encrypt and decrypt, Key.prototype.getKeyID and getKeyIDs, Message.prototype.getEncryptionKeyID and getSigningKeyID, etc.
  • Rename all openpgp.enum.*.value_names to camelCase openpgp.enum.*.valueNames (#1093)
  • Remove openpgp.util (#1175)

Configuration changes

  • Rename all openpgp.config.option_names to camelCase openpgp.config.optionNames (#1088)
    • Rename openpgp.config.versionstring to versionString, and commentstring to commentString
  • Don't add version and comment strings to armored messages and keys by default
  • Rename openpgp.config.ignore_mdc_error to allowUnauthenticatedMessages, and add a warning in the documentation that this option is insecure
  • Remove the option to generate non-integrity-protected messages (openpgp.config.integrityProtect)
  • openpgp.config.aeadProtect now controls whether private key encryption uses AEAD; previously this was dependent on the key version, and could only be used for v5 keys
  • All top-level functions (including the new ones mentioned above) now take a config property of their options parameter
    • This can be helpful when you want to change the configuration for a single function call, rather than all OpenPGP.js function cals
    • For example, you can now generate a single V5 key as follows:
      const { privateKey } = await openpgp.generateKey({
        userIDs: [{ name: 'Test', email: 'test@email' }],
        config: { v5Keys: true }
      });
    • Config options that aren't passed still default to openpgp.config

Low-level API changes

  • Rename openpgp.packet.* to openpgp.*Packet
  • Rename openpgp.packet.Userid to openpgp.UserIDPacket
  • Rename openpgp.packet.Literal to openpgp.LiteralDataPacket
  • Rename openpgp.packet.Compressed to openpgp.CompressedDataPacket
  • Rename openpgp.packet.SymmetricallyEncrypted to openpgp.SymmetricallyEncryptedDataPacket
  • Rename openpgp.packet.SymEncryptedIntegrityProtected to openpgp.SymEncryptedIntegrityProtectedDataPacket
  • Rename openpgp.packet.SymEncryptedAEADProtected to openpgp.AEADEncryptedDataPacket

  • Rename openpgp.enums.packet.userid to openpgp.enums.packet.userID
  • Rename openpgp.enums.packet.literal to openpgp.enums.packet.literalData
  • Rename openpgp.enums.packet.compressed to openpgp.enums.packet.compressedData
  • Rename openpgp.enums.packet.symmetricallyEncrypted to openpgp.enums.packet.symmetricallyEncryptedData
  • Rename openpgp.enums.packet.symEncryptedIntegrityProtected to openpgp.enums.packet.symEncryptedIntegrityProtectedData
  • Rename openpgp.enums.packet.symEncryptedAEADProtected to openpgp.enums.packet.aeadEncryptedData

  • Rename openpgp.message.generateSessionKey to openpgp.Message.generateSessionKey
  • Rename openpgp.message.encryptSessionKey to openpgp.Message.encryptSessionKey

Examples

Generate an armored key pair

v4:

import * as openpgp from 'openpgp';
const { privateKeyArmored, publicKeyArmored, revocationCertificate } = await openpgp.generateKey({ userIds: [{ name: 'Test', email: 'test@email' }] });

v5:

import { generateKey } from 'openpgp';
const { privateKey, publicKey, revocationCertificate } = await generateKey({ userIDs: [{ name: 'Test', email: 'test@email' }] });
Generate a private key object

v4:

import * as openpgp from 'openpgp';
const { key } = await generateKey({ userIds: [{ name: 'Test', email: 'test@email' }] });

v5:

import { generateKey } from 'openpgp';
const { privateKey } = await generateKey({ userIDs: [{ name: 'Test', email: 'test@email' }], format: 'object' });
Parse a public key (armored)

v4:

import * as openpgp from 'openpgp';
const publicKey = (await openpgp.key.readArmored(armoredKey)).keys[0];

v5:

import { readKey } from 'openpgp';
const publicKey = await readKey({ armoredKey });
Parse a public key (binary)

v4:

import * as openpgp from 'openpgp';
const publicKey = (await openpgp.key.read(armoredKey)).keys[0];

v5:

import { readKey } from 'openpgp';
const publicKey = await readKey({ binaryKey });
Parse one or more public keys (armored)

v4:

import * as openpgp from 'openpgp';
const publicKeys = (await openpgp.key.readArmored(armoredKeys)).keys;

v5:

import { readKeys } from 'openpgp';
const publicKeys = await readKeys({ armoredKeys });
Parse and decrypt a private key (armored)

v4:

import * as openpgp from 'openpgp';
const privateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
await privateKey.decrypt(passphrase);

v5:

import { readPrivateKey, decryptKey } from 'openpgp';
const privateKey = await readPrivateKey({ armoredKey }); // Or `readKey` - `readPrivateKey` is helpful for better TypeScript type hinting.
const decryptedPrivateKey = await decryptKey({ privateKey, passphrase });
Encrypt and sign text message (armored output)

v4:

import * as openpgp from 'openpgp';
const message = openpgp.message.fromText(text);
const encrypted = await openpgp.encrypt({ publicKeys: publicKeys, privateKeys: privateKeys, message });
console.log(encrypted.data); // String

v5:

import { createMessage, encrypt } from 'openpgp';
const message = await createMessage({ text });
const encrypted = await encrypt({ encryptionKeys: publicKeys, signingKeys: privateKeys, message });
console.log(encrypted); // String
Encrypt and sign binary message (binary output)

v4:

import * as openpgp from 'openpgp';
const message = openpgp.message.fromBinary(data);
const encrypted = await openpgp.encrypt({ publicKeys: publicKeys, privateKeys: privateKeys, message, format: 'binary' });
console.log(encrypted.message.packets.write()); // Uint8Array

v5:

import { createMessage, encrypt } from 'openpgp';
const message = await createMessage({ binary: data });
const encrypted = await encrypt({ encryptionKeys: publicKeys, signingKeys: privateKeys, message, format: 'binary' });
console.log(encrypted); // Uint8Array
Encrypt and sign message (inspect packets)

v4:

import * as openpgp from 'openpgp';
const encrypted = await openpgp.encrypt({ publicKeys: publicKeys, privateKeys: privateKeys, message, armor: false });
console.log(encrypted.message.packets); // Array

v5:

import { encrypt, readMessage } from 'openpgp';
const encrypted = await encrypt({ encryptionKeys: publicKeys, signingKeys: privateKeys, message, format: 'object' });
console.log(encrypted.packets); // Array
Decrypt and verify text message (armored input)

v4:

import * as openpgp from 'openpgp';
const message = openpgp.message.readArmored(armoredMessage);
const decrypted = await openpgp.decrypt({ privateKeys: privateKeys, publicKeys: publicKeys, message });
console.log(decrypted.data); // String
console.log(decrypted.signatures[0].valid); // Boolean

v5:

import { readMessage, decrypt } from 'openpgp';
const message = await readMessage({ armoredMessage });
const decrypted = await decrypt({ decryptionKeys: privateKeys, verificationKeys: publicKeys, message });
console.log(decrypted.data); // String
console.log(await decrypted.signatures[0].verified); // Boolean
Decrypt and verify binary message (binary input)

v4:

import * as openpgp from 'openpgp';
const message = openpgp.message.read(binaryMessage);
const decrypted = await openpgp.decrypt({ privateKeys: privateKeys, publicKeys: publicKeys, message });
console.log(decrypted.data); // String
console.log(decrypted.signatures[0].valid); // Boolean

v5:

import { readMessage, decrypt } from 'openpgp';
const message = await readMessage({ binaryMessage });
const decrypted = await decrypt({ decryptionKeys: privateKeys, verificationKeys: publicKeys, message, format: 'binary' });
console.log(decrypted.data); // Uint8Array
console.log(await decrypted.signatures[0].verified); // Boolean
Decrypt and verify message, and throw if it's not verified

v4:

import * as openpgp from 'openpgp';
const decrypted = await openpgp.decrypt({ privateKeys: privateKeys, publicKeys: publicKeys, message });
if (!decrypted.signatures.some(signature => signature.valid)) {
  throw new Error("Couldn't find valid signature in message");
}

v5:

import { decrypt } from 'openpgp';
const decrypted = await decrypt({ decryptionKeys: privateKeys, verificationKeys: publicKeys, message, expectSigned: true });
Sign cleartext message (armored output)

v4:

import * as openpgp from 'openpgp';
const message = openpgp.cleartext.fromText(text);
const signed = await openpgp.sign({ privateKeys: privateKeys, message });
console.log(signed.data); // String

v5:

import { createCleartextMessage, sign } from 'openpgp';
const message = await createCleartextMessage({ text });
const signed = await sign({ signingKeys: privateKeys, message });
console.log(signed); // String
Sign text message (binary output)

v4:

import * as openpgp from 'openpgp';
const message = openpgp.message.fromText(text);
const signed = await openpgp.sign({ privateKeys: privateKeys, message, armor: false });
console.log(signed.message.packets.write()); // Uint8Array

v5:

import { createMessage, sign } from 'openpgp';
const message = await createMessage({ text });
const signed = await sign({ signingKeys: privateKeys, message, format: 'binary' });
console.log(signed); // Uint8Array
Detached-sign cleartext message (armored output)

v4:

import * as openpgp from 'openpgp';
const message = openpgp.cleartext.fromText(text);
const signed = await openpgp.sign({ privateKeys: privateKeys, message, detached: true });
console.log(signed.signature); // String

v5:

import { createMessage, sign } from 'openpgp';
const message = await createMessage({ text: util.removeTrailingSpaces(text) });
const signed = await sign({ signingKeys: privateKeys, message, detached: true });
console.log(signed); // String
Detached-sign binary message (binary output)

v4:

import * as openpgp from 'openpgp';
const message = openpgp.message.fromText(text);
const signed = await openpgp.sign({ privateKeys: privateKeys, message, detached: true, armor: false });
console.log(signed.signature.packets.write()); // Uint8Array

v5:

import { createMessage, sign } from 'openpgp';
const message = await createMessage({ text });
const signed = await sign({ signingKeys: privateKeys, message, detached: true, format: 'binary' });
console.log(signed); // Uint8Array
Verify signed text message (armored input)

v4:

import * as openpgp from 'openpgp';
const message = await openpgp.message.readArmored(armor);
const verified = await openpgp.verify({ publicKeys: publicKeys, message });
console.log(openpgp.util.nativeEOL(openpgp.util.decode_utf8(verified.data))); // String
console.log(verified.signatures); // Array

v5:

import { readMessage, verify } from 'openpgp';
const message = await readMessage({ armoredMessage });
const verified = await verify({ verificationKeys: publicKeys, message });
console.log(verified.data); // String
console.log(verified.signatures); // Array
Verify signed binary message (binary input)

v4:

import * as openpgp from 'openpgp';
const message = await openpgp.message.read(binary);
const verified = await openpgp.verify({ publicKeys: publicKeys, message });
console.log(verified.data); // Uint8Array
console.log(verified.signatures); // Array

v5:

import { readMessage, verify } from 'openpgp';
const message = await readMessage({ binaryMessage });
const verified = await verify({ verificationKeys: publicKeys, message, format: 'binary' });
console.log(verified.data); // Uint8Array
console.log(verified.signatures); // Array
Encrypt session keys (armored output)

v4:

import * as openpgp from 'openpgp';
const encrypted = await openpgp.encryptSessionKey({ publicKeys: publicKeys, data, algorithm });
console.log(encrypted.message.armor()); // String

v5:

import { encryptSessionKey } from 'openpgp';
const encrypted = await encryptSessionKey({ encryptionKeys: publicKeys, data, algorithm });
console.log(encrypted); // String
Encrypt session keys (binary output)

v4:

import * as openpgp from 'openpgp';
const encrypted = await openpgp.encryptSessionKey({ publicKeys: publicKeys, data, algorithm });
console.log(encrypted.message.packets.write()); // Uint8Array

v5:

import { encryptSessionKey } from 'openpgp';
const encrypted = await encryptSessionKey({ encryptionKeys: publicKeys, data, algorithm, format: 'binary' });
console.log(encrypted); // Uint8Array
Streaming-encrypt text message on Node.js (armored output)

v4:

import * as openpgp from 'openpgp';
const data = fs.createReadStream(filename, { encoding: 'utf8' });
const message = openpgp.message.fromText(data);
const encrypted = await openpgp.encrypt({ publicKeys: publicKeys, message });
encrypted.data.on('data', chunk => {
  console.log(openpgp.util.Uint8Array_to_str(chunk)); // String
});

v5:

import { createMessage, encrypt } from 'openpgp';
const data = fs.createReadStream(filename, { encoding: 'utf8' });
const message = await createMessage({ text: data });
const encrypted = await encrypt({ encryptionKeys: publicKeys, message });
encrypted.on('data', chunk => {
  console.log(chunk); // String
});
Streaming-encrypt binary message on Node.js (binary output)

v4:

import * as openpgp from 'openpgp';
const data = fs.createReadStream(filename);
const message = openpgp.message.fromBinary(data);
const encrypted = await openpgp.encrypt({ publicKeys: publicKeys, message, armor: false });
openpgp.stream.webToNode(encrypted.message.packets.write()).pipe(targetStream);

v5:

import { createMessage, encrypt } from 'openpgp';
const data = fs.createReadStream(filename);
const message = await createMessage({ binary: data });
const encrypted = await encrypt({ encryptionKeys: publicKeys, message, format: 'binary' });
encrypted.pipe(targetStream);