mirror of
https://github.com/openpgpjs/openpgpjs.git
synced 2026-03-12 11:39:23 +00:00
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:
137
src/type/s2k/argon2.js
Normal file
137
src/type/s2k/argon2.js
Normal 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;
|
||||
@@ -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
46
src/type/s2k/index.js
Normal 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 };
|
||||
Reference in New Issue
Block a user