crypto-refresh: add support for Argon2 S2K (#1597)

In terms of API, this feature is backwards compatible, no breaking changes.
However, since a Wasm module is loaded for the Argon2 computation, browser apps
might need to make changes to their CSP policy in order to use the feature.

Newly introduced config fields:
- `config.s2kType` (defaulting to `enums.s2k.iterated`): s2k to use on
password-based encryption as well as private key encryption;
- `config.s2kArgon2Params` (defaulting to "uniformly safe settings" from Argon
RFC): parameters to use on encryption when `config.s2kType` is set to
`enums.s2k.argon2`;
This commit is contained in:
larabr
2023-04-04 14:22:13 +02:00
committed by larabr
parent 204f32791d
commit ebf22f2ee7
14 changed files with 387 additions and 32 deletions

View File

@@ -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:

View File

@@ -91,6 +91,7 @@ export default {
simple: 0,
salted: 1,
iterated: 3,
argon2: 4,
gnu: 101
},

View File

@@ -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;
}
}
}));
}));

View File

@@ -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);

View File

@@ -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);

137
src/type/s2k/argon2.js Normal file
View File

@@ -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<Uint8Array>} 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;

View File

@@ -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;

46
src/type/s2k/index.js Normal file
View File

@@ -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 };