diff --git a/.github/test-suite/config.json.template b/.github/test-suite/config.json.template index 8e9a9d3ff..32c74d177 100644 --- a/.github/test-suite/config.json.template +++ b/.github/test-suite/config.json.template @@ -4,7 +4,8 @@ "id": "sop-openpgpjs-branch", "path": "__SOP_OPENPGPJS__", "env": { - "OPENPGPJS_PATH": "__OPENPGPJS_BRANCH__" + "OPENPGPJS_PATH": "__OPENPGPJS_BRANCH__", + "OPENPGPJS_CUSTOM_PROFILES": "{\"generate-key\": { \"post-quantum\": { \"description\": \"generate post-quantum v6 keys (relying on ML-DSA + ML-KEM)\", \"options\": { \"type\": \"pqc\", \"config\": { \"v6Keys\": true } } } } }" } }, { diff --git a/openpgp.d.ts b/openpgp.d.ts index 7a64ea601..8b8a44ce4 100644 --- a/openpgp.d.ts +++ b/openpgp.d.ts @@ -326,8 +326,10 @@ interface Config { showVersion: boolean; showComment: boolean; aeadProtect: boolean; + ignoreSEIPDv2FeatureFlag: boolean; allowUnauthenticatedMessages: boolean; allowUnauthenticatedStream: boolean; + allowForwardedMessages: boolean; minRSABits: number; passwordCollisionCheck: boolean; ignoreUnsupportedPackets: boolean; @@ -702,7 +704,7 @@ export type EllipticCurveName = 'ed25519Legacy' | 'curve25519Legacy' | 'nistP256 interface GenerateKeyOptions { userIDs: MaybeArray; passphrase?: string; - type?: 'ecc' | 'rsa' | 'curve25519' | 'curve448'; + type?: 'ecc' | 'rsa' | 'curve25519' | 'curve448' | 'pqc'; curve?: EllipticCurveName; rsaBits?: number; keyExpirationTime?: number; @@ -713,8 +715,9 @@ interface GenerateKeyOptions { } export type KeyOptions = GenerateKeyOptions; -export interface SubkeyOptions extends Pick { +export interface SubkeyOptions extends Pick { sign?: boolean; + forwarding?: boolean; } export declare class KeyID { @@ -822,7 +825,7 @@ export namespace enums { aeadEncryptedData = 20 } - export type publicKeyNames = 'rsaEncryptSign' | 'rsaEncrypt' | 'rsaSign' | 'elgamal' | 'dsa' | 'ecdh' | 'ecdsa' | 'eddsaLegacy' | 'aedh' | 'aedsa' | 'ed25519' | 'x25519' | 'ed448' | 'x448'; + export type publicKeyNames = 'rsaEncryptSign' | 'rsaEncrypt' | 'rsaSign' | 'elgamal' | 'dsa' | 'ecdh' | 'ecdsa' | 'eddsaLegacy' | 'aedh' | 'aedsa' | 'ed25519' | 'x25519' | 'ed448' | 'x448' | 'pqc_mlkem_x25519' | 'pqc_mldsa_ed25519'; export enum publicKey { rsaEncryptSign = 1, rsaEncrypt = 2, @@ -837,7 +840,9 @@ export namespace enums { x25519 = 25, x448 = 26, ed25519 = 27, - ed448 = 28 + ed448 = 28, + pqc_mlkem_x25519 = 105, + pqc_mldsa_ed25519 = 107 } export enum curve { @@ -891,6 +896,7 @@ export namespace enums { encryptStorage = 8, splitPrivateKey = 16, authentication = 32, + forwardedCommunication = 64, sharedPrivateKey = 128 } @@ -937,3 +943,24 @@ export namespace enums { gnu = 101 } } + +export declare class Argon2S2K { + static reloadWasmModule(): void; + static ARGON2_WASM_MEMORY_THRESHOLD_RELOAD: number; + constructor(config: Config); + salt: Uint8Array; + /** @throws Argon2OutOfMemoryError */ + produceKey(passphrase: string, keySize: number): Promise; +} + +interface KDFParamsData { + version: number; + hash: enums.hash; + cipher: enums.symmetric; + replacementFingerprint?: Uint8Array; +} + +export class KDFParams { + constructor(data: KDFParamsData); + write(forReplacementParams?: boolean): Uint8Array; +} diff --git a/package-lock.json b/package-lock.json index 3e1861dba..a64524545 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,19 @@ { - "name": "openpgp", + "name": "@protontech/openpgp", "version": "6.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "openpgp", + "name": "@protontech/openpgp", "version": "6.1.0", "license": "LGPL-3.0+", "devDependencies": { "@noble/ciphers": "^1.0.0", - "@noble/curves": "^1.6.0", + "@noble/curves": "^1.7.0", + "@noble/ed25519": "^1.7.3", "@noble/hashes": "^1.5.0", + "@noble/post-quantum": "^0.4.0", "@openpgp/jsdoc": "^3.6.11", "@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/tweetnacl": "^1.0.4-1", @@ -942,12 +944,13 @@ } }, "node_modules/@noble/curves": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", - "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", + "integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==", "dev": true, + "license": "MIT", "dependencies": { - "@noble/hashes": "1.5.0" + "@noble/hashes": "1.6.0" }, "engines": { "node": "^14.21.3 || >=16" @@ -956,11 +959,50 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/ed25519": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", + "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@noble/hashes": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", - "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", + "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.4.0.tgz", + "integrity": "sha512-pkTt+KEmgKWPo9/MbJtZRnX4AqNNZ5Ui6DwpKfr7WDSxP5b8pxf/SC1Nblzr8LHs+3gc+GEeUvq54u7zg1RHFw==", "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "dev": true, + "license": "MIT", "engines": { "node": "^14.21.3 || >=16" }, diff --git a/package.json b/package.json index 6d52e3862..c648bca5b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "openpgp", + "name": "@protontech/openpgp", "description": "OpenPGP.js is a Javascript implementation of the OpenPGP protocol. This is defined in RFC 4880.", "version": "6.1.0", "license": "LGPL-3.0+", @@ -63,8 +63,10 @@ }, "devDependencies": { "@noble/ciphers": "^1.0.0", - "@noble/curves": "^1.6.0", + "@noble/curves": "^1.7.0", + "@noble/ed25519": "^1.7.3", "@noble/hashes": "^1.5.0", + "@noble/post-quantum": "^0.4.0", "@openpgp/jsdoc": "^3.6.11", "@openpgp/seek-bzip": "^1.0.5-git", "@openpgp/tweetnacl": "^1.0.4-1", @@ -114,6 +116,6 @@ }, "repository": { "type": "git", - "url": "https://github.com/openpgpjs/openpgpjs" + "url": "https://github.com/ProtonMail/openpgpjs" } } diff --git a/rollup.config.js b/rollup.config.js index f61c53cce..2da87fa25 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -61,8 +61,8 @@ const nodeBuild = { input: 'src/index.js', external: nodeBuiltinModules.concat(nodeDependencies), output: [ - { file: 'dist/node/openpgp.cjs', format: 'cjs', name: pkg.name, banner, intro }, - { file: 'dist/node/openpgp.min.cjs', format: 'cjs', name: pkg.name, banner, intro, plugins: [terser(terserOptions)], sourcemap: true }, + { file: 'dist/node/openpgp.cjs', format: 'cjs', name: 'openpgp', banner, intro }, + { file: 'dist/node/openpgp.min.cjs', format: 'cjs', name: 'openpgp', banner, intro, plugins: [terser(terserOptions)], sourcemap: true }, { file: 'dist/node/openpgp.mjs', format: 'es', banner, intro }, { file: 'dist/node/openpgp.min.mjs', format: 'es', banner, intro, plugins: [terser(terserOptions)], sourcemap: true } ].map(options => ({ ...options, inlineDynamicImports: true })), @@ -85,8 +85,8 @@ const fullBrowserBuild = { input: 'src/index.js', external: nodeBuiltinModules.concat(nodeDependencies), output: [ - { file: 'dist/openpgp.js', format: 'iife', name: pkg.name, banner, intro }, - { file: 'dist/openpgp.min.js', format: 'iife', name: pkg.name, banner, intro, plugins: [terser(terserOptions)], sourcemap: true }, + { file: 'dist/openpgp.js', format: 'iife', name: 'openpgp', banner, intro }, + { file: 'dist/openpgp.min.js', format: 'iife', name: 'openpgp', banner, intro, plugins: [terser(terserOptions)], sourcemap: true }, { file: 'dist/openpgp.mjs', format: 'es', banner, intro }, { file: 'dist/openpgp.min.mjs', format: 'es', banner, intro, plugins: [terser(terserOptions)], sourcemap: true } ].map(options => ({ ...options, inlineDynamicImports: true })), @@ -100,6 +100,13 @@ const fullBrowserBuild = { commonjs({ ignore: nodeBuiltinModules.concat(nodeDependencies) }), + replace({ + include: 'node_modules/@noble/ed25519/**', + // Rollup ignores the `browser: { crypto: false }` directive in package.json, since `exports` are present, + // hence we need to manually drop it. + "import * as nodeCrypto from 'crypto'": 'const nodeCrypto = null', + delimiters: ['', ''] + }), replace({ 'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`, "import { createRequire } from 'module';": 'const createRequire = () => () => {}', @@ -127,6 +134,13 @@ const lightweightBrowserBuild = { commonjs({ ignore: nodeBuiltinModules.concat(nodeDependencies) }), + replace({ + include: 'node_modules/@noble/ed25519/**', + // Rollup ignores the `browser: { crypto: false }` directive in package.json, since `exports` are present, + // hence we need to manually drop it. + "import * as nodeCrypto from 'crypto'": 'const nodeCrypto = null', + delimiters: ['', ''] + }), replace({ 'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`, "import { createRequire } from 'module';": 'const createRequire = () => () => {}', @@ -145,7 +159,7 @@ const testBuild = { plugins: [ alias({ entries: { - openpgp: `./dist/${process.env.npm_config_lightweight ? 'lightweight/' : ''}openpgp.mjs` + '@protontech/openpgp': `./dist/${process.env.npm_config_lightweight ? 'lightweight/' : ''}openpgp.mjs` } }), resolve({ @@ -158,6 +172,13 @@ const testBuild = { ignore: nodeBuiltinModules.concat(nodeDependencies), requireReturnsDefault: 'preferred' }), + replace({ + include: 'node_modules/@noble/ed25519/**', + // Rollup ignores the `browser: { crypto: false }` directive in package.json, since `exports` are present, + // hence we need to manually drop it. + "import * as nodeCrypto from 'crypto'": 'const nodeCrypto = null', + delimiters: ['', ''] + }), replace({ "import { createRequire } from 'module';": 'const createRequire = () => () => {}', delimiters: ['', ''] diff --git a/src/config/config.js b/src/config/config.js index 9e77710f7..def9cda1c 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -50,6 +50,14 @@ export default { * @property {Boolean} aeadProtect */ aeadProtect: false, + /** + * Whether to disable encrypton using SEIPDv2 even if the encryption keys include the SEIPDv2 feature flag. + * If true, SEIPDv1 (i.e. no AEAD) packets are always used instead. + * SEIPDv2 is a more secure and faster choice, but it is not necessarily compatible with other libs and our mobile apps. + * @memberof module:config + * @property {Boolean} ignoreSEIPDv2FeatureFlag + */ + ignoreSEIPDv2FeatureFlag: false, /** * When reading OpenPGP v4 private keys (e.g. those generated in OpenPGP.js when not setting `config.v5Keys = true`) * which were encrypted by OpenPGP.js v5 (or older) using `config.aeadProtect = true`, @@ -149,6 +157,13 @@ export default { * @property {Boolean} allowUnauthenticatedStream */ allowUnauthenticatedStream: false, + /** + * Allow decrypting forwarded messages, using keys with 0x40 ('forwarded communication') flag. + * Note: this is related to a **non-standard feature**. + * @memberof module:config + * @property {Boolean} allowForwardedMessages + */ + allowForwardedMessages: false, /** * Minimum RSA key size allowed for key generation and message signing, verification and encryption. * The default is 2047 since due to a bug, previous versions of OpenPGP.js could generate 2047-bit keys instead of 2048-bit ones. diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index 0edf846a0..82da08fb8 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -23,16 +23,23 @@ * @module crypto/crypto */ -import { rsa, elliptic, elgamal, dsa } from './public_key'; +import { rsa, elliptic, elgamal, dsa, hmac, postQuantum } from './public_key'; import { getRandomBytes } from './random'; import { getCipherParams } from './cipher'; import ECDHSymkey from '../type/ecdh_symkey'; +import ShortByteString from '../type/short_byte_string'; +import { computeDigest, getHashByteLength } from './hash'; +import config from '../config'; import KDFParams from '../type/kdf_params'; +import { SymAlgoEnum, AEADEnum, HashEnum } from '../type/enum'; import enums from '../enums'; import util from '../util'; import OID from '../type/oid'; import { UnsupportedError } from '../packet/packet'; import ECDHXSymmetricKey from '../type/ecdh_x_symkey'; +import { getAEADMode } from './cipherMode'; + +export { isPostQuantumAlgo } from './public_key/post_quantum'; /** * Encrypts data using specified algorithm and public key parameters. @@ -40,12 +47,13 @@ import ECDHXSymmetricKey from '../type/ecdh_x_symkey'; * @param {module:enums.publicKey} keyAlgo - Public key algorithm * @param {module:enums.symmetric|null} symmetricAlgo - Cipher algorithm (v3 only) * @param {Object} publicParams - Algorithm-specific public key parameters - * @param {Uint8Array} data - Session key data to be encrypted + * @param {Object} privateParams - Algorithm-specific private key parameters + * @param {Uint8Array} data - Data to be encrypted * @param {Uint8Array} fingerprint - Recipient fingerprint * @returns {Promise} Encrypted session key parameters. * @async */ -export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, data, fingerprint) { +export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, privateParams, data, fingerprint) { switch (keyAlgo) { case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncryptSign: { @@ -75,6 +83,27 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, dat const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey }); return { ephemeralPublicKey, C }; } + case enums.publicKey.aead: { + if (!privateParams) { + throw new Error('Cannot encrypt with symmetric key missing private parameters'); + } + const { cipher: algo } = publicParams; + const algoValue = algo.getValue(); + const { keyMaterial } = privateParams; + const aeadMode = config.preferredAEADAlgorithm; + const mode = getAEADMode(config.preferredAEADAlgorithm); + const { ivLength } = mode; + const iv = getRandomBytes(ivLength); + const modeInstance = await mode(algoValue, keyMaterial); + const c = await modeInstance.encrypt(data, iv, new Uint8Array()); + return { aeadMode: new AEADEnum(aeadMode), iv, c: new ShortByteString(c) }; + } + case enums.publicKey.pqc_mlkem_x25519: { + const { eccPublicKey, mlkemPublicKey } = publicParams; + const { eccCipherText, mlkemCipherText, wrappedKey } = await postQuantum.mlkem.encrypt(keyAlgo, eccPublicKey, mlkemPublicKey, data); + const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey }); + return { eccCipherText, mlkemCipherText, C }; + } default: return []; } @@ -94,8 +123,8 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, dat * @throws {Error} on sensitive decryption error, unless `randomPayload` is given * @async */ -export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, sessionKeyParams, fingerprint, randomPayload) { - switch (algo) { +export async function publicKeyDecrypt(keyAlgo, publicKeyParams, privateKeyParams, sessionKeyParams, fingerprint, randomPayload) { + switch (keyAlgo) { case enums.publicKey.rsaEncryptSign: case enums.publicKey.rsaEncrypt: { const { c } = sessionKeyParams; @@ -125,7 +154,24 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, throw new Error('AES session key expected'); } return elliptic.ecdhX.decrypt( - algo, ephemeralPublicKey, C.wrappedKey, A, k); + keyAlgo, ephemeralPublicKey, C.wrappedKey, A, k); + } + case enums.publicKey.aead: { + const { cipher: algo } = publicKeyParams; + const algoValue = algo.getValue(); + const { keyMaterial } = privateKeyParams; + + const { aeadMode, iv, c } = sessionKeyParams; + + const mode = getAEADMode(aeadMode.getValue()); + const modeInstance = await mode(algoValue, keyMaterial); + return modeInstance.decrypt(c.data, iv, new Uint8Array()); + } + case enums.publicKey.pqc_mlkem_x25519: { + const { eccSecretKey, mlkemSecretKey } = privateKeyParams; + const { eccPublicKey, mlkemPublicKey } = publicKeyParams; + const { eccCipherText, mlkemCipherText, C } = sessionKeyParams; + return postQuantum.mlkem.decrypt(keyAlgo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey, C.wrappedKey); } default: throw new Error('Unknown public key encryption algorithm.'); @@ -191,6 +237,27 @@ export function parsePublicKeyParams(algo, bytes) { const A = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(algo)); read += A.length; return { read, publicParams: { A } }; } + case enums.publicKey.hmac: + case enums.publicKey.aead: { + const algo = new SymAlgoEnum(); read += algo.read(bytes); + const digestLength = getHashByteLength(enums.hash.sha256); + const digest = bytes.subarray(read, read + digestLength); read += digestLength; + return { read: read, publicParams: { cipher: algo, digest } }; + } + case enums.publicKey.pqc_mlkem_x25519: { + const eccPublicKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccPublicKey.length; + const mlkemPublicKey = util.readExactSubarray(bytes, read, read + 1184); read += mlkemPublicKey.length; + return { read, publicParams: { eccPublicKey, mlkemPublicKey } }; + } + case enums.publicKey.pqc_mldsa_ed25519: { + const eccPublicKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.ed25519)); read += eccPublicKey.length; + const mldsaPublicKey = util.readExactSubarray(bytes, read, read + 1952); read += mldsaPublicKey.length; + return { read, publicParams: { eccPublicKey, mldsaPublicKey } }; + } + case enums.publicKey.pqc_slhdsa_shake128s: { + const slhdsaPublicKey = util.readExactSubarray(bytes, read, read + 32); read += slhdsaPublicKey.length; + return { read, publicParams: { slhdsaPublicKey } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -200,10 +267,10 @@ export function parsePublicKeyParams(algo, bytes) { * 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 + * @param {Object} publicParams - (ECC and symmetric 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) { +export async function parsePrivateKeyParams(algo, bytes, publicParams) { let read = 0; switch (algo) { case enums.publicKey.rsaEncrypt: @@ -248,6 +315,37 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) { const k = util.readExactSubarray(bytes, read, read + payloadSize); read += k.length; return { read, privateParams: { k } }; } + case enums.publicKey.hmac: { + const { cipher: algo } = publicParams; + const keySize = getHashByteLength(algo.getValue()); + const hashSeed = bytes.subarray(read, read + 32); read += 32; + const keyMaterial = bytes.subarray(read, read + keySize); read += keySize; + return { read, privateParams: { hashSeed, keyMaterial } }; + } + case enums.publicKey.aead: { + const { cipher: algo } = publicParams; + const hashSeed = bytes.subarray(read, read + 32); read += 32; + const { keySize } = getCipherParams(algo.getValue()); + const keyMaterial = bytes.subarray(read, read + keySize); read += keySize; + return { read, privateParams: { hashSeed, keyMaterial } }; + } + case enums.publicKey.pqc_mlkem_x25519: { + const eccSecretKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccSecretKey.length; + const mlkemSeed = util.readExactSubarray(bytes, read, read + 64); read += mlkemSeed.length; + const { mlkemSecretKey } = await postQuantum.mlkem.mlkemExpandSecretSeed(algo, mlkemSeed); + return { read, privateParams: { eccSecretKey, mlkemSecretKey, mlkemSeed } }; + } + case enums.publicKey.pqc_mldsa_ed25519: { + const eccSecretKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.ed25519)); read += eccSecretKey.length; + const mldsaSeed = util.readExactSubarray(bytes, read, read + 32); read += mldsaSeed.length; + const { mldsaSecretKey } = await postQuantum.mldsa.mldsaExpandSecretSeed(algo, mldsaSeed); + return { read, privateParams: { eccSecretKey, mldsaSecretKey, mldsaSeed } }; + } + case enums.publicKey.pqc_slhdsa_shake128s: { + const slhdsaSecretKey = util.readExactSubarray(bytes, read, read + 64); read += slhdsaSecretKey.length; + + return { read, privateParams: { slhdsaSecretKey } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -297,6 +395,26 @@ export function parseEncSessionKeyParams(algo, bytes) { const C = new ECDHXSymmetricKey(); C.read(bytes.subarray(read)); return { ephemeralPublicKey, C }; } + // Algorithm-Specific Fields for symmetric AEAD encryption: + // - AEAD algorithm + // - Starting initialization vector + // - Symmetric key encryption of "m" dependent on cipher and AEAD mode prefixed with a one-octet length + // - An authentication tag generated by the AEAD mode. + case enums.publicKey.aead: { + const aeadMode = new AEADEnum(); read += aeadMode.read(bytes.subarray(read)); + const { ivLength } = getAEADMode(aeadMode.getValue()); + + const iv = bytes.subarray(read, read + ivLength); read += ivLength; + const c = new ShortByteString(); read += c.read(bytes.subarray(read)); + + return { aeadMode, iv, c }; + } + case enums.publicKey.pqc_mlkem_x25519: { + const eccCipherText = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccCipherText.length; + const mlkemCipherText = util.readExactSubarray(bytes, read, read + 1088); read += mlkemCipherText.length; + const C = new ECDHXSymmetricKey(); C.read(bytes.subarray(read)); + return { eccCipherText, mlkemCipherText, C }; // eccCipherText || mlkemCipherText || len(C) || C + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -314,9 +432,24 @@ export function serializeParams(algo, params) { enums.publicKey.ed25519, enums.publicKey.x25519, enums.publicKey.ed448, - enums.publicKey.x448 + enums.publicKey.x448, + enums.publicKey.aead, + enums.publicKey.hmac, + enums.publicKey.pqc_mlkem_x25519, + enums.publicKey.pqc_mldsa_ed25519, + enums.publicKey.pqc_slhdsa_shake128s ]); + + const excludedFields = { + [enums.publicKey.pqc_mlkem_x25519]: new Set(['mlkemSecretKey']), // only `mlkemSeed` is serialized + [enums.publicKey.pqc_mldsa_ed25519]: new Set(['mldsaSecretKey']) // only `mldsaSeed` is serialized + }; + const orderedParams = Object.keys(params).map(name => { + if (excludedFields[algo]?.has(name)) { + return new Uint8Array(); + } + const param = params[name]; if (!util.isUint8Array(param)) return param.write(); return algosWithNativeRepresentation.has(algo) ? param : util.uint8ArrayToMPI(param); @@ -329,10 +462,11 @@ export function serializeParams(algo, params) { * @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 + * @param {module:enums.symmetric|enums.hash} symmetric - Hash or cipher algorithm for symmetric keys * @returns {Promise<{ publicParams: {Object}, privateParams: {Object} }>} The parameters referenced by name. * @async */ -export function generateParams(algo, bits, oid) { +export async function generateParams(algo, bits, oid, symmetric) { switch (algo) { case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncryptSign: @@ -372,6 +506,29 @@ export function generateParams(algo, bits, oid) { privateParams: { k }, publicParams: { A } })); + case enums.publicKey.hmac: { + const keyMaterial = await hmac.generate(symmetric); + return createSymmetricParams(keyMaterial, new HashEnum(symmetric)); + } + case enums.publicKey.aead: { + const keyMaterial = generateSessionKey(symmetric); + return createSymmetricParams(keyMaterial, new SymAlgoEnum(symmetric)); + } + case enums.publicKey.pqc_mlkem_x25519: + return postQuantum.mlkem.generate(algo).then(({ eccSecretKey, eccPublicKey, mlkemSeed, mlkemSecretKey, mlkemPublicKey }) => ({ + privateParams: { eccSecretKey, mlkemSeed, mlkemSecretKey }, + publicParams: { eccPublicKey, mlkemPublicKey } + })); + case enums.publicKey.pqc_mldsa_ed25519: + return postQuantum.mldsa.generate(algo).then(({ eccSecretKey, eccPublicKey, mldsaSeed, mldsaSecretKey, mldsaPublicKey }) => ({ + privateParams: { eccSecretKey, mldsaSeed, mldsaSecretKey }, + publicParams: { eccPublicKey, mldsaPublicKey } + })); + case enums.publicKey.pqc_slhdsa_shake128s: + return postQuantum.slhdsa.generate(algo).then(({ slhdsaSecretKey, slhdsaPublicKey }) => ({ + privateParams: { slhdsaSecretKey }, + publicParams: { slhdsaPublicKey } + })); case enums.publicKey.dsa: case enums.publicKey.elgamal: throw new Error('Unsupported algorithm for key generation.'); @@ -380,6 +537,21 @@ export function generateParams(algo, bits, oid) { } } +async function createSymmetricParams(key, algo) { + const seed = getRandomBytes(32); + const bindingHash = await computeDigest(enums.hash.sha256,seed); + return { + privateParams: { + hashSeed: seed, + keyMaterial: key + }, + publicParams: { + cipher: algo, + digest: bindingHash + } + }; +} + /** * Validate algorithm-specific key parameters * @param {module:enums.publicKey} algo - The public key algorithm @@ -434,6 +606,35 @@ export async function validateParams(algo, publicParams, privateParams) { const { k } = privateParams; return elliptic.ecdhX.validateParams(algo, A, k); } + case enums.publicKey.hmac: { + const { cipher: algo, digest } = publicParams; + const { hashSeed, keyMaterial } = privateParams; + const keySize = getHashByteLength(algo.getValue()); + return keySize === keyMaterial.length && + util.equalsUint8Array(digest, await computeDigest(enums.hash.sha256, hashSeed)); + } + case enums.publicKey.aead: { + const { cipher: algo, digest } = publicParams; + const { hashSeed, keyMaterial } = privateParams; + const { keySize } = getCipherParams(algo.getValue()); + return keySize === keyMaterial.length && + util.equalsUint8Array(digest, await computeDigest(enums.hash.sha256, hashSeed)); + } + case enums.publicKey.pqc_mlkem_x25519: { + const { eccSecretKey, mlkemSeed } = privateParams; + const { eccPublicKey, mlkemPublicKey } = publicParams; + return postQuantum.mlkem.validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed); + } + case enums.publicKey.pqc_mldsa_ed25519: { + const { eccSecretKey, mldsaSeed } = privateParams; + const { eccPublicKey, mldsaPublicKey } = publicParams; + return postQuantum.mldsa.validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed); + } + case enums.publicKey.pqc_slhdsa_shake128s: { + const { slhdsaSecretKey } = privateParams; + const { slhdsaPublicKey } = publicParams; + return postQuantum.slhdsa.validateParams(algo, slhdsaPublicKey, slhdsaSecretKey); + } default: throw new Error('Unknown public key algorithm.'); } diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 66937b2cb..3b8120f3b 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -50,9 +50,9 @@ function buildEcdhParam(public_algo, oid, kdfParams, fingerprint) { return util.concatUint8Array([ oid.write(), new Uint8Array([public_algo]), - kdfParams.write(), + kdfParams.write(true), util.stringToUint8Array('Anonymous Sender '), - fingerprint + kdfParams.replacementFingerprint || fingerprint ]); } diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index 79d68db4d..5c6b7998e 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -20,7 +20,9 @@ * @module crypto/public_key/elliptic/eddsa */ -import ed25519 from '@openpgp/tweetnacl'; +import naclEd25519 from '@openpgp/tweetnacl'; // better constant-timeness as it uses Uint8Arrays over BigInts +import { verify as nobleEd25519Verify } from '@noble/ed25519'; + import util from '../../../util'; import enums from '../../../enums'; import { getHashByteLength } from '../../hash'; @@ -48,11 +50,15 @@ export async function generate(algo) { seed: b64ToUint8Array(privateKey.d, true) }; } catch (err) { - if (err.name !== 'NotSupportedError' && err.name !== 'OperationError') { // Temporary (hopefully) fix for WebKit on Linux + if ( + err.name !== 'NotSupportedError' && + err.name !== 'OperationError' && // Temporary (hopefully) fix for WebKit on Linux + err.name !== 'SyntaxError' // Temporary fix for Palemoon throwing 'SyntaxError' + ) { throw err; } const seed = getRandomBytes(getPayloadSize(algo)); - const { publicKey: A } = ed25519.sign.keyPair.fromSeed(seed); + const { publicKey: A } = naclEd25519.sign.keyPair.fromSeed(seed); return { A, seed }; } @@ -100,11 +106,11 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe return { RS: signature }; } catch (err) { - if (err.name !== 'NotSupportedError') { + if (err.name !== 'NotSupportedError' && err.name !== 'SyntaxError') { // Temporary fix for Palemoon throwing 'SyntaxError' throw err; } const secretKey = util.concatUint8Array([privateKey, publicKey]); - const signature = ed25519.sign.detached(hashed, secretKey); + const signature = naclEd25519.sign.detached(hashed, secretKey); return { RS: signature }; } @@ -146,10 +152,10 @@ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) { const verified = await webCrypto.verify('Ed25519', key, RS, hashed); return verified; } catch (err) { - if (err.name !== 'NotSupportedError') { + if (err.name !== 'NotSupportedError' && err.name !== 'SyntaxError') { // Temporary fix for Palemoon throwing 'SyntaxError' throw err; } - return ed25519.sign.detached.verify(hashed, RS, publicKey); + return nobleEd25519Verify(RS, hashed, publicKey); } case enums.publicKey.ed448: { @@ -177,7 +183,7 @@ export async function validateParams(algo, A, seed) { * and expect A == A' * TODO: move to sign-verify using WebCrypto (same as ECDSA) when curve is more widely implemented */ - const { publicKey } = ed25519.sign.keyPair.fromSeed(seed); + const { publicKey } = naclEd25519.sign.keyPair.fromSeed(seed); return util.equalsUint8Array(A, publicKey); } diff --git a/src/crypto/public_key/elliptic/eddsa_legacy.js b/src/crypto/public_key/elliptic/eddsa_legacy.js index 682f53bba..e4eb8d95b 100644 --- a/src/crypto/public_key/elliptic/eddsa_legacy.js +++ b/src/crypto/public_key/elliptic/eddsa_legacy.js @@ -21,7 +21,7 @@ * @module crypto/public_key/elliptic/eddsa_legacy */ -import nacl from '@openpgp/tweetnacl'; +import naclEd25519 from '@openpgp/tweetnacl'; // better constant-timeness as it uses Uint8Arrays over BigInts import util from '../../../util'; import enums from '../../../enums'; import { getHashByteLength } from '../../hash'; @@ -101,7 +101,7 @@ export async function validateParams(oid, Q, k) { * Derive public point Q' = dG from private key * and expect Q == Q' */ - const { publicKey } = nacl.sign.keyPair.fromSeed(k); + const { publicKey } = naclEd25519.sign.keyPair.fromSeed(k); const dG = new Uint8Array([0x40, ...publicKey]); // Add public key prefix return util.equalsUint8Array(Q, dG); diff --git a/src/crypto/public_key/hmac.js b/src/crypto/public_key/hmac.js new file mode 100644 index 000000000..f74f20d7a --- /dev/null +++ b/src/crypto/public_key/hmac.js @@ -0,0 +1,67 @@ +import enums from '../../enums'; +import util from '../../util'; + +const supportedHashAlgos = new Set([enums.hash.sha1, enums.hash.sha256, enums.hash.sha512]); + +const webCrypto = util.getWebCrypto(); +const nodeCrypto = util.getNodeCrypto(); + +export async function generate(hashAlgo) { + if (!supportedHashAlgos.has(hashAlgo)) { + throw new Error('Unsupported hash algorithm.'); + } + const hashName = enums.read(enums.webHash, hashAlgo); + + const crypto = webCrypto || nodeCrypto.webcrypto.subtle; + const key = await crypto.generateKey( + { + name: 'HMAC', + hash: { name: hashName } + }, + true, + ['sign', 'verify'] + ); + const exportedKey = await crypto.exportKey('raw', key); + return new Uint8Array(exportedKey); +} + +export async function sign(hashAlgo, key, data) { + if (!supportedHashAlgos.has(hashAlgo)) { + throw new Error('Unsupported hash algorithm.'); + } + const hashName = enums.read(enums.webHash, hashAlgo); + + const crypto = webCrypto || nodeCrypto.webcrypto.subtle; + const importedKey = await crypto.importKey( + 'raw', + key, + { + name: 'HMAC', + hash: { name: hashName } + }, + false, + ['sign'] + ); + const mac = await crypto.sign('HMAC', importedKey, data); + return new Uint8Array(mac); +} + +export async function verify(hashAlgo, key, mac, data) { + if (!supportedHashAlgos.has(hashAlgo)) { + throw new Error('Unsupported hash algorithm.'); + } + const hashName = enums.read(enums.webHash, hashAlgo); + + const crypto = webCrypto || nodeCrypto.webcrypto.subtle; + const importedKey = await crypto.importKey( + 'raw', + key, + { + name: 'HMAC', + hash: { name: hashName } + }, + false, + ['verify'] + ); + return crypto.verify('HMAC', importedKey, mac, data); +} diff --git a/src/crypto/public_key/index.js b/src/crypto/public_key/index.js index f49b4d24e..67403de5c 100644 --- a/src/crypto/public_key/index.js +++ b/src/crypto/public_key/index.js @@ -7,3 +7,5 @@ export * as rsa from './rsa'; export * as elgamal from './elgamal'; export * as elliptic from './elliptic'; export * as dsa from './dsa'; +export * as hmac from './hmac'; +export * as postQuantum from './post_quantum'; diff --git a/src/crypto/public_key/post_quantum/index.js b/src/crypto/public_key/post_quantum/index.js new file mode 100644 index 000000000..6aa2b574d --- /dev/null +++ b/src/crypto/public_key/post_quantum/index.js @@ -0,0 +1,31 @@ +import * as mlkem from './ml_kem'; +import * as mldsa from './ml_dsa'; +import * as slhdsa from './slh_dsa'; +import enums from '../../../enums'; + +export { + mlkem, + mldsa, + slhdsa +}; + +const pqcAlgos = new Set([ + enums.publicKey.pqc_mldsa_ed25519, + enums.publicKey.pqc_mlkem_x25519, + enums.publicKey.pqc_slhdsa_shake128s +]); + +export function isPostQuantumAlgo(algo) { + return pqcAlgos.has(algo); +} + +export function getRequiredHashAlgo(signatureAlgo) { + switch (signatureAlgo) { + case enums.publicKey.pqc_mldsa_ed25519: + return mldsa.getRequiredHashAlgo(signatureAlgo); + case enums.publicKey.pqc_slhdsa_shake128s: + return slhdsa.getRequiredHashAlgo(signatureAlgo); + default: + throw new Error('Unsupported signature algorithm'); + } +} diff --git a/src/crypto/public_key/post_quantum/ml_dsa/combiner.js b/src/crypto/public_key/post_quantum/ml_dsa/combiner.js new file mode 100644 index 000000000..c5a378f05 --- /dev/null +++ b/src/crypto/public_key/post_quantum/ml_dsa/combiner.js @@ -0,0 +1,70 @@ +import enums from '../../../../enums'; +import * as mldsa from './ml_dsa_pure'; +import * as eccdsa from './ecc_dsa'; + +export async function generate(algo) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { eccSecretKey, eccPublicKey } = await eccdsa.generate(algo); + const { mldsaSeed, mldsaSecretKey, mldsaPublicKey } = await mldsa.generate(algo); + return { eccSecretKey, eccPublicKey, mldsaSeed, mldsaSecretKey, mldsaPublicKey }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, dataDigest) { + if (hashAlgo !== getRequiredHashAlgo(signatureAlgo)) { + // The signature hash algo MUST be set to the specified algorithm, see + // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1. + throw new Error('Unexpected hash algorithm for PQC signature'); + } + + switch (signatureAlgo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { eccSignature } = await eccdsa.sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, dataDigest); + const { mldsaSignature } = await mldsa.sign(signatureAlgo, mldsaSecretKey, dataDigest); + + return { eccSignature, mldsaSignature }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function verify(signatureAlgo, hashAlgo, eccPublicKey, mldsaPublicKey, dataDigest, { eccSignature, mldsaSignature }) { + if (hashAlgo !== getRequiredHashAlgo(signatureAlgo)) { + // The signature hash algo MUST be set to the specified algorithm, see + // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1. + throw new Error('Unexpected hash algorithm for PQC signature'); + } + + switch (signatureAlgo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const eccVerifiedPromise = eccdsa.verify(signatureAlgo, hashAlgo, eccPublicKey, dataDigest, eccSignature); + const mldsaVerifiedPromise = mldsa.verify(signatureAlgo, mldsaPublicKey, dataDigest, mldsaSignature); + const verified = await eccVerifiedPromise && await mldsaVerifiedPromise; + return verified; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export function getRequiredHashAlgo(signatureAlgo) { + // See https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1. + switch (signatureAlgo) { + case enums.publicKey.pqc_mldsa_ed25519: + return enums.hash.sha3_256; + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function validateParams(algo, eccPublicKey, eccSecretKey, mldsaPublicKey, mldsaSeed) { + const eccValidationPromise = eccdsa.validateParams(algo, eccPublicKey, eccSecretKey); + const mldsaValidationPromise = mldsa.validateParams(algo, mldsaPublicKey, mldsaSeed); + const valid = await eccValidationPromise && await mldsaValidationPromise; + return valid; +} diff --git a/src/crypto/public_key/post_quantum/ml_dsa/ecc_dsa.js b/src/crypto/public_key/post_quantum/ml_dsa/ecc_dsa.js new file mode 100644 index 000000000..9bf3dfbe4 --- /dev/null +++ b/src/crypto/public_key/post_quantum/ml_dsa/ecc_dsa.js @@ -0,0 +1,46 @@ +import * as eddsa from '../../elliptic/eddsa'; +import enums from '../../../../enums'; + +export async function generate(algo) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { A, seed } = await eddsa.generate(enums.publicKey.ed25519); + return { + eccPublicKey: A, + eccSecretKey: seed + }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function sign(signatureAlgo, hashAlgo, eccSecretKey, eccPublicKey, dataDigest) { + switch (signatureAlgo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { RS: eccSignature } = await eddsa.sign(enums.publicKey.ed25519, hashAlgo, null, eccPublicKey, eccSecretKey, dataDigest); + + return { eccSignature }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function verify(signatureAlgo, hashAlgo, eccPublicKey, dataDigest, eccSignature) { + switch (signatureAlgo) { + case enums.publicKey.pqc_mldsa_ed25519: + return eddsa.verify(enums.publicKey.ed25519, hashAlgo, { RS: eccSignature }, null, eccPublicKey, dataDigest); + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function validateParams(algo, eccPublicKey, eccSecretKey) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: + return eddsa.validateParams(enums.publicKey.ed25519, eccPublicKey, eccSecretKey); + default: + throw new Error('Unsupported signature algorithm'); + } +} diff --git a/src/crypto/public_key/post_quantum/ml_dsa/index.js b/src/crypto/public_key/post_quantum/ml_dsa/index.js new file mode 100644 index 000000000..6fafb6936 --- /dev/null +++ b/src/crypto/public_key/post_quantum/ml_dsa/index.js @@ -0,0 +1,2 @@ +export { generate, sign, verify, validateParams, getRequiredHashAlgo } from './combiner'; +export { expandSecretSeed as mldsaExpandSecretSeed } from './ml_dsa_pure'; diff --git a/src/crypto/public_key/post_quantum/ml_dsa/ml_dsa_pure.js b/src/crypto/public_key/post_quantum/ml_dsa/ml_dsa_pure.js new file mode 100644 index 000000000..0165e71e6 --- /dev/null +++ b/src/crypto/public_key/post_quantum/ml_dsa/ml_dsa_pure.js @@ -0,0 +1,69 @@ +import enums from '../../../../enums'; +import util from '../../../../util'; +import { getRandomBytes } from '../../../random'; + +export async function generate(algo) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const mldsaSeed = getRandomBytes(32); + const { mldsaSecretKey, mldsaPublicKey } = await expandSecretSeed(algo, mldsaSeed); + + return { mldsaSeed, mldsaSecretKey, mldsaPublicKey }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +/** + * Expand ML-DSA secret seed and retrieve the secret and public key material + * @param {module:enums.publicKey} algo - Public key algorithm + * @param {Uint8Array} seed - secret seed to expand + * @returns {Promise<{ mldsaPublicKey: Uint8Array, mldsaSecretKey: Uint8Array }>} + */ +export async function expandSecretSeed(algo, seed) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { ml_dsa65 } = await import('../noble_post_quantum'); + const { secretKey: mldsaSecretKey, publicKey: mldsaPublicKey } = ml_dsa65.keygen(seed); + + return { mldsaSecretKey, mldsaPublicKey }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function sign(algo, mldsaSecretKey, dataDigest) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { ml_dsa65 } = await import('../noble_post_quantum'); + const mldsaSignature = ml_dsa65.sign(mldsaSecretKey, dataDigest); + return { mldsaSignature }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function verify(algo, mldsaPublicKey, dataDigest, mldsaSignature) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { ml_dsa65 } = await import('../noble_post_quantum'); + return ml_dsa65.verify(mldsaPublicKey, dataDigest, mldsaSignature); + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function validateParams(algo, mldsaPublicKey, mldsaSeed) { + switch (algo) { + case enums.publicKey.pqc_mldsa_ed25519: { + const { mldsaPublicKey: expectedPublicKey } = await expandSecretSeed(algo, mldsaSeed); + return util.equalsUint8Array(mldsaPublicKey, expectedPublicKey); + } + default: + throw new Error('Unsupported signature algorithm'); + } +} diff --git a/src/crypto/public_key/post_quantum/ml_kem/ecc_kem.js b/src/crypto/public_key/post_quantum/ml_kem/ecc_kem.js new file mode 100644 index 000000000..1233eba8a --- /dev/null +++ b/src/crypto/public_key/post_quantum/ml_kem/ecc_kem.js @@ -0,0 +1,51 @@ +import * as ecdhX from '../../elliptic/ecdh_x'; +import enums from '../../../../enums'; + +export async function generate(algo) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: { + const { A, k } = await ecdhX.generate(enums.publicKey.x25519); + return { + eccPublicKey: A, + eccSecretKey: k + }; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +export async function encaps(eccAlgo, eccRecipientPublicKey) { + switch (eccAlgo) { + case enums.publicKey.pqc_mlkem_x25519: { + const { ephemeralPublicKey: eccCipherText, sharedSecret: eccKeyShare } = await ecdhX.generateEphemeralEncryptionMaterial(enums.publicKey.x25519, eccRecipientPublicKey); + + return { + eccCipherText, + eccKeyShare + }; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +export async function decaps(eccAlgo, eccCipherText, eccSecretKey, eccPublicKey) { + switch (eccAlgo) { + case enums.publicKey.pqc_mlkem_x25519: { + const eccKeyShare = await ecdhX.recomputeSharedSecret(enums.publicKey.x25519, eccCipherText, eccPublicKey, eccSecretKey); + return eccKeyShare; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +export async function validateParams(algo, eccPublicKey, eccSecretKey) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: + return ecdhX.validateParams(enums.publicKey.x25519, eccPublicKey, eccSecretKey); + default: + throw new Error('Unsupported KEM algorithm'); + } +} diff --git a/src/crypto/public_key/post_quantum/ml_kem/index.js b/src/crypto/public_key/post_quantum/ml_kem/index.js new file mode 100644 index 000000000..29c8ba96d --- /dev/null +++ b/src/crypto/public_key/post_quantum/ml_kem/index.js @@ -0,0 +1,2 @@ +export { generate, encrypt, decrypt, validateParams } from './kem_combiner'; +export { expandSecretSeed as mlkemExpandSecretSeed } from './ml_kem_pure'; diff --git a/src/crypto/public_key/post_quantum/ml_kem/kem_combiner.js b/src/crypto/public_key/post_quantum/ml_kem/kem_combiner.js new file mode 100644 index 000000000..9d5e1319c --- /dev/null +++ b/src/crypto/public_key/post_quantum/ml_kem/kem_combiner.js @@ -0,0 +1,56 @@ +import * as eccKem from './ecc_kem'; +import * as mlKem from './ml_kem_pure'; +import * as aesKW from '../../../aes_kw'; +import util from '../../../../util'; +import enums from '../../../../enums'; +import { computeDigest } from '../../../hash'; + +export async function generate(algo) { + const { eccPublicKey, eccSecretKey } = await eccKem.generate(algo); + const { mlkemPublicKey, mlkemSeed, mlkemSecretKey } = await mlKem.generate(algo); + + return { eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed, mlkemSecretKey }; +} + +export async function encrypt(algo, eccPublicKey, mlkemPublicKey, sessioneKeyData) { + const { eccKeyShare, eccCipherText } = await eccKem.encaps(algo, eccPublicKey); + const { mlkemKeyShare, mlkemCipherText } = await mlKem.encaps(algo, mlkemPublicKey); + const kek = await multiKeyCombine(algo, eccKeyShare, eccCipherText, eccPublicKey, mlkemKeyShare, mlkemCipherText, mlkemPublicKey); + const wrappedKey = await aesKW.wrap(enums.symmetric.aes256, kek, sessioneKeyData); // C + return { eccCipherText, mlkemCipherText, wrappedKey }; +} + +export async function decrypt(algo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey, encryptedSessionKeyData) { + const eccKeyShare = await eccKem.decaps(algo, eccCipherText, eccSecretKey, eccPublicKey); + const mlkemKeyShare = await mlKem.decaps(algo, mlkemCipherText, mlkemSecretKey); + const kek = await multiKeyCombine(algo, eccKeyShare, eccCipherText, eccPublicKey, mlkemKeyShare, mlkemCipherText, mlkemPublicKey); + const sessionKey = await aesKW.unwrap(enums.symmetric.aes256, kek, encryptedSessionKeyData); + return sessionKey; +} + +async function multiKeyCombine(algo, ecdhKeyShare, ecdhCipherText, ecdhPublicKey, mlkemKeyShare, mlkemCipherText, mlkemPublicKey) { + // LAMPS-aligned and NIST compatible combiner, proposed in: https://mailarchive.ietf.org/arch/msg/openpgp/NMTCy707LICtxIhP3Xt1U5C8MF0/ + // 2a. KDF(mlkemSS || tradSS || tradCT || tradPK || Domain) + // where Domain is "Domain" for LAMPS, and "mlkemCT || mlkemPK || algId || const" for OpenPGP + const encData = util.concatUint8Array([ + mlkemKeyShare, + ecdhKeyShare, + ecdhCipherText, + ecdhPublicKey, + // domSep + mlkemCipherText, + mlkemPublicKey, + new Uint8Array([algo]), + util.encodeUTF8('OpenPGPCompositeKDFv1') + ]); + + const kek = await computeDigest(enums.hash.sha3_256, encData); + return kek; +} + +export async function validateParams(algo, eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSeed) { + const eccValidationPromise = eccKem.validateParams(algo, eccPublicKey, eccSecretKey); + const mlkemValidationPromise = mlKem.validateParams(algo, mlkemPublicKey, mlkemSeed); + const valid = await eccValidationPromise && await mlkemValidationPromise; + return valid; +} diff --git a/src/crypto/public_key/post_quantum/ml_kem/ml_kem_pure.js b/src/crypto/public_key/post_quantum/ml_kem/ml_kem_pure.js new file mode 100644 index 000000000..0651547b0 --- /dev/null +++ b/src/crypto/public_key/post_quantum/ml_kem/ml_kem_pure.js @@ -0,0 +1,72 @@ +import enums from '../../../../enums'; +import util from '../../../../util'; +import { getRandomBytes } from '../../../random'; + +export async function generate(algo) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: { + const mlkemSeed = getRandomBytes(64); + const { mlkemSecretKey, mlkemPublicKey } = await expandSecretSeed(algo, mlkemSeed); + + return { mlkemSeed, mlkemSecretKey, mlkemPublicKey }; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +/** + * Expand ML-KEM secret seed and retrieve the secret and public key material + * @param {module:enums.publicKey} algo - Public key algorithm + * @param {Uint8Array} seed - secret seed to expand + * @returns {Promise<{ mlkemPublicKey: Uint8Array, mlkemSecretKey: Uint8Array }>} + */ +export async function expandSecretSeed(algo, seed) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: { + const { ml_kem768 } = await import('../noble_post_quantum'); + const { publicKey: encapsulationKey, secretKey: decapsulationKey } = ml_kem768.keygen(seed); + + return { mlkemPublicKey: encapsulationKey, mlkemSecretKey: decapsulationKey }; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +export async function encaps(algo, mlkemRecipientPublicKey) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: { + const { ml_kem768 } = await import('../noble_post_quantum'); + const { cipherText: mlkemCipherText, sharedSecret: mlkemKeyShare } = ml_kem768.encapsulate(mlkemRecipientPublicKey); + + return { mlkemCipherText, mlkemKeyShare }; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +export async function decaps(algo, mlkemCipherText, mlkemSecretKey) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: { + const { ml_kem768 } = await import('../noble_post_quantum'); + const mlkemKeyShare = ml_kem768.decapsulate(mlkemCipherText, mlkemSecretKey); + + return mlkemKeyShare; + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} + +export async function validateParams(algo, mlkemPublicKey, mlkemSeed) { + switch (algo) { + case enums.publicKey.pqc_mlkem_x25519: { + const { mlkemPublicKey: expectedPublicKey } = await expandSecretSeed(algo, mlkemSeed); + return util.equalsUint8Array(mlkemPublicKey, expectedPublicKey); + } + default: + throw new Error('Unsupported KEM algorithm'); + } +} diff --git a/src/crypto/public_key/post_quantum/noble_post_quantum.ts b/src/crypto/public_key/post_quantum/noble_post_quantum.ts new file mode 100644 index 000000000..041030442 --- /dev/null +++ b/src/crypto/public_key/post_quantum/noble_post_quantum.ts @@ -0,0 +1,9 @@ +/** + * This file is needed to dynamic import noble-post-quantum libs. + * Separate dynamic imports are not convenient as they result in multiple chunks, + * which ultimately share a lot of code and need to be imported together + * when it comes to Proton's ML-DSA + ML-KEM keys. + */ + +export { ml_kem768 } from '@noble/post-quantum/ml-kem'; +export { ml_dsa65 } from '@noble/post-quantum/ml-dsa'; diff --git a/src/crypto/public_key/post_quantum/slh_dsa/index.js b/src/crypto/public_key/post_quantum/slh_dsa/index.js new file mode 100644 index 000000000..517c26e7d --- /dev/null +++ b/src/crypto/public_key/post_quantum/slh_dsa/index.js @@ -0,0 +1,75 @@ +import enums from '../../../../enums'; +import { getRandomBytes } from '../../../random'; + + +export async function generate(algo) { + switch (algo) { + case enums.publicKey.pqc_slhdsa_shake128s: { + const { slh_dsa_shake_128s } = await import('@noble/post-quantum/slh-dsa'); + const { secretKey: slhdsaSecretKey, publicKey: slhdsaPublicKey } = slh_dsa_shake_128s.keygen(); + + return { slhdsaSecretKey, slhdsaPublicKey }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function sign(signatureAlgo, hashAlgo, slhdsaSecretKey, dataDigest) { + if (hashAlgo !== getRequiredHashAlgo(signatureAlgo)) { + // The signature hash algo MUST be set to the specified algorithm, see + // https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html#section-6.1.1 + throw new Error('Unexpected hash algorithm for PQC signature'); + } + + switch (signatureAlgo) { + case enums.publicKey.pqc_slhdsa_shake128s: { + const { slh_dsa_shake_128s } = await import('@noble/post-quantum/slh-dsa'); + const slhdsaSignature = slh_dsa_shake_128s.sign(slhdsaSecretKey, dataDigest); + return { slhdsaSignature }; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function verify(signatureAlgo, hashAlgo, slhdsaPublicKey, dataDigest, { slhdsaSignature }) { + if (hashAlgo !== getRequiredHashAlgo(signatureAlgo)) { + // The signature hash algo MUST be set to the specified algorithm, see + // https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html#section-6.1.1 + throw new Error('Unexpected hash algorithm for PQC signature'); + } + + switch (signatureAlgo) { + case enums.publicKey.pqc_slhdsa_shake128s: { + const { slh_dsa_shake_128s } = await import('@noble/post-quantum/slh-dsa'); + return slh_dsa_shake_128s.verify(slhdsaPublicKey, dataDigest, slhdsaSignature); + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export async function validateParams(algo, slhdsaPublicKey, slhdsaSecretKey) { + switch (algo) { + case enums.publicKey.pqc_slhdsa_shake128s: { + // TODO check if more performant validation is possible via public key re-derivation + const randomBytes = getRandomBytes(16); + const { slhdsaSignature } = await sign(algo, slhdsaSecretKey, randomBytes); + const trialSignatureVerified = await verify(algo, slhdsaPublicKey, randomBytes, slhdsaSignature); + return trialSignatureVerified; + } + default: + throw new Error('Unsupported signature algorithm'); + } +} + +export function getRequiredHashAlgo(signatureAlgo) { + // See https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html#section-6.1.1 + switch (signatureAlgo) { + case enums.publicKey.pqc_slhdsa_shake128s: + return enums.hash.sha3_256; + default: + throw new Error('Unsupported signature algorithm'); + } +} diff --git a/src/crypto/signature.js b/src/crypto/signature.js index 3156ebddd..24966a46a 100644 --- a/src/crypto/signature.js +++ b/src/crypto/signature.js @@ -3,9 +3,10 @@ * @module crypto/signature */ -import { elliptic, rsa, dsa } from './public_key'; +import { elliptic, rsa, dsa, hmac, postQuantum } from './public_key'; import enums from '../enums'; import util from '../util'; +import ShortByteString from '../type/short_byte_string'; import { UnsupportedError } from '../packet/packet'; /** @@ -65,7 +66,20 @@ export function parseSignatureParams(algo, signature) { const RS = util.readExactSubarray(signature, read, read + rsSize); read += RS.length; return { read, signatureParams: { RS } }; } - + case enums.publicKey.hmac: { + const mac = new ShortByteString(); read += mac.read(signature.subarray(read)); + return { read, signatureParams: { mac } }; + } + case enums.publicKey.pqc_mldsa_ed25519: { + const eccSignatureSize = 2 * elliptic.eddsa.getPayloadSize(enums.publicKey.ed25519); + const eccSignature = util.readExactSubarray(signature, read, read + eccSignatureSize); read += eccSignature.length; + const mldsaSignature = util.readExactSubarray(signature, read, read + 3309); read += mldsaSignature.length; + return { read, signatureParams: { eccSignature, mldsaSignature } }; + } + case enums.publicKey.pqc_slhdsa_shake128s: { + const slhdsaSignature = util.readExactSubarray(signature, read, read + 7856); read += slhdsaSignature.length; + return { read, signatureParams: { slhdsaSignature } }; + } default: throw new UnsupportedError('Unknown signature algorithm.'); } @@ -80,12 +94,13 @@ export function parseSignatureParams(algo, signature) { * @param {module:enums.hash} hashAlgo - Hash algorithm * @param {Object} signature - Named algorithm-specific signature parameters * @param {Object} publicParams - Algorithm-specific public key parameters + * @param {Object} privateParams - Algorithm-specific private key parameters * @param {Uint8Array} data - Data for which the signature was created * @param {Uint8Array} hashed - The hashed data * @returns {Promise} True if signature is valid. * @async */ -export async function verify(algo, hashAlgo, signature, publicParams, data, hashed) { +export async function verify(algo, hashAlgo, signature, publicParams, privateParams, data, hashed) { switch (algo) { case enums.publicKey.rsaEncryptSign: case enums.publicKey.rsaEncrypt: @@ -121,6 +136,22 @@ export async function verify(algo, hashAlgo, signature, publicParams, data, hash const { A } = publicParams; return elliptic.eddsa.verify(algo, hashAlgo, signature, data, A, hashed); } + case enums.publicKey.hmac: { + if (!privateParams) { + throw new Error('Cannot verify HMAC signature with symmetric key missing private parameters'); + } + const { cipher: algo } = publicParams; + const { keyMaterial } = privateParams; + return hmac.verify(algo.getValue(), keyMaterial, signature.mac.data, hashed); + } + case enums.publicKey.pqc_mldsa_ed25519: { + const { eccPublicKey, mldsaPublicKey } = publicParams; + return postQuantum.mldsa.verify(algo, hashAlgo, eccPublicKey, mldsaPublicKey, hashed, signature); + } + case enums.publicKey.pqc_slhdsa_shake128s: { + const { slhdsaPublicKey } = publicParams; + return postQuantum.slhdsa.verify(algo, hashAlgo, slhdsaPublicKey, hashed, signature); + } default: throw new Error('Unknown signature algorithm.'); } @@ -176,6 +207,21 @@ export async function sign(algo, hashAlgo, publicKeyParams, privateKeyParams, da const { seed } = privateKeyParams; return elliptic.eddsa.sign(algo, hashAlgo, data, A, seed, hashed); } + case enums.publicKey.hmac: { + const { cipher: algo } = publicKeyParams; + const { keyMaterial } = privateKeyParams; + const mac = await hmac.sign(algo.getValue(), keyMaterial, hashed); + return { mac: new ShortByteString(mac) }; + } + case enums.publicKey.pqc_mldsa_ed25519: { + const { eccPublicKey } = publicKeyParams; + const { eccSecretKey, mldsaSecretKey } = privateKeyParams; + return postQuantum.mldsa.sign(algo, hashAlgo, eccSecretKey, eccPublicKey, mldsaSecretKey, hashed); + } + case enums.publicKey.pqc_slhdsa_shake128s: { + const { slhdsaSecretKey } = privateKeyParams; + return postQuantum.slhdsa.sign(algo, hashAlgo, slhdsaSecretKey, hashed); + } default: throw new Error('Unknown signature algorithm.'); } diff --git a/src/enums.js b/src/enums.js index 85f0a5d71..a6930e075 100644 --- a/src/enums.js +++ b/src/enums.js @@ -50,6 +50,18 @@ export default { 'brainpoolP512r1': 'brainpoolP512r1' }, + /** KDF parameters flags + * Non-standard extensions (for now) to allow email forwarding + * @enum {Integer} + * @readonly + */ + kdfFlags: { + /** Specify fingerprint to use instead of the recipient's */ + replace_fingerprint: 0x01, + /** Specify custom parameters to use in the KDF digest computation */ + replace_kdf_params: 0x02 + }, + /** A string to key specifier type * @enum {Integer} * @readonly @@ -95,7 +107,17 @@ export default { /** Ed25519 (Sign only) */ ed25519: 27, /** Ed448 (Sign only) */ - ed448: 28 + ed448: 28, + /** Post-quantum ML-KEM-768 + X25519 (Encrypt only) */ + pqc_mlkem_x25519: 105, + /** Post-quantum ML-DSA-64 + Ed25519 (Sign only) */ + pqc_mldsa_ed25519: 107, + /** Post-quantum SLH-DSA-128S (Sign only) */ + pqc_slhdsa_shake128s: 109, + /** Persistent symmetric keys: encryption algorithm */ + aead: 100, + /** Persistent symmetric keys: authentication algorithm */ + hmac: 101 }, /** {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC4880, section 9.2} @@ -382,6 +404,8 @@ export default { splitPrivateKey: 16, /** 0x20 - This key may be used for authentication. */ authentication: 32, + /** This key may be used for forwarded communications */ + forwardedCommunication: 64, /** 0x80 - The private component of this key may be in the * possession of more than one person. */ sharedPrivateKey: 128 diff --git a/src/index.js b/src/index.js index 75b320a8d..47b46e050 100644 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,9 @@ export { CleartextMessage, readCleartextMessage, createCleartextMessage } from ' export * from './packet'; +export { default as KDFParams } from './type/kdf_params'; +export { default as Argon2S2K, Argon2OutOfMemoryError } from './type/s2k/argon2'; + export * from './encoding/armor'; export { default as enums } from './enums'; diff --git a/src/key/factory.js b/src/key/factory.js index 955802a3d..af1430f08 100644 --- a/src/key/factory.js +++ b/src/key/factory.js @@ -75,6 +75,8 @@ function createKey(packetlist) { * @param {Date} options.date Creation date of the key and the key signatures * @param {Object} config - Full configuration * @param {Array} options.subkeys (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}] + * @param {String} options.symmetricHash (optional) hash algorithm for symmetric keys + * @param {String} options.symmetricCipher (optional) cipher algorithm for symmetric keys * sign parameter defaults to false, and indicates whether the subkey should sign rather than encrypt * @returns {Promise<{{ key: PrivateKey, revocationCertificate: String }}>} * @async @@ -137,7 +139,8 @@ export async function reformat(options, config) { helper.getLatestValidSignature(subkey.bindingSignatures, secretKeyPacket, enums.signature.subkeyBinding, dataToVerify, null, config) ).catch(() => ({})); return { - sign: bindingSignature.keyFlags && (bindingSignature.keyFlags[0] & enums.keyFlags.signData) + sign: bindingSignature.keyFlags && (bindingSignature.keyFlags[0] & enums.keyFlags.signData), + forwarding: bindingSignature.keyFlags && (bindingSignature.keyFlags[0] & enums.keyFlags.forwardedCommunication) }; })); } @@ -216,8 +219,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options, conf signatureProperties.preferredHashAlgorithms = createPreferredAlgos([ enums.hash.sha512, enums.hash.sha256, - enums.hash.sha3_512, - enums.hash.sha3_256 + ...(secretKeyPacket.version === 6 ? [enums.hash.sha3_512, enums.hash.sha3_256] : []) ], config.preferredHashAlgorithm); signatureProperties.preferredCompressionAlgorithms = createPreferredAlgos([ enums.compression.uncompressed, diff --git a/src/key/helper.js b/src/key/helper.js index 50c151b0a..68331edfe 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -9,7 +9,7 @@ import { SignaturePacket } from '../packet'; import enums from '../enums'; -import { getPreferredCurveHashAlgo, getHashByteLength } from '../crypto'; +import { getPreferredCurveHashAlgo, getHashByteLength, publicKey, isPostQuantumAlgo } from '../crypto'; import util from '../util'; import defaultConfig from '../config'; @@ -17,7 +17,7 @@ export async function generateSecretSubkey(options, config) { const secretSubkeyPacket = new SecretSubkeyPacket(options.date, config); secretSubkeyPacket.packets = null; secretSubkeyPacket.algorithm = enums.write(enums.publicKey, options.algorithm); - await secretSubkeyPacket.generate(options.rsaBits, options.curve); + await secretSubkeyPacket.generate(options.rsaBits, options.curve, options.symmetric); await secretSubkeyPacket.computeFingerprintAndKeyID(); return secretSubkeyPacket; } @@ -26,7 +26,7 @@ export async function generateSecretKey(options, config) { const secretKeyPacket = new SecretKeyPacket(options.date, config); secretKeyPacket.packets = null; secretKeyPacket.algorithm = enums.write(enums.publicKey, options.algorithm); - await secretKeyPacket.generate(options.rsaBits, options.curve, options.config); + await secretKeyPacket.generate(options.rsaBits, options.curve, options.symmetric); await secretKeyPacket.computeFingerprintAndKeyID(); return secretKeyPacket; } @@ -94,7 +94,9 @@ export async function createBindingSignature(subkey, primaryKey, options, config signatureType: enums.signature.keyBinding }, options.date, undefined, undefined, undefined, config); } else { - signatureProperties.keyFlags = [enums.keyFlags.encryptCommunication | enums.keyFlags.encryptStorage]; + signatureProperties.keyFlags = options.forwarding ? + [enums.keyFlags.forwardedCommunication] : + [enums.keyFlags.encryptCommunication | enums.keyFlags.encryptStorage]; } if (options.keyExpirationTime > 0) { signatureProperties.keyExpirationTime = options.keyExpirationTime; @@ -115,6 +117,12 @@ export async function createBindingSignature(subkey, primaryKey, options, config * @async */ export async function getPreferredHashAlgo(targetKeys, signingKeyPacket, date = new Date(), targetUserIDs = [], config) { + // For PQC, the returned hash algo MUST be set to the specified algorithm, see + // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1. + if (isPostQuantumAlgo(signingKeyPacket.algorithm)) { + return publicKey.postQuantum.getRequiredHashAlgo(signingKeyPacket.algorithm); + } + /** * If `preferredSenderAlgo` appears in the prefs of all recipients, we pick it; otherwise, we use the * strongest supported algo (`defaultAlgo` is always implicitly supported by all keys). @@ -227,7 +235,7 @@ export async function getPreferredCompressionAlgo(keys = [], date = new Date(), export async function getPreferredCipherSuite(keys = [], date = new Date(), userIDs = [], config = defaultConfig) { const selfSigs = await Promise.all(keys.map((key, i) => key.getPrimarySelfSignature(date, userIDs[i], config))); const withAEAD = keys.length ? - selfSigs.every(selfSig => selfSig.features && (selfSig.features[0] & enums.features.seipdv2)) : + !config.ignoreSEIPDv2FeatureFlag && selfSigs.every(selfSig => selfSig.features && (selfSig.features[0] & enums.features.seipdv2)) : config.aeadProtect; if (withAEAD) { @@ -388,13 +396,26 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) { options.type = options.type || subkeyDefaults.type; options.curve = options.curve || subkeyDefaults.curve; options.rsaBits = options.rsaBits || subkeyDefaults.rsaBits; + options.symmetricHash = options.symmetricHash || subkeyDefaults.symmetricHash; + options.symmetricCipher = options.symmetricCipher || subkeyDefaults.symmetricCipher; options.keyExpirationTime = options.keyExpirationTime !== undefined ? options.keyExpirationTime : subkeyDefaults.keyExpirationTime; options.passphrase = util.isString(options.passphrase) ? options.passphrase : subkeyDefaults.passphrase; options.date = options.date || subkeyDefaults.date; options.sign = options.sign || false; + options.forwarding = options.forwarding || false; + if (options.sign && options.forwarding) { + throw new Error('Incompatible options: "sign" and "forwarding" cannot be set together'); + } switch (options.type) { + case 'pqc': + if (options.sign) { + options.algorithm = enums.publicKey.pqc_mldsa_ed25519; + } else { + options.algorithm = enums.publicKey.pqc_mlkem_x25519; + } + break; case 'ecc': // NB: this case also handles legacy eddsa and x25519 keys, based on `options.curve` try { options.curve = enums.write(enums.curve, options.curve); @@ -420,6 +441,23 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) { case 'rsa': options.algorithm = enums.publicKey.rsaEncryptSign; break; + case 'symmetric': + if (options.sign) { + options.algorithm = enums.publicKey.hmac; + try { + options.symmetric = enums.write(enums.hash, options.symmetricHash); + } catch (e) { + throw new Error('Unknown hash algorithm'); + } + } else { + options.algorithm = enums.publicKey.aead; + try { + options.symmetric = enums.write(enums.symmetric, options.symmetricCipher); + } catch (e) { + throw new Error('Unknown symmetric algorithm'); + } + } + break; default: throw new Error(`Unsupported key type ${options.type}`); } @@ -435,6 +473,9 @@ export function validateSigningKeyPacket(keyPacket, signature, config) { case enums.publicKey.eddsaLegacy: case enums.publicKey.ed25519: case enums.publicKey.ed448: + case enums.publicKey.hmac: + case enums.publicKey.pqc_mldsa_ed25519: + case enums.publicKey.pqc_slhdsa_shake128s: if (!signature.keyFlags && !config.allowMissingKeyFlags) { throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`'); } @@ -453,6 +494,8 @@ export function validateEncryptionKeyPacket(keyPacket, signature, config) { case enums.publicKey.ecdh: case enums.publicKey.x25519: case enums.publicKey.x448: + case enums.publicKey.aead: + case enums.publicKey.pqc_mlkem_x25519: if (!signature.keyFlags && !config.allowMissingKeyFlags) { throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`'); } @@ -475,7 +518,8 @@ export function validateDecryptionKeyPacket(keyPacket, signature, config) { case enums.publicKey.elgamal: case enums.publicKey.ecdh: case enums.publicKey.x25519: - case enums.publicKey.x448: { + case enums.publicKey.x448: + case enums.publicKey.pqc_mlkem_x25519: { const isValidSigningKeyPacket = !signature.keyFlags || (signature.keyFlags[0] & enums.keyFlags.signData) !== 0; if (isValidSigningKeyPacket && config.allowInsecureDecryptionWithSigningKeys) { // This is only relevant for RSA keys, all other signing algorithms cannot decrypt @@ -484,7 +528,8 @@ export function validateDecryptionKeyPacket(keyPacket, signature, config) { return !signature.keyFlags || (signature.keyFlags[0] & enums.keyFlags.encryptCommunication) !== 0 || - (signature.keyFlags[0] & enums.keyFlags.encryptStorage) !== 0; + (signature.keyFlags[0] & enums.keyFlags.encryptStorage) !== 0 || + (config.allowForwardedMessages && (signature.keyFlags[0] & enums.keyFlags.forwardedCommunication) !== 0); } default: return false; diff --git a/src/key/private_key.js b/src/key/private_key.js index e82fa118f..2a9b2f292 100644 --- a/src/key/private_key.js +++ b/src/key/private_key.js @@ -39,14 +39,28 @@ class PrivateKey extends PublicKey { toPublic() { const packetlist = new PacketList(); const keyPackets = this.toPacketList(); + let symmetricFound = false; for (const keyPacket of keyPackets) { + if (symmetricFound && + keyPacket.constructor.tag === enums.packet.Signature) { + continue; + } else if (symmetricFound) { + symmetricFound = false; + } switch (keyPacket.constructor.tag) { case enums.packet.secretKey: { + if (keyPacket.algorithm === enums.publicKey.aead || keyPacket.algorithm === enums.publicKey.hmac) { + throw new Error('Cannot create public key from symmetric private'); + } const pubKeyPacket = PublicKeyPacket.fromSecretKeyPacket(keyPacket); packetlist.push(pubKeyPacket); break; } case enums.packet.secretSubkey: { + if (keyPacket.algorithm === enums.publicKey.aead || keyPacket.algorithm === enums.publicKey.hmac) { + symmetricFound = true; + break; + } const pubSubkeyPacket = PublicSubkeyPacket.fromSecretSubkeyPacket(keyPacket); packetlist.push(pubSubkeyPacket); break; @@ -219,10 +233,12 @@ class PrivateKey extends PublicKey { * Generates a new OpenPGP subkey, and returns a clone of the Key object with the new subkey added. * Supports RSA and ECC keys, as well as the newer Curve448 and Curve25519. * Defaults to the algorithm and bit size/curve of the primary key. DSA primary keys default to RSA subkeys. - * @param {ecc|rsa|curve25519|curve448} options.type The subkey algorithm: ECC, RSA, Curve448 or Curve25519 (new format). + * @param {ecc|rsa|curve25519|curve448|symmetric} options.type The subkey algorithm: ECC, RSA, Curve448 or Curve25519 (new format). * Note: Curve448 and Curve25519 are not widely supported yet. * @param {String} options.curve (optional) Elliptic curve for ECC keys * @param {Integer} options.rsaBits (optional) Number of bits for RSA subkeys + * @param {String} options.symmetricCipher (optional) Symmetric algorithm for persistent symmetric aead keys + * @param {String} options.symmetricHash (optional)Hash lgorithm for persistent symmetric hmac keys * @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 diff --git a/src/openpgp.js b/src/openpgp.js index 1b8cfa642..9cb29a73d 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -37,13 +37,15 @@ import { checkKeyRequirements } from './key/helper'; * The generated primary key will have signing capabilities. By default, one subkey with encryption capabilities is also generated. * @param {Object} options * @param {Object|Array} options.userIDs - User IDs as objects: `{ name: 'Jo Doe', email: 'info@jo.com' }` - * @param {'ecc'|'rsa'|'curve448'|'curve25519'} [options.type='ecc'] - The primary key algorithm type: ECC (default for v4 keys), RSA, Curve448 or Curve25519 (new format, default for v6 keys). + * @param {'ecc'|'rsa'|'curve448'|'curve25519'|'symmetric'} [options.type='ecc'] - The primary key algorithm type: ECC (default for v4 keys), RSA, Curve448, Curve25519 (new format, default for v6 keys) or symmetric. * Note: Curve448 and Curve25519 (new format) are not widely supported yet. * @param {String} [options.passphrase=(not protected)] - The passphrase used to encrypt the generated private key. If omitted or empty, the key won't be encrypted. * @param {Number} [options.rsaBits=4096] - Number of bits for RSA keys * @param {String} [options.curve='curve25519Legacy'] - Elliptic curve for ECC keys: * curve25519Legacy (default), nistP256, nistP384, nistP521, secp256k1, * brainpoolP256r1, brainpoolP384r1, or brainpoolP512r1 +* @param {String} options.symmetricCipher (optional) Symmetric algorithm for persistent symmetric aead keys +* @param {String} options.symmetricHash (optional)Hash lgorithm for persistent symmetric hmac keys * @param {Date} [options.date=current date] - Override the creation date of the key and the key signatures * @param {Number} [options.keyExpirationTime=0 (never expires)] - Number of seconds from the key creation time after which the key expires * @param {Array} [options.subkeys=a single encryption subkey] - Options for each subkey e.g. `[{sign: true, passphrase: '123'}]` @@ -55,7 +57,7 @@ import { checkKeyRequirements } from './key/helper'; * @async * @static */ -export async function generateKey({ userIDs = [], passphrase, type, curve, rsaBits = 4096, keyExpirationTime = 0, date = new Date(), subkeys = [{}], format = 'armored', config, ...rest }) { +export async function generateKey({ userIDs = [], passphrase, type, curve, rsaBits = 4096, symmetricHash = 'sha256', symmetricCipher = 'aes256', keyExpirationTime = 0, date = new Date(), subkeys = [{}], format = 'armored', config, ...rest }) { config = { ...defaultConfig, ...config }; checkConfig(config); if (!type && !curve) { type = config.v6Keys ? 'curve25519' : 'ecc'; // default to new curve25519 for v6 keys (legacy curve25519 cannot be used with them) @@ -74,7 +76,7 @@ export async function generateKey({ userIDs = [], passphrase, type, curve, rsaBi throw new Error(`rsaBits should be at least ${config.minRSABits}, got: ${rsaBits}`); } - const options = { userIDs, passphrase, type, rsaBits, curve, keyExpirationTime, date, subkeys }; + const options = { userIDs, passphrase, type, rsaBits, curve, keyExpirationTime, date, subkeys, symmetricHash, symmetricCipher }; try { const { key, revocationCertificate } = await generate(options, config); @@ -82,7 +84,7 @@ export async function generateKey({ userIDs = [], passphrase, type, curve, rsaBi return { privateKey: formatObject(key, format, config), - publicKey: formatObject(key.toPublic(), format, config), + publicKey: type !== 'symmetric' ? formatObject(key.toPublic(), format, config) : null, revocationCertificate }; } catch (err) { diff --git a/src/packet/public_key.js b/src/packet/public_key.js index 4d2235fcc..42b0f13a3 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -17,7 +17,7 @@ import KeyID from '../type/keyid'; import defaultConfig from '../config'; -import { computeDigest, parsePublicKeyParams, serializeParams } from '../crypto'; +import { computeDigest, isPostQuantumAlgo, parsePublicKeyParams, serializeParams } from '../crypto'; import enums from '../enums'; import util from '../util'; import { UnsupportedError } from './packet'; @@ -138,6 +138,11 @@ class PublicKeyPacket { ) { throw new Error('Legacy curve25519 cannot be used with v6 keys'); } + // The composite ML-DSA + EdDSA schemes MUST be used only with v6 keys. + // The composite ML-KEM + ECDH schemes MUST be used only with v6 keys. + if (this.version !== 6 && isPostQuantumAlgo(this.algorithm)) { + throw new Error('Unexpected key version: post-quantum algorithms can only be used with v6 keys'); + } this.publicParams = publicParams; pos += read; @@ -264,7 +269,7 @@ class PublicKeyPacket { /** * Returns algorithm information - * @returns {Object} An object of the form {algorithm: String, bits:int, curve:String}. + * @returns {Object} An object of the form {algorithm: String, bits:int, curve:String, symmetric:String}. */ getAlgorithmInfo() { const result = {}; @@ -275,6 +280,8 @@ class PublicKeyPacket { result.bits = util.uint8ArrayBitLength(modulo); } else if (this.publicParams.oid) { result.curve = this.publicParams.oid.getName(); + } else if (this.publicParams.cipher) { + result.symmetric = this.publicParams.cipher.getName(); } return result; } diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index 5a751d050..c4d060d5d 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -21,6 +21,12 @@ import enums from '../enums'; import util from '../util'; import { UnsupportedError } from './packet'; +const algosWithV3CleartextSessionKeyAlgorithm = new Set([ + enums.publicKey.x25519, + enums.publicKey.x448, + enums.publicKey.pqc_mlkem_x25519 +]); + /** * Public-Key Encrypted Session Key Packets (Tag 1) * @@ -128,7 +134,7 @@ class PublicKeyEncryptedSessionKeyPacket { } this.publicKeyAlgorithm = bytes[offset++]; this.encrypted = parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(offset)); - if (this.publicKeyAlgorithm === enums.publicKey.x25519 || this.publicKeyAlgorithm === enums.publicKey.x448) { + if (algosWithV3CleartextSessionKeyAlgorithm.has(this.publicKeyAlgorithm)) { if (this.version === 3) { this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm); } else if (this.encrypted.C.algorithm !== null) { @@ -182,8 +188,9 @@ class PublicKeyEncryptedSessionKeyPacket { const sessionKeyAlgorithm = this.version === 3 ? this.sessionKeyAlgorithm : null; const fingerprint = key.version === 5 ? key.getFingerprintBytes().subarray(0, 20) : key.getFingerprintBytes(); const encoded = encodeSessionKey(this.version, algo, sessionKeyAlgorithm, this.sessionKey); + const privateParams = algo === enums.publicKey.aead ? key.privateParams : null; this.encrypted = await publicKeyEncrypt( - algo, sessionKeyAlgorithm, key.publicParams, encoded, fingerprint); + algo, sessionKeyAlgorithm, key.publicParams, privateParams, encoded, fingerprint); } /** @@ -210,7 +217,7 @@ class PublicKeyEncryptedSessionKeyPacket { if (this.version === 3) { // v3 Montgomery curves have cleartext cipher algo - const hasEncryptedAlgo = this.publicKeyAlgorithm !== enums.publicKey.x25519 && this.publicKeyAlgorithm !== enums.publicKey.x448; + const hasEncryptedAlgo = !algosWithV3CleartextSessionKeyAlgorithm.has(this.publicKeyAlgorithm); this.sessionKeyAlgorithm = hasEncryptedAlgo ? sessionKeyAlgorithm : this.sessionKeyAlgorithm; if (sessionKey.length !== getCipherParams(this.sessionKeyAlgorithm).keySize) { @@ -230,6 +237,7 @@ function encodeSessionKey(version, keyAlgo, cipherAlgo, sessionKeyData) { case enums.publicKey.rsaEncryptSign: case enums.publicKey.elgamal: case enums.publicKey.ecdh: + case enums.publicKey.aead: // add checksum return util.concatUint8Array([ new Uint8Array(version === 6 ? [] : [cipherAlgo]), @@ -238,6 +246,7 @@ function encodeSessionKey(version, keyAlgo, cipherAlgo, sessionKeyData) { ]); case enums.publicKey.x25519: case enums.publicKey.x448: + case enums.publicKey.pqc_mlkem_x25519: return sessionKeyData; default: throw new Error('Unsupported public key algorithm'); @@ -250,7 +259,8 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) { case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncryptSign: case enums.publicKey.elgamal: - case enums.publicKey.ecdh: { + case enums.publicKey.ecdh: + case enums.publicKey.aead: { // verify checksum in constant time const result = decryptedData.subarray(0, decryptedData.length - 2); const checksum = decryptedData.subarray(decryptedData.length - 2); @@ -285,6 +295,7 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) { } case enums.publicKey.x25519: case enums.publicKey.x448: + case enums.publicKey.pqc_mlkem_x25519: return { sessionKeyAlgorithm: null, sessionKey: decryptedData diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index 287f888fa..0704d81f3 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -17,7 +17,7 @@ import PublicKeyPacket from './public_key'; import { newS2KFromConfig, newS2KFromType } from '../type/s2k'; -import { computeDigest, getCipherParams, parsePrivateKeyParams, serializeParams, generateParams, validateParams, getRandomBytes, cipherMode } from '../crypto'; +import { computeDigest, getCipherParams, parsePrivateKeyParams, serializeParams, generateParams, validateParams, getRandomBytes, cipherMode, isPostQuantumAlgo } from '../crypto'; import enums from '../enums'; import util from '../util'; import defaultConfig from '../config'; @@ -221,7 +221,7 @@ class SecretKeyPacket extends PublicKeyPacket { } } try { - const { read, privateParams } = parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); + const { read, privateParams } = await parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); if (read < cleartext.length) { throw new Error('Error reading MPIs'); } @@ -479,7 +479,7 @@ class SecretKeyPacket extends PublicKeyPacket { } try { - const { privateParams } = parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); + const { privateParams } = await parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); this.privateParams = privateParams; } catch (err) { throw new Error('Error reading MPIs'); @@ -523,7 +523,7 @@ class SecretKeyPacket extends PublicKeyPacket { } } - async generate(bits, curve) { + async generate(bits, curve, symmetric) { // The deprecated OIDs for Ed25519Legacy and Curve25519Legacy are used in legacy version 4 keys and signatures. // Implementations MUST NOT accept or generate v6 key material using the deprecated OIDs. if (this.version === 6 && ( @@ -532,7 +532,10 @@ class SecretKeyPacket extends PublicKeyPacket { )) { throw new Error(`Cannot generate v6 keys of type 'ecc' with curve ${curve}. Generate a key of type 'curve25519' instead`); } - const { privateParams, publicParams } = await generateParams(this.algorithm, bits, curve); + if (this.version !== 6 && isPostQuantumAlgo(this.algorithm)) { + throw new Error(`Cannot generate v${this.version} keys of type 'pqc'. Generate a v6 key instead`); + } + const { privateParams, publicParams } = await generateParams(this.algorithm, bits, curve, symmetric); this.privateParams = privateParams; this.publicParams = publicParams; this.isEncrypted = false; diff --git a/src/packet/signature.js b/src/packet/signature.js index 24963fe0f..43d5d7682 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -781,9 +781,10 @@ class SignaturePacket { } this.params = await this.params; + const privateParams = this.publicKeyAlgorithm === enums.publicKey.hmac ? key.privateParams : null; this[verified] = await signature.verify( - this.publicKeyAlgorithm, this.hashAlgorithm, this.params, key.publicParams, + this.publicKeyAlgorithm, this.hashAlgorithm, this.params, key.publicParams, privateParams, toHash, hash ); diff --git a/src/type/enum.js b/src/type/enum.js new file mode 100644 index 000000000..e697f30c7 --- /dev/null +++ b/src/type/enum.js @@ -0,0 +1,78 @@ +// OpenPGP.js - An OpenPGP implementation in javascript +// Copyright (C) 2015-2016 Decentral +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 3.0 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +/** + * Wrapper for enums + * + * @requires enums + * @module type/enum.js + */ + + +import enums from '../enums'; + +const type_enum = type => class EnumType { + constructor(data) { + if (typeof data === 'undefined') { + this.data = null; + } else { + this.data = data; + } + } + + /** + * Read an enum entry + * @param {Uint8Array} input Where to read the symmetric algo from + */ + read(input) { + const data = input[0]; + this.data = enums.write(type, data); + return 1; + } + + /** + * Write an enum as an integer + * @returns {Uint8Array} An integer representing the algorithm + */ + write() { + return new Uint8Array([this.data]); + } + + /** + * Get the name of the enum entry + * @returns {string} The name string + */ + getName() { + return enums.read(type, this.data); + } + + /** + * Get the integer value of the enum entry + * @returns {Number} The integer value + */ + getValue() { + return this.data; + } +}; +const AEADEnum = type_enum(enums.aead); +const SymAlgoEnum = type_enum(enums.symmetric); +const HashEnum = type_enum(enums.hash); + +export { AEADEnum, SymAlgoEnum, HashEnum }; + +export default type_enum; diff --git a/src/type/kdf_params.js b/src/type/kdf_params.js index 84728b3c1..7a3724742 100644 --- a/src/type/kdf_params.js +++ b/src/type/kdf_params.js @@ -26,22 +26,31 @@ import { UnsupportedError } from '../packet/packet'; * Alternative 1) [NIST-SP800-56A] with the KDF hash function that is * SHA2-256 [FIPS-180-3] or stronger is REQUIRED. * @module type/kdf_params - * @private */ +import util from '../util'; + +const VERSION_FORWARDING = 0xFF; class KDFParams { /** - * @param {enums.hash} hash - Hash algorithm - * @param {enums.symmetric} cipher - Symmetric algorithm + * @param {Integer} version Version, defaults to 1 + * @param {enums.hash} hash Hash algorithm + * @param {enums.symmetric} cipher Symmetric algorithm + * @param {Uint8Array} replacementFingerprint (forwarding only) fingerprint to use instead of recipient one (v5 keys, the 20 leftmost bytes of the fingerprint) */ constructor(data) { if (data) { - const { hash, cipher } = data; + const { version, hash, cipher, replacementFingerprint } = data; + this.version = version || 1; this.hash = hash; this.cipher = cipher; + + this.replacementFingerprint = replacementFingerprint; } else { + this.version = null; this.hash = null; this.cipher = null; + this.replacementFingerprint = null; } } @@ -51,20 +60,44 @@ class KDFParams { * @returns {Number} Number of read bytes. */ read(input) { - if (input.length < 4 || input[0] !== 3 || input[1] !== 1) { + if (input.length < 4 || (input[1] !== 1 && input[1] !== VERSION_FORWARDING)) { throw new UnsupportedError('Cannot read KDFParams'); } + const totalBytes = input[0]; + this.version = input[1]; this.hash = input[2]; this.cipher = input[3]; - return 4; + let readBytes = 4; + + if (this.version === VERSION_FORWARDING) { + const fingerprintLength = totalBytes - readBytes + 1; // acount for length byte + this.replacementFingerprint = input.slice(readBytes, readBytes + fingerprintLength); + readBytes += fingerprintLength; + } + return readBytes; } /** * Write KDFParams to an Uint8Array + * @param {Boolean} [forReplacementParams] - forwarding only: whether to serialize data to use for replacement params * @returns {Uint8Array} Array with the KDFParams value */ - write() { - return new Uint8Array([3, 1, this.hash, this.cipher]); + write(forReplacementParams) { + if (!this.version || this.version === 1 || forReplacementParams) { + return new Uint8Array([3, 1, this.hash, this.cipher]); + } + + const forwardingFields = util.concatUint8Array([ + new Uint8Array([ + 3 + this.replacementFingerprint.length, + this.version, + this.hash, + this.cipher + ]), + this.replacementFingerprint + ]); + + return forwardingFields; } } diff --git a/src/type/s2k/argon2.js b/src/type/s2k/argon2.js index be357b508..179acf2b3 100644 --- a/src/type/s2k/argon2.js +++ b/src/type/s2k/argon2.js @@ -23,9 +23,26 @@ export class Argon2OutOfMemoryError extends Error { let loadArgonWasmModule; let argon2Promise; // reload wasm module above this treshold, to deallocated used memory -const ARGON2_WASM_MEMORY_THRESHOLD_RELOAD = 2 << 19; +// (cannot be declared as a simple `static` field as its not supported by Safari 14) +let ARGON2_WASM_MEMORY_THRESHOLD_RELOAD = 2 << 19; class Argon2S2K { + static get ARGON2_WASM_MEMORY_THRESHOLD_RELOAD() { + return ARGON2_WASM_MEMORY_THRESHOLD_RELOAD; + } + + static set ARGON2_WASM_MEMORY_THRESHOLD_RELOAD(memoryThreshold) { + ARGON2_WASM_MEMORY_THRESHOLD_RELOAD = memoryThreshold; + } + + static reloadWasmModule() { + if (!loadArgonWasmModule) return; + + // it will be awaited if needed at the next `produceKey` invocation + argon2Promise = loadArgonWasmModule(); + argon2Promise.catch(() => {}); + } + /** * @param {Object} [config] - Full configuration, defaults to openpgp.config */ @@ -125,10 +142,8 @@ class Argon2S2K { }); // a lot of memory was used, reload to deallocate - if (decodedM > ARGON2_WASM_MEMORY_THRESHOLD_RELOAD) { - // it will be awaited if needed at the next `produceKey` invocation - argon2Promise = loadArgonWasmModule(); - argon2Promise.catch(() => {}); + if (decodedM > Argon2S2K.ARGON2_WASM_MEMORY_THRESHOLD_RELOAD) { + Argon2S2K.reloadWasmModule(); } return hash; } catch (e) { diff --git a/src/type/short_byte_string.js b/src/type/short_byte_string.js new file mode 100644 index 000000000..1611e16fd --- /dev/null +++ b/src/type/short_byte_string.js @@ -0,0 +1,32 @@ +import util from '../util'; + +class ShortByteString { + constructor(data) { + if (typeof data === 'undefined') { + data = new Uint8Array([]); + } + if (!util.isUint8Array(data)) { + throw new Error('data must be in the form of a Uint8Array'); + } + this.data = data; + this.length = this.data.byteLength; + } + + write() { + return util.concatUint8Array([new Uint8Array([this.length]), this.data]); + } + + read(input) { + if (input.length >= 1) { + const length = input[0]; + if (input.length >= length + 1) { + this.data = input.subarray(1, 1 + length); + this.length = length; + return 1 + length; + } + } + throw new Error('Invalid octet string'); + } +} + +export default ShortByteString; diff --git a/test/benchmarks/memory_usage.js b/test/benchmarks/memory_usage.js index 50673b67d..206410a42 100644 --- a/test/benchmarks/memory_usage.js +++ b/test/benchmarks/memory_usage.js @@ -4,7 +4,7 @@ import path from 'path'; import { writeFileSync, unlinkSync } from 'fs'; import { fork } from 'child_process'; import { fileURLToPath } from 'url'; -import * as openpgp from 'openpgp'; +import * as openpgp from '@protontech/openpgp'; /** * Benchmark max memory usage recorded during execution of the given function. @@ -18,7 +18,7 @@ const benchmark = async function(fn) { // the code to execute must be written to a file writeFileSync(tmpFileName, ` const assert = require('assert'); -const openpgp = require('openpgp'); +const openpgp = require('@protontech/openpgp'); let maxMemoryComsumption; let activeSampling = false; @@ -335,6 +335,20 @@ class MemoryBenchamrkSuite { await decryptedData.pipeTo(sink); }); + suite.add('openpgp.encrypt/decryptSessionKeys (argon2)', async () => { + const config = { s2kType: openpgp.enums.s2k.argon2 }; + const passwords = 'password'; + const sessionKey = { + algorithm: 'aes128', + data: require('crypto').getRandomValues(new Uint8Array(16)) + }; + const encrypted = await openpgp.encryptSessionKey({ ...sessionKey, passwords, config, format: 'object' }); + assert(encrypted.packets.length === 1); + const skesk = encrypted.packets[0]; + assert(skesk.s2k.type === 'argon2'); + await openpgp.decryptSessionKeys({ message: encrypted, passwords }); + }); + const stats = await suite.run(); // Print JSON stats to stdout console.log(JSON.stringify(stats, null, 4)); diff --git a/test/benchmarks/time.js b/test/benchmarks/time.js index 24ba752ac..575378bf8 100644 --- a/test/benchmarks/time.js +++ b/test/benchmarks/time.js @@ -1,6 +1,6 @@ import Benchmark from 'benchmark'; import { readToEnd } from '@openpgp/web-stream-tools'; -import * as openpgp from 'openpgp'; +import * as openpgp from '@protontech/openpgp'; const wrapAsync = func => ({ fn: async deferred => { diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index 0fd63fbb0..be7c6c44a 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -6,7 +6,7 @@ import openpgp from '../initOpenpgp.js'; import * as crypto from '../../src/crypto'; import util from '../../src/util.js'; -export default () => describe('API functional testing', function() { +export default () => describe('API functional testing', async function() { const RSAPublicKeyMaterial = util.concatUint8Array([ new Uint8Array([0x08,0x00,0xac,0x15,0xb3,0xd6,0xd2,0x0f,0xf0,0x7a,0xdd,0x21,0xb7, 0xbf,0x61,0xfa,0xca,0x93,0x86,0xc8,0x55,0x5a,0x4b,0xa6,0xa4,0x1a, @@ -196,15 +196,15 @@ export default () => describe('API functional testing', function() { const algoRSA = openpgp.enums.publicKey.rsaEncryptSign; const RSAPublicParams = crypto.parsePublicKeyParams(algoRSA, RSAPublicKeyMaterial).publicParams; - const RSAPrivateParams = crypto.parsePrivateKeyParams(algoRSA, RSAPrivateKeyMaterial).privateParams; + const RSAPrivateParams = (await crypto.parsePrivateKeyParams(algoRSA, RSAPrivateKeyMaterial)).privateParams; const algoDSA = openpgp.enums.publicKey.dsa; const DSAPublicParams = crypto.parsePublicKeyParams(algoDSA, DSAPublicKeyMaterial).publicParams; - const DSAPrivateParams = crypto.parsePrivateKeyParams(algoDSA, DSAPrivateKeyMaterial).privateParams; + const DSAPrivateParams = (await crypto.parsePrivateKeyParams(algoDSA, DSAPrivateKeyMaterial)).privateParams; const algoElGamal = openpgp.enums.publicKey.elgamal; const elGamalPublicParams = crypto.parsePublicKeyParams(algoElGamal, elGamalPublicKeyMaterial).publicParams; - const elGamalPrivateParams = crypto.parsePrivateKeyParams(algoElGamal, elGamalPrivateKeyMaterial).privateParams; + const elGamalPrivateParams = (await crypto.parsePrivateKeyParams(algoElGamal, elGamalPrivateKeyMaterial)).privateParams; const data = util.stringToUint8Array('foobar'); @@ -214,7 +214,7 @@ export default () => describe('API functional testing', function() { openpgp.enums.publicKey.rsaEncryptSign, openpgp.enums.hash.sha1, RSAPublicParams, RSAPrivateParams, data, await crypto.computeDigest(2, data) ); const success = await crypto.signature.verify( - openpgp.enums.publicKey.rsaEncryptSign, openpgp.enums.hash.sha1, RSAsignedData, RSAPublicParams, data, await crypto.computeDigest(2, data) + openpgp.enums.publicKey.rsaEncryptSign, openpgp.enums.hash.sha1, RSAsignedData, RSAPublicParams, null, data, await crypto.computeDigest(2, data) ); return expect(success).to.be.true; }); @@ -224,7 +224,7 @@ export default () => describe('API functional testing', function() { openpgp.enums.publicKey.dsa, openpgp.enums.hash.sha1, DSAPublicParams, DSAPrivateParams, data, await crypto.computeDigest(2, data) ); const success = await crypto.signature.verify( - openpgp.enums.publicKey.dsa, openpgp.enums.hash.sha1, DSAsignedData, DSAPublicParams, data, await crypto.computeDigest(2, data) + openpgp.enums.publicKey.dsa, openpgp.enums.hash.sha1, DSAsignedData, DSAPublicParams, null, data, await crypto.computeDigest(2, data) ); return expect(success).to.be.true; @@ -239,7 +239,7 @@ export default () => describe('API functional testing', function() { openpgp.enums.publicKey.ed448, openpgp.enums.hash.sha512, { A }, { seed }, data, toSign ); const success = await crypto.signature.verify( - openpgp.enums.publicKey.ed448, openpgp.enums.hash.sha512, signedData, { A }, data, toSign + openpgp.enums.publicKey.ed448, openpgp.enums.hash.sha512, signedData, { A }, null, data, toSign ); return expect(success).to.be.true; @@ -271,7 +271,7 @@ export default () => describe('API functional testing', function() { it('Asymmetric using RSA with eme_pkcs1 padding', function () { const symmKey = crypto.generateSessionKey(openpgp.enums.symmetric.aes256); - return crypto.publicKeyEncrypt(algoRSA, openpgp.enums.symmetric.aes256, RSAPublicParams, symmKey).then(RSAEncryptedData => { + return crypto.publicKeyEncrypt(algoRSA, openpgp.enums.symmetric.aes256, RSAPublicParams, null, symmKey).then(RSAEncryptedData => { return crypto.publicKeyDecrypt( algoRSA, RSAPublicParams, RSAPrivateParams, RSAEncryptedData ).then(data => { @@ -282,7 +282,7 @@ export default () => describe('API functional testing', function() { it('Asymmetric using Elgamal with eme_pkcs1 padding', function () { const symmKey = crypto.generateSessionKey(openpgp.enums.symmetric.aes256); - return crypto.publicKeyEncrypt(algoElGamal, openpgp.enums.symmetric.aes256, elGamalPublicParams, symmKey).then(ElgamalEncryptedData => { + return crypto.publicKeyEncrypt(algoElGamal, openpgp.enums.symmetric.aes256, elGamalPublicParams, null, symmKey).then(ElgamalEncryptedData => { return crypto.publicKeyDecrypt( algoElGamal, elGamalPublicParams, elGamalPrivateParams, ElgamalEncryptedData ).then(data => { diff --git a/test/crypto/ecdh.js b/test/crypto/ecdh.js index 72a8892ba..6796d75bd 100644 --- a/test/crypto/ecdh.js +++ b/test/crypto/ecdh.js @@ -267,9 +267,9 @@ export default () => describe('ECDH key exchange @lightweight', function () { for (const { vector } of vectors) { const lowOrderPoint = util.hexToUint8Array(vector); const { A: K_A, k: a } = await elliptic_curves.ecdhX.generate(openpgp.enums.publicKey.x448); - await expect(elliptic_curves.ecdhX.encrypt(openpgp.enums.publicKey.x448, data, lowOrderPoint)).to.be.rejectedWith(/Invalid private or public key received|expected valid u|low order point/); + await expect(elliptic_curves.ecdhX.encrypt(openpgp.enums.publicKey.x448, data, lowOrderPoint)).to.be.rejectedWith(/invalid private or public key received|expected valid u|low order point/); const dummyWrappedKey = new Uint8Array(32); // expected to be unused - await expect(elliptic_curves.ecdhX.decrypt(openpgp.enums.publicKey.x448, lowOrderPoint, dummyWrappedKey, K_A, a)).to.be.rejectedWith(/Invalid private or public key received|expected valid u|low order point/); + await expect(elliptic_curves.ecdhX.decrypt(openpgp.enums.publicKey.x448, lowOrderPoint, dummyWrappedKey, K_A, a)).to.be.rejectedWith(/invalid private or public key received|expected valid u|low order point/); } }); @@ -361,4 +361,31 @@ export default () => describe('ECDH key exchange @lightweight', function () { }); }); }); + + describe('KDF parameters', function () { + const fingerprint = new Uint8Array([ + 177, 183, 116, 123, 76, 133, 245, 212, 151, 243, + 236, 71, 245, 86, 3, 168, 101, 74, 209, 105 + ]); + + it('Valid serialization', async function () { + const cipher = openpgp.enums.symmetric.aes256; + const hash = openpgp.enums.hash.sha256; + + const v1 = new KDFParams({ cipher, hash }); + const v1Copy = new KDFParams({}); + v1Copy.read(v1.write()); + expect(v1Copy).to.deep.equal(v1); + + const forwardingFlags = new KDFParams({ + cipher, + hash, + version: 0xFF, + replacementFingerprint: fingerprint + }); + const forwardingFlagsCopy = new KDFParams(); + forwardingFlagsCopy.read(forwardingFlags.write()); + expect(forwardingFlagsCopy).to.deep.equal(forwardingFlags); + }); + }); }); diff --git a/test/crypto/hmac.js b/test/crypto/hmac.js new file mode 100644 index 000000000..61b62e144 --- /dev/null +++ b/test/crypto/hmac.js @@ -0,0 +1,34 @@ +import { use as chaiUse, expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import +chaiUse(chaiAsPromised); + +import { sign, verify } from '../../src/crypto/public_key/hmac'; +import enums from '../../src/enums'; +import util from '../../src/util'; + +export default () => describe('HMAC', function () { + it('Test vectors', async function() { + const vectors = [ + { + algo: enums.hash.sha256, + key: util.hexToUint8Array('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), + data: util.hexToUint8Array('4869205468657265'), + expected: util.hexToUint8Array('b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7') + }, + { + algo: enums.hash.sha512, + key: util.hexToUint8Array('4a656665'), + data: util.hexToUint8Array('7768617420646f2079612077616e7420666f72206e6f7468696e673f'), + expected: util.hexToUint8Array('164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737') + } + ]; + + await Promise.all(vectors.map(async vec => { + const res = await sign(vec.algo, vec.key, vec.data); + const verified = await verify(vec.algo, vec.key, vec.expected, vec.data); + expect(util.equalsUint8Array(res, vec.expected)).to.be.true; + expect(verified).to.be.true; + })); + }); +}); + diff --git a/test/crypto/index.js b/test/crypto/index.js index a4c73ae5c..4f90ebf32 100644 --- a/test/crypto/index.js +++ b/test/crypto/index.js @@ -8,11 +8,13 @@ import testECDH from './ecdh'; import testPKCS5 from './pkcs5'; import testAESKW from './aes_kw'; import testHKDF from './hkdf'; +import testHMAC from './hmac'; import testGCM from './gcm'; import testEAX from './eax'; import testOCB from './ocb'; import testRSA from './rsa'; import testValidate from './validate'; +import testPQC from './postQuantum'; export default () => describe('Crypto', function () { testBigInteger(); @@ -25,9 +27,11 @@ export default () => describe('Crypto', function () { testPKCS5(); testAESKW(); testHKDF(); + testHMAC(); testGCM(); testEAX(); testOCB(); testRSA(); testValidate(); + testPQC(); }); diff --git a/test/crypto/postQuantum.js b/test/crypto/postQuantum.js new file mode 100644 index 000000000..831dac2b8 --- /dev/null +++ b/test/crypto/postQuantum.js @@ -0,0 +1,2299 @@ +/* eslint-disable max-lines */ +import { use as chaiUse, expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import +chaiUse(chaiAsPromised); + +import openpgp from '../initOpenpgp.js'; +import { generateParams, publicKeyEncrypt, publicKeyDecrypt } from '../../src/crypto/crypto.js'; +import { sign, verify } from '../../src/crypto/signature.js'; + +// Test vector from https://www.ietf.org/archive/id/draft-ietf-openpgp-pqc-06.html#appendix-A.2.1 +const mldsaEd25519AndMlkemX25519PrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +-----END PGP PRIVATE KEY BLOCK-----`; + +const mldsaEd25519AndMlkemX25519PublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +xscKBlHQxoBrAAAHwGy44RkYq173huDlFbFTF0FbPOsQdZRherJYVBKlJKH8+IJe +miR3kgJaw42LGYryBJuCAavtf1M+Nyh3IR06sEJ226n+LLb72/uGCrlqRlUQkSbz +/EHjiOogerpgiuz6D8gHDqdNlwvtuO0Cw+CbCFAsDbSOp8zqwd3Qq1poCSLueLc1 +DIJe8TkZrmqbLA1LMyQAvT7ALQjGAP33wzKB1fGDv8napjWSiCgT03EdvRa39pEZ +I7Inu5y0RkYdsTKt03HkcApB3XM8n4xolj3NYrUc7f1Mqqlz+5d5ydcFRmnos+Cm +fDzj6cdaseT9IV8BfHjVNMXpozpxkr8Dwaz7a2SrSnJIp9N2k3nlCVMAGYj4qyWO +dmRjevSvJbPccX7ar4V7g+a/wr7Ec+beB+J3ya87SXI1PYj7wc6wIPCZRqxk+X9Z +JLESYmoOV3eQQwdOgjiWp5xWAn7LJ+MBC6cFcQRMfFPDuZlbwv2gQ6vX2HTpHqaO +oE86wKXYfpHqgTukExxFdx7A6JlAcxYi57g0xfCllxDD9wXDirsBMX4eGK/d+/1c +E9Cf7yTJQlQfL+Dtpsqr3qfrGHvxiwxVmzsp7yM9J75ZZoQpNC22jUgOG4U9r5N5 +Op30/340M1ysMKzVgduz+tE3ewU8Mm8lNe/TU5R2ZN5TDV5cT1O70MLzjPhECfRU +pKCyhnBVidNLZ7GGOmYQyj2zXcmQ0cM1By/ohWz2fnOxH702Y+QdRv0r52hlvCY9 +RoNnlax5tEN7zE8cVgLVfVN0UXU+klVSlHfLPMy4gZK2/s7LylVcabRhTE9MG3hU +fbtpA0KPQU4v8BartJNOwGAXx1qpCxppDZHWVnp8xO5cVhLENe0CoUn8S9wO2M6B +ggbn+35F7pMONASGH3uELt4XzibzNGFJCmUUPOOHfFPwrqtfjvYkPySLTTp21j47 +2ab+FfVBFBkK1FOlnz3ufA2U/0+LvRAIfdJIfRzES+Gx8g5u2eh7VO8pwmw2zexW +3Cp/4LNisLyg/vv+UgMdWfh66A5r6ssXN9MT8oytWcMcsng0FlmCVKehbea/8ziO +rVJ9RdGaiAv2EF1mm+5ZDi9asOemwdzX39277NfFYXcBpigqFEqMVbhoUGQu635Y +eZbNBuTSlq7s7hVgGzMszeImuv0cVFLkk5z/JvYkbO/cuGFcN9xYhHXj1R/6DNqf +JyRAKhBheMkxGNVSV+7Ot5V1mRMGpt3UmnkowooTGHux69oYFRx8gBvMNs/+Kbub +n+dgPA2fbwCR8kWc6jvewU4g0fjNiSZgFuM4EYMMAeEAZTFNw4Cm8TWZ6Wmo1Hax +aJVvnJWRGXF4TP4I6lX3am7dMiofK5Og4BxymDabzJKo8uiOYXkyUUTfWfzDGdpZ +bF+hW4qcvE650KP+Bf4rOOSfOJ6SFt2y0srSJpR+H3EfLk76WpDqiy1DlBpV8E74 +sEJK7CcwzsaSyxpUzk4N+KJAa7ahkL1/Rw9ZFf0PEkquMIOoK0MXlQ24jlTDCAD5 +7ynzhLqWfxhj6KNRbS2M8wVrf7WBjgjHlRIaWZFbpC6nUP0wqNXIG7ZlMfRQQjHH +jTlPFFe8kcq9Xcw3oKZmln4yHhstR960DFYGELvTR+9ozFiYoxgAwdJC7748EtvD +ASXWZqhQ1g3lXEDHbJ8PAdU3GxeYSeUiC80Hro/0uf20tLGU3kdyfbmMjBrnDzWF +bOwcm/76V15YIgFiOENXWivesqNY4eZ4MSB7ZcJm3wdvJ2ZsUzcsVmj0x7bxgld5 +hsl4uEJxKgVwUf0Hg8AJQJ/p0PjFiwWF9wttOZynrSfamWur/kmmhKPS0NqPFEXE +I70eMuPVCGI8oLvfK93RqZqGUsaURVrsDNH2fvx5sh1/iaXtl3fTcZyrcLYUck83 +4sY/KUi+/NHfZpj82EhVjOvQ/2f0tqADLeYXy1bFXE/T/6ooN/eEV8yKSwL43mjB +q9iaURTOFOqftc7Bv+IzOxCDjpYIoQKmHa8VeDSEMURBfn22amDA900dgwbEMpaP ++L/SrxkcIzYie//XYePcyhGjAuIQine/+vAE9fBZ4zS0tnoAsktlD7S0EPNKrlfi +H7kHNh9ueRwzOxUHomMzRzTet8zY3ZxXXYAqD9W6LiUbnK+4a7LtsF+xRSL9XsBN +YevQFSZ2i6d8Xoqx6hRB4GVGCopQSAixwnSqoYw1x9tL+u/rYS1hNeEFN06MBvry +CfKvy4ZeGXI3Pd0xyyqAcFyF7biYxWPoNkm8mQEvuKdAFvSXRQd9lrkcH+QEkiML +eJUw2jPVxG39r8wJ1yrHzPsizQWUb+s99OO4heuhw7PTeIIT7cg217iAJL+GrkCb +oEn6BdFr96D8kn4SHGbyW51ZvTmwIAfpOqp3Lt4ph4//N3HJ1yhVmF4rtgjGSoJ9 +nJIngjMb20dRis55Pe1n7w5CXM+VTo+YUTKokiBLNtIlNRHtWdvE6C29se2Zs0Tt +hNn4QzQNXWh4k3o1zD51V8FLUz6hROGelleVf8MPcKtYPIJn1JDRnhgfT3bNnbh0 +Dp8AKmeymVAKVPTU8mQDHmVQy4V6DGQ6Enn7sBXzKobOEGpI3+JFYnd3Deu2dT/2 +Fh0ildKLKNiZW21YjRMGBJcf8mySmg2XBoWX0hTCzMwGH2sMAAAAQAWCUdDGgAML +CQcDFQoIAhYAApsDAh4JIqEGvyYrJBdwAqyK5dxtpHwFbSKrmQbUfQeVK3XDWAIZ +AcoFJwkCBwIAAAAAq9gQSFUCQFY6OmE5d4M+wa5Gewj4Nh0m+5BRgHk7tMWj9f+b +N2LHBV0XPmKybmFtcpIO4zTJopJVrKOgMKyGXQhtuERlw2HndacJQxF3zI2uXQe1 +ifdzzbnWmEZfU5U1gc7m/tq9ufZi9srdjm9uj0FSA/hr72AZLVZG/Qvh8Raw+AMa +wv9LSrrjf2QzGAtdjtQtaZCJ2aVU9DAd9EM+sYcjVefoHHqgiITS76+ACQG/RLA2 +/Xi6DYoVOn98QXMD5UAzTK1JjtgLiLwvAlpuLH/WK9hIQfXob468cs3TDsKexi2U +X+0xV57wR24DRjph4Zr0Y/tc2hfP7/QfM7djBp3AvBlFGLSgKcoD4c6KVdqzyCsJ +tgNDuQQR+VaUJcXM3IJuAIZ765CiUPvZw2QC3N9p49v0JAdQytZbXyZhl60fglIW +WuujJqh2ae2l1PZpYD1TvLCgewQVwPlZDbmSczJzdzIzQU3/FK4SazbXLscb6V1+ +V3XX4Co5Ap/tdaXNHHcDHPJXEmSeyPXd+JcZDyVQ5KcPq9q1M5Etgb8OcVN6HUt1 +rU7UuUjw7mddMyPsX2oznUIa3ZEyPMpb50zTR2BKucEYLMs2ZwZ7ZbdYz8HHl4eP +27HyaYAleEKuTg1dqMIo1o9lEkMPReB8pr6019BfBBQWdhz/YXZNXSDaqvOERkWU +d1gkd8HLOW1vPRCFIvHxjFGRlxj5X2Q7x3/msu7Q43/JbTuI/Z9+ljw1pilXO1pl +TfmFsmfhPeKPyde8h728ZroYdKPlzM39OKILsJQim49+r2OvirVEhPIRfWEBccRk +4f1s/PbExbDXQum9hnhZPUwsGl8Tpo+CncD/jo4mN/08I9ErsjGKJ7w72whbPSX2 +VuXkAwxjWrYvXAqu3/REIZU0Z6Db4t3t3nxRFggZfF4zsI+qiPySpP4vk4rnWl+Q +bGHrbljgw0XRnjl1eaTCINr2jm/1QE0u/PtPzVXvaN+MG7PcGi0nAhDG1VqoX+EY +YaCUuVB3SQ5bgOQTPhAkfhIeQVEoMHySe3oV11fpUt0TKX2xI+KWWa2Z6wflrz1D +nRfxclwxK4hBawdyUjjbX0cmatXwPhi/0NTLEyjn39LhCgMk/sYGIFu/Si0Mhggc +zo9ibybQbsiFNP9Itwi3ruEL8JeOrd//CUFf7ifJo6cHxEsJpk7RQvjXXs7K9tQY +cIcJj5VBLfffGhuuTJ6ZllpolMasizgYA6JsCaYdM4IhDEvyR5PcxqfV0xoGuU3S +8EkU0ZekY3vZKFx6BgvZaJKWyM6KnEu8MkN0Vb+2FO+5/WF+uyKJfvO+M2vzpinD +Xc4q2caDSRniNmR+v/tfY3zTX11NHYPCsd7GOsJaXXT1xvE4D7T0SV7S7RDj4OBN ++JPkGDWYhhVOEtRqO66ZeqFVI6vRsafb98qgxwvF8AQ7LMRDwdu9b3JKUnFd7C0f +eTmf4vdZkINNFzrVRyLdyIk7dcPi0qPhm+yXzWf8egfOgQ/VwfkuaRL2kt6wTt9e +bf1KN7V3WwQOcHhGr/yTgYi0kbxMYrwa1CNxFkssLkdSeHJKQi6PWGo21RlgFP4m +YKMNOFyIPxSnTZeRnkuOOCZSAHWaIlEa6YgA4xbXFtBU8UaJGfNTWN88qTh762Ft +intkpkOzDeAoLBFoCv6jqs8yE+mQiia6Ao2HmOYJx1iYaTbDxcANCrs56UWdyfkz +0qE7Qwqgqk2YURjj9mBWwoadmV+Z7NiO8qWuj+x44OgWzNVlXbVtL4VqSF9vUCng +lyzYJ2noWclWhyuuuBqNawJQsdrpdc0xdJHnB1LLXW2BrR98QO0zvOhqIWTji9jd +DXHftXzEe+0LF6JyIm8GPTAwUtRdAxO1BpcMP/MQfd/ke+AhOzX2IyZhL/Aivbfc +FSR3O8YMaEckZeEADhi+yrSmFhil6UqHUi30REtVro+kdsbnHT+zPAYSloxv6w37 +hS5becBRO8UpWD9USJqtTRJ3BjhlB5oDY33pF/jxI/cQZbaBv3Sbb1aolxOgkUgg +6sXPNWMlJBn7SB+YLOSAQ5sCt2h1nJZ286QN1aV6VzGI0IIgcJpXC/7jxfpvrTIm +VtqL2+RZva38rpiC6Rf9iy7gwkoO9pk37CE5l0PvdZS9TDOw3axlc1Co1SpDjyvB +JPWWn+W99sTsE3dopWGyuJ28UvMH3xm/mTP9lbJz+5CBfMLT+YANG9zlj9z7FxJB +H2t+hw/oQN63QQAgieRuK6D4xO5kdj7IW8Y7NuLiMNuCeFBbPCVNmo0dH+nbgHZ4 +L8Z40h9A12Yn06fBFWSZV9ZGqzw2jYZ6yB25FaCOPZC3BTOyCF9gNLcX/HsyY0/D +oyoLcFIeyAdLEPqDPy7dOcLUzVINCVY0X0TZpEAvL/e5pDBt3NpBzApYUKYVCQ7m +SC3iZ/NlAa0L2ma8SlHxllDlG64t3IF+X/mYFc5pvgiCLJ+Qb38tikxrEjWZw/tP +35jWEOG8DCgWsuLwlJ6TMwSaESye3lmjTpmAfikIuqz0rkJyYyoW7GW5j4hgPxO0 +/VRWbmzUat5s2S7vCFWs5RD2qe5MMzCTHFd0jnyIVBL4VCamfcTiAbXQfzUzHl3V +dup2KLC2INktej2c2pLFuDHjulY+ltZCia9s+cRgsqEXb+Z68vbNjiPVOpO2noYs +RFebC9U024fUKUtBeyvVJhZnksZKZ33uIzRkC9pzB13RxVf9bhpCl9AnetsIy41o +ChIkJDuUQ7yq2Tggsd1dELRiG/RlfHFzDNguRD8LN7waDiY4FXECufj8OmWbRoqp +yc1jexwJFN3wfbnecmLpyf3Rk7/hWEVWoJ8XQkPk9Ynrr/wIVd/zU9YzPVJVB/K+ +1u1U9ac5i93kb1K1oDnREmgrImlbfEju+L6EqYNOvpgyPbHL8Gcn7UlyZQUCRAk2 +csGu1b9Id+4X82LXNC/wjl3ZK9g25QW0qb7iEkQ0DVDJVpgdni+n+cPOkhtpCvD9 +u7b81Gzxz4DzI5JBhAcVcDEyLFGVnJPpCi8LzPHmjKEZXVIzF5Inug7I488fANSE +ttRD+cjYVYb+QFzjMWI46F4umIKmMY8v+R9LOCvc1Fpara2rQaoEkyfC9RLXea9z +k/o6VEAN4PkTnPj3y4wYUqXS+oDkLKFhDOWQoa7OHBQgE1xp9gz6e5wPkag5Jj+E +bvrLuFWLr7EDkx9rKHrS52Z3qZkgev8f4gOwoIM4kp9Mo8/gvvFknI/yEe23Rkda +pdpaLeLs5NTUxZaPVv9pXM5i9kMBSXAesI+QZzcymT4nIAeZCXbzUHoc9VuIdqUD +RSWR7siPVmDdKpa3ka8+8m49tlfBOPT8WtHPpEicqfBAnWZnaNSoBUIaJv3LU9fo +8h8BLKsnHWX4VNv5qfT8mjKBFNU64dkox3AuD8xKbd6xgaIfMUWpyrkLzHHw6YXH +VpWkWAdDU9VpckqLlFOqJKq8hi4NRAXMhW1GWOCYkmfnx5/0we+U4EocS7WraHKC +8HMRbh7E0ObrqMwjlwiFRuWEYYIxC36bbEJG7tkfJTm67J6r2Rf3jgRplS1PA4W2 +mcTQYTQ0Qqf6QnJQ+NzXtKJPfuVkF/UZaAhT7XcvfP00+yb9seHX+H2UIAZ6rIvO +e/OH8KbH3Frf5+YZi0CJ+w/zHxWbE1ne8ZPJAZp8d4yzAC0vCZQymtkq9L+cDdgD +aP69HpiL1/RSHg7liQzJG+2AZCRDb2A8iYPiAZnD+JnOiTh7W/14OJawuPVWZYrY +7y19J25WI3jAJ1ezur6p+G5NmlZoCtiFvDmEEWg95twQPkD2vs5PW4q+zjse/zFo +ReGmVJtCs9T+U9HGaLbHLYteA7IlIw+8XEVZkHDW8XMkcZVOBbKcpJ+5gszpcGR5 +XpRNf9D0KsD9MXyYGtS9MNfNaopZF5yFVz0m9we7vqAN7nZBVNIHIWXYNsjKaeQS +pyOwn0lmk9L9yb0dRHVwr+00oGqxWgM8I5DlJqJCUA05RngdHhtb1m7d8B4OP0Yc +0hYQBlDUpsBgF8666sO1qBM73V9fadfo3O3CgaZLGPIV104jhAaChocqaX40Rlb2 +hJrDeCqFwl8oJ78yp17pvm1Y2YTQH+p1deYodm9Q3emfukA74M/zXoMxonmIapJr +Zn+1pO+9lZoWW+sYWYWXlzGk7YXjQXXo77F9lX5VnazHNtjkwn2RbcKrFa3NxpfM +ybov2013eoKfM2xit7B40SpMByVhmeL6HeLFaqXiHqBm3KM5vMOSLPXDSX4idXtc +NgDzlxaDvO55+DoW1c5ZslaZEvK7J7tgUNNoel0TEZF+spA7BurnByRs/e3kIKYu +5t7IWvMO09DH5PBvbf3aiN5V8+22JX/hPdfaA/wibZWMlyrvm/AoENyrdwo6fTaO +IaTLg7G9fKk8E5mbzaya/gTUPX9jfjf/BkVt6bl6egEdX2GnqegKVKKrtb7S3gwj +KDhNgI+ZmrjQ7jaMxf9MXWGbzfImQlpkdAAAAAAAAAAAAAAAAAAHDxsfJSrNLlBR +QyB1c2VyIChUZXN0IEtleSkgPHBxYy10ZXN0LWtleUBleGFtcGxlLmNvbT7CzLgG +E2sMAAAALAWCUdDGgAIZASKhBr8mKyQXcAKsiuXcbaR8BW0iq5kG1H0HlSt1w1gC +GQHKAAAAAPJjEKH2vbCxOnVz6yo4ofGv2PAC6L1Y635O6BSXvXCojWo8gycKqAGl +NUproxxClmkDa3U6HEx02iFa+brfyV6uL/nzEN07d61+K9J1ZDhaAbUAOE6DibKt +itg9kPa9bv1V4LSOdd3r2qo3JR4IPNdS+Snb7o77M7HbA7ziWSrBduxtF7mGuiIJ +txEDNtt6heYAIExUt1cO02UO8p1r7xcQXqH2x46Hf/B6api/J1j/4uEtP78wkG6K +tNE69Gw9NRe2PTKvKFp3jayq2I6pkjf5KvV9G13z9MvxyRjnTARTcVcqNNwFhvQv +4t1lRXRJxeYmkMFKLnbdZeBdSWAUUKBofy1dP6zJOk11iycofDRZ2eqpWBeejuGW +hdjpCKFilrybBSNLB831v7qqW6d5NpKu1kCngqfDf1GCP5d+JePsPk2aRZ8zzvqy +kvyWLvenmrQrH7yKw9l4N4X8HdLWIPq/iVkaUVO2lHkAJxZ5Z6ciG557++Gdu6uz +M5hdfyD6xzLxEHHJ7MBw9zQ/UI5h7LRAu17PIvAsxCIroTECyx9w2GAbSjCQXdh9 +P7zamnCprnBHpOStJRcVjvC4tDfCfrOUIBSD+g4hMIaMhqP6C6zALn6mbMHr1hLT +RqV99tMjnNX5QH52z1Dr/P/+9ZqVlA3R7td7Yw2buJyPdFRGKamxihHepQxFhOvB +43uEZexzC2O1jIaMxquBdis5NqvvtdtJQ2kVvSBEYw6Kg3bjC0b6XmoTGn5fBEQH +rHVvFOz9TZzZDnF+vQHC2G7Tv6QDldxz8MR0SzMkP7WPo7KdFwNFWVvoofKsCN3L +NcoWnwpeLcYnpD2hX6YktQKr9s01o30/ICA7hiJ2Hzqp4U4XD6g2Cn+E8RfU3ZJm +QzXVJchrNyN2cD31vN3uKB/Qb9/iQ4DGH/rwmYqKNmfba6dRDa0djPKh7FcbgAO1 +8I6i32UB1U31wg3YSZtCmokCgsEsfn/7XI781yj0Dy64/iXwPavO02fG7pLdwJ5n +mLseghGxyNyB92GWCREsP8BFAttEP9Hs64Xp+PpwYxX+ZgrVvfv5bM3Mi9SU8I3B +mASiMJz3Hb+ouuEH51WROKVZSJecLhfgkGekVvyFdqgP3rldidaBEegkJhOgxeq9 +cck8DUwyWheVxh6kFM0J8evOzJQQmwdQ5HkVTp5XJgQMvDV62t3s7Obd1fLW+Q2h +Bu6ODy1qh7eZKfu3DEbHlzS/HP1zqkQJKphQqIw0AAO11+I/Vd11NOuNqB6TrjW6 +UmXWFM2v3WqJpDmYIuz6WlVUB/2cXcCO0/jPoPaTcuKuZ9VoG1KFBie+OrWQfzeD +hX5CdIoG7E6LRh9PsqT4/9TM8LI5aMGHajIGCHsi3OhsR+9NwgS0rn8qZjmTqrvq +L646JY4C4Z2Bnuk/xpEmqjkAok0xQZNWAX9OGJ7IIER0ThD1Lvbvjm5j/jg4cdjJ +SvZc9ifXwR8oJsf3YKFkH7ZlfWKg3eU/olrIovu+EaFEIVEjWjDqfzP5OENNB2Z/ +12lYrwllSdDxhGr/5VedO5KnKy9NrfJJdykCo2UbGQADZcKLVlNFVS0MhO4s5G5n +/1p+md2iHbXZW10Gt5oLR4pFYM9i1Lw5BP42RVXfHSpvR4FTJ00s4/nXhTd5xjdc +uKT/7tNQ+wlgINMRke/WhAcm1HRHDyxi0lEpyazoeur0ZMJ4GHV4IwU/nzqTN4uS +TLt5EdhFu/OICCHmeb/7zAJDvJqSuYqNJGT9U1zFSA1aGl6nRlhtkq/0OD9Z2Nw0 +pAI2du0/F7XA9ERfxYMTZFU62i6H/bm4+nJ9UwQcvdTLx7l7aRMY3q6l7FZqIpqr +zhK+AzTYfyH31XNDG9BrrUni4vH+QKQ5kgCGzD5iirwLTYNI97hwT3Q0kJAJbau0 +az2cGvjEELwud6GEcfc/L6BCjS8frzA9eFYK3BirUuKppkrq0BlmA1DFsUMzx2Xt ++AAv3nu7cvQAr6gxpFS5DeYIGZFyrNO//L9QCTKLXek+rm11O40GVv+ztAjuVoc7 +DcXQ3nttGDrAiGjdNvQyEImLcwnZ/CD/6TUiS7gEvBeotd4zaWaI4n6btP3V+qXk +FujfoDRzk9RtTqI17S9ENJFnCPxOmO9o2tchqttnk7fGHP3ZXalGtX+tyl+05C51 +/UVrIr/3FWCvSoditLRMUqMEei0qFERA/SxCv28JE9eCXmfD7XBcWgmXTteubG3x +yWrpttxkXbNqBf1HqVz30sV1XeKPHz4VGzInuhJes/ziSn+LsYTDCcTnAadoAM1p +LEHhJdGMHTZWCl166Mz8xRWdHnM8HJZriAQCU0rqfSUPrMnuhUKWnOiyBYtkcPd+ +65XCZWjN3f0CCNn7A5XZ3RAA6xrZRTsez+MtOAQM6vng3oabChPJwNIxGBum3H5/ +iw5WWsr+PjQ4xG+qOyZEAw2dSvtOXB/ez+5uDqBrKxGSCzQ6qeIZ1posKEV0GEXV +uUEzPyBjh3Z+gXH7hTLESI/vQK1J8YKdAUXOuwFwBLALYxQKWOMeGH/nnIR98Tbb +KEjBYrQitB42fXFd0DT7uUd8Df7zGv6hPbblu9eL5FrOxf74gqVcTXtn1UCLgm5A +cG57gf4H9lA3JsoiI8bXaW2PylZtYGtZ/z3ldShdQd+YI6ViLkEYrWOiJfbE5tyy +1Mp6wzPcVi0zFW1qVqpCczdMLyHzkd8skysvpYGoKPJSjWEieYMhzM8HsuXSVMZ6 +jM4vn3wwn4A3x5+Gl1wKVkbJEVwkXP60eeWgknKNu4swwMZ87gdL+vn9uEJnYqjX +S7rk/b8f9bg+Du99ovynCSj9ywYjVNqKTtWe6ak+/B8CWLVKXgEvJla+ynFVfie9 +E755maHIyq4tjAwBqsBD0ZuSCpKD1tLrxJZzQajHe6Qtd7rHKo81mqdmpX3+cgm5 +bSJcki2ZD7prwwQMBMRaqrzG7W3RgEvnUZAyOJr6NBIDAJqKNUtj4USAjeO7ISX7 +ZCWrEKUIPnjQcCQzFk7H2w9dNEtTn9Oc/5zI9vk/YAobnweo8SqrJAJLywpKq2by +NkMDfBu7Z1qjXPY9q/hd+y9Q4VulqAHQvQB57vghvJpdhR6wjS1oCKRcrRYql4Iz +ugHu3rVCdvlj33KX6Mqsa/1hwqNd6lPJdFVDeuiQvRgdAyF/SpfBofehfpvkSf+I +gmSbIeQWI1QDlRL2LllJHmk6ktZJ3OTUIZ8ttPX/wLF8H8PzeH+YI+y6/qXq73JK +9hIL0mz/KsENA7/gajBUxqK8TWCdKqQC23lxkdLCEiZ0+IEVhP8XZdmb/wnRR1Iv +wG+bjPHBnzCp3rtbNGvrAG3vmnBmFaZVFUDQSmObj944rHt5vuLCjuSzcxWH7yzU +xLtnBQ263Qfd43/KHVODycW9PeaPc77rwq4ksl/VU/m6xpUr7HtVWUFLfPHoAwBK +GLTOQcgkvwrn+8Kboih1OMmZ4cPtcvL5XdVB3GwkkFpTWxk7VOnU70IuC+un6iCa +1LpmRz06pEYvTiXITjM0GE2QLbNvQIkKsGnSUlwfNAKLtLLKP4jJCZMUgyXbsPZo +buJb3CBXwG9pPdZYqi997qptUBiXEKnynVypqaJpaj8t59VIqOSbHyTsrIn5Eptn +xAg+N/QbwZKc6Gl/yD3ChyOiHflf/Y/bnnMnKfcDs4kAzeO2fEB8JM7Diqb8vGwF +tDSoxcyyP9QNAb/BaeXuWBIhLsUi2paDsQOSjFeVKxI+8uI0al20vHn2bV+aUAsj +Th++5LGHPJZEp4aFziO/T9LwAlx9E2DGqdw8wOS8MN5Ogs/gcqx5gJUw6zxb+f0/ +dkFAQokQebH/G6K83TAlXovnml5BaZ23ELLt2L7oDt7iestXxLp1acKCAn3iub26 +FC8V6Ht1UFDUnP1WaaGF4CQy2X+U1MH6ExlZ7KGc8jC2v02EXfnTFTYcqhIldBJl +W7RKFrdUNfoplK3orJyZRE2wA5PoYJXYeiozyRi0LUb8fH6wEBNRnRQDjZLa6MX4 +vwGBraabcgo3kpjIBJ387Csro2V35WU7A/svcmeZ4sskwQDGbQZZHznUPT3OSIaa +2AA6HgN9LZK4druc+0vyTnE3SMe3cIDU+2aXXf+EaHpqa4SL+xjvSHXLujPZi4uN ++Uel6chvORxE222SUhY6t1naNPw0b27sF0ASUl4MMIwiyoizWek2rV4qqSEtCTvg +AFVea45jL6Uv9cSrD8BzEnO36sZQdRM3ujrhXb9LpfO9ocMxB2Yozvpy3ecDvt7Q +I0zgf37HPylfQ2GSFIXaudDb9vfMPy/fNO9lwp8tAnHn7HtAXO2vBKkgf7oti/vr +AUPp3VtQrk9oclM0R6bXDf1EefDvTqX3a+NL9gdtGfy+q2JVW5Et4kix4vP7mqSF +Uvfj6ReIxeRqOmyvTIarLF9FdS61Ny6/B5QoLjdqsLry/gtKdH6LyOP8GCdHjJ+1 +xszm/guHx8nSAwoTGiUyaZCS8/0KRlCapwAAAAAAAAAACBAaHyovzsQKBlHQxoBp +AAAEwC5vpHgHg/y02hzkOQ6+ZH9+u1sjjRDrg4EfmhSAeogdCqBaDwEUEVuK3Mip +jfkKf6CYBDhbT3CrVWwvFlAiaqMTZmykhcGYgbNeG9YJWpyQFca4oja6Ztku1MUH +fjyR4/J1j7wdY0w6YuBYiIw53UVIBSeh/SijdpogwArN3+AWeDjO5vMjW7YEpUU3 +eWFNakm+DJapkduw9KfAmEl+ZegHI5lX/PtXTthLYhs+GkARf0mUdWGwfNa/lyiT +RiKeIbEq9FIjcUEMmAwaXWdIvNrBJ2dsg0IkQDE9yLkw6AFhI3oeZ0pFyjhk/XDD +onW2WRp9YCS8kyw9OSSByeSKd9CeBachnMOreBuUmvYBxwUiWsK2F4eaMgYdpihi +y6vLt7NgjDKOdjqsPhEAYeFCy6Ev5UJ/mOY009qbqmGJycpsgLyLpkqM2+dYWFgT +cYynw6Ad5mAQEzcFs2kL2BITHaS/AaoD3XCL1cYdbKRqKgOPzmyuhzZybHkoHBC2 +jcQY4zo1lcSrZrgqOsNbjaMnAgDJ+jF+1AvLoQESdViMQNS3NPJm5MOwhZIAV8Io +AQg+fLeKveM56sSAjaFqVqsVoWCcqYMdqHom0lYs30uabyylumQcAGugGBWKIoCU +/EOjx1ZzPxAtd+VP/LSCUUBAnMFus/vL2fzEH7BFT2RasKUZXJsQxQJz85cFuqZd +O1vJz5VIaxWDsOVJawIELaUoDYaAVuJg0ldJB0kLvOJGwhw628ggScNgVCu2uImp +jZVFMgJkY4d6JoWLJMGneJkmyNti4io2BBmaTyhLpyiAGeuXDwIKOSXM44lfLedo +c8s+AflfwFgdSbxYRWuJC4OF4PWQdroutSARljVULKEc/QoWPYSYV+hPA/xyXko3 +LKo7lKknnZl5s6J2LvKbb2p6CcpvYKazyuxQXlgZBmVmkUsXfqVD0OCFXWExIPQs +PHQ10FSK0zKPeZZh8RUHAQJD1QEFSJXFACxvfZw0s4FMyQItn0HIF2ogFcGmj0dA +HChPVre9RiIhyoJy6BqAEDnPSREBVGaDtUNefGiCLHemYiN2uUvGxqzIXYaGkfBj +6+ah7KkwKYuUJtaHArk5JRQ2wwanXjaQoFQqHwxxPoJ4XLQtvbe5UoLBzWxRCoQR +HuVn4aaFcENPGBp5aiaY3yFQuFScfsaEi+h6FlCo9ggTbSK5D2tOgwlUxkZjk5S+ +LHe4hQoU6ZAf9HdLzsNr3ZULyCRigivH9oguRdoXnxO2CcdETPu3WgQfA3OH7QgX +JDaH8NGj9/lqF0JnO1AA1KkT15uZtpOYSCzM5GJpqgCW6QRDFlCLSixrXzN5fflx +7ow2YVEnNIYAE2gvVaORuBinQ5QhwUFzWHhd3YpzbSAfQbcKNqkairCx64p6AAJM +Rbosd/tK8omiTAS8JIkRd/kIt+rDKgtYkWQFRURyjrVkunmwI6SrQMIw1SsBIyFl +IJWp0Cd4kQsyCUMB9RZgsIt91YAvwJZ4SNGuAAmNoyO44ZI5/nHA+pEmp6d93Xut +v9cPN4p4ApMdX7QaxLCc3qY+C9zMMsWk9fC7kfNU/OGNfZiexLM2Tm+57JxZ0I/3 +c5i+CTkeCqVTVevvaWRbUTF6EHzCzLgGGGsMAAAALAWCUdDGgAKbDCKhBr8mKyQX +cAKsiuXcbaR8BW0iq5kG1H0HlSt1w1gCGQHKAAAAAE4REJBeK4w6BgxSX5H9c9AQ +u2CHzklqkYzjcuYVYiTqZfmGpwudE3u171PNzppoBZv3NUVHJdEoEeHhg8bGwpi+ +zbAxv6foQqp1+YvPeZqcKjAPwODlB5pXM0fF33GZm6dNaGKq07MnAaKeGMp5N5HZ +LDyfc4TdCk3oCfJrwZ1aEAEsDmKwtrujdqJWdgzlXsvMJMRE9im5GuadIIbkNIMF +56GnQvzmZsLZFZ5SYEfyn1RqCTC8LvLmLUv3PiPomURReIXT77FZy13FHczLjZMW +MsnPwHsnFcqRsmgx27vKBdKSUXKPI7w0wte9zUNEHvCN95xc32CLI1pIjFoYq/5V +XLrdjoo2CDR8POamYnrmQGhVrxOI/RDJS5PTB9Zb+QHgqKY9DEOG/2VT64kn0eiw +40E2PZdTF8dS6KA+arWKdiiBwF3iHzaGAUX0EoJ6BFxgyXF8G15ABZL7G1is96Oh +pDpWzlfVgZGxAJq2jFY870QrD39/EocfSpUP0kRbIL2a6xt3omwPhPPfDUVNnMug +9n0km10ITZ6+gjN5PzTFqnI1w6+uCcjzwH//9y6W1fUgY+Cl8rxCskvZkDEWMkIh +RVSsLK4Gzb6yhvLH0cG02ikJuv1ERYagtAVi9pOPliFlX3QNvIWopfKhoDsHjB0O +M3TlqREdTH2J9YlviJ96uyfpp4h9qdBj+9Z7uCcxz2nE5YNsh2cKGMawCPGjWJMn +csOVkPmc7k7oe0fSwh4dECAaiLysJaP7WtfoaT+68afqcmnm0oMf6+w2Kvh4gpUn +jMaj7QRT5cmg5Oem0UMEtAQnNCcAH1ButBAbsGu4V38R3C/TS82mowQ/VkxQG6tp +nNdFOgFOWYjxizoAXHdZxnGTKfzjJU3sZSumZWBVKKfa8PIkMbh1re85MrXNKhrj +S+Du3ZElz0fygmx3kcfEOGPuI9qq3T/x31zVfB2vXnXDgW4GQqyGsIj+KAw1/npl +MYn9KkSYVapu2MZ3s6m2X6pdo8NFAFyO4h2r+UdIHr03KZ5mXj62Bk+391vPIGow +Wnv2ENNeK4tBpZUY+ZP56OPY97ZvZhuoFqec9JkCDr3bZeQsf3qrFG7hjiOTNUoU +jUjYTiChSRevydJuE4W9x24ddB+oBKr4zBTjStZzrJ9GKpdxxCT3SkMrx95mOsuI +sNDSUgCZXGuVXHLCXmhi1hh+AoJcGc117Oh0J1VIcvYBQzvLSMC0t5ZAKB2XcDqV +I8ocUdsXA/v7Z4EAESDi21Ln7Cw2Rh/+0GkrADwv7srrYuYQK3/1OqUvL1MmKvCZ +5QUoqq+KMzCXF7Zoum4IGf701Hba+8bOhN/6Ohf2BkDdrYe1bieG8yvsGSzXdy9w +SHt0dfAV6KVKgk+mbciFBKbvplLstMmoagC+IKUIF6OA2uQhsZrnVbq/mTc5G5RD +/8YFe5kj/iE8bhdQnRTdiaCsYW0rrfgTle4g5F3xVKXLTaC52NK4y8sZAEXHm+EP +Xo+odH24XDh6nCNbkXRjq6R9pm7/LmRe8NV6nnV0yMGCzEqvEIe3UyQnauGemwwr +YAT4CzYgBVjnqESomwg35+439aZomHLiAnSA94AeQqON0/nvrCs8i4WChmDKk/5A +ZeaSsJCKliKYq88tv7uklCvMK/M0vYw8f3g5l+iWTzjM3mhs+VZ3sA/WlXXc/b6w +c6aQRH2gyuaXvoAqRzZBGmlYKd8XvGR7NeY2gp9mwVZ4vT/WbQUzD1PowtI5uPN6 +UNPL4HpzYRYnIR0pLNozIY9S0/PT20W4lTv63shs0LxnjOZQBHZuriaGL9Y3tRjK +ja58uQYjfHdkcMdu24CnKglZRykzYjmriU7ixSOdJ0ezTeoA/ECBG/Z+rXBQqVRd ++qXfqUSildqmDZ26dD9IQf+Iydku3o5tBwl8WjmDzILR2XOrDhj7B9Pz0DFgynxf +AM8BZpH8G+z7jLswm7LCCAliHLb5pQmPwqlbl+CE2Qlc2grfp0T3H5pwdcxymaMC +sXl3DF9ZoNwPCbozckQ9Qf6RpTC8Nm20r0wQlTF4LQuN409ZHGohdOUzCDW+6XYv +W/ykzsZFYBuOPmjvLp55hEk5LmT9sQ2jxJIX3/znkGDD8hFWNpgxsaDqBSn8i+M1 +0locMkaKys56dd5gr/unObrFMsWEE6nB8Rep7V9jao9Q+NpT8e2QbqQlMwuErxUd +kOyIFi0hp1zFNybEe/xwq3WriYBsS+MCEb9PesaHKwa3JcpaskaCSdnjEUjIhSI2 +HAkIfK6uLdZZ5tU3W3Xi03qdqB9i+xYKkCsA0ED0ZAVh/z4rw0KSaws2TogUyFkw +fC5+ie/aJAQd2Vnh6L1ZM5D3rxb7HizIGrHvpYgvgEiSLmB5Md2ALxLA9PX8mWz6 +O6/dJt2aUUzbRVCO50CnhPt9yktdx4m1JnKozvW/rKv4SwJ4T8omIpnr1g870Wn9 +JtCfaRjGqhVLUqVQqC13AHLTjRBJqeJCtp3gaJSgas5MchsLOww5kOZoeClTUSCr +WRSlk85RFKrq1hmkVBOvnsytaytS5U/lcuamG2Cw1KFsipmwJH/FLCjxUiHkbsdh +1ZCAxQzjhvMkUCnPI7rJBhWZPBlM1KyqytOvMPTNUcPA9AFj1Ks0UqABgLSWSoyf +iFm2FAZ/fDC4qDZmRkfrRnOvLOt2eVlKri6hKu2ag+MHUJbZilj65ryRC9RxmIs3 +jfWGr6UxH9Sim4kTxEWhfWccvNx4G/0yPyuDcKmjAXF1uqMZEte6a42S86LN/5Ma +erGMqgk4iT6hDa3LPA2WOsv+lNaWESVOYT4jHZNBUFhgRJzDre+vgniqyJpZBzWc +YMY8zcQz7PiCOXPm+XNLIr2tKeZAGuC4uiHqPSG4RGXtK05GG0af7VofZbSsySp/ +hJ8RAAt+sAi1XlSpE4G7th6SSeMvrQwndkPO7N/8JCTlVCrm7MvZEw7uPs8wN+Qz +TyXcuMY7dtSBcXPrSJZFdRloCV2ueVgfGfX5qfL3xz5yjlWuDookqGrgT8gvN7dU +bn5hLltGy0EZZRkg+6jhqS+LLSoNmWHnU2ndzyjhntIknmAstlLsgUEljBDELOdj +ioelnauM57gIajtMk45hXyxtvmX9ElEdZG4nA7zLStEHUGX73QGwV3IY0DPunz23 +ztrctmku8az6krOmcFjTLi4OzETxe5ypqsUsc8LZ95IqKKCZYUdGCQfBm0PVPlap +Ci3rLh2uM5nxzwvZLYX2SeAoMxS8YckUEtkLsinAuVAI4ESYCgQqt46x1HsE4r14 +Y10FDfPiJVLvlKKQzPiZD3LJ1WNWMbinubcoorBQWQF+ir1fPmr1kdbvtjVbXp2k +M+HY894swnUwOfHF8fM1vCCbSbWDk9J3psvhrcF5dDiMn2Jx0UiU+JbQDhm2O9hy +yBVUh49yfU5m3TLuOfSxDo3MaYUgHHZ4XgQpSDbdDegs/JINxTlqU71jP51MLf1s +xu46EiurJIqY5nOuIBgq6WXISjaImkQxXuRgbM0Qe9GRnOMCv6ldEXz56XM9Sscm +qsaaZVeQNaSrbqRwJONpIzYmXdMkVBklq7Hn0SyAHi+BIzsuBq9O19y85y9xzqVw +DGUz8IZ5kRNGuO5mgq/IEDEMeEntLKmKDkv0lDCk3c6QYvXtGsvn0A8l2sFDynnZ +BJCA8X6PJtJhKSlHUx44oFEwLd7IfBlNHXEAeMcAYLcBv4n632kvOc6DkP9gKI80 +k/nHh9OJJ/Pf8ymleXC52Ah91PkcXtDqISdxZtRGWsE9Ic1JcjiI1Dwc4gLn3DiT +Gk93oLwbMHOBVwXxnusqbprY9WzajBr+Rt4cbH9G0Gev4AUUA61Mzw00lhxKz1yb +GSdmzp6ohAljkPba8X7H4Q3JNsQ9Npb7DUsfjHxQj4qd2MaQmhFAeRfEUSOQSj7r +ADgYkkjkmCZr2kzUYowpWhe20jzfb5CGW/eNrjiZdzWw3ViENke1BKrrrAwmTrU4 +gFjC3K7SBBarOBDzFZ80BmcdXrzFoLVr5Lbvjs9EMDxdzLdwEtslxdN92J9q1Bsn +W1a+uJY72mL84R5dL4ywVF+TcJdhvs5fZzIr1+P08JAdBUWEXdm7IoMHEAONPzsc +m0EZ9RW8MGgaCa+gwS4XjLEh7kSN/LQC0DXDUS+09gbJlVVbte3TaHSpZTt7Kl6t +9KtXHlPXU6sl3ursFByqzcqnLSdd46NjJVqQB+TjiKglpWIn9x+jMgb+bkKK/dwy +eTaWZ7FCkSIDmfnKe2+tiDs+0ZqbbgaHYfLpWe+y8u7GfakRAehUKedjRFvjCqKI +s2tlR4r8Pc0coNAAfasPRc2iCIOl/E6Q8Psp0MDIR7or8z/OgjUEafVEBVZML9/z +u4djz5Ql0U7RxmOoFnZ+TC1rnnozQBcBNVgFntlM3dC/5wjTzH0K/cDIofaYbadc +WmFbdK/B+gwukZuv6/ADssjK6jFXXnCtvOghLjtOVKDo/QsSLzhDpNsAAAAAAAAA +AAAAAAAAAAAABQwRGCAn +-----END PGP PUBLIC KEY BLOCK-----`; + +const slhdsaPrivateKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +9a62xlfyV+zkm+ZJy/tiWJ9x31uTupvXeZq8QixWnaqZT1xbvy3Ip4nmDZ3GSUDZ +1OePc/R8CQGt2kddKb+HAPoEsL3U+1PNoKGuYmTPFDzeVGroNfHVBawKr56Frl7x +ptghYMHGZ5L87bpaQVnRMcD6RU3LXlAk8orkNTo3+2aY+qGnDvWHqonJ49hl7MIS +xYQ85EzMUIpXeZQrcHNcIYTQHMdOTSXXEZgWEMx4GwGXBO+FYKffM2G10t2/TmCK +dsjALcHAa7+vP1Pixy2CQ6TFsl2kfcveMWxF6j0mfkhu6Wt30w3n+qcyKu6jrKPL +8JueQsimrhIyn3a2MMbLWkmPYh4w/MAcOdycumLN4r6TfZNPrR8wMzKyjqtYLopH +deOD3BdYNDJlYEbP2FOmc/NYZL12NNyeJBVVUIcuJEBj4gqLNUIzXOSN876IdF/g +EbKN8BMLT0Fi0h7md/DzduDRP/3uUw3oEPAhIbsADL6etx8ixPmcpUJMNM6wpyBy +CWfkezmjhZeM8Iuw+WVmQHBeW88cfdJtWMQ6cn5F3/7+X/+XAwI7ECCQp2xax+ZI +fgB3BLnb3viEKzuZPq7U65YnzvKUu5shseXxU5TaPxaU/Y7gpihe0zux4xGAGekp +yc51zkqzHOw+2b0svCTCq6Wla/2E4bL9oNuhisKBfzIES0QwdQUOXQU/AzkzvGEO +iYXnsfvucws7sUp+xe7w/mmL/Qpuw9g2d8vYeIKS/NZN1qvTTIg/6Mih63OGARCu +vn8RqQX4A57NuZN4hSvz6cRFAxwPqwecW3cJ6kMznLWQRcggnXracBGNWapWj5xj +cl0gHmYHN33GFv602vglGrVJq/ovwkTrmdpjG9OtChWg7FPWRWL4W+gckazRRZyF +Xc4SxFzka8xdlHXpx839DAp5czgZ9J1aQ0PlyCMHh021KH1GcjmmmR98fVakO9Un +4eswJDUfVYm/dm5L3Ahu2bQnzsh/vEyqeaUBrTNT/yKxbGmSVzmZhcEn7WiZmQwj +eOZkmA6fhBbaEY41o8Z3Px9+A+Hvm3hO9iQHobYzz3n1A7lG6HfmCEtONhMbl9Et +AQzYPbg7z8Wn/8Wh6e9pl7+zR2hSNYww/F0JXilAsb9S4qTt8fe0Uk5wHYMyIOJb +8N8qhXnletvT6P0QUrftmvBREQdpZj9Va2jGyrbDKjJmS5CKUlYDvEAg5pjmm7Of +I1loqcWuSoZnOVraKq5zRCV07B6i3yB6VeionV5ifODsYgSL05klwORYsHlfhpIu +n+On4pkjIe/Z4WZ/qry8tAeEh6unqeH19/+0qzk7WuHo3mUJZqa/jj5nlWTxbn5K +zmCI6UVzadC08yj+lwTDPeCT78MdKbEGy0MuK+LBUrqu0GPVF2QbpekbwHvHnk3K +NEg7QJOmt6Bxi5OeERRqqSZ1JN+p6DdiMM44UvR6NgnLp/CLOS4BvzsI3uljBO76 +R3NkP+d1VIJeFcijxSKfcYoAAIssF22EoHPHcd7563KLSRhUnRVlnGTBpkTOHVGt +x8x9GEGURveua23fdmCK8/N0+35+U5mPpXiv7KH6lBWMS3B9e+tllZIKfErClfKv +KyWeQDkcWzezk3e7W3NbemFoqPWWRklN3icgTeDO7HfT3u/k5zaPDrSePsN2NynT +aygPHUgYEy55MW32nR09kndIzsbTSnHNoD2MUCy476Avrl/TcMVsmh6ddiZE7RWq +4GRfeSpDgW0lNi8Mf5m6NUrJi2PNI5LvKbroQnBCEPWSPR8Qz+HSQhC5pdknCXTp +70cC4SPoeY3syL97wOpQCnpU3zH73Y+ZFzpfPVtBP1mibXY2Zbpuw1l3LexHp9If +/ZYnp00M6NUP5amIvSiZj6P9K/i7nF+x6ioifQ/w3VxUNaYiy1JWr1Iykgxcexh6 +qcrdsPA3gSi1kPN58ADV5WKoLQtNHRlN9mX4OkDd2mudjQtl9He00aWVzs384HWs +wNexkaz71cvPo23/5ZXVhW9jmJpzcSKJp5jBb+6C42JuXVn0h0UMbxtUMHnmzYNc +qLJAFOio1Fqsvj+uFsQRf5ohGQSjeHPM+sEBJmBn44BpxdywvD409IfpCsBBn8M7 +hcGpJhxBtmLAQN/+KksCGw9HkyBSSWBOSByLX2APsj+H9056CnntjOqH0tlyn8gb +5bOOqjxmTzwgoSpqs3rQzUvk8eTA6wbwUMXO6PiYKXLGXIg7TQf4GZ33VCKHS1Iv +wB0EAmiaWbIrnIowOzGfmOOB8FxprQpxbYdEGspTVaTRAuPjZMJlGOjLYhxPygAM +ISEARJxz2c0OjXnA9p4C7l8GU6h7Ajl33plzC2GJUSL0EftLDmSHN+1mUSgamehy +GZfKhWzcYfQ+QOoaePYWCU5gFsI1OJ3QV62/MY6op4ahnYt1Ocwveq0GwzacQv72 +xzkLxolnLtADjLB+i7dP/MvT6da7+uuKWyQ9TX/ZE0cisHGhcG/JoDFRdamGHjFG +177X3g1aOq0FHT/sXDWVpz8RtOczvlaHjWEUEC+ItRos7Rbptg1J092goJ8JNSMy +pysQdWWX9eIcjnASlfYtrcuO0j4iU0ganfk0N/Jqd/DLhCbtsvWnEC9ABnFqvinH +SflUvc1W0KYYD8nJMrKBEdVaFCVvAxBy+N/Q4IdLpVDCm+w506CDwYu0U8swY6AG +vmBbq+ljmzvMKuQk6FvADq3esOVH7KBXtDeCqpz8v6n1ycKHs3IAnCwLULw4xErF +hN3NC/+gyWQwy1uuFuRvuMnu9VqIeOMPS97U05rah9OjaF9yvHXs2i27yyNgZRwJ +6s4yfydJY4NsiQx9aBn6988htxUScrtsDN9WGgrdkc60RsMTok8JtjankCcIaBds +o3HmG+KwKispwQYbos4hEYn+oZxnfvj9gIDEcMDuZ6yr21sx+jolw4SFPsVFLhTW +jcNU6Jhtg67vqVOFuNlC9PioCSKmpm83l2Xmiqnqgu0afC4KJ1inTStP17J7S0zy +hsp6f+G4pwlzywHMuAlFZnXSmHDk7DrRF/Fct85EbsMcqyQl8ZZeoDo+LyZKnoW7 +L35XEpFN2DKcJeK1yfvgRXXsd9OERBpMsj5QDkj2NUzc0X837PyoqnVNQTo+uDOR +WBQdm5H2zIN3PJ4QafV+0Jiz6zBShoCyIyO2z98iREaYeEP0H2X+/JdBxDtDtIGe +ZQaY+C7eon6yvn0w7y/mlilTRRHf6z3QzUq5/PNFFCFULfRx0SxrKSNwcDVrUxN7 +dVWAGLCLQbHYhMR7plGdSEuyjsoiaSdAT3y+gs23B6hy2A96kEaVFEPtMe1uedHv +jpbchR+0twWVCTSbMDcngsUeBHRsGQs2o8d87aoJHmjI8B7bJlJejCPOjusX02au +uHvbjadk02Vsa3vVLSJtZNlnlMc9MxlFzLjYxK9jxdjuPqrQ1Hy4DgaJ/hLBiVXN +I3NsaGRzYSAoU2xoZHNhKSA8c2xoZHNhQHRlc3R2ZWN0b3I+wt47BhNtDAAAACwF +gmeSYDwCGQEioQZOqXq1uuhUiJ00G4oQ4t5m1gtB4I0FABRjornsIzkUOAAAAAA+ +3xBpx32kgeCXWvtKWuiEi7YDiLoNy8zW12vBlEDpfQbfzb45dkqSmHhilO6TSNKS +LnGRej5t8VHc5I0b8oM7Btu8tZRi9t4dhXr3ed7cdpBFgXK42oMgB/dyENKBFErL +2bKIxj/5gYNmdQujX/1EWdYQOnuxce0sGWjNz51/mQ8neXPSJFBV9nHXE48TSu/1 +2e31AT2Kb9cJqvIkZ/3r9MdpxgNNeIQI1kWGTsQXlb6ufeU3f7CWQYvnlRPC6wTN +4SMzYl5/4kYvP5dUN9PmlAAWsdXehLp0UHVM0J/asfefPTWCCoviSgjwpU/oD024 +NFi2XTdmaj2BkFxhGmQyib+M/Na5r6pfUBQAqR0ySA7O8vAyTqSxAzMxYMBMCgXM +4J8Zpmwa1sYokdvk4C1fMxvl6xVDmNGHE1Pgr2dkTJobB56gqIe7tFYvBtdvXf+N +4a5klKAqmk2U+mEC9+c8fio0TeCB28eOBsQvTJaedUD4TUqB+Y8wGVgkSj3F+vCj +MFGAz5L101CBWRFt1exVGz6VvV2JhFuEtQW6QR7yF7WP0bX6QvP/xrvykcuBwL1Y +FuPiFxY92/e+ng1Oy88t+0VuMBPrB4ohbwYdbsSCh0e2c76rit/UbBEDDZ4K0dXX +yg5MW7sXS9/q35rGyXyzbPG72OEgI1Rfn09c4PEQz+Ys5s4lUYABJNVCzyZHx54s +6Nfqws1pUmIzA07qqizExfUia0ScITN8VN+cAM+IRVuKV3VxIik5N4DV87qHbKw1 +MrX/CkNeGUqOWj8mMlPrVyFKzxp6CsAgbhH5Dhswk6KWUpn4KSYTLwtG7EoXaXq2 +zZTwtkYtEj97rjfCFgEqDDwQvvfBuCn0surazJ51FPRAo0IgUnDCJZPH37D9uXla +hbDOYi9W1ojrNTpSi7cT5pUBOiPxg8Rgn3KcIleljM0jtjJEMSJ9G0TAtzu6bZ6N +X9swD0xbvV7uhlY8T77/hMS1/KQJXkLIpwkC9b0zjPG+j/V2itb07Xt7i1KVR7UP +RB5Ah49VHSqogM9UH94fGDvB52V3j81rrCTm5ChE8wyQE1XMFTjFNH+zENIAqinz +zVgF/1l+pPsoA9QtVV4u1/2QhruDzQrdOLxojQq7BU7lf51kI2UJP1SPvk4DEjUs +bMORBpKfekPk2RWKSf0y+UoIIACLiBfMP3J8Rs/2nAn3BCBEpx2yRlVo4By2wWRF +91OZbTugEK+MKznp518W/23c2wZpTxl++PuBQHS2Vz6JdIUuUknIlO1nsBdQvg0N +2cLOdw6eKyS9ijVnB4rEpdbhszTg0aYcbmY468U5jhbr8i8urbK8F0EA/zIYnBzW +3pV1Jl4tgUg7+JCI8QvoDk9l0h6MCqzf3gtJlCRKtz3xnufImbg4+v/R0TRK3bTJ +loGhM/NqcAFlsf9zM2S+s6kGIohTndJFz34PVlJQOouPBreYiu3ddzcsPWjlcSqX +I4hLBfw0nOflJ71+ZXKAe4Cz+ltfEJZOZapo1nIlfqNbX+iT4obC1vjVEyP39MDw +/KUWZsm1eQ15AC2qYzowCtBsNDQ8J8nzW3bQYnEVnO7Gsra8gP7ofuR6XxGN62H6 +9Ufagq3m5D7ySmfy68+iGRWT7qThaEGiBTVtJHQf657NXG3TYBy1pn7aH0kLUhx4 +S2dxuYyFDAl6mXY4FNjK+iji7xPGtCaUdzGMNiOT53ViYhjTetHZMf9JG9lppgnS +ncNVAf/Sd8BgOO7gwAZVFHo3/EIDwvXm7xex4cJcfEQWiTsAz/6rus0Dbo5N9ccM +TwdBl4f9SEJx1Ua9hPSQbjZ/Ezzhe2QBAR9uHmFfHPP4dRiHxQ4sUYT/TM5lIHvI +SlLy7debuFR2aQ//ZT/AVJv+sdFQUm+IhpUPmxkndCiHm3jBq1ll9gZb0dKxmjxB +xWmQmIVIJEYgvzMxeXxE9qFDNlyVGQwkrjSVgY0Xw5nWuDAZxesJ+KuA3aWMYUWj +tPw/Rp8ki3kR1cHQtBHfdPgwULPuBDX2zU6Dg21y75XYygFfYLsWPryewcWxYsN2 +RQKciqyxX7AfNwh+ct4HtVPi5pcmmhNlbvjYoXUfmz5wv/8NJzpbj83+veZ+/EVC +wNYrq0A5sh+69gpQgd2FGZcSYfDcGFV41do0X2S0sw67of7vHkIpyWeNw5HR1jsD +fj6b4lA12Dq0mI4IC9a/49Gn+KlaKm2vaUlBaAFi6FM9/SWQHTnzFqLDjFQ+wH4D +d6WP7YGUm2MrM9wY5KM7LZkW/iExIVY1d1Cusowft3xiki32H0Gx+hptsuWRWrzW +Jtbx1m70ZTYaIQeC2hh+ZPnzUR3QL9fWfZQ7NOzY7pgqgZaNnfU5RnMfG/koJtc0 +rtzKOduaNfh12AIJ1vsK8hlqRoVWgwQHCvHHsvXTf9+dpWNQYNKhE0/CfGXCrKv7 +46S2D8/V/VzzVHdJPnjAsZkpc+u3drTBk9H2MWQm/mU8iDoXlANtiTkVc+JalP+U +kubXJ3N7XdMq+YR2+8R/vaJs6A4kzHbI/Wv1GjhwHwpLVvhvbjP9APoA14npPPZc +DBD2D7BLg+1cJmV2aoL4DtjvPaVZUyEJLZM8QpalhZkp+Hf+Qya/zsLBDTrT7KjS +5eKZ4RACLnHBDF8cilQL0BobPMjTODuYDEdQpuNiZE+nVW3vzbaWwuMtMTxL69Yt +be3R2HDeMpkHsoTX0wYz5kuspLfC7FDvZfb1QyQ5hSzwIYGWbbu/3TqeBEjUNW9j +Mu6mYVFGqF+A/D2nTfOi0xcnElyj7wRmWPQP1JbzWvOR7KnKQaJ0ewOSHZT9CCjM +l+EWWHQTUR5fL/E6tZ67tN/7CzLiNzafp2M4O8TIfMflH38hbF4ykN7DQl/hHazJ +RZ94YVVxrSObtUslY47BOnTaYW78kEbs9TN0PAZfTIuhrlverlZuYj+Y70CDBHFL +3rB1IqH+XxatIdSkUgxVwEuFjUTmFPkUuNEddDT1VUFhb5GnHza44jA9BPDSd4wJ +4uGy7ShGA8mp9VvBxOU29d8md9Zd6WV7x03OigW/dl9tILOSIOtwJQHtYvpw4Bt9 +KcRMt9XwRU3nbzGhZHLmTkmWRos/j1J+YCESNy9bWACnUL+Zb6SmrxgGvmYaW6Js +vtuSrdWSwiUH/cscS1uomKN2aggvtvFlgnyn9iMJzeGeTH6WjDF8qi+B7pq2sr7x +orduPnSvtwiJaUQFRzvVFRhyblrhcbAVwuJsgNxxQj1xNgFstlATnoC8C4QYBTnP +dtyYoh+i0VgtuNObqFf5lvpcVYR71lLemepXMuhM5ezwZ/FyfkhDr5hO+GoINh9+ +mGT4zP6RfTOZaXTW6GL7/Uut1PJHaDQLX+HF1xoFHuJzYE3si9EfanwLcbsqwD7l +JTnoydLYZy0+ZVyRCdMG5yj/WfFDuFllCFAo7SHHk/NHyhRqZHpwyC0Dt4kCiBmW +gpPnmE4aQpdIx0v6JgYHh7uBsz26XptfDSw5llbAgFYcn4zDr0YhvbMoonHzcnRr +65N8dR9Twz3u+AIOzYtE8XcBL0SNuF5qSGTxqI62njDP3HMFnNctgs2Fs9dmWLVN +BfTFduBUNqOnbRCx7JI+jdqir4QiRr7Rwt3IgQZOtvQwB1R68/4vzWYJKAJRMN5M +J7xLpivkzaTWokSEGkk/olbTxX3XwgVzRamOgsRW4cXWQ6MbbewbVFs8sx9MguoO +LrXZG3/YCgfgbJtaFRqoPj94FZg7DkBXzibDaALFB8xOf4lU9wG2qQnnnVPL3HY7 +7AFaLGymtW2lVbpEEhLJjt87BnpMco65V6FRQ82I5onfhA6Cw+yjGtf3VNMHdToy +wiEC4nsPBJqUluKh0TPgBpXdO1wwV+rS8vlZUQxvgux7mjSO7mdPPknUAG8S8g/f +BCyWOxYYvkgcxue8w0hyWZ7qzjV8AKgVUSLHvs4asIqlZCBozVtNUSI7v7ciJ6mn +mCFpcy95f7kXmDaCKcvUmuDDx3FeiqGj10EvwuNxKvwUsUtgeeXsoxKAJVQLjMeP +g9aPKeDqJnKL3YkMf5pi/mpRVuutBEY5UrOoodHOzc274ClgXHAR5sJ3tT5G5h8B +QBih8SjR1U41Oh6f2XRx7lB0dRrSwgzrfibgruXQ2limddzimPw/b+iwagrmCGfP +oTXReD73Tqqiq7IiIobrg/6qsiL2eFOfIpgTRSzdt63JK+iWkjBK3VmU4yIFh7QT +mBzdrsFpJFED+tc6wUepmYUdmCMtQqsR0TLBG8wc8TGsaZisfqkEmkfFn9ITvw9W +bbExjkEspUocY7Sp83RP4xNqWCTlEsD0Jq+oVmNWfIqpcY6UcA+4ONDKLsAjOSIQ +P7jNXdTxJuWZuKbJNN0Gmz9qPIkQtVk43dvvTCOdjvqj7QNxMAmvc4QLdtiWekf2 +cl5YGu4ewwvsF95KTQgjo71UoQlg3b3Iyy1ULhWZsFpWTqbX7MXWrgpHi2rqUhrP +AJtpevM/KG2U4/8WgzltQuSbcW1sjxvknCkuCf+vbzpYIOHiie2dYZWFrA5HY/hx +LU3k1PRx81ogqLYBcZco+Exq8wouOooXvSJfURACZ4hg8Hd8jHxYbsHEOpmilARf +zqponQU6TSMbX91/PUMxP9aaMSnfppzq9py6g8IrtlJVOndp9lx4qNpwKCm1jQ5O +5SY1wK+fYkO8ToePnREpn/heYOdZkDY0X/8eJxFEYZch+1qGsVYt8Eq0qz0i7nId +pJ9ryGhLNXH+OMWckl+mXlqgrYcORsj9bINcGTEff5yf97DEFN3DfOMIhYiPUkyX +O/JpN5fK4egMHJ5ifxi+CDoQaHXnfJF586CxZ+hcSiPeDQoB/k6LKriMtyOwj8de +5SJsGPQCGnFUtRv0FUB1dCjrHBmPA42q44uXExCNY7M3luLo16DijOVj/TDO+1Th +PqLRiiNR4aJk3y3r/OItiyFY/mho3gTlrRmpsQt0jgH6ecnVJ8ZmOjpg9Bvf62IC +2lrxLw1tfeRkWVkBI/NZQsvPP2oU7Rb1+VDCYUF6+hqD3CRKzEpD43UrVv+WqQRc +/2wFoDhrXo9EgyZrxD5G+0PY0EvLBui3aI50/RA/b//uXkhNXN84AkKhQ5fATP09 +yiRmeozqC+ONZhUJ83m+jh0ntNuNNqRIfbef2Cv3CUFcM+9L7k291zODb2JgNjif +MugoSjkPL28mRw5w06fRCPjAQ23qg/jAp1XjMTN2HWUDr81odbYiMuz1vVGEqfCg +6SPoD5qsP2zmmaZdBlP1i3ENJgP3kBBWa8oDiREa1HvRcCm89Dr0umNNt0oJ4ioN +qMezb0Eso6Of7WXWkseM5Jfg5Nfu7qqSzvUDkxrcJzrNeXaMEpwdbkLi5SjJM4Mi +EtH7UsPqtOkCD4UXfQBQIAtlicPpi+sJnztXkoYDR0OSzgxYEsfVFkSm5ed9/d5/ +9oFLfi7nwG+LMOPZ42k8Djv62VjcTPW3iLB2DoRYmPyfXczjm1istd05q+IQZBnM +jpFOa5Ry6IJ1bV4o+NnB0w01S5dfqROioXrpUya6b9kBVWuqweHea3AitqqYHF6J +V5MwmmUxSe+FudnyDSS8t5z1TAotuiHy0adJFxba7N5ME3dxXu2AmD6yPuksQbq1 +Cbh2/eqrbIy1AOp0CzMSkfbx+v5G3nC0KORTcaFBoljEwA797LQMZwXq8uFacTPK +PUD+y2FD5Ya7mbPd0rD6SS7ZZe+lWsUpVVI4zSRjI4pmnX1yjqn6ieu4x66H1/GM +QeClEa9g0C6uttfIkOLoicAObHanc11h1m32HOwq7/KwqYM1xboYcb92K/VwKwLF +5OI2X34vE30uhnfedUJ/0Yw6koWsFYFIuKRRmGUOfFNqGjA6Zxi03BZxqY+m/Llq +fQm1O3YfJpcDllVV+/iRgsr/mDDu6eY54Fl/yiaEs20Ha9y+EzutRJBFJh8swpox +U0cOv+QJ4fE24Q4qH7hsxWm3JPSjK92+3ygDZoXo69aHvqEdCuxr1IBndqYPcc85 +BRyNlIx0pLECSCHKTDUZyml0nQFlvOeR+mt5FmKoiL7VIsx0iicavQjU2u4AkPkh +xKPw9Gt3+4B41llzgjMs8i3AsnYoPuWeGnXbks01p+jFiMh4dywNXIbXeInSAti2 +SbCbPWUMK6CISVwvUltQw16Cd9oLbJNleHmPOXQVGjJHMYRl2B3ySMNJT40cUyLS +JUebREU5VRhbZshjDF8FMHRNgMBEsZ7H3MvEP++KLLGv4z0ak2p+HjWV3zKP1M7N +vz1J7SB5J4W6YXAozVrNPfsAKW/SoAybuVu7hdz5yE+TyNPlpkI/j2d4V/S7a2UZ +81uISjKBdtNeXtq9Th4F5GV4OeRXYxNpd529Br8HwrTDx7rL8EY0KAqFgVRmlDmY +YWmlo7DgnZj+TUpra2rjHVxDhgk4lVe7iIL05sQqn8YpaVGUhDF0PiLsJaP4V+n7 +JdYBWdvN8wDfice3Co43o9fTUUzdfMG9Ec7wjCOSsghAuCFndEOoDCRofOEC0/oV +tUzTt8GNMyPJ1csK6+069t/32EhZTQr7bGXuZfstKcdujdsYn174LV8ZyfayMccK +NFrraepM5p6DzbkMeRkA7aLrBHIa2++b5iTD7j7FOHmkmOUhaX7kc8L4SjR+d/Qo +4OUv/79fHXdAoWXYdr/Wu8Nj4knLHiAmo6m/LlsfVA5BOqxr5ezwVTc/EaLFYr1t +hzWgySnDxSaxxs1J1oaeyTSFj0lbuByDhHPgSAYdew8l+f98M442R/U8AFTPBhsV +/exM6a3ireF1lbTjJb8iTI9yDJwKUumlSKRzxVeMnUpgWGJL+Mc4zu1Mw7rTAKFE +L3eCcEdamoQOd9WPYF0DQ+PamcC0IAKSEJ3nsMwrlOulwlIdkL08JWxagu4Cze5A +IXOxPsfXqyv3wPqRu2mqcjyvfMKy5P4S7h0jNP85ZgzrPRUiGoeIDzg75JxAoIVz +PjA7/OR/Oq1KOuTZccFoJK2CsAC6LFiyZqEy2IIb5Ww1O90ArrmTFgiwHhBmhfp6 +O62UoSGUI2gU7PdLgI++Bl8Lb5kulUoQyap7oTN6+mYV3UI9cSuIacA75Nn5+ulf +opTS3Ha1OIYFPfaAyDJ/68h4IrM6FtGZLrZ88rkSw19xyuK1dYZNgnwyoUSuJ4w2 +5gQmZo4m7yThlXYiuLwl3mo0JOsJjbIyh2/ABoef4vRpQAUPwH0Fz1l5DV/LZxzw +WHP3QnERhvF36ojfF2T2gLzi5RIirdZF0b7kkeJnwWjUi/ZOFUAI0AR/c6NrpjX0 +X9gLplgJ8fyRobv0c9ibVuExGMgRfDNmF3j/pRMqkPSbXLrrcxFCR54hEgOioNSK +QPhd3nax3I8KojRj7qfwEsOTu2m7uOVN0gbr0Gny511i2WxXWh+OX3DAJ0lu+kya +QVrERwhD1dKDzlnyvPia3bilWdhW3ppe6mTl7081CCdupdcZAA4NSX2mvmeBFmVR +DN27psQaAMq5q5bZXQsjlqdlBWex7QfEqTpjq/AM6YZKFHnEOVHrU7EPVW3q0N8h +m2vjkaHJzImFWsaSFl8TgjAGnHtMAi8h0bFxOHmvcDcU5izC8MCrcjdNil6kaEjp +CrkLRwKIhZM5+zIif6mv1vDH0Zq5pNw+7OoPQtS/Nyv6lpKoNGZQJZFAY+ZcPJkI +JlzAMe+SULFFpH2v23EJcUO++CxcEPTPkH1BRoLMhEtWNTcjEip7BXEeJdGsOQVG +FkTMpgboMvINQSxgOYgmi6y6G4Z2B25Tg8LLrcqBwfTC+pG4rILE7CxmfioJXiIu +/yCspkIXzrvBft0YYkZLuu6rZNegGMC+5/AjAcU29UGdodQm5rMLYq4tur+f9ON6 +VATN9Qpr+Y1vFJUjLkC/UaBw59Lcq7R0aelJSpCPWvwtd1YzhskWOyDhDK/sSiQi +eb79IzeIGMFrDlpV9pnEDg1XrBv0t4qv8MymWmAIJsby3RkhsNrL2LGt22OY/Fm1 +tMWd4vXWncSSw3mWPiPvvsI4vKMLDJXoEKfVjdHzAc4iaO+iJILLoSb8VLcLmqP6 +5LRooSFndR5c1ySUiWfbi/up0YGWApRxzPMTTW+/OYGPZgLFkttHdus9lvE2aLkn +7vjsJ64RLHNwHhViNgmTA/ouI8Y7PVzvTyaZXqH7t97/guzwJq5rX7FZFZ/fcMt2 ++DyJGoszstKo3iLK0YT3GzG6HpsDJBnADXQoh1owgoj8J5lfW/geS0CN/LR07f5o +ymAB0zbkRbXa7ccckDvdZ7IVCwlneNGqcJBgl6FG4Ajnm+AbJv8Y8pu9KclnNOgk +89mwdnmfV1uoZPTf7YoVlS9/nWSMqM1HKmYiK62MiMbKf1JmYqgHVu4gzldVETD5 +9eeE7jqjEjEW5TR5ZmtyG7W/sDjZ+PiO3JNoMJKDtOPyfdndCfzlyVvXYFLmqY1S +uNb2yFATdWexHlky+ugzrvnYBYuIazwjOU02KCqgRdk9CGiICAtCpdYjqAIP9l60 +Lu0cQ0a27POjZPvzwPvRYVtNrrOHt+BlazRlUTdE9+63E8A/So2dJWF50UQyxGs3 +pDFYnYmRqsyhIJy1Dx4l++lqeszW9V/EY+ePAYPVQ9ZJaKPhJ4+We2fBmgg0gfN+ +UPMWrUh01J19CNtI4oRZ+kEeedLgBRmHBJ9q/y5GpP7rsBVcIfO6kV9tau8n5JtR +SG+RAusUtUU62Yl1kICKMU4XMsweTYrFf9vIHSy7awyMMCujymwot96E7jis8gW+ +GBs40KJ94TLLAW+AGAPYIbnJJKzvTMpwIU28KlCZYAw49dr3APuPA+lhOo4tcr3h ++KJThTA4Z3YNoTErMu3Uco4IHhw9Tz5bFw+sRi3FI6IBiWM+VN5D7rVYK3261Tc4 +Y1l4E0qkm4nE34hCErLfRY4u+m1f9C6GTV31Dp3shGf/YGWUwfrJrozBVkY/C41W +TmBQgbLD6JsgGpnbFdN6wDsO+91iAqfK0oIiyVf9YIx946y0MWkOwQnoA0n56Tyj +mDGl9BCpo881GrP9V5D4fmhb5hnV9uZgJWaNBXOvHR7WqzXc7OdWRaQgqbMv853F +2IL/MK0uM4ma4tbbN5OIAYIiaAsNLCmN9FcPXQcFfk2fG5gNwJrDCUj+1YQGyyWX +h0DnjIv0j/dSukCiVz0aYS/RPxrPxOoL4rvjd/irDQ3SPDpmn0aDzJqCtL6QOKD2 +TMubqt10rK3RzQRzSGLu1mSDSOmWSMX51sgNOr3nIbwejHTKwI7aDZ69p//sz5RN +VxpoBUC5Axs+fO+wWfHgjrWW4hFcSWcaG7DmP5OmUNvzGuKZYKvwhF72CpZMI482 +xtRb33hFxEkahEiphG12HoSg+63o0iWP5rh1TXNVpX3/0N3F8bTi2PePUjOggKZV +OOUyaZWKCTq04jwRbBvKDsWSo4q6L0If37uiZLBu2e961SnYGgb7xFQzlSxmyKOh +BbYk5yw1H0n6K4iF+hm8zX1Iopyl5HnLRUSLPoBoJLPrzD5unmZaisGrD05AJouT +Anw6px6syPgCmK0yFToBOCy45Be8JZw1RIuZk0zgWq1DUc08MHFbUXkxVx20EgbG +Vay2677KR6ySjq3SXW0NZpCIxqtmyVPF1K4N/7LODKNY1U5oKuoi8iP0UuSUhY9E +bm3ZTGsQnEdk6GjHw2aXwPqjnij6VX7E2o9E6y0emMJvHxa87D0erxBXyYiJFU2i +HSGmxwDWT7KLATALmsGZNMDi8CAGf+jVv/fVos7ImEZduFjfLqA+5uJ6Y1JuW7M9 +E3p3WOzH3G7bapMDn7F86LHMnuDWZs4kO88HJ5qCBgLwMtrURloiyWGPUh8P6ahY +nwd9AYFE4RSZ5Ks1dxPoYhFw48jx2EUoGaSIhgLYZ/z1XwPEU2ikVMnkFeKhXNwk +T5z52dqO2U9KvYfm3+X/Uycs6zk9G+nlhZzRsQQ2emv4LIxyXr/ucamvc0Wgigtv +mD28gDtw5MJKAFycev2bzcWzT+I2501HVyLWuG3X4cHDP6HmFUoKOd+rl8TYffSY +8Ar19+EAOunMuYmYs+X1ENrf7eds/y5OaOk2h+Aq+kKiXpTVvGuhWrrAJbFgGMCC +w1PD5ycQCRD/3gejJjMuX0eUrjRo0hNjrjG2/M3gxr05ywSFk8pXAm/W8Ro/KfaN +21GhWWMWAVANkySRc0UmeRajrOleGgbN+TzZi7ZK0S0xHs/YjIezD0QjeAWTatod +1tsvosKN+oRqQqB8s/bu3+7pz4cs8YFwOy5wkv87MePk9TfHRP9RjNDtSpDYNGiR +YJEZcYOXeFrRHf9q0veSyRdjD9HHkp47KPTaUEL/UwnRY7wPFH7pNCikQ20jswhr +St5O0MDzaSXyuRO0cxgiEokxjvAvd1NiRWZ0xUBb6MDF5uPKOBd1in/+TVJYbS8h +kDYT6Zq3liwyjjqUvDczrSWRwhOECRSsbh/2XgU4U8CrtyaejSyGglbGYy5t6zx8 +FOzHxGsGZ5JgOWkAAATAH84M+fqVCgKIWHvz87f58x0/pUuDLKrEh4ouYhUSFGqM +SLQpmcEWlAsZ5p4KWVqc07eQSiauxoVf51Cdxk9Ug79ctnsTSozsOEcpkaP/Fzw7 +MR/QmmbZiVvlicR1GjxzKxWWxjZUhEU75nV7Bwx/HHZd4k8+A8O63C+9uyIYcoOw +KnhrlBhkMVZ2O4We4J6MjCfFs1mufH77nJBBDJioMIIRdxQMlqQsPKklK1Lwowtp +6MyRqAJPwVYnHADk6jvRQADU1rKaZ5ZoOSez7KxVM3GIa1qFIrrU47fxh4e+xaLB +NrUERnkBJMeqkLCDQnC4xLXw254RVBZE9lXP2gm/BAydhcxhrHGDBByFNo2rIhWA +JiLs03lzAYUjXJRliB6/6V1xtwHz6XLmYXCWN4UK272Ue3rukha1ZJxvVQCUa8Ew +MYtpwcwKwbTwIm23K7+S514yCzA8BLdN1nemiIrs5SVm+TMNV1HkSoMG1Gqu6pt0 +BASMnKHpx2RiU5MnyrJWhSNYx2+OtZBJmqy6ZXwVFK6J7BQ6WF5u+yrs7GVr9pXR +NokXYjWH2ZTyAzhmRQ/ZGE+qFZpqVa5aOBPpZXt6+KC7ix6oYg+ztVevcTNSrJ9X +JKHahVPsU7jVylHqeouNl0sGVVr4QHIj3KByewin+QpwdUtSW3+co5ZhnLhkoQpB +tSNJGxlUJrRz6rzTZVFkXFx7MZXaUW2FnCzpqFctZgQ+MQb4OCXO+l8j8LDw2xJN +Ykkotzs/fJyG7G452Rem12e0amTU8po6pzrJsUOEOc5YPFmOE3IfNhQZPAYosm4j +UQ+94DlQbApvKmaO9s8XgEGVE0AkuEKuPEbEmD4gsQ9Q1zdZvKfdl2p3uEJMuBbE +eQppUAVnhG22J7vFRjVnJ3UicYsq0QQLfCfMA8RmupEdgZdQK57FC4c368HZuIWt +hQ2M03PjJ0ybBq+GOwrcmnTcm4ELtTMGdBoSmUKx5JbxtbJFmQFs2RpY5HqZ/B+B +tZzhXLXrAqrCdho1VDE4ardUiI605DWjpZFxZpnlhqL3axZE3KhasYIpVT7J4U4Y +Ky42ETyO9mRz57fWNCZulpSzoKqXBK9DPDZmuQQD9K8MJxQNu02ptzJG47OFZ2JD +GAaKx31mx4mrLLX1ZcFsTFUK4IgdERAexbaYEm0wB65enAaw90futMJvkE46oHuM +FSiwFliPZ0jKUyLoVckt0EEYQDhWrDdeVzEZcCnmYZrZlwlzLFfmoamte6NmIFMM +Kkpg0kJ9Ayj6p0TFADUihmQyI38R+lFX0a6p9VqTyVZHIUCCVinVsZgdcisqtSxW +WagY5JgYsUtZRC0r5bcpFHk7hmNuNHNpY1Rf80RfGorIw5Sn2U1YlIufVpkSSkU/ +mX1F0lPJ9mtO5jB2li4G6FE6ac9v+z2qBhh9NrpzzCxsmzKI28hV85pHwn+s8w6j +jEUgQJdhoytip1s20VlR3Hd+kaVxuCF9qiiEUcNfhAlrEDWqt0mW7AqpsXUIiMnZ ++izb+6R3zCZGhLfqp7P5cTjLY8cwPBL33BtQd6cR2LzBiIat+4joscJ3U4yraDNm +skQXpb7XlI0cEnjr3ceSNd+mcOOSQJhjOuUpQ/CB7wDzeBol2c6ONP/oV6qtSrf0 +ccHj4SDiRzVja6lngavM/PDTvhtLJqS4t/TuBZwHEcLkeMzucTakLSidCSWwcz/9 +6g8yr9RBZh0ae+VXm3HnpLO3mvMl28TS45qfvyLW1pfC3jsGGG0MAAAALAWCZ5Jg +OQKbDCKhBk6perW66FSInTQbihDi3mbWC0HgjQUAFGOiuewjORQ4AAAAABcUEMk/ +6qEuwvf+FwjYLfbK+jIWZBYLhoyq2oiJxvjIOzJ6ij6/KntUm8/WbYHGIx//RKHf +5j8hjmHCtQcFnLZhjEgDnWhePDdt2G9idoTVIUkZL+egigx5JJvIh2w0zbFI8WUI +kui0qcZQusHtc/o7dqoo4zu4hyJKsWJpOzr9+n6p5jK98k9nBzFUHQT9WJP1vNWo +nfbjhfo/HkQXgU0Qj7WUgThG5GBCm2wMVZWUN9R3ynEc4t+L0VFyo8go9d+X+Lbh +FoHFIrcSU6ZFv2cERMVVU4sXCw80gwGZe1f+vl1ZeARj+9kQbcS8qbWTfb+cpwtl +K5Xm8KZhsriWEMgAjEmuGPuUvZqrmqb8aYxO4TlYfybUzLxYphqZPGvcXdP1N2c6 +LTF04vNFn5fpvr5IrM5I0x9kXRhMJpewnyeFCQaJ3PXE8pOENfho7g+bqL7b8Rj6 +i8hIwLLJjqcy6gxfsJ2Xxa5wzp5roDKQ8hL9kw2gKNbFry39Er9Kkn9qANEcc32q +UfQFmI6fKHObKkCwiWi/pxWqFl2gBnRd5NXxw4V07ZLEeGqyD7tWzSdM3750dlBo +370iqBmG6TWwDt7zofgjiqMPi2CWHewa4yX/tdh8VErO5wRVP4Upk30Bkiy4XMO6 +vKePuWzQrDafGhcbtX8XgImAIk+BKSVkDz31/QwH5f69Hx6OU0y+FpwRpIyCqSST +Q31jkHpnERDllwfze74/ZE3NmpVthHp0l4CSUrFgtW0KmvtERUyKoplhY0Gx5OZC +wrorDxvv3nzsRUXjH2hWkg8bjx2ShbJA4MFMA2RgT1XN65Iz1DSrbmhTL6l8Ilq+ +huIdgTSJ8AtvgN6arxemTlcPK1YdX6CHCtH3BFKOPU2EWzqlr5daMU6pqXaq6N5+ +hrAHySe9/TR0A//bY/8v6+/xM2+4N4DDqgiM3YBEWq2Oxvy9BnPvi5xj96bRhGT+ +Cc2A0NV2ZJMZMEBxwcwnVMVJDGeBrlldZ0lN70yuQQFl5SbWwb8iFNCMQpnrToYw +AYVnFa8+n0pr9WWHwpChAF7+C8eY/5GXvPNdj1kuevr8ex8tTGpFQlDT/lUKB7xw +K+kPUfAOhH2Eq+LHDWkVOBi7T1rUilzlYedbQPZ/cK37ddxwjRRiN7L9S+UM+pc6 +7Un7dOexFF975mBk0Pjj2XzwkdiqDjHmUwcwYtaM1aO8XqVlEqvlok9s6z1tBnN4 +lTItJIhkZ7q6C5jNv0kUzWbiTxTepSkGbaJy18z6KsFltOTx9t0qhMZYbhWkGYLE +mzeBgyHtIcUaqRP++0JemkC1az9RjOVdde4tQkI36xt76WT+trI0Sw3VAPcRW4oS +iOwgepbL/yVo1W1EFCuy1hs0rb7W3ReuTqRKbKpm8R38owb66X66Lzc+JtToI09g +6gaRGg33li56gqo/XPedCm9l1x+H5zummxpYvvwk7HLz7XZhtDeeyf0oNRcw3ypk +WFheTF0Mkr2wnSvdFm82tFc6K4i81I7xTwCpemXmPKB+gKWKJ6OcOQMIj1u3X1RN +uDIfppvpdKm7o6wFb7Z3gOs0cU95C0XyRkVdyAnyZ8XmcFfN2fFtPfGyZuO8MyUR +xlQwKJghbQ7nwyXsyHVw+6URNeBQxCZm1f0w2AG8AecJjGDUdcUPPNhisif6bhCY +sM8Qa7Hp2bFhYzXmBV3Ffppwj0y1lQSisEBGfculaaRImjhFGPKgMncoVcA1UpMS +Z7gSqEYu3y32wzBGmHNVqvAtBtLrlBKMoi8ENLhWABwXvwK3hweLwYWZzyBy1pJN +ZA0h4DVV8JM4hG0uTrWJ6GXPq01RubhaAFMgwTdLEH+CXSRNbMTSrAatpkDkV531 +NAkx4Jd9+JHKfXkB4fa8qTt+2ooK4WTsdX42RIW9ZfykXtPcANyGVUOJk8WEutPh +f/m65ttkhy6yBWq2ibe5fkVW+OVHD3L2WpBNNB/dEm2MEtiCjKRDdA2f/9d+sNTm +2wZRnuq4py/tIJJlhwan8DLbhjBoUYMHJ4WghRu+EPTJh9M4ssnNPHSHJiqngX36 +v+AiIylIuZO2ARsP8tVLG34QJ+RicR/J3WnBZUfTTZuksrgo7UddbouPLgeY/r57 +y8P0djaTuVY9p43R2o4oRHQcR+gHSMJQohtaTeKTQmVq/vnUODPEOQa1PDXmW3Me +JrFkVjKwOe0X67KrXOudbtK0mdvfgMjzCk5qcDtHPpEMZtNx3lDJWGfWOKuxoMb1 +CyoHVZK48FW94yFxItT7n/8CiElUNls0wUXFL4KSzc1Op/E/YVzbwI7og6gVYQVk +F/exfiMyZzMxWkwfyAsn+1PPSHOovneovXZaF1Ii7SCyPjLyrfUXlZRmn3bjE12r +F8apdrZ4nMISBdGPTjYeZPKvsmjw4hnrZbz5xtlxHAF6FZX/RTHkb67hsCgzYy6r +8f5FPRjzdiDKa64CerM51/F7A1vVwKZ03KgV1AGGKR+64qam/jzOy1T1bPXsRFBw +8m2ELlGwfZslZxs2yS+EPEEs5D78ciY1D95ADfQdCeV56cvc6EfkTwIkYJQQ5nSl +QSTIJ6J30VCVpr+pBx2BEidO8iYXnU7HyudoFGhGFmlXpL1/94PcjklHPX0/WD5D +JQ0pjEcG392EC34SYTPKMGpXVikuytMn4EF8ypjkEDNOFo6pEeNGhC9NGJUYUIS/ +/UOCiuu2Qv7VbDdtUtw69rQpuDhYJQsly2t0UhUyK/ll+vRTMATLz0w2RQZ40iOR +EriTV36mMhDHT8FCdFTloOG/5J1WgNz1QaHcS+v0Dznc8rWvMV1A2HH86BltNiZP +w7yt1Lapj03Js33aNGgoHfBc902otEWduTHo2DzMG6bS0cNaqjyyj6nwSISuQbBP +cKzz6HBreXPW+NDJsNux2jPOVb6lYYEKCravEYmjmT/sI/8UDybycjFfoE2BRGbb +wLzcHSjan10XJOmZnROtEOXiurpnsW/q+wnOpvgyngOMwA//gWeegHM4m+NDDR0s +igHn3rlc3ooavBIH+EM5KDrc2qttaaYfI7lGCRLRYhvYY8sGoLRT5q0d8YcESpRq +/mFkRZi9VYtDkJ66Oe0HEora7lZ2u3vDFTmMePqKRJWkJ21mF1leF86k/emjnMRQ +oqN6yuc4wo0cTUUTG5yS0U773nLNhnYBdWbHVxwYlWWMHy8GisbrThVYLAFuOcJ5 +tRHhSUKCvz82bf7QP4LLONrGe/T8WlUMzxSWIFsKeaFEqpXPzUmSZWmYL1cFi+3s +NS2FTD4HcNicKX8GJCofxuRMVp/LYzqibJHHaRE9rFP1NFK9FHPkUOnNG27VY8gQ +/h9Cce0GeEWTEzn3/NxNX6/Fkn3ANfcrC6z48Bopr9hT3o1Xb38kBzr0/2b1lsVW +J9tuvt1Gtpt/Spo1MER8ZZsUZlISQUdV8t8X8jibthWQBVJkYiDgg/9VSp45eeeo +KRj0+H7y0LuPkkXOkwrOTvWWNz08ghRO2oHvQSHCa/yF4yeCYiIxnrDtnHFI6dM9 +ycPbcZY3WE5nfji7hpTO3vLpVjiPN56+MkTxemqlsY2ca6mR2aCGW/zS8QewG1oP +9Ngtk9LpPurLnGJhEkZp+53erISxsXBdYijIicI1EeDNgGYbgYFB0JhD2dk+DrIq +Fazyabmx1yIFy3nhWAz/GyWmqwDh+9cVYf4tWhdUzyuNBpY0BKXIbfjsX+HWfiRi +mN96AFZQ3PB40jao5KLXtgAn/ZNcL/wSMije3erKUrOiYe1TPfGJ0XWM1RT0rwl3 +rpvBKAskopB4lbd2M9ypOcnYvXBOkSgZ3+SwkzoxiMFT8DGouGVjwUqeYiSSSdzK +8XB6hoN7fMZ2571DGzDoYbjVPpsuK+wpUFIbF6vhqMo4wAIFMs7caoqd26wtdN+F +nXSfU3kO61wpoFWy373wWis0eJDfmRcgtHXvg4J+ughn/RxsP9zCvROaJhrPeWaC +rVyE9i/PY/FtMcBxIwsh2tqvdsxKhR+1BUpyW8fzVlC+Pc4I++MoPgsz3BnemI79 +AZVId9/CEsOwuf+ZQjcZbCMR906mplo5hRLkK0v1RoXOkfpsvKiy/d+ZNqFoB2Bv +s3h6frHaVoP7RIOU3gdTZp64glnwLLGr97beyy3bBfRIHKGdW16lES1W+Y29j/Pg ++0PhQYer80Zb3We1gL8b95JNg2ju7dz4hYwSwxTLKI/S9r22VqVEaYZLsvk43crU +zGEochz671EcgrTOpxmtRm5ULvV4qZIYltxOrsvBvSbWNnTa5wG8YVMGY1RzA9q1 +ImmC8r8iAlW1o8w+vUhfQIRW+eUixhfcNxcE7511GN51GDdxxDKAdtMSQtZtj6Hr +heD9+3lDnl4vh0x3hF3pVTVFBYM82YGKs9/IGIiy8PeWztmbV/ZN50dlnyVYXq3i +wrdct0pBb/nzciq19mBQACouRdA3brxoR//lLAq9U9DShG4UdMwXYYghO44hCP4G +zQwjFV0gK3DT2lcVyCcTTwD1IQxiQedjbA6ttuaPWNW3GE5YjRd30NLXdNGSFmXK +FoUpi02hxKZRcTcfCsQzkyAi0TCp8krFAXdIi9dlAg5dpEaVCSUJY7Io31RZQ1uH +tnq0cifLPGU4wCC98TjgehvUPLxUvoU79aZxFm3/yY2tqKSjgPLnQDaLSsnGfdWi +bE4Dgvmuwzs5K175k5/NWEY9Tb7OkQQznF1Fwhc5aQOe6irVJ3zsRt5yHufn+05n +987X9TcFRjz+fR6cnf1hf9SN2Xdq0On5dMQnPegl+iGGay6bRQpsWuBvItDqA88L +fuB7B9XbgSvkE7mO9aM148edLPeO0JqXtYbrChukuCbrP1Uo5d8I8v3WvzKOyI0r ++UZlXjmbffu4J3cUhSzy+7NAXLeaKUfbPY6urGc6FgGW3g35QzisMI0IKOSIiNDe +xSzYOBkkalaxie4WrWZFn2cSotYtosNmg3HBN7+SQH93Al9m3mq9rC/twSYtnyRg +XyyL6J8dpFCCOYpP9EulGvUSEHTqTtN22GauBpmcJ03MgTEpnmu942AlgQVg01Mn +jw28NAP6CM8Qi+JbC8gWmSik/+oDKndF63dFM2S3F3YfnC0IRz/QtUKVQhhGs0DV +O7w13zndRNSwmuJoI1LQYo2DSdhUAYX0VHFUBtgptDC/u2UNJT0LdTfoezhXo6ev +oPoiSPmTckHdIP0jksjBTU8DdcXv4C9OX9mB2fbYOh6W02gDiEwFWmKpn80XSGW0 ++nzQ5YP9w0Uqt6TQU1r7WIfn8LvxmwNqECjVZZa4F5VQnWHEmVIq2Y+N2B2ccNfn +AR540NIx95s0eP3JszK+d/oVGaEfin+P+V+AxSnUzt0E0AyPF/StiO7SYRfeksOY +vXT3Fgla+qSwHkYLgw7/4tIr2GSes3d5PnJ+JD34U3VMTONFpeLNyVijeGAyyDh5 +YQJPB2HbCXdhHQWdOz+skhcnbIUlbSGqOnJDRWB3y7PDB9a52kEQWUcRoDeOptxv +wruI3IFdH6XNnBgu9KR09dGYWRdTmnZRSkJRK3YDs0GyXKBz6MxSchSYhCAsy8/G +NMtM0J5gGyum5IORUoyVLupzkgooHjMjWDbIjkdeex9hIuixk4knU+RSdr9jxV+i +Bkxm8vpOvxh1MIaUYjy3f91mRf1b+B4vPZQq4YHp8ohauBKzhfKmcvPeSarpI1cx +gyBZ2ckyf1n/RW2w71J8c/Tu6UuGjwB+bWGMClgdm3cy62FssH4Va2upw4ZWqydw +xST7Mm/G8J3YAFxtqJQ6d13xVNspSUJ8bfi6jsLRdInjGu0qjSeuGWjH5pw3dzbo +Qsbdwd0zaP7oJLNeEhE5uNNvLsoYWhzcUnx/fZ7PgsrufiJNOcB6KNpmnoS4d9Jb +6oYdPkwHQUkprGlAMySvzr/RhdzYI6EErZEk5SS/cpGcO4sMAE8F9JKapiGE/H6s +CsvwkWnIDs56dFp9cICDj9sS5+MSUSsppAEnsBSslkq+jPpBJdFembHaklu2PrOK +IM2+MN4I2nqancFEvuJDgjvUaJu24h5VAwF6LEOFdhgYZfKHoXHyZzq/TS22k1fV +83Vvi6Fi8JNv7l63WWRHKaPOTmebUS2YICxYkex0FG3Nsz6Sgpk5tN1FOVoyyIdW +L9Hpzamlr8/XAaPtGYsqz3HADD25BRxu9gCHXG//5dwlyRmRQPPXuM4tz7axazty +JAmDsKyBbFkO8y0pIlzE4TO39n0sTHVE6rAWM2wV8cO7dxpwToqMbUHgHAPjKtYz +OmCzy3B2C90xmwLnXeFqLtN1/40dWpRqRvGA0cnQNnL1lZ/+2E1PcIsbpeHT9FuI +z4+dbNsL5iCqbnGLxl75n9oIUyTM5YN22c14qDwKhQkzEn1AUhqHTBOPK7C7aHPQ +lXvcL9g6C89bvjEa+zWZvKTQfwoLHEhV0+ePMqruO1CBvjd40vi4qUb3S1J0OhfX +Q8Jjtv44rFr9nYCVgIGVNHj81hYHhjjhiNAz9u/ddU1AnALqZBiFAI6acdTUGmLQ +PDaQ+J4Et9D9lE8UCN5U700kNdsnPxQDPPwaOpE7C3hM9hmILCypKMFbD1P3/pXK +s8F3KztMstPzIYjOigY+6Pta2AvwADuOTJSvVUBTBMDy+uB+LnpEVsoCzkeF9x9d +CC5C20MQIG7+poc/b+/8tTSiZbQVRjncVEno6jY7eAqwlAiSGwKzoPzAu20Eq1R/ +Osf2MM5vm6uRHUyjY/rnxjDtyk5XS+Xq/k4Bk5iTimZ3dyx4cKZKV3Adyp2SzUPb +AchG57zSmKagZmqlbUowBm9uW8CZf+qypQymn3jMcs/Yk2J5sPDc/Mw0vyx3YFpx +q6QraPQS4cL4atvB4q88spBgrO1FH3CtA/eWHkRNkfzj0Uivgwic4KgWqgVyO2qp +ceLnCemoG9nb63BYvzI7SRNjW434sTFo7HDevlLSTkEWkT8GJAlv8UjF8TbEUmCN +KqbLdQOTY/GInjDbHZ+rW9nEONDSo1h81NTYAaj4zQJgwqvdILs3AsGgotJQOy8S +F3rUMpOw2bJxpkvONVenZW9zb+rzni9l/FAEHqbKr25yJSejED0XLAfkAIrbz/PN +D7SSb/NmQ99l3ZMsDL9sGmOvGyah0hx6KQxWSs2F6Bsh65OyqkIUQ8jMON8pTgez +nuTYTC8PGoRED70lOXTbkSGtqJszbXYybzN5+Ao1YSz22/42D0vY2jCjxicKbxbE +T7x2Ac1+OCGObNVdRxjFWzMXtPCTbcQHdqW+SHWKV4ynStor9nRTuU2vWqYDDrLA +pH1PpRpfMlsJt1LqyVRpLPnRETrNTrFXrNX/1xKvMGJSDuQ7qqXNHb3IDEPnI4Ss +veI+NXD7qr7f1k2WFpNAMz2pcM9lpZ7b5U+JzwDHk6yhnBkNrD6PdPD15cFGv5jD +9dJEaOIRa+O9PD3/GLg3k82ScmSV46V1xPiS+w2xXnbfcePN7dxok7QZTLQXd9OP +pzywpkZg+5kNePshBrao6tDUIiWh4xLdAtWi3ZCB6IRvIVtOYbIhcBXTdnogY7C5 +7VEkp85F4JpjuXboR892zXo1vg6gleyal85fheFu38tVK+r6agF8BaphFhN4NeW/ +2u1pOGEfG9E0YaU42bavBUox8mj9f3IUO8PznTu0digEs5DdjslFVdusCaZ2Wjun +z1nAu2Z3oLakWYc328MQPqvqBZkIhbeNsMvpbzmlzVwSCnsCQ3+TiFS86jm7bRK2 +bboVyA2neqiVZVYGcrv/uswstaKhjVZ6SB9olM2Go+G3WoFUjO2y1RXloJtyxpTV +9g3e1xN50e7Lx4TbrUTw0ApZMruoFkW2/sIPmQ7uSqAXS9806HQ4F4EPvDXalu+8 +7M78VyUnK+IrOUItxcIZ7Ci7bPnMMXKolpZc0ShlcvdqDKbdIdvfr04QZYjbBFoU +96uCUXF57RWtiZpqPM+bTw3JisSnDclhZpBW63mY6k+tRgpFbX5WQzryO/qvtvcM +C/1PEDjoYKxdqcknthreXfOKJ1D0Mv6klx5FVTlov4aUYHBaSFJEOZxE6Vx7EuE2 +B/9fcUmlSdWwWt3LgWPfW93sXNmoruBp9LApGIzJuOV55E2IPlRLtFVFs66Myb4X +cnVjd8YyvlblJZCNu7JwWL1elV4L/tBK9DpdNRzbRbZ21hnknu39Xr7dXXMlsHaP +T27jGoJSVlMVB5FZpaylD5ArqDU6eo8d/4CnGGpfb7mFupNvfRVW6l53orPBoAYS +9XyNublL9tnjn1GKgfQiufdjQo+YqiCAhYyzBP93JQHsSDi9QxRRhQFg4OwpwuQY +HZMTbJCO/at1jiBIXVzwWmNgCTbmdWUlYIH/0B7tM1YlquujerrE2HTLI2CRZp6g +s3RAuAxDiK7v16Ug3pj+s+uYPkuEetoQsbw/mKY7fDnv132cBkLMStK3mdyS9OxU +yqg6pMFPpdfsUXn4NXJM8PsihEiw5w/w6ROQnP/Oget4Btp1Ga4mjtlg4AuFU92O +SE9xoVcAjRl+X999+h+dBhlZkNhdVVTFf3lmLFgHGLRNsEQUFIdl5zG828BxqM+h +7nu7UhZ3f9tPPxYU/yVZkUk8N3pJTeiduaee3f/rfQheHLeai4Dp2QFAbW5bbNTx +nmKvD91Tz79zdMIYPtD0Ka0UWt3pWfyuqYbMRH5kSFDAt5exqZcyBVHp61pWKIpN +lmM452LapD5Q1/1pTbgJlM//40gFZWuc010Buk2TyE8/eMkP/M3fB7iWawhklk5J +IWlt+ljWXoHRwVIc8gD/HX/bsQ0YP9sSjRFwWup/yfHQJz2TE9mJLLLbNftFWice +W/I2YgHyPL5f8buJNzNQ9kXNe+el/Iaqy+58zrrdS4bQVbaMHJC7k21nIdN6KAzQ +vtZzai2aSXSSWcjAnOjV6hN/E3Zb5zvzz95HUehKJ1Rs/UIwGeBqB4xJvHW1ZicH +eQXu/RXbnnh29ynPTx9XDCjZRdcgT04TQ6sM9nSM57OKfMlIitEeCCmHBV0XY6b/ +rQhHQG4mn2PebbtulQnBhFIji63EgnW55Yr+pbKOx1I3K/x3TNDCu5H/EyoG288Y +3adI9EktPmWhbOIM+9/7PTNxlRo+SUryP4OYA4pSJLWsC7eaZ+hbYA0J51iKRV8y +6FDEo4h7C6BJnTMHTDEaLFCBR91j7uGHcB4dGmCWm6zHC+GKvL83saI+1LzHZaUX +dGd37WXw7v7wL1/+LlS3CNOvmDJlV2xlyTK4ZOQ/zh9rk18TPXbMTgRNWwmP86PA +Ak6LK2Bg4BoPd8vRoSavhFQBriYxyw7cF2DoiDskT5wAz8crXuMiie0bEZMjg4Ku +LKSPDgcLREyk/h48PkwnxOUMsdmv2lDem7hkg7si6WFaR+00PCN3O2ulBrOfmul9 +z5kDj7jFTmXpbED1xjQYvlXetBSfM74gIUr1wvCkkilprhr14kiniXVUg2FwvDdt +9PXUtXBmF6Y0FstRbNKNPMkHCxYHfjrOQCcNvroEvJl79mJshKC9nRdelM5M6pN8 +fCdjHNMHxWFfLfL5Je5ZPTc7CmkVx6bp2bh2AthdoQPObQs5GfHMNaXCXlAXJuxK +2SP8GSHCveVI3sleWApRCRV4dAICSMOXbYVzc3bBv8VSXxgczU57f/IpTMD7+ktW +/2KyfoLtLp5jF9H8fpuVXc1fWTdcmSXMzBC/dsSlpnLhr5SsSCVbppkf7pn/FR74 +XpKFuUW9YtNqWv7fTAIYCjoTym14UklQU1ZeCgQF+mUj6YdgF0F4gS5/6FlshlFI +OymsW7/FrrCcX4I0stcozj1XztSog2crFdqW2wOZf8SNgeFETySI0/9cnX23DKtG +f5+1PhuktRvHOpObIwbmCmTzz45d7VqWcVXmk88OpqZ7WjS9qTqfQ/78xxSzQ0GJ +rq+XKO/4cU9m+1zcI5esc0XGqAXG53SN6EQ7P9i3Mpj+280c8A5lbnGNSNV2YJHC +72RbcDqBzzv6Xn6AJdKlKvq4S4sK9/3iZ2TiVMc3eKSKEqwGKFdayDhATZUzqt/t +amA4tq0arH29ROnkGytAy6E5la0rpsBO2rpE4WRsMXKanI2L2ulffrg8UF6U90NO +4qPFsfsxhS1H+NljrAILCHHTjH8ymn0nQ36xBmSGTA31uu36/fY6vM4WIrmOJrIg +5lJvHvEKmayCXCGcJfCqPo73aA/YcFweWiuU8T49zKe0nbabo0cIvgcre37XgVPK +Wk/vtSqtJNe94OsZS0ss6vffpdItALabBFQpXDdn0TTKLhQz0/D8byrKRKDU2K1a +kibr+uvOjK+0aOOyx+Ahd17m3jnHUzgqZ/6srjg/vA8Ufuk0KKRDbSOzCGtK3k7Q +wPNpJfK5E7RzGCISiTGO8C93U2JFZnTFQFvowMXm48o4F3WKf/5NUlhtLyGQNhPp +mreWLDKOOpS8NzOtJZHCE4QJFKxuH/ZeBThTwKu3Jp6NLIaCVsZjLm3rPHwU7A== +=jk4+ +-----END PGP PRIVATE KEY BLOCK-----`; + +const slhdsaPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +FKxuH/ZeBThTwKu3Jp6NLIaCVsZjLm3rPHwU7A== +=W1ZV +-----END PGP PUBLIC KEY BLOCK-----`; + + +export default () => describe('PQC', function () { + it('ML-KEM + X25519 - Generate/encrypt/decrypt', async function () { + const sessionKey = { data: new Uint8Array(16).fill(1), algorithm: 'aes128' }; + + const { privateParams, publicParams } = await generateParams(openpgp.enums.publicKey.pqc_mlkem_x25519); + const encryptedSessionKeyParams = await publicKeyEncrypt(openpgp.enums.publicKey.pqc_mlkem_x25519, undefined, publicParams, null, sessionKey.data); + const decryptedSessionKey = await publicKeyDecrypt(openpgp.enums.publicKey.pqc_mlkem_x25519, publicParams, privateParams, encryptedSessionKeyParams); + expect(decryptedSessionKey).to.deep.equal(sessionKey.data); + }); + + it('ML-KEM + X25519 - private key is correctly serialized using the seed instead of the expanded secret key material', async function () { + const armoredKey = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +zdmQk3zZBuwE/KAXTAY= +-----END PGP PRIVATE KEY BLOCK-----`; + + const { data: expectedBinaryKey } = await openpgp.unarmor(armoredKey); + + const privateKey = await openpgp.readKey({ armoredKey }); + expect(privateKey.write()).to.deep.equal(expectedBinaryKey); + }); + + it('ML-KEM + X25519 - Test vector', async function () { + const armoredMessage = `-----BEGIN PGP MESSAGE----- + +wcPUA7w9P/Y4tvvTafGcykbHOZebb4UEiUzy7MdOx0v3nPOv8JfjdcbC3Ucz +4couZg5vJI7En+EBaT8f2aU+ZTmCmuZMVy5BHp9YKLhGmSU6tT3eUrhh63Z9 +IC44aS0m0vH/4f1vpsJmbpuPLxi+Q6DS4Sl4UOr0U8FOXzcb6uGMhWtOOdwI +iPZKgVpjgzwzxM1xh44hJz0iRSWKPPnFg9mbIHVPtuDMDNUYdOkRQhyQKYQE +z/ok0X9jCFKmAvKHpNjl4lFCxwcscJ5q5OYPO9I2BrJMyK2oyHmtgCojSvW1 +hSTeVNoXO0cwfQzWSNBQvBQEcuQ5gGKkQtbkBGds2LUmf2PASbZb8sZkN6EQ +8zdljhU3r4mHjTdNedTi8bN00/VQwsKFeN928f9k6SZDCU0XQxq/wZKmDhnI +vhvd5GbzFgeFSiPV+WY2sajZvh2cPuz0SGhW91ue0h3F2yKEzSQrloxZvV6Q +aFHWtBdzwx/Tx4oAvLvbBjV5qKC/C5f4RTNBoZ1doa4y8jxeeBEXBR7K7+mz +sS5GkVv5tpI1Z8UO/I450XdSYFS+dESgpZNLwy3Hu4TFMAxH78jjN21byr7S +Nx0ZmYrqQcfrW9m4YFsqpn50CJfbzRven84tgM9LqONIufCFhPv6K57MPSWR +l/YAut3fT2Mt/Bc3RS0xqiH7pi+/RxzKzY2nJ1takd2r4uWFLU3oLZBZrgck +M9+ZW9wwlgLOt5zmG+zZG9yfnpCBQIdmEJ31mH+j7oGemoM7r9UuLhbizlEE +Nx/TtsEXpuKTbzsrxjp9zVhgoF0ivV9mKWuGY1yGYaeT73RbyIzxVYjJTPsZ +Z9AWXNOmaRTFmrHHstQGz6AwcAZXoQ3Od2hiZ9BtgQ92Q5YZICFH3cbjCIEU +Pvj0f/1ruhHO20Q+uUcACAWRjWD9r1ijMnY1ke5GA+LZNXstEnzZPCqrhk5Z +NIFhuu+o47fakDvciMEqbdk6L2r620GLFG6F7cOO0vm/SrOm4drrvIpTbUe4 +DrgXhQuusUiXHKm4oB1/zNTEOpL4wqXLyyqcsN3EWXb3j+ZMwo+hm/ynEvPl +Gj76dvaJx5hRAtga4ZD8L6E1LS82bMmQKTp3uBJ3vcBxhKsSAYXO3ZQWeq0X +YSyvJiyy7We7gsjjN7RcYm8tOohoGvr2thf3LhN7hTBJWU1js7yq6UlAY+0A +so0dbkne00G77pRsy+nRYjVOFTm+57U/7UUTKsDtZf2Vsrv/nfAK2l507WAJ +8oioHhaDb4kxR92UR+cPkdqNzeMFEShgkRTRNG8d6cyRBa5/sYPtuSrKB0VX +S/t2UXgFWqTWPzcRv/NTDq89+yY5UvwfSFlRaKa32DHcUYq9L77Wq3bEZJiE +KYWFPla4cz1PnckU6DGUXxMAe2KrJ/ALhnTXavwLptqKSktgMS6FfBT8NJTX +Cf691DI9fikwHXZVGEKdh2CQpyZkJCoe+qOhKHEwrErMHHaajYYBHkGk6P9y +UifBYpdwZcspCcv6z78hlprAKcSAnXRKj2zIP5z36dbTN3z+vEa8HKM3lfAz +9Vbc34PSOgHX0jeD+RNwY7bt13n8MmNwsEUIUyHl/ivKTqhp2MKGnc622oXb +MdhqSPQkCscEVIT6fTh/vIyMTs0= +=OmpP +-----END PGP MESSAGE-----`; + + const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +zdmQk3zZBuwE/KAXTAY= +-----END PGP PRIVATE KEY BLOCK-----` + }); + + const { data: decryptedData } = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage }), + decryptionKeys: privateKey + }); + expect(decryptedData).to.equal('Testing\n'); + }); + + it('ML-DSA + Ed25519 - Generate/sign/verify', async function () { + const digest = new Uint8Array(32).fill(1); + const hashAlgo = openpgp.enums.hash.sha3_256; + + const { privateParams, publicParams } = await generateParams(openpgp.enums.publicKey.pqc_mldsa_ed25519); + const signature = await sign(openpgp.enums.publicKey.pqc_mldsa_ed25519, hashAlgo, publicParams, privateParams, null, digest); + const verified = await verify(openpgp.enums.publicKey.pqc_mldsa_ed25519, hashAlgo, signature, publicParams, null, null, digest); + expect(verified).to.be.true; + }); + + it('ML-DSA + Ed25519 - private key is correctly serialized using the seed instead of the expanded secret key material', async function () { + const armoredKey = mldsaEd25519AndMlkemX25519PrivateKey; + + const { data: expectedBinaryKey } = await openpgp.unarmor(armoredKey); + + const privateKey = await openpgp.readKey({ armoredKey }); + expect(privateKey.write()).to.deep.equal(expectedBinaryKey); + }); + + it('ML-DSA + Ed25519 - Test vector', async function () { + const privateKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PrivateKey }); + const publicKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PublicKey }); + + // `getSigningKey()` internally verifies the ML-DSA binding sigs + const signingKey1 = await privateKey.getSigningKey(); + const signingKey2 = await publicKey.getSigningKey(); + + expect(signingKey1.getKeyID().equals(signingKey2.getKeyID())).to.be.true; + }); + + it('ML-DSA + Ed25519 - throws on unexpected signature algorithm', async () => { + // The signature hash algo MUST be set to the specified algorithm, see + // https://datatracker.ietf.org/doc/html/draft-ietf-openpgp-pqc#section-5.2.1. + const privateKeyWithInvalidSignatures = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +QkebRJCZxZG/RCVh66PXOpv3JKQY2VscDzf7x22FDxuCDiKR72Mi15XydCi6fnQI +ug1/j6olqArvt51WpBoM/aN5uD7fuzqcv5OaCXuhyck6lhLRfp0/YRaYdlciihOb +1jHtdO4545qpeb/qETayE7R88zrUQLkiNsjdGdxHgs+MHl64KFC3D6mDvkqnFdqb +7Nd4FxDCBGv5ktQlKFKhGWtrR7PWnOYif6JqzzgsxWnC1nmVEAeWtw17s4tuke4c +Rh5owUNJBZZygKBJsn37H5cXoZHw8uzmESNxgMxw9zVUoeOc+l4eX5ynM+FxmXhx +IhOH1sQluFb1MpQglVPBrWzz/2NKvV9tsQq+lISpoewliGawL6Vh7X8qyvzad1Lm +ofEp0p9RrUwl/iGveCR0bdX11D5P/JyetN97OmQoLGmI9FWWrCZ46rlqMuH6+53i +ejXDUGXOnn0D27y3MdmFtklIMOMNapEt5Bz7iFbtA0uxPBD6TjlYwP5l6FEzjVyE +/KeOyv+oVZzTFsKW2mfyfPrj2N+DyPrMknZzI2joNGUkKfcuRcO681OU6tX3bj01 +sTWo7F6wg4ZQdT6Aje2ryScj24eHAjbGRqI7VQGvDx/kYmSXdrbhHG4Hnmk6mhG5 +NO1kR6Wu8EioJ2wCjfxBmzHGu4RP2+pWji8KIVeIy3LYfyKpgP8jxx62VbGYrUV4 +Iag+OPBukfe3QLhr3p5iMnoPI0ihwNYmCzCvJ76IKLmfjwchvT/W0a29syQuKcNa +ofjSihIlUs3ug1T/ZG3Xyl7NF2h0YFHnF7Rpm3WRSZQsePbMGmwiEck0UFP/Z/DP +Fe1V8lkH3hEty4NtMUqtn5F1VHalFs4C5UYKHtBRuZvqjvr0xnbZDFouCc7m4Vjp +lzsS1tSjLl6EAyonAbbQ3vCtTn3v9k8Ro8J1p9mRHA1NdBCTI9J8Lkml+f1IIn8g +VdHTTiYcqDxM/zRPmC5ve6fAD+BFh7qvDkIGqpB0FIkHnstjp2/XFaxJ1Xso7e0Q +Yo30tzVyBFNu8KQF8Okeh2Mz6M2oKn8GInjeDMWD9DxlZwHOnlcWuXFeurj79G5Y +iUffg9el18KCwIMrnDD48xUqv1gYDRy/1LAp3AE3nnm5cIPq1Rp4Nsi8DQBOW/ZL +J7HW1IXs6o2U7Hytlx+KLZ2a3ETlZ5Nu86OwZYhErsLxDd3wF/85NYUzMxvO5uAV +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 +-----END PGP PRIVATE KEY BLOCK-----` }); + + await expect(privateKeyWithInvalidSignatures.getSigningKey()).to.be.rejectedWith(/Unexpected hash algorithm for PQC signature/); + }); + + it('ML-DSA + ML-KEM - Test vector: decrypt/verify', async function () { + const privateKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PrivateKey }); + + const publicKey = privateKey.toPublic(); + + // NB: this is NOT the test vector from draft-ietf-openpgp-pqc-06, + // since we use a different KEM combiner + const armoredMessage = `-----BEGIN PGP MESSAGE----- + +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 +jXoyrGhDDakI/1XygauU5DpNuhgApoA= +-----END PGP MESSAGE-----`; + + const { data: decryptedData, signatures: [{ verified }] } = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage }), + decryptionKeys: privateKey, + verificationKeys: publicKey + }); + expect(decryptedData).to.equal('Testing\n'); + expect(await verified).to.be.true; + }); + + it('ML-DSA + ML-KEM - encrypt/sign and decrypt/verify', async function () { + const privateKey = await openpgp.readKey({ armoredKey: mldsaEd25519AndMlkemX25519PrivateKey }); + const publicKey = privateKey.toPublic(); + + const plaintext = 'Testing\n'; + const encrypted = await openpgp.encrypt({ message: await openpgp.createMessage({ text: plaintext }), signingKeys: privateKey, encryptionKeys: publicKey }); + const { data, signatures: [{ verified }] } = await openpgp.decrypt({ message: await openpgp.readMessage({ armoredMessage: encrypted }), verificationKeys: publicKey, decryptionKeys: privateKey }); + expect(data).to.equal(plaintext); + await expect(verified).to.eventually.be.true; + }); + + // TODO key generation & signing test skipped for now as it's too slow + it.skip('SLH-DSA - generate/sign/verify', async function () { + const digest = new Uint8Array(32).fill(1); + const hashAlgo = openpgp.enums.hash.sha3_256; + + // TODO: key generation test skipped for now as it's too slow + const { privateParams, publicParams } = await generateParams(openpgp.enums.publicKey.pqc_slhdsa_shake256s); + const signature = await sign(openpgp.enums.publicKey.pqc_slhdsa_shake128s, hashAlgo, publicParams, privateParams, null, digest); + const verified = await verify(openpgp.enums.publicKey.pqc_slhdsa_shake128s, hashAlgo, signature, publicParams, null, null, digest); + expect(verified).to.be.true; + + // test that unexpected hash algo is rejected both on signing and verification + const unexpectedHashAlgo = openpgp.enums.hash.sha256; + await expect(sign(openpgp.enums.publicKey.pqc_slhdsa_shake128s, unexpectedHashAlgo, publicParams, privateParams, null, digest)).to.be.rejectedWith(/Unexpected hash algorithm for PQC signature/); + await expect(verify(openpgp.enums.publicKey.pqc_slhdsa_shake128s, hashAlgo, signature, publicParams, null, null, digest)).to.be.rejectedWith(/Unexpected hash algorithm for PQC signature/); + }); + + it('SLH-DSA - Test vector', async function () { + const privateKey = await openpgp.readKey({ armoredKey: slhdsaPrivateKey }); + const publicKey = await openpgp.readKey({ armoredKey: slhdsaPublicKey }); + + // `getSigningKey()` internally verifies the ML-DSA binding sigs + const signingKey1 = await privateKey.getSigningKey(); + const signingKey2 = await publicKey.getSigningKey(); + + expect(signingKey1.getKeyID().equals(signingKey2.getKeyID())).to.be.true; + }); +}); diff --git a/test/crypto/validate.js b/test/crypto/validate.js index 8ed63089c..ca448c74e 100644 --- a/test/crypto/validate.js +++ b/test/crypto/validate.js @@ -81,7 +81,7 @@ async function cloneKeyPacket(key) { } async function generatePrivateKeyObject(options) { - const config = { rejectCurves: new Set() }; + const config = { rejectCurves: new Set(), ...options.config }; const { privateKey } = await openpgp.generateKey({ ...options, userIDs: [{ name: 'Test', email: 'test@test.com' }], format: 'object', config }); return privateKey; } @@ -314,6 +314,48 @@ export default () => { }); }); + describe('PQC parameter validation', function() { + let pqcSigningKey; + let pqcEncryptionSubkey; + before(async () => { + pqcSigningKey = await generatePrivateKeyObject({ type: 'pqc', config: { v6Keys: true } }); + pqcEncryptionSubkey = pqcSigningKey.subkeys[0]; + }); + + it('generated params are valid', async function() { + await expect(pqcSigningKey.keyPacket.validate()).to.not.be.rejected; + await expect(pqcEncryptionSubkey.keyPacket.validate()).to.not.be.rejected; + }); + + it('detect invalid ML-KEM public key part', async function() { + const keyPacket = await cloneKeyPacket(pqcEncryptionSubkey); + const { mlkemPublicKey } = keyPacket.publicParams; + mlkemPublicKey[0]++; + await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); + }); + + it('detect invalid ECC-KEM key part', async function() { + const keyPacket = await cloneKeyPacket(pqcEncryptionSubkey); + const { eccPublicKey } = keyPacket.publicParams; + eccPublicKey[0]++; + await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); + }); + + it('detect invalid ML-DSA public key part', async function() { + const keyPacket = await cloneKeyPacket(pqcSigningKey); + const { mldsaPublicKey } = keyPacket.publicParams; + mldsaPublicKey[0]++; + await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); + }); + + it('detect invalid ECC part', async function() { + const keyPacket = await cloneKeyPacket(pqcSigningKey); + const { eccPublicKey } = keyPacket.publicParams; + eccPublicKey[0]++; + await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); + }); + }); + describe('DSA parameter validation', function() { let dsaKey; before(async () => { diff --git a/test/general/forwarding.js b/test/general/forwarding.js new file mode 100644 index 000000000..ad9ec71b0 --- /dev/null +++ b/test/general/forwarding.js @@ -0,0 +1,101 @@ +import { expect } from 'chai'; + +import openpgp from '../initOpenpgp.js'; + +const charlieKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZAdtGBYJKwYBBAHaRw8BAQdAcNgHyRGEaqGmzEqEwCobfUkyrJnY8faBvsf9 +R2c5ZzYAAP9bFL4nPBdo04ei0C2IAh5RXOpmuejGC3GAIn/UmL5cYQ+XzRtjaGFy +bGVzIDxjaGFybGVzQHByb3Rvbi5tZT7CigQTFggAPAUCZAdtGAmQFXJtmBzDhdcW +IQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbAwIeAQIZAQILBwIVCAIWAAIiAQAAJKYA +/2qY16Ozyo5erNz51UrKViEoWbEpwY3XaFVNzrw+b54YAQC7zXkf/t5ieylvjmA/ +LJz3/qgH5GxZRYAH9NTpWyW1AsdxBGQHbRgSCisGAQQBl1UBBQEBB0CxmxoJsHTW +TiETWh47ot+kwNA1hCk1IYB9WwKxkXYyIBf/CgmKXzV1ODP/mRmtiBYVV+VQk5MF +EAAA/1NW8D8nMc2ky140sPhQrwkeR7rVLKP2fe5n4BEtAnVQEB3CeAQYFggAKgUC +ZAdtGAmQFXJtmBzDhdcWIQRl2gNflypl1XjRUV8Vcm2YHMOF1wIbUAAAl/8A/iIS +zWBsBR8VnoOVfEE+VQk6YAi7cTSjcMjfsIez9FYtAQDKo9aCMhUohYyqvhZjn8aS +3t9mIZPc+zRJtCHzQYmhDg== +=lESj +-----END PGP PRIVATE KEY BLOCK-----`; + +const fwdCiphertextArmored = `-----BEGIN PGP MESSAGE----- + +wV4DB27Wn97eACkSAQdA62TlMU2QoGmf5iBLnIm4dlFRkLIg+6MbaatghwxK+Ccw +yGZuVVMAK/ypFfebDf4D/rlEw3cysv213m8aoK8nAUO8xQX3XQq3Sg+EGm0BNV8E +0kABEPyCWARoo5klT1rHPEhelnz8+RQXiOIX3G685XCWdCmaV+tzW082D0xGXSlC +7lM8r1DumNnO8srssko2qIja +=pVRa +-----END PGP MESSAGE-----`; + +export default () => describe('Forwarding', function() { + it('can decrypt forwarded ciphertext', async function() { + const charlieKey = await openpgp.readKey({ armoredKey: charlieKeyArmored }); + + await expect(openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage: fwdCiphertextArmored }), + decryptionKeys: charlieKey + })).to.be.rejectedWith(/Error decrypting message/); + + const result = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage: fwdCiphertextArmored }), + decryptionKeys: charlieKey, + config: { allowForwardedMessages: true } + }); + + expect(result.data).to.equal('Message for Bob'); + }); + + it('supports serialising key with KDF params for forwarding', async function() { + const charlieKey = await openpgp.readKey({ armoredKey: charlieKeyArmored }); + + const serializedKey = charlieKey.write(); + const { data: expectedSerializedKey } = await openpgp.unarmor(charlieKeyArmored); + expect(serializedKey).to.deep.equal(expectedSerializedKey); + }); + + it('generates subkey with forwarding flag (0x40)', async function() { + const { privateKey: armoredKey } = await openpgp.generateKey({ userIDs: { email: 'test@forwarding.it' }, subkeys: [{ forwarding: true }, {}] }); + const privateKey = await openpgp.readKey({ armoredKey }); + + expect(privateKey.subkeys[0].bindingSignatures[0].keyFlags[0]).to.equal(openpgp.enums.keyFlags.forwardedCommunication); + expect(privateKey.subkeys[1].bindingSignatures[0].keyFlags[0]).to.equal(openpgp.enums.keyFlags.encryptCommunication | openpgp.enums.keyFlags.encryptStorage); + }); + + it('reformatting a key preserves its forwarding flags (0x40)', async function() { + // two subkeys, the first with forwarding flag, the second with standard encryption ones + const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZPhkahYJKwYBBAHaRw8BAQdARUPOBft22XPObTCYNRD2VB8ESYHOZsII +XrpUHn2AstUAAQCl30ZHts8cyRRXw7B2595L8RIovkwxhnCRTqe+V92+2BFK +zRQ8dGVzdEBmb3J3YXJkaW5nLml0PsKMBBAWCgA+BYJk+GRqBAsJBwgJkLvy +KUWO/JamAxUICgQWAAIBAhkBApsDAh4BFiEEM00dF5bOjezdbhYlu/IpRY78 +lqYAAP6uAQDt7Xxoh+VUB/xkOX1cj7at7U7zrKAxq7Xh1YbGM+RHKgEAgRoz +UGXKsQigC2KyXGW0nObT8RfUcQIUyrkVdImWiAjHXQRk+GRqEgorBgEEAZdV +AQUBAQdA1E/PrQHG7g8UW7v7fKwgc0x+jTHp8cOa3SGAqd3Pc3gDAQgHAAD/ +TY0mClFVWkDM/W6CnN7pOO36baJ0o1LJAVHucDTbxOgSMMJ4BBgWCAAqBYJk ++GRqCZC78ilFjvyWpgKbQBYhBDNNHReWzo3s3W4WJbvyKUWO/JamAABzegEA +mP3WSG1pceOppv5ncSoZJ9GZoaiXxnkk2TyLvmBQi7kA/1MoAjQDjF3XbX8y +ScSjs3juhSAQ/MnFj8RsDaI7XdIBx10EZPhkahIKKwYBBAGXVQEFAQEHQEyC +E9n5Jo23u9OfoVcUwEfQj4yAMhNBII3j5ePRDaYXAwEIBwAA/2M7YfJN9jV4 +LuiY7ldrWsd875xA5s6I6/8aOtUHuJcYEmPCeAQYFggAKgWCZPhkagmQu/Ip +RY78lqYCmwwWIQQzTR0Xls6N7N1uFiW78ilFjvyWpgAA5KEBAKaoHbyi3wpr +jt2m75fdx10rDOxJDR9H6ilI5ygLWeLsAPoCozX/3KhXLx8WbTe7MFcGl47J +YdgLdgXl0dn/xdXjCQ== +=eC8z +-----END PGP PRIVATE KEY BLOCK-----` }); + + const { privateKey: reformattedKey } = await openpgp.reformatKey({ privateKey, userIDs: { email: 'test@forwarding.it' }, format: 'object' }); + + expect(reformattedKey.subkeys[0].bindingSignatures[0].keyFlags[0]).to.equal(openpgp.enums.keyFlags.forwardedCommunication); + expect(reformattedKey.subkeys[1].bindingSignatures[0].keyFlags[0]).to.equal(openpgp.enums.keyFlags.encryptCommunication | openpgp.enums.keyFlags.encryptStorage); + }); + + it('refuses to encrypt using encryption key with forwarding flag (0x40)', async function() { + const charlieKey = await openpgp.readKey({ armoredKey: charlieKeyArmored }); + + await expect(openpgp.encrypt({ + message: await openpgp.createMessage({ text: 'abc' }), + encryptionKeys: charlieKey + })).to.be.rejectedWith(/Could not find valid encryption key packet/); + }); +}); diff --git a/test/general/index.js b/test/general/index.js index 478a0c5a5..c7af1fe10 100644 --- a/test/general/index.js +++ b/test/general/index.js @@ -4,6 +4,7 @@ import testArmor from './armor.js'; import testPacket from './packet.js'; import testSignature from './signature.js'; import testKey from './key.js'; +import testForwarding from './forwarding.js'; import testOpenPGP from './openpgp.js'; import testConfig from './config.js'; import testOID from './oid.js'; @@ -20,6 +21,7 @@ export default () => describe('General', function () { testPacket(); testSignature(); testKey(); + testForwarding(); testOpenPGP(); testConfig(); testOID(); diff --git a/test/general/key.js b/test/general/key.js index c2a43dc54..ded152832 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -2261,7 +2261,11 @@ function versionSpecificTests() { ]); } const hash = openpgp.enums.hash; - expect(selfSignature.preferredHashAlgorithms).to.eql([hash.sha512, hash.sha256, hash.sha3_512, hash.sha3_256]); + expect(selfSignature.preferredHashAlgorithms).to.eql( + openpgp.config.v6Keys ? + [hash.sha512, hash.sha256, hash.sha3_512, hash.sha3_256] : + [hash.sha512, hash.sha256] + ); const compr = openpgp.enums.compression; expect(selfSignature.preferredCompressionAlgorithms).to.eql([compr.uncompressed, compr.zlib, compr.zip]); @@ -2313,7 +2317,11 @@ function versionSpecificTests() { ]); } const hash = openpgp.enums.hash; - expect(selfSignature.preferredHashAlgorithms).to.eql([hash.sha224, hash.sha512, hash.sha256, hash.sha3_512, hash.sha3_256]); + expect(selfSignature.preferredHashAlgorithms).to.eql( + openpgp.config.v6Keys ? + [hash.sha224, hash.sha512, hash.sha256, hash.sha3_512, hash.sha3_256] : + [hash.sha224, hash.sha512, hash.sha256] + ); const compr = openpgp.enums.compression; expect(selfSignature.preferredCompressionAlgorithms).to.eql([compr.zlib, compr.uncompressed, compr.zip]); @@ -3347,7 +3355,7 @@ QgqsfguR1PqPuJxpXV4bSr6CGAAAAA== })).to.be.rejectedWith(/Unknown curve OID/); }); - it('Parsing ECDH key with unknown kdf param version', async function() { + it.skip('Parsing ECDH key with unknown kdf param version', async function() { // subkey with unknown kdfParam version 255. Parsing should not fail, the subkey should simply dropped const key = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- @@ -4604,6 +4612,21 @@ I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg== expect(v6Key.subkeys).to.have.length(1); }); + it('should throw when trying to add a ML-KEM or ML-DSA PQC key to a v4 key', async function() { + const v4Key = await openpgp.decryptKey({ + privateKey: await openpgp.readKey({ armoredKey: priv_key_rsa }), + passphrase: 'hello world' + }); + expect(v4Key.keyPacket.version).to.equal(4); + expect(v4Key.subkeys).to.have.length(1); + // try adding an ML-KEM subkey + await expect(v4Key.addSubkey({ type: 'pqc', sign: false })).to.be.rejectedWith(/Cannot generate v4 keys of type 'pqc'/); + expect(v4Key.subkeys).to.have.length(1); + // try adding an ML-DSA subkey + await expect(v4Key.addSubkey({ type: 'pqc', sign: true })).to.be.rejectedWith(/Cannot generate v4 keys of type 'pqc'/); + expect(v4Key.subkeys).to.have.length(1); + }); + it('should throw when trying to encrypt a subkey separately from key', async function() { const privateKey = await openpgp.decryptKey({ privateKey: await openpgp.readKey({ armoredKey: priv_key_rsa }), @@ -4725,6 +4748,71 @@ I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg== await subkey.verify(); }); + it('create and add a new symmetric subkey to a rsa key', async function() { + const privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readKey({ armoredKey: priv_key_rsa }), + passphrase: 'hello world' + }); + const total = privateKey.getSubkeys().length; + const opt2 = { type: 'symmetric', symmetricCipher: 'aes256' }; + let newPrivateKey = await privateKey.addSubkey(opt2); + const armoredKey = await newPrivateKey.armor(); + newPrivateKey = await openpgp.readKey({ armoredKey: armoredKey }); + const subKey = newPrivateKey.getSubkeys()[total]; + expect(subKey).to.exist; + expect(newPrivateKey.getSubkeys().length).to.be.equal(total + 1); + expect(subKey.getAlgorithmInfo().symmetric).to.be.equal('aes256'); + expect(subKey.keyPacket.publicParams.digest).to.exist.and.to.have.length(32); + expect(subKey.keyPacket.privateParams.keyMaterial).to.exist.and.to.have.length(32); + await subKey.verify(privateKey.primaryKey); + }); + + it('create and add a new rsa key with a symmetric encryption subkey', async function() { + const userId = { name: 'test', email: 'a@b.com' }; + const opt = { rsaBits: 512, userIDs: [userId], subkeys:[{ type: 'symmetric', symmetricCipher: 'aes256' }], format: 'object' }; + + const { privateKey } = await openpgp.generateKey(opt); + const armoredKey = await privateKey.armor(); + const newKey = await openpgp.readKey({ armoredKey: armoredKey }); + const subKey = newKey.getSubkeys()[0]; + expect(newKey.getSubkeys().length).to.be.equal(1); + expect(subKey).to.exist; + expect(subKey.getAlgorithmInfo().symmetric).to.be.equal('aes256'); + expect(subKey.keyPacket.publicParams.cipher).to.exist; + expect(subKey.keyPacket.publicParams.cipher.getValue()).to.be.equal(openpgp.enums.symmetric.aes256); + expect(subKey.keyPacket.publicParams.digest).to.exist.and.to.have.length(32); + expect(subKey.keyPacket.privateParams.keyMaterial).to.exist.and.to.have.length(32); + await subKey.verify(newKey.primaryKey); + }); + + it('create and add a new encrypted rsa key with a symmetric encryption subkey', async function() { + const userId = { name: 'test', email: 'a@b.com' }; + const opt = { rsaBits: 512, userIDs: [userId], subkeys:[{ type: 'symmetric', symmetricCipher: 'aes256' }], format: 'object' }; + + const { privateKey } = await openpgp.generateKey(opt); + const subKey = privateKey.getSubkeys()[0]; + expect(privateKey.getSubkeys().length).to.be.equal(1); + expect(subKey).to.exist; + expect(subKey.getAlgorithmInfo().symmetric).to.be.equal('aes256'); + expect(subKey.keyPacket.publicParams.cipher).to.exist; + expect(subKey.keyPacket.publicParams.cipher.getValue()).to.be.equal(openpgp.enums.symmetric.aes256); + expect(subKey.keyPacket.publicParams.digest).to.exist.and.to.have.length(32); + expect(subKey.keyPacket.privateParams.keyMaterial).to.exist.and.to.have.length(32); + await subKey.verify(privateKey.primaryKey); + }); + + it('create and add a symmetric signing key', async function() { + const opt = { userIDs: [{ name: 'test', email: 'a@b.com' }], type: 'symmetric', symmetricHash: 'sha256', format: 'object' }; + const { privateKey } = await openpgp.generateKey(opt); + const signed = await openpgp.sign({ message: await openpgp.createMessage({ text: 'the data to be signed' }), signingKeys: privateKey, format: 'binary' }); + const message = await openpgp.readMessage({ binaryMessage: signed }); + const { signatures } = await openpgp.verify({ message, verificationKeys: [privateKey], expectSigned: true }); + expect(signatures).to.exist; + expect(signatures.length).to.be.equal(1); + expect(signatures[0].keyID.toHex()).to.be.equal(privateKey.getKeyID().toHex()); + expect(await signatures[0].verified).to.be.true; + }); + it('sign/verify data with the new subkey correctly using curve25519 (legacy format)', async function() { const userID = { name: 'test', email: 'a@b.com' }; const opt = { curve: 'curve25519', userIDs: [userID], format: 'object', subkeys:[] }; diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 3eb42d70b..3b684ffd9 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -2406,6 +2406,17 @@ k0mXubZvyl4GBg== expect(seipd).to.be.instanceOf(openpgp.SymEncryptedIntegrityProtectedDataPacket); expect(seipd.version).to.equal(2); expect(seipd.aeadAlgorithm).to.equal(openpgp.enums.aead.ocb); + + const encryptedWithoutAEAD = await openpgp.encrypt({ + message: await openpgp.createMessage({ text: 'test' }), + encryptionKeys: [v4PrivateKeyWithOCBPref, v6PrivateKeyWithOCBPref], + format: 'object', + config: { ignoreSEIPDv2FeatureFlag: true } + }); + + const seipdV1 = encryptedWithoutAEAD.packets[2]; + expect(seipdV1).to.be.instanceOf(openpgp.SymEncryptedIntegrityProtectedDataPacket); + expect(seipdV1.version).to.equal(1); }); it('should support encrypting to a key without features (missing SEIPDv1 feature)', async function () { diff --git a/test/general/packet.js b/test/general/packet.js index acda172c0..f6d2651ee 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -1225,6 +1225,119 @@ V+HOQJQxXJkVRYa3QrFUehiMzTeqqMdgC6ZqJy7+ }); }); + describe('Persistent symmetric keys', function() { + it('Parse persistent key encrypted session key packet (PKESK encrypted with persistent symmetric key)', () => { + const serializedPacket = new Uint8Array([ + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x64, 0x01, 0x4c, 0x80, 0x3d, 0x9b, 0xb6, + 0x44, 0x13, 0x25, 0x90, 0x24, 0x6e, 0x59, 0x0d, + 0x68, 0xee, 0x1e, 0x23, 0x75, 0x8a, 0x90, 0x85, + 0x78, 0x1b, 0xf7, 0xc4, 0x4d, 0x58, 0xc7, 0x64, + 0xd2, 0xe5, 0xb3, 0x4f, 0xf6, 0x6e, 0xef, 0x53, + 0xc4, 0xc3, 0x76, 0x7b, 0xba, 0xf9, 0x03, 0x86, + 0xee, 0xc0, 0x9d, 0x60, 0x23, 0xa6, 0x8a + ]); + const expectedIV = new Uint8Array([ + 0x4c, 0x80, 0x3d, 0x9b, 0xb6, 0x44, 0x13, 0x25, + 0x90, 0x24, 0x6e, 0x59, 0x0d, 0x68, 0xee, 0x1e + ]); + const expectedCiphertext = new Uint8Array([ + 0x75, 0x8a, 0x90, 0x85, 0x78, 0x1b, 0xf7, 0xc4, + 0x4d, 0x58, 0xc7, 0x64, 0xd2, 0xe5, 0xb3, 0x4f, + 0xf6, 0x6e, 0xef, 0x53, 0xc4, 0xc3, 0x76, 0x7b, + 0xba, 0xf9, 0x03, 0x86, 0xee, 0xc0, 0x9d, 0x60, + 0x23, 0xa6, 0x8a + ]); + const packet = new openpgp.PublicKeyEncryptedSessionKeyPacket(); + packet.read(serializedPacket); + + expect(packet.publicKeyAlgorithm).to.equal(openpgp.enums.publicKey.aead); + expect(packet.encrypted.aeadMode.data).to.equal(openpgp.enums.aead.eax); + expect(util.equalsUint8Array(packet.encrypted.iv, expectedIV)).to.be.true; + expect(util.equalsUint8Array(packet.encrypted.c.data, expectedCiphertext)).to.be.true; + }); + + it('Parse signature packet from persistent symmetric key', () => { + const serializedPacket = new Uint8Array([ + 0x04, 0x00, 0x65, 0x08, 0x00, 0x10, 0x05, 0x02, + 0x60, 0x64, 0x52, 0x28, 0x09, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x6b, 0x1d, 0x40, 0x76, 0xf9, 0x35, 0xf8, + 0xe4, 0xc5, 0x2f, 0x49, 0xce, 0xf7, 0x91, 0x23, + 0xb4, 0x00, 0x3b, 0x77, 0x92, 0x60, 0x2a, 0xfe, + 0x2e, 0x4c, 0xf5, 0x5f, 0x6c, 0x75, 0x80, 0x5a, + 0x3a, 0xf4, 0x38, 0x09, 0x22, 0x97, 0x1a, 0x01, + 0x2e, 0x8a, 0x59, 0x86, 0x26, 0x77, 0x79, 0xe1, + 0xb7, 0xd7, 0x4d, 0x7c, 0xa6, 0x61, 0xe7, 0x00, + 0xf1, 0x4b, 0xa4, 0x5b, 0xc8, 0x3a, 0xe2, 0x35, + 0x9e, 0x67, 0x4a, 0x44 + ]); + + const expectedMAC = new Uint8Array([ + 0x76, 0xf9, 0x35, 0xf8, 0xe4, 0xc5, 0x2f, 0x49, + 0xce, 0xf7, 0x91, 0x23, 0xb4, 0x00, 0x3b, 0x77, + 0x92, 0x60, 0x2a, 0xfe, 0x2e, 0x4c, 0xf5, 0x5f, + 0x6c, 0x75, 0x80, 0x5a, 0x3a, 0xf4, 0x38, 0x09, + 0x22, 0x97, 0x1a, 0x01, 0x2e, 0x8a, 0x59, 0x86, + 0x26, 0x77, 0x79, 0xe1, 0xb7, 0xd7, 0x4d, 0x7c, + 0xa6, 0x61, 0xe7, 0x00, 0xf1, 0x4b, 0xa4, 0x5b, + 0xc8, 0x3a, 0xe2, 0x35, 0x9e, 0x67, 0x4a, 0x44 + ]); + + const packet = new openpgp.SignaturePacket(); + packet.read(serializedPacket); + + const readMAC = packet.params.mac.data; + + expect(packet.publicKeyAlgorithm).to.equal(openpgp.enums.publicKey.hmac); + expect(util.equalsUint8Array(readMAC, expectedMAC)).to.be.true; + }); + + it('encrypt/decrypt using persistent symmetric key', async () => { + const persistentSymmetricKeyPacket = new openpgp.SecretSubkeyPacket(); + persistentSymmetricKeyPacket.version = 4; + persistentSymmetricKeyPacket.algorithm = openpgp.enums.publicKey.aead; + await persistentSymmetricKeyPacket.generate(null, null, openpgp.enums.symmetric.aes256); + await persistentSymmetricKeyPacket.computeFingerprintAndKeyID(); + + const sessionKey = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]); + + const pkesk = openpgp.PublicKeyEncryptedSessionKeyPacket.fromObject({ + version: 3, + encryptionKeyPacket: persistentSymmetricKeyPacket, + sessionKey, + sessionKeyAlgorithm: openpgp.enums.symmetric.aes256 + }); + await pkesk.encrypt(persistentSymmetricKeyPacket); + + const parsedPKESK = new openpgp.PublicKeyEncryptedSessionKeyPacket(); + parsedPKESK.read(pkesk.write()); + await parsedPKESK.decrypt(persistentSymmetricKeyPacket); + expect(util.equalsUint8Array(sessionKey, parsedPKESK.sessionKey)).to.be.true; + }); + + it('throws when encrypting with encrypted symmetric key', async () => { + const persistentSymmetricKeyPacket = new openpgp.SecretSubkeyPacket(); + persistentSymmetricKeyPacket.version = 4; + persistentSymmetricKeyPacket.algorithm = openpgp.enums.publicKey.aead; + await persistentSymmetricKeyPacket.generate(null, null, openpgp.enums.symmetric.aes256); + await persistentSymmetricKeyPacket.computeFingerprintAndKeyID(); + await persistentSymmetricKeyPacket.encrypt('passphrase'); + await persistentSymmetricKeyPacket.clearPrivateParams(); + + const sessionKey = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]); + + const pkesk = openpgp.PublicKeyEncryptedSessionKeyPacket.fromObject({ + version: 3, + encryptionKeyPacket: persistentSymmetricKeyPacket, + sessionKey, + sessionKeyAlgorithm: openpgp.enums.symmetric.aes256 + }); + + await expect(pkesk.encrypt(persistentSymmetricKeyPacket)).to.be.rejectedWith('Cannot encrypt with symmetric key missing private parameters'); + }); + }); + describe('PacketList parsing', function () { it('Ignores unknown packet version with `config.ignoreUnsupportedPackets` enabled', async function() { const armoredSignature = `-----BEGIN PGP SIGNATURE----- diff --git a/test/general/x25519.js b/test/general/x25519.js index e0cc8628f..788f54810 100644 --- a/test/general/x25519.js +++ b/test/general/x25519.js @@ -241,7 +241,7 @@ export default () => describe('X25519 Cryptography (legacy format)', function () expect(R).to.deep.eq(r); expect(S).to.deep.eq(s); }), - signature.verify(22, openpgp.enums.hash.sha256, { r: R, s: S }, publicParams, undefined, data).then(result => { + signature.verify(22, openpgp.enums.hash.sha256, { r: R, s: S }, publicParams, null, undefined, data).then(result => { expect(result).to.be.true; }) ]); diff --git a/test/initOpenpgp.js b/test/initOpenpgp.js index 4fd4a360e..3df7f453f 100644 --- a/test/initOpenpgp.js +++ b/test/initOpenpgp.js @@ -3,7 +3,7 @@ * at the top of the test bundle, and that the config is initialised before the tests code runs (incl. that outside of `describe`). */ -import * as openpgp from 'openpgp'; +import * as openpgp from '@protontech/openpgp'; if (typeof window !== 'undefined') { window.openpgp = openpgp; diff --git a/test/typescript/definitions.ts b/test/typescript/definitions.ts index 2d74b77cc..0387d217e 100644 --- a/test/typescript/definitions.ts +++ b/test/typescript/definitions.ts @@ -17,7 +17,7 @@ import { generateSessionKey, encryptSessionKey, decryptSessionKeys, LiteralDataPacket, PacketList, CompressedDataPacket, PublicKeyPacket, PublicSubkeyPacket, SecretKeyPacket, SecretSubkeyPacket, CleartextMessage, WebStream, NodeWebStream, -} from 'openpgp'; +} from '@protontech/openpgp'; (async () => { diff --git a/tsconfig.json b/tsconfig.json index 1b8fdcb00..4b1fc1a11 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "moduleResolution": "Bundler", "allowJs": true, "paths": { - "openpgp": [ "." ] + "@protontech/openpgp": [ "." ] }, } }