Skip to content

Commit 69e773d

Browse files
committed
Add config.maxArgon2MemoryExponent for argon2 memory limit (openpgpjs#1943)
Add `config.maxArgon2MemoryExponent` for argon2 memory limit This limit is applied both on encryption (if `config.s2kType` is set to `enums.s2k.argon2`) and decryption. If the input memory exponent exceeds this value, the library will not attempt the argon2 key derivation and instead directly throw an `Argon2OutOfMemoryError` error.
1 parent 8c3eff0 commit 69e773d

File tree

11 files changed

+44
-17
lines changed

11 files changed

+44
-17
lines changed

src/config/config.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface Config {
3535
s2kType: enums.s2k.iterated | enums.s2k.argon2;
3636
s2kIterationCountByte: number;
3737
s2kArgon2Params: { passes: number, parallelism: number; memoryExponent: number; };
38+
maxArgon2MemoryExponent: number;
3839
maxUserIDLength: number;
3940
maxDecompressedMessageSize: number;
4041
knownNotations: string[];

src/config/config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,17 @@ export default {
136136
parallelism: 4, // lanes
137137
memoryExponent: 16 // 64 MiB of RAM
138138
},
139+
/**
140+
* Max memory exponent allowed for Argon2 memory allocation (e.g. `maxArgon2MemoryExponent: 20` corresponds
141+
* to a memory limit of 2**20 = 1GiB).
142+
* This limit is applied both on encryption (if `config.s2kType` is set to `enums.s2k.argon2`)
143+
* and decryption.
144+
* If the input memory exponent exceeds this value, the library will not attempt the argon2 key derivation
145+
* and instead directly throw an `Argon2OutOfMemoryError` error.
146+
* NB: on encryption, if `s2kArgon2Params.memoryExponent` is larger than `maxArgon2MemoryExponent`,
147+
* the operation will fail.
148+
*/
149+
maxArgon2MemoryExponent: Infinity,
139150
/**
140151
* Allow decryption of messages without integrity protection.
141152
* This is an **insecure** setting:

src/message.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export class Message {
185185
}
186186
await Promise.all(packets.map(async function(skeskPacket) {
187187
try {
188-
await skeskPacket.decrypt(password);
188+
await skeskPacket.decrypt(password, config);
189189
decryptedSessionKeyPackets.push(skeskPacket);
190190
} catch (err) {
191191
util.printDebugError(err);
@@ -460,7 +460,7 @@ export class Message {
460460
if (passwords) {
461461
const testDecrypt = async function(keyPacket, password) {
462462
try {
463-
await keyPacket.decrypt(password);
463+
await keyPacket.decrypt(password, config);
464464
return 1;
465465
} catch {
466466
return 0;

src/openpgp.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ export async function decryptKey({ privateKey, passphrase, config, ...rest }) {
194194
try {
195195
await Promise.all(clonedPrivateKey.getKeys().map(key => (
196196
// try to decrypt each key with any of the given passphrases
197-
util.anyPromise(passphrases.map(passphrase => key.keyPacket.decrypt(passphrase)))
197+
util.anyPromise(passphrases.map(passphrase => key.keyPacket.decrypt(passphrase, config)))
198198
)));
199199

200200
await clonedPrivateKey.validate(config);

src/packet/secret_key.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ class SecretKeyPacket extends PublicKeyPacket {
399399
this.usedModernAEAD = !this.isLegacyAEAD; // legacy AEAD does not guarantee integrity of public key material
400400

401401
const serializedPacketTag = writeTag(this.constructor.tag);
402-
const key = await produceEncryptionKey(this.version, this.s2k, passphrase, this.symmetric, this.aead, serializedPacketTag, this.isLegacyAEAD);
402+
const key = await produceEncryptionKey(this.version, this.s2k, passphrase, this.symmetric, this.aead, serializedPacketTag, this.isLegacyAEAD, config);
403403

404404
const modeInstance = await mode(this.symmetric, key);
405405
this.iv = this.isLegacyAEAD ? getRandomBytes(blockSize) : getRandomBytes(mode.ivLength);
@@ -411,7 +411,7 @@ class SecretKeyPacket extends PublicKeyPacket {
411411
} else {
412412
this.s2kUsage = 254;
413413
this.usedModernAEAD = false;
414-
const key = await produceEncryptionKey(this.version, this.s2k, passphrase, this.symmetric);
414+
const key = await produceEncryptionKey(this.version, this.s2k, passphrase, this.symmetric, undefined, undefined, undefined, config);
415415
this.iv = getRandomBytes(blockSize);
416416
this.keyMaterial = await cipherMode.cfb.encrypt(this.symmetric, key, util.concatUint8Array([
417417
cleartext,
@@ -426,10 +426,11 @@ class SecretKeyPacket extends PublicKeyPacket {
426426
* {@link SecretKeyPacket.isDecrypted} should be false, as
427427
* otherwise calls to this function will throw an error.
428428
* @param {String} passphrase - The passphrase for this private key as string
429+
* @param {Object} config
429430
* @throws {Error} if the key is already decrypted, or if decryption was not successful
430431
* @async
431432
*/
432-
async decrypt(passphrase) {
433+
async decrypt(passphrase, config = defaultConfig) {
433434
if (this.isDummy()) {
434435
return false;
435436
}
@@ -446,7 +447,7 @@ class SecretKeyPacket extends PublicKeyPacket {
446447
const serializedPacketTag = writeTag(this.constructor.tag); // relevant for AEAD only
447448
if (this.s2kUsage === 254 || this.s2kUsage === 253) {
448449
key = await produceEncryptionKey(
449-
this.version, this.s2k, passphrase, this.symmetric, this.aead, serializedPacketTag, this.isLegacyAEAD);
450+
this.version, this.s2k, passphrase, this.symmetric, this.aead, serializedPacketTag, this.isLegacyAEAD, config);
450451
} else if (this.s2kUsage === 255) {
451452
throw new Error('Encrypted private key is authenticated using an insecure two-byte hash');
452453
} else {
@@ -569,18 +570,19 @@ class SecretKeyPacket extends PublicKeyPacket {
569570
* @param {module:enums.aead} [aeadMode] - for AEAD-encrypted keys only (excluding v5)
570571
* @param {Uint8Array} [serializedPacketTag] - for AEAD-encrypted keys only (excluding v5)
571572
* @param {Boolean} [isLegacyAEAD] - for AEAD-encrypted keys from RFC4880bis (v4 and v5 only)
573+
* @param {Object} config
572574
* @returns encryption key
573575
* @access private
574576
*/
575-
async function produceEncryptionKey(keyVersion, s2k, passphrase, cipherAlgo, aeadMode, serializedPacketTag, isLegacyAEAD) {
577+
async function produceEncryptionKey(keyVersion, s2k, passphrase, cipherAlgo, aeadMode, serializedPacketTag, isLegacyAEAD, config) {
576578
if (s2k.type === 'argon2' && !aeadMode) {
577579
throw new Error('Using Argon2 S2K without AEAD is not allowed');
578580
}
579581
if (s2k.type === 'simple' && keyVersion === 6) {
580582
throw new Error('Using Simple S2K with version 6 keys is not allowed');
581583
}
582584
const { keySize } = getCipherParams(cipherAlgo);
583-
const derivedKey = await s2k.produceKey(passphrase, keySize);
585+
const derivedKey = await s2k.produceKey(passphrase, keySize, config);
584586
if (!aeadMode || keyVersion === 5 || isLegacyAEAD) {
585587
return derivedKey;
586588
}

src/packet/sym_encrypted_session_key.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,16 +157,17 @@ class SymEncryptedSessionKeyPacket {
157157
/**
158158
* Decrypts the session key with the given passphrase
159159
* @param {String} passphrase - The passphrase in string form
160+
* @param {Object} config
160161
* @throws {Error} if decryption was not successful
161162
* @async
162163
*/
163-
async decrypt(passphrase) {
164+
async decrypt(passphrase, config = defaultConfig) {
164165
const algo = this.sessionKeyEncryptionAlgorithm !== null ?
165166
this.sessionKeyEncryptionAlgorithm :
166167
this.sessionKeyAlgorithm;
167168

168169
const { blockSize, keySize } = getCipherParams(algo);
169-
const key = await this.s2k.produceKey(passphrase, keySize);
170+
const key = await this.s2k.produceKey(passphrase, keySize, config);
170171

171172
if (this.version >= 5) {
172173
const mode = cipherMode.getAEADMode(this.aeadAlgorithm, true);
@@ -206,7 +207,7 @@ class SymEncryptedSessionKeyPacket {
206207
this.s2k.generateSalt();
207208

208209
const { blockSize, keySize } = getCipherParams(algo);
209-
const key = await this.s2k.produceKey(passphrase, keySize);
210+
const key = await this.s2k.produceKey(passphrase, keySize, config);
210211

211212
if (this.sessionKey === null) {
212213
this.sessionKey = generateSessionKey(this.sessionKeyAlgorithm);

src/type/s2k/argon2.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,16 @@ class Argon2S2K {
114114
* Produces a key using the specified passphrase and the defined
115115
* hashAlgorithm
116116
* @param {String} passphrase - Passphrase containing user input
117+
* @param {Number} keySize
118+
* @param {Object} config
117119
* @returns {Promise<Uint8Array>} Produced key with a length corresponding to `keySize`
118120
* @throws {Argon2OutOfMemoryError|Errors}
119121
* @async
120122
*/
121-
async produceKey(passphrase, keySize) {
123+
async produceKey(passphrase, keySize, config) {
124+
if (this.encodedM > config.maxArgon2MemoryExponent) {
125+
throw new Argon2OutOfMemoryError('Argon2 required memory exceeds `config.maxArgon2MemoryExponent`');
126+
}
122127
const decodedM = 2 << (this.encodedM - 1);
123128

124129
try {

src/type/s2k/generic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ class GenericS2K {
156156
* hashAlgorithm hash length
157157
* @async
158158
*/
159-
async produceKey(passphrase, numBytes) {
159+
async produceKey(passphrase, numBytes, _config) {
160160
passphrase = util.encodeUTF8(passphrase);
161161

162162
const arr = [];

test/general/openpgp.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { getPreferredCipherSuite } from '../../src/key';
1414

1515
import * as input from './testInputs.js';
1616

17-
const detectBrowser = () => typeof navigator === 'object';
17+
const detectBrowser = () => typeof navigator === 'object' && !navigator.userAgent.includes('Node.js');
1818

1919
const pub_key = [
2020
'-----BEGIN PGP PUBLIC KEY BLOCK-----',
@@ -2562,7 +2562,7 @@ XfA3pqV4mTzF
25622562
expect(decryptedSessionKey.algorithm).to.equal('aes128');
25632563
} catch (err) {
25642564
if (detectBrowser()) { // Expected to fail in the CI, especially in Browserstack
2565-
expect(err.message).to.match(/Could not allocate required memory/);
2565+
expect(err.message).to.match(/Could not allocate required memory|Argon2 required memory exceeds `config.maxArgon2MemoryExponent`/);
25662566
}
25672567
}
25682568
});

test/initOpenpgp.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ if (typeof window !== 'undefined') {
1212

1313
openpgp.config.s2kIterationCountByte = 0;
1414

15+
if (typeof window !== 'undefined' &&
16+
/** Mobile Safari 26 reloads the page if Argon2 tries to allocate memory above 1GB */
17+
window.navigator.userAgent.match(/Version\/26\.\d(\.\d)* (Mobile\/\w+ )Safari/)) {
18+
19+
openpgp.config.maxArgon2MemoryExponent = 20;
20+
}
21+
1522
if (!globalThis.TransformStream) {
1623
Object.assign(globalThis, webStreamsPonyfill);
1724
}

0 commit comments

Comments
 (0)