diff --git a/openpgp.d.ts b/openpgp.d.ts index 56175b26..c161efac 100644 --- a/openpgp.d.ts +++ b/openpgp.d.ts @@ -332,7 +332,9 @@ interface Config { v5Keys: boolean; preferredAEADAlgorithm: enums.aead; aeadChunkSizeByte: number; + s2kType: enums.s2k.iterated | enums.s2k.argon2; s2kIterationCountByte: number; + s2kArgon2Params: { passes: number, parallelism: number; memoryExponent: number; }; minBytesForWebCrypto: number; maxUserIDLength: number; knownNotations: string[]; @@ -909,4 +911,12 @@ export namespace enums { utf8 = 117, mime = 109 } + + enum s2k { + simple = 0, + salted = 1, + iterated = 3, + argon2 = 4, + gnu = 101 + } } diff --git a/package-lock.json b/package-lock.json index b568e01f..0e86bc5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,9 @@ "@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-node-resolve": "^7.1.3", "@rollup/plugin-replace": "^2.3.2", + "@rollup/plugin-wasm": "^6.1.2", "@types/chai": "^4.2.14", + "argon2id": "^1.0.1", "benchmark": "^2.1.4", "bn.js": "^4.11.8", "chai": "^4.3.6", @@ -693,6 +695,23 @@ "rollup": "^1.20.0 || ^2.0.0" } }, + "node_modules/@rollup/plugin-wasm": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-wasm/-/plugin-wasm-6.1.2.tgz", + "integrity": "sha512-YdrQ7zfnZ54Y+6raCev3tR1PrhQGxYKSTajGylhyP0oBacouuNo6KcNCk+pYKw9M98jxRWLFFca/udi76IDXzg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/pluginutils": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.9.tgz", @@ -952,6 +971,12 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, + "node_modules/argon2id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/argon2id/-/argon2id-1.0.1.tgz", + "integrity": "sha512-rsiD3lX+0L0CsiZARp3bf9EGxprtuWAT7PpiJd+Fk53URV0/USOQkBIP1dLTV8t6aui0ECbymQ9W9YCcTd6XgA==", + "dev": true + }, "node_modules/argparse": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", @@ -7999,6 +8024,13 @@ "magic-string": "^0.25.5" } }, + "@rollup/plugin-wasm": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-wasm/-/plugin-wasm-6.1.2.tgz", + "integrity": "sha512-YdrQ7zfnZ54Y+6raCev3tR1PrhQGxYKSTajGylhyP0oBacouuNo6KcNCk+pYKw9M98jxRWLFFca/udi76IDXzg==", + "dev": true, + "requires": {} + }, "@rollup/pluginutils": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.9.tgz", @@ -8214,6 +8246,12 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, + "argon2id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/argon2id/-/argon2id-1.0.1.tgz", + "integrity": "sha512-rsiD3lX+0L0CsiZARp3bf9EGxprtuWAT7PpiJd+Fk53URV0/USOQkBIP1dLTV8t6aui0ECbymQ9W9YCcTd6XgA==", + "dev": true + }, "argparse": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", diff --git a/package.json b/package.json index 6dc92a05..b48b07fc 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,9 @@ "@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-node-resolve": "^7.1.3", "@rollup/plugin-replace": "^2.3.2", + "@rollup/plugin-wasm": "^6.1.2", "@types/chai": "^4.2.14", + "argon2id": "^1.0.1", "benchmark": "^2.1.4", "bn.js": "^4.11.8", "chai": "^4.3.6", diff --git a/rollup.config.js b/rollup.config.js index 6e1b5104..97656497 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -6,10 +6,25 @@ import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import replace from '@rollup/plugin-replace'; import { terser } from 'rollup-plugin-terser'; +import { wasm } from '@rollup/plugin-wasm'; + import pkg from './package.json'; const nodeDependencies = Object.keys(pkg.dependencies); +const wasmOptions = { + node: { targetEnv: 'node' }, + browser: { targetEnv: 'browser', maxFileSize: undefined } // always inlline (our wasm files are small) +}; + +const getChunkFileName = (chunkInfo, extension) => { + // index files result in chunks named simply 'index', so we rename them to include the package name + if (chunkInfo.name === 'index') { + const packageName = chunkInfo.facadeModuleId.split('/').at(-2); // assume index file is under the root folder + return `${packageName}.${extension}`; + } + return `[name].${extension}`; +}; const banner = `/*! OpenPGP.js v${pkg.version} - ` + @@ -50,7 +65,8 @@ export default Object.assign([ 'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`, 'require(': 'void(', delimiters: ['', ''] - }) + }), + wasm(wasmOptions.browser) ] }, { @@ -68,14 +84,15 @@ export default Object.assign([ commonjs(), replace({ 'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}` - }) + }), + wasm(wasmOptions.node) ] }, { input: 'src/index.js', output: [ - { dir: 'dist/lightweight', entryFileNames: 'openpgp.mjs', chunkFileNames: '[name].mjs', format: 'es', banner, intro }, - { dir: 'dist/lightweight', entryFileNames: 'openpgp.min.mjs', chunkFileNames: '[name].min.mjs', format: 'es', banner, intro, plugins: [terser(terserOptions)], sourcemap: true } + { dir: 'dist/lightweight', entryFileNames: 'openpgp.mjs', chunkFileNames: chunkInfo => getChunkFileName(chunkInfo, 'mjs'), format: 'es', banner, intro }, + { dir: 'dist/lightweight', entryFileNames: 'openpgp.min.mjs', chunkFileNames: chunkInfo => getChunkFileName(chunkInfo, 'min.mjs'), format: 'es', banner, intro, plugins: [terser(terserOptions)], sourcemap: true } ], preserveEntrySignatures: 'allow-extension', plugins: [ @@ -89,7 +106,8 @@ export default Object.assign([ 'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`, 'require(': 'void(', delimiters: ['', ''] - }) + }), + wasm(wasmOptions.browser) ] }, { @@ -110,7 +128,8 @@ export default Object.assign([ "import openpgpjs from '../../..';": `import * as openpgpjs from '/dist/${process.env.npm_config_lightweight ? 'lightweight/' : ''}openpgp.mjs'; window.openpgp = openpgpjs;`, 'require(': 'void(', delimiters: ['', ''] - }) + }), + wasm(wasmOptions.browser) ] } ].filter(config => { diff --git a/src/config/config.js b/src/config/config.js index ccfee7e1..6f2baadb 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -76,12 +76,41 @@ export default { */ v5Keys: false, /** - * {@link https://tools.ietf.org/html/rfc4880#section-3.7.1.3|RFC4880 3.7.1.3}: - * Iteration Count Byte for S2K (String to Key) + * S2K (String to Key) type, used for key derivation in the context of secret key encryption + * and password-encrypted data. Weaker s2k options are not allowed. + * Note: Argon2 is the strongest option but not all OpenPGP implementations are compatible with it + * (pending standardisation). + * @memberof module:config + * @property {enums.s2k.argon2|enums.s2k.iterated} s2kType {@link module:enums.s2k} + */ + s2kType: enums.s2k.iterated, + /** + * {@link https://tools.ietf.org/html/rfc4880#section-3.7.1.3| RFC4880 3.7.1.3}: + * Iteration Count Byte for Iterated and Salted S2K (String to Key). + * Only relevant if `config.s2kType` is set to `enums.s2k.iterated`. + * Note: this is the exponent value, not the final number of iterations (refer to specs for more details). * @memberof module:config * @property {Integer} s2kIterationCountByte */ s2kIterationCountByte: 224, + /** + * {@link https://tools.ietf.org/html/draft-ietf-openpgp-crypto-refresh-07.html#section-3.7.1.4| draft-crypto-refresh 3.7.1.4}: + * Argon2 parameters for S2K (String to Key). + * Only relevant if `config.s2kType` is set to `enums.s2k.argon2`. + * Default settings correspond to the second recommendation from RFC9106 ("uniformly safe option"), + * to ensure compatibility with memory-constrained environments. + * For more details on the choice of parameters, see https://tools.ietf.org/html/rfc9106#section-4. + * @memberof module:config + * @property {Object} params + * @property {Integer} params.passes - number of iterations t + * @property {Integer} params.parallelism - degree of parallelism p + * @property {Integer} params.memoryExponent - one-octet exponent indicating the memory size, which will be: 2**memoryExponent kibibytes. + */ + s2kArgon2Params: { + passes: 3, + parallelism: 4, // lanes + memoryExponent: 16 // 64 MiB of RAM + }, /** * Allow decryption of messages without integrity protection. * This is an **insecure** setting: diff --git a/src/enums.js b/src/enums.js index 220ed807..33982062 100644 --- a/src/enums.js +++ b/src/enums.js @@ -91,6 +91,7 @@ export default { simple: 0, salted: 1, iterated: 3, + argon2: 4, gnu: 101 }, diff --git a/src/message.js b/src/message.js index ce922dcc..5a88e7b0 100644 --- a/src/message.js +++ b/src/message.js @@ -18,6 +18,7 @@ import * as stream from '@openpgp/web-stream-tools'; import { armor, unarmor } from './encoding/armor'; import KeyID from './type/keyid'; +import { Argon2OutOfMemoryError } from './type/s2k'; import defaultConfig from './config'; import crypto from './crypto'; import enums from './enums'; @@ -183,6 +184,9 @@ export class Message { decryptedSessionKeyPackets.push(skeskPacket); } catch (err) { util.printDebugError(err); + if (err instanceof Argon2OutOfMemoryError) { + exception = err; + } } })); })); diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index a0df643a..cab07335 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -16,7 +16,7 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import PublicKeyPacket from './public_key'; -import S2K from '../type/s2k'; +import { newS2KFromConfig, newS2KFromType } from '../type/s2k'; import crypto from '../crypto'; import enums from '../enums'; import util from '../util'; @@ -115,7 +115,8 @@ class SecretKeyPacket extends PublicKeyPacket { // - [Optional] If string-to-key usage octet was 255, 254, or 253, a // string-to-key specifier. The length of the string-to-key // specifier is implied by its type, as described above. - this.s2k = new S2K(); + const s2kType = bytes[i++]; + this.s2k = newS2KFromType(s2kType); i += this.s2k.read(bytes.subarray(i, bytes.length)); if (this.s2k.type === 'gnu-dummy') { @@ -279,7 +280,7 @@ class SecretKeyPacket extends PublicKeyPacket { delete this.unparseableKeyMaterial; this.isEncrypted = null; this.keyMaterial = null; - this.s2k = new S2K(config); + this.s2k = newS2KFromType(enums.s2k.gnu, config); this.s2k.algorithm = 0; this.s2k.c = 0; this.s2k.type = 'gnu-dummy'; @@ -310,8 +311,8 @@ class SecretKeyPacket extends PublicKeyPacket { throw new Error('A non-empty passphrase is required for key encryption.'); } - this.s2k = new S2K(config); - this.s2k.salt = crypto.random.getRandomBytes(8); + this.s2k = newS2KFromConfig(config); + this.s2k.generateSalt(); const cleartext = crypto.serializeParams(this.algorithm, this.privateParams); this.symmetric = enums.symmetric.aes256; const key = await produceEncryptionKey(this.s2k, passphrase, this.symmetric); diff --git a/src/packet/sym_encrypted_session_key.js b/src/packet/sym_encrypted_session_key.js index a383c711..6e940779 100644 --- a/src/packet/sym_encrypted_session_key.js +++ b/src/packet/sym_encrypted_session_key.js @@ -15,7 +15,7 @@ // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import S2K from '../type/s2k'; +import { newS2KFromConfig, newS2KFromType } from '../type/s2k'; import defaultConfig from '../config'; import crypto from '../crypto'; import enums from '../enums'; @@ -89,7 +89,8 @@ class SymEncryptedSessionKeyPacket { } // A string-to-key (S2K) specifier, length as defined above. - this.s2k = new S2K(); + const s2kType = bytes[offset++]; + this.s2k = newS2KFromType(s2kType); offset += this.s2k.read(bytes.subarray(offset, bytes.length)); if (this.version === 5) { @@ -178,8 +179,8 @@ class SymEncryptedSessionKeyPacket { this.sessionKeyEncryptionAlgorithm = algo; - this.s2k = new S2K(config); - this.s2k.salt = crypto.random.getRandomBytes(8); + this.s2k = newS2KFromConfig(config); + this.s2k.generateSalt(); const { blockSize, keySize } = crypto.getCipher(algo); const encryptionKey = await this.s2k.produceKey(passphrase, keySize); diff --git a/src/type/s2k/argon2.js b/src/type/s2k/argon2.js new file mode 100644 index 00000000..f79e3ef6 --- /dev/null +++ b/src/type/s2k/argon2.js @@ -0,0 +1,137 @@ +import defaultConfig from '../../config'; +import enums from '../../enums'; +import util from '../../util'; +import crypto from '../../crypto'; + +const ARGON2_TYPE = 0x02; // id +const ARGON2_VERSION = 0x13; +const ARGON2_SALT_SIZE = 16; + +export class Argon2OutOfMemoryError extends Error { + constructor(...params) { + super(...params); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, Argon2OutOfMemoryError); + } + + this.name = 'Argon2OutOfMemoryError'; + } +} + +// cache argon wasm module +let loadArgonWasmModule; +let argon2Promise; +// reload wasm module above this treshold, to deallocated used memory +const ARGON2_WASM_MEMORY_THRESHOLD_RELOAD = 2 << 19; + +class Argon2S2K { + /** + * @param {Object} [config] - Full configuration, defaults to openpgp.config + */ + constructor(config = defaultConfig) { + const { passes, parallelism, memoryExponent } = config.s2kArgon2Params; + + this.type = 'argon2'; + /** @type {Uint8Array} 16 bytes of salt */ + this.salt = null; + /** @type {Integer} number of passes */ + this.t = passes; + /** @type {Integer} degree of parallelism (lanes) */ + this.p = parallelism; + /** @type {Integer} exponent indicating memory size */ + this.encodedM = memoryExponent; + } + + generateSalt() { + this.salt = crypto.random.getRandomBytes(ARGON2_SALT_SIZE); + } + + /** + * Parsing function for argon2 string-to-key specifier. + * @param {Uint8Array} bytes - Payload of argon2 string-to-key specifier + * @returns {Integer} Actual length of the object. + */ + read(bytes) { + let i = 0; + + this.salt = bytes.subarray(i, i + 16); + i += 16; + + this.t = bytes[i++]; + this.p = bytes[i++]; + this.encodedM = bytes[i++]; // memory size exponent, one-octect + + return i; + } + + /** + * Serializes s2k information + * @returns {Uint8Array} Binary representation of s2k. + */ + write() { + const arr = [ + new Uint8Array([enums.write(enums.s2k, this.type)]), + this.salt, + new Uint8Array([this.t, this.p, this.encodedM]) + ]; + + return util.concatUint8Array(arr); + } + + /** + * Produces a key using the specified passphrase and the defined + * hashAlgorithm + * @param {String} passphrase - Passphrase containing user input + * @returns {Promise} Produced key with a length corresponding to `keySize` + * @throws {Argon2OutOfMemoryError|Errors} + * @async + */ + async produceKey(passphrase, keySize) { + const decodedM = 2 << (this.encodedM - 1); + + try { + // on first load, the argon2 lib is imported and the WASM module is initialized. + // the two steps need to be atomic to avoid race conditions causing multiple wasm modules + // being loaded when `argon2Promise` is not initialized. + loadArgonWasmModule = loadArgonWasmModule || (await import('argon2id')).default; + argon2Promise = argon2Promise || loadArgonWasmModule(); + + // important to keep local ref to argon2 in case the module is reloaded by another instance + const argon2 = await argon2Promise; + + const passwordBytes = util.encodeUTF8(passphrase); + const hash = argon2({ + version: ARGON2_VERSION, + type: ARGON2_TYPE, + password: passwordBytes, + salt: this.salt, + tagLength: keySize, + memorySize: decodedM, + parallelism: this.p, + passes: this.t + }); + + // 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(() => {}); + } + return hash; + } catch (e) { + if (e.message && ( + e.message.includes('Unable to grow instance memory') || // Chrome + e.message.includes('failed to grow memory') || // Firefox + e.message.includes('WebAssembly.Memory.grow') || // Safari + e.message.includes('Out of memory') // Safari iOS + )) { + throw new Argon2OutOfMemoryError('Could not allocate required memory for Argon2'); + } else { + throw e; + } + } + } +} + +export default Argon2S2K; diff --git a/src/type/s2k.js b/src/type/s2k/generic.js similarity index 92% rename from src/type/s2k.js rename to src/type/s2k/generic.js index 250c7e25..ab9b2855 100644 --- a/src/type/s2k.js +++ b/src/type/s2k/generic.js @@ -28,17 +28,17 @@ * @private */ -import defaultConfig from '../config'; -import crypto from '../crypto'; -import enums from '../enums'; -import { UnsupportedError } from '../packet/packet'; -import util from '../util'; +import defaultConfig from '../../config'; +import crypto from '../../crypto'; +import enums from '../../enums'; +import { UnsupportedError } from '../../packet/packet'; +import util from '../../util'; -class S2K { +class GenericS2K { /** * @param {Object} [config] - Full configuration, defaults to openpgp.config */ - constructor(config = defaultConfig) { + constructor(s2kType, config = defaultConfig) { /** * Hash function identifier, or 0 for gnu-dummy keys * @type {module:enums.hash | 0} @@ -48,7 +48,7 @@ class S2K { * enums.s2k identifier or 'gnu-dummy' * @type {String} */ - this.type = 'iterated'; + this.type = enums.read(enums.s2k, s2kType); /** @type {Integer} */ this.c = config.s2kIterationCountByte; /** Eight bytes of salt in a binary string. @@ -57,6 +57,14 @@ class S2K { this.salt = null; } + generateSalt() { + switch (this.type) { + case 'salted': + case 'iterated': + this.salt = crypto.random.getRandomBytes(8); + } + } + getCount() { // Exponent bias, defined in RFC4880 const expbias = 6; @@ -71,11 +79,6 @@ class S2K { */ read(bytes) { let i = 0; - try { - this.type = enums.read(enums.s2k, bytes[i++]); - } catch (err) { - throw new UnsupportedError('Unknown S2K type.'); - } this.algorithm = bytes[i++]; switch (this.type) { @@ -196,4 +199,4 @@ class S2K { } } -export default S2K; +export default GenericS2K; diff --git a/src/type/s2k/index.js b/src/type/s2k/index.js new file mode 100644 index 00000000..8a725c18 --- /dev/null +++ b/src/type/s2k/index.js @@ -0,0 +1,46 @@ +import defaultConfig from '../../config'; +import Argon2S2K, { Argon2OutOfMemoryError } from './argon2'; +import GenericS2K from './generic'; +import enums from '../../enums'; +import { UnsupportedError } from '../../packet/packet'; + +const allowedS2KTypesForEncryption = new Set([enums.s2k.argon2, enums.s2k.iterated]); + +/** + * Instantiate a new S2K instance of the given type + * @param {module:enums.s2k} type + * @oaram {Object} [config] + * @returns {Object} New s2k object + * @throws {Error} for unknown or unsupported types + */ +export function newS2KFromType(type, config = defaultConfig) { + switch (type) { + case enums.s2k.argon2: + return new Argon2S2K(config); + case enums.s2k.iterated: + case enums.s2k.gnu: + case enums.s2k.salted: + case enums.s2k.simple: + return new GenericS2K(type, config); + default: + throw new UnsupportedError('Unsupported S2K type'); + } +} + +/** + * Instantiate a new S2K instance based on the config settings + * @oaram {Object} config + * @returns {Object} New s2k object + * @throws {Error} for unknown or unsupported types + */ +export function newS2KFromConfig(config) { + const { s2kType } = config; + + if (!allowedS2KTypesForEncryption.has(s2kType)) { + throw new Error('The provided `config.s2kType` value is not allowed'); + } + + return newS2KFromType(s2kType, config); +} + +export { Argon2OutOfMemoryError }; diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 1ae43b01..f03f13b5 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -15,6 +15,7 @@ const { isAEADSupported } = require('../../src/key'); const input = require('./testInputs'); const detectNode = () => typeof globalThis.process === 'object' && typeof globalThis.process.versions === 'object'; +const detectBrowser = () => typeof navigator === 'object'; const pub_key = [ '-----BEGIN PGP PUBLIC KEY BLOCK-----', @@ -1243,6 +1244,25 @@ VFBLG8uc9IiaKann/DYBAJcZNZHRSfpDoV2pUA5EAEi2MdjxkRysFQnYPRAu expect(unlocked.isDecrypted()).to.be.true; }); + it('should support encrypting with argon2 s2k', async function() { + const key = await openpgp.readKey({ armoredKey: gnuDummyKeySigningSubkey }); + const locked = await openpgp.encryptKey({ + privateKey: key, + passphrase: passphrase, + config: { s2kType: openpgp.enums.s2k.argon2 } + }); + expect(key.isDecrypted()).to.be.true; + expect(locked.isDecrypted()).to.be.false; + expect(locked.keyPacket.isDummy()).to.be.true; + const unlocked = await openpgp.decryptKey({ + privateKey: locked, + passphrase: passphrase + }); + expect(key.isDecrypted()).to.be.true; + expect(unlocked.isDecrypted()).to.be.true; + expect(unlocked.keyPacket.isDummy()).to.be.true; + }); + it('should encrypt gnu-dummy key', async function() { const key = await openpgp.readKey({ armoredKey: gnuDummyKeySigningSubkey }); const locked = await openpgp.encryptKey({ @@ -2150,6 +2170,50 @@ VFBLG8uc9IiaKann/DYBAJcZNZHRSfpDoV2pUA5EAEi2MdjxkRysFQnYPRAu data: util.hexToUint8Array('3e99c1bb485e70a1fcef09a7ad8d38d171015243bbdd853e1a2b0e334d122ff3') })).to.be.rejectedWith(/No encryption keys or passwords provided/); }); + + it('supports decrypting with argon2 s2k (memory-heavy params)', async function() { + const passwords = 'password'; + // Test vector from https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-07.html#appendix-A.8.1 + const armoredMessage = `-----BEGIN PGP MESSAGE----- +Comment: Encrypted using AES with 128-bit key +Comment: Session key: 01FE16BBACFD1E7B78EF3B865187374F + +wycEBwScUvg8J/leUNU1RA7N/zE2AQQVnlL8rSLPP5VlQsunlO+ECxHSPgGYGKY+ +YJz4u6F+DDlDBOr5NRQXt/KJIf4m4mOlKyC/uqLbpnLJZMnTq3o79GxBTdIdOzhH +XfA3pqV4mTzF +-----END PGP MESSAGE-----`; + const expectedSessionKey = util.hexToUint8Array('01FE16BBACFD1E7B78EF3B865187374F'); + + try { + const [decryptedSessionKey] = await openpgp.decryptSessionKeys({ + message: await openpgp.readMessage({ armoredMessage }), + passwords + }); + expect(decryptedSessionKey.data).to.deep.equal(expectedSessionKey); + expect(decryptedSessionKey.algorithm).to.equal('aes128'); + } catch (err) { + if (detectBrowser()) { // Expected to fail in the CI, especially in Browserstack + expect(err.message).to.match(/Could not allocate required memory/); + } + } + + }); + + // keep this after the 'memory-heavy' test to confirm that the Wasm module was successfully reloaded + it('supports encrypting with argon2 s2k', async function() { + const config = { s2kType: openpgp.enums.s2k.argon2 }; + const passwords = 'password'; + const sessionKey = { + algorithm: 'aes128', + data: util.hexToUint8Array('01FE16BBACFD1E7B78EF3B865187374F') + }; + const encrypted = await openpgp.encryptSessionKey({ ...sessionKey, passwords, config, format: 'object' }); + expect(encrypted.packets).to.have.length(1); + const skesk = encrypted.packets[0]; + expect(skesk.s2k.type).to.equal('argon2'); + const [decryptedSessionKey] = await openpgp.decryptSessionKeys({ message: encrypted, passwords }); + expect(decryptedSessionKey).to.deep.equal(sessionKey); + }); }); describe('encrypt, decrypt, sign, verify - integration tests', function() { diff --git a/test/general/packet.js b/test/general/packet.js index 212e874f..8fd963ac 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -1042,7 +1042,7 @@ kePFjAnu9cpynKXu3usf8+FuBw2zLsg1Id1n7ttxoAte416KjBN9lFBt8mcu await expect( openpgp.PacketList.fromBinary(binaryMessage, allAllowedPackets, { ...openpgp.config, ignoreUnsupportedPackets: false }) - ).to.be.rejectedWith(/Unknown S2K type/); + ).to.be.rejectedWith(/Unsupported S2K type/); }); it('Throws on disallowed packet even with tolerant mode enabled', async function() {