mirror of
https://github.com/openpgpjs/openpgpjs.git
synced 2025-07-04 12:02:29 +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:
parent
204f32791d
commit
ebf22f2ee7
10
openpgp.d.ts
vendored
10
openpgp.d.ts
vendored
@ -332,7 +332,9 @@ interface Config {
|
|||||||
v5Keys: boolean;
|
v5Keys: boolean;
|
||||||
preferredAEADAlgorithm: enums.aead;
|
preferredAEADAlgorithm: enums.aead;
|
||||||
aeadChunkSizeByte: number;
|
aeadChunkSizeByte: number;
|
||||||
|
s2kType: enums.s2k.iterated | enums.s2k.argon2;
|
||||||
s2kIterationCountByte: number;
|
s2kIterationCountByte: number;
|
||||||
|
s2kArgon2Params: { passes: number, parallelism: number; memoryExponent: number; };
|
||||||
minBytesForWebCrypto: number;
|
minBytesForWebCrypto: number;
|
||||||
maxUserIDLength: number;
|
maxUserIDLength: number;
|
||||||
knownNotations: string[];
|
knownNotations: string[];
|
||||||
@ -909,4 +911,12 @@ export namespace enums {
|
|||||||
utf8 = 117,
|
utf8 = 117,
|
||||||
mime = 109
|
mime = 109
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum s2k {
|
||||||
|
simple = 0,
|
||||||
|
salted = 1,
|
||||||
|
iterated = 3,
|
||||||
|
argon2 = 4,
|
||||||
|
gnu = 101
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
38
package-lock.json
generated
38
package-lock.json
generated
@ -22,7 +22,9 @@
|
|||||||
"@rollup/plugin-commonjs": "^11.1.0",
|
"@rollup/plugin-commonjs": "^11.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||||
"@rollup/plugin-replace": "^2.3.2",
|
"@rollup/plugin-replace": "^2.3.2",
|
||||||
|
"@rollup/plugin-wasm": "^6.1.2",
|
||||||
"@types/chai": "^4.2.14",
|
"@types/chai": "^4.2.14",
|
||||||
|
"argon2id": "^1.0.1",
|
||||||
"benchmark": "^2.1.4",
|
"benchmark": "^2.1.4",
|
||||||
"bn.js": "^4.11.8",
|
"bn.js": "^4.11.8",
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
@ -693,6 +695,23 @@
|
|||||||
"rollup": "^1.20.0 || ^2.0.0"
|
"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": {
|
"node_modules/@rollup/pluginutils": {
|
||||||
"version": "3.0.9",
|
"version": "3.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.9.tgz",
|
||||||
@ -952,6 +971,12 @@
|
|||||||
"integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=",
|
"integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=",
|
||||||
"dev": true
|
"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": {
|
"node_modules/argparse": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
|
||||||
@ -7999,6 +8024,13 @@
|
|||||||
"magic-string": "^0.25.5"
|
"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": {
|
"@rollup/pluginutils": {
|
||||||
"version": "3.0.9",
|
"version": "3.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.9.tgz",
|
||||||
@ -8214,6 +8246,12 @@
|
|||||||
"integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=",
|
"integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=",
|
||||||
"dev": true
|
"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": {
|
"argparse": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
|
||||||
|
@ -64,7 +64,9 @@
|
|||||||
"@rollup/plugin-commonjs": "^11.1.0",
|
"@rollup/plugin-commonjs": "^11.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||||
"@rollup/plugin-replace": "^2.3.2",
|
"@rollup/plugin-replace": "^2.3.2",
|
||||||
|
"@rollup/plugin-wasm": "^6.1.2",
|
||||||
"@types/chai": "^4.2.14",
|
"@types/chai": "^4.2.14",
|
||||||
|
"argon2id": "^1.0.1",
|
||||||
"benchmark": "^2.1.4",
|
"benchmark": "^2.1.4",
|
||||||
"bn.js": "^4.11.8",
|
"bn.js": "^4.11.8",
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
|
@ -6,10 +6,25 @@ import resolve from '@rollup/plugin-node-resolve';
|
|||||||
import commonjs from '@rollup/plugin-commonjs';
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
import replace from '@rollup/plugin-replace';
|
import replace from '@rollup/plugin-replace';
|
||||||
import { terser } from 'rollup-plugin-terser';
|
import { terser } from 'rollup-plugin-terser';
|
||||||
|
import { wasm } from '@rollup/plugin-wasm';
|
||||||
|
|
||||||
|
|
||||||
import pkg from './package.json';
|
import pkg from './package.json';
|
||||||
|
|
||||||
const nodeDependencies = Object.keys(pkg.dependencies);
|
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 =
|
const banner =
|
||||||
`/*! OpenPGP.js v${pkg.version} - ` +
|
`/*! OpenPGP.js v${pkg.version} - ` +
|
||||||
@ -50,7 +65,8 @@ export default Object.assign([
|
|||||||
'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`,
|
'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`,
|
||||||
'require(': 'void(',
|
'require(': 'void(',
|
||||||
delimiters: ['', '']
|
delimiters: ['', '']
|
||||||
})
|
}),
|
||||||
|
wasm(wasmOptions.browser)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -68,14 +84,15 @@ export default Object.assign([
|
|||||||
commonjs(),
|
commonjs(),
|
||||||
replace({
|
replace({
|
||||||
'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`
|
'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`
|
||||||
})
|
}),
|
||||||
|
wasm(wasmOptions.node)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: 'src/index.js',
|
input: 'src/index.js',
|
||||||
output: [
|
output: [
|
||||||
{ dir: 'dist/lightweight', entryFileNames: 'openpgp.mjs', chunkFileNames: '[name].mjs', format: 'es', banner, intro },
|
{ dir: 'dist/lightweight', entryFileNames: 'openpgp.mjs', chunkFileNames: chunkInfo => getChunkFileName(chunkInfo, '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.min.mjs', chunkFileNames: chunkInfo => getChunkFileName(chunkInfo, 'min.mjs'), format: 'es', banner, intro, plugins: [terser(terserOptions)], sourcemap: true }
|
||||||
],
|
],
|
||||||
preserveEntrySignatures: 'allow-extension',
|
preserveEntrySignatures: 'allow-extension',
|
||||||
plugins: [
|
plugins: [
|
||||||
@ -89,7 +106,8 @@ export default Object.assign([
|
|||||||
'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`,
|
'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`,
|
||||||
'require(': 'void(',
|
'require(': 'void(',
|
||||||
delimiters: ['', '']
|
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;`,
|
"import openpgpjs from '../../..';": `import * as openpgpjs from '/dist/${process.env.npm_config_lightweight ? 'lightweight/' : ''}openpgp.mjs'; window.openpgp = openpgpjs;`,
|
||||||
'require(': 'void(',
|
'require(': 'void(',
|
||||||
delimiters: ['', '']
|
delimiters: ['', '']
|
||||||
})
|
}),
|
||||||
|
wasm(wasmOptions.browser)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
].filter(config => {
|
].filter(config => {
|
||||||
|
@ -75,13 +75,42 @@ export default {
|
|||||||
* @property {Boolean} v5Keys
|
* @property {Boolean} v5Keys
|
||||||
*/
|
*/
|
||||||
v5Keys: false,
|
v5Keys: false,
|
||||||
|
/**
|
||||||
|
* 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}:
|
* {@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)
|
* 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
|
* @memberof module:config
|
||||||
* @property {Integer} s2kIterationCountByte
|
* @property {Integer} s2kIterationCountByte
|
||||||
*/
|
*/
|
||||||
s2kIterationCountByte: 224,
|
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.
|
* Allow decryption of messages without integrity protection.
|
||||||
* This is an **insecure** setting:
|
* This is an **insecure** setting:
|
||||||
|
@ -91,6 +91,7 @@ export default {
|
|||||||
simple: 0,
|
simple: 0,
|
||||||
salted: 1,
|
salted: 1,
|
||||||
iterated: 3,
|
iterated: 3,
|
||||||
|
argon2: 4,
|
||||||
gnu: 101
|
gnu: 101
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
import * as stream from '@openpgp/web-stream-tools';
|
import * as stream from '@openpgp/web-stream-tools';
|
||||||
import { armor, unarmor } from './encoding/armor';
|
import { armor, unarmor } from './encoding/armor';
|
||||||
import KeyID from './type/keyid';
|
import KeyID from './type/keyid';
|
||||||
|
import { Argon2OutOfMemoryError } from './type/s2k';
|
||||||
import defaultConfig from './config';
|
import defaultConfig from './config';
|
||||||
import crypto from './crypto';
|
import crypto from './crypto';
|
||||||
import enums from './enums';
|
import enums from './enums';
|
||||||
@ -183,6 +184,9 @@ export class Message {
|
|||||||
decryptedSessionKeyPackets.push(skeskPacket);
|
decryptedSessionKeyPackets.push(skeskPacket);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
util.printDebugError(err);
|
util.printDebugError(err);
|
||||||
|
if (err instanceof Argon2OutOfMemoryError) {
|
||||||
|
exception = err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
|
||||||
import PublicKeyPacket from './public_key';
|
import PublicKeyPacket from './public_key';
|
||||||
import S2K from '../type/s2k';
|
import { newS2KFromConfig, newS2KFromType } from '../type/s2k';
|
||||||
import crypto from '../crypto';
|
import crypto from '../crypto';
|
||||||
import enums from '../enums';
|
import enums from '../enums';
|
||||||
import util from '../util';
|
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
|
// - [Optional] If string-to-key usage octet was 255, 254, or 253, a
|
||||||
// string-to-key specifier. The length of the string-to-key
|
// string-to-key specifier. The length of the string-to-key
|
||||||
// specifier is implied by its type, as described above.
|
// 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));
|
i += this.s2k.read(bytes.subarray(i, bytes.length));
|
||||||
|
|
||||||
if (this.s2k.type === 'gnu-dummy') {
|
if (this.s2k.type === 'gnu-dummy') {
|
||||||
@ -279,7 +280,7 @@ class SecretKeyPacket extends PublicKeyPacket {
|
|||||||
delete this.unparseableKeyMaterial;
|
delete this.unparseableKeyMaterial;
|
||||||
this.isEncrypted = null;
|
this.isEncrypted = null;
|
||||||
this.keyMaterial = null;
|
this.keyMaterial = null;
|
||||||
this.s2k = new S2K(config);
|
this.s2k = newS2KFromType(enums.s2k.gnu, config);
|
||||||
this.s2k.algorithm = 0;
|
this.s2k.algorithm = 0;
|
||||||
this.s2k.c = 0;
|
this.s2k.c = 0;
|
||||||
this.s2k.type = 'gnu-dummy';
|
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.');
|
throw new Error('A non-empty passphrase is required for key encryption.');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.s2k = new S2K(config);
|
this.s2k = newS2KFromConfig(config);
|
||||||
this.s2k.salt = crypto.random.getRandomBytes(8);
|
this.s2k.generateSalt();
|
||||||
const cleartext = crypto.serializeParams(this.algorithm, this.privateParams);
|
const cleartext = crypto.serializeParams(this.algorithm, this.privateParams);
|
||||||
this.symmetric = enums.symmetric.aes256;
|
this.symmetric = enums.symmetric.aes256;
|
||||||
const key = await produceEncryptionKey(this.s2k, passphrase, this.symmetric);
|
const key = await produceEncryptionKey(this.s2k, passphrase, this.symmetric);
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
// License along with this library; if not, write to the Free Software
|
// License along with this library; if not, write to the Free Software
|
||||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
// 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 defaultConfig from '../config';
|
||||||
import crypto from '../crypto';
|
import crypto from '../crypto';
|
||||||
import enums from '../enums';
|
import enums from '../enums';
|
||||||
@ -89,7 +89,8 @@ class SymEncryptedSessionKeyPacket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A string-to-key (S2K) specifier, length as defined above.
|
// 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));
|
offset += this.s2k.read(bytes.subarray(offset, bytes.length));
|
||||||
|
|
||||||
if (this.version === 5) {
|
if (this.version === 5) {
|
||||||
@ -178,8 +179,8 @@ class SymEncryptedSessionKeyPacket {
|
|||||||
|
|
||||||
this.sessionKeyEncryptionAlgorithm = algo;
|
this.sessionKeyEncryptionAlgorithm = algo;
|
||||||
|
|
||||||
this.s2k = new S2K(config);
|
this.s2k = newS2KFromConfig(config);
|
||||||
this.s2k.salt = crypto.random.getRandomBytes(8);
|
this.s2k.generateSalt();
|
||||||
|
|
||||||
const { blockSize, keySize } = crypto.getCipher(algo);
|
const { blockSize, keySize } = crypto.getCipher(algo);
|
||||||
const encryptionKey = await this.s2k.produceKey(passphrase, keySize);
|
const encryptionKey = await this.s2k.produceKey(passphrase, keySize);
|
||||||
|
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
|
* @private
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import defaultConfig from '../config';
|
import defaultConfig from '../../config';
|
||||||
import crypto from '../crypto';
|
import crypto from '../../crypto';
|
||||||
import enums from '../enums';
|
import enums from '../../enums';
|
||||||
import { UnsupportedError } from '../packet/packet';
|
import { UnsupportedError } from '../../packet/packet';
|
||||||
import util from '../util';
|
import util from '../../util';
|
||||||
|
|
||||||
class S2K {
|
class GenericS2K {
|
||||||
/**
|
/**
|
||||||
* @param {Object} [config] - Full configuration, defaults to openpgp.config
|
* @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
|
* Hash function identifier, or 0 for gnu-dummy keys
|
||||||
* @type {module:enums.hash | 0}
|
* @type {module:enums.hash | 0}
|
||||||
@ -48,7 +48,7 @@ class S2K {
|
|||||||
* enums.s2k identifier or 'gnu-dummy'
|
* enums.s2k identifier or 'gnu-dummy'
|
||||||
* @type {String}
|
* @type {String}
|
||||||
*/
|
*/
|
||||||
this.type = 'iterated';
|
this.type = enums.read(enums.s2k, s2kType);
|
||||||
/** @type {Integer} */
|
/** @type {Integer} */
|
||||||
this.c = config.s2kIterationCountByte;
|
this.c = config.s2kIterationCountByte;
|
||||||
/** Eight bytes of salt in a binary string.
|
/** Eight bytes of salt in a binary string.
|
||||||
@ -57,6 +57,14 @@ class S2K {
|
|||||||
this.salt = null;
|
this.salt = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateSalt() {
|
||||||
|
switch (this.type) {
|
||||||
|
case 'salted':
|
||||||
|
case 'iterated':
|
||||||
|
this.salt = crypto.random.getRandomBytes(8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getCount() {
|
getCount() {
|
||||||
// Exponent bias, defined in RFC4880
|
// Exponent bias, defined in RFC4880
|
||||||
const expbias = 6;
|
const expbias = 6;
|
||||||
@ -71,11 +79,6 @@ class S2K {
|
|||||||
*/
|
*/
|
||||||
read(bytes) {
|
read(bytes) {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
try {
|
|
||||||
this.type = enums.read(enums.s2k, bytes[i++]);
|
|
||||||
} catch (err) {
|
|
||||||
throw new UnsupportedError('Unknown S2K type.');
|
|
||||||
}
|
|
||||||
this.algorithm = bytes[i++];
|
this.algorithm = bytes[i++];
|
||||||
|
|
||||||
switch (this.type) {
|
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 };
|
@ -15,6 +15,7 @@ const { isAEADSupported } = require('../../src/key');
|
|||||||
const input = require('./testInputs');
|
const input = require('./testInputs');
|
||||||
|
|
||||||
const detectNode = () => typeof globalThis.process === 'object' && typeof globalThis.process.versions === 'object';
|
const detectNode = () => typeof globalThis.process === 'object' && typeof globalThis.process.versions === 'object';
|
||||||
|
const detectBrowser = () => typeof navigator === 'object';
|
||||||
|
|
||||||
const pub_key = [
|
const pub_key = [
|
||||||
'-----BEGIN PGP PUBLIC KEY BLOCK-----',
|
'-----BEGIN PGP PUBLIC KEY BLOCK-----',
|
||||||
@ -1243,6 +1244,25 @@ VFBLG8uc9IiaKann/DYBAJcZNZHRSfpDoV2pUA5EAEi2MdjxkRysFQnYPRAu
|
|||||||
expect(unlocked.isDecrypted()).to.be.true;
|
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() {
|
it('should encrypt gnu-dummy key', async function() {
|
||||||
const key = await openpgp.readKey({ armoredKey: gnuDummyKeySigningSubkey });
|
const key = await openpgp.readKey({ armoredKey: gnuDummyKeySigningSubkey });
|
||||||
const locked = await openpgp.encryptKey({
|
const locked = await openpgp.encryptKey({
|
||||||
@ -2150,6 +2170,50 @@ VFBLG8uc9IiaKann/DYBAJcZNZHRSfpDoV2pUA5EAEi2MdjxkRysFQnYPRAu
|
|||||||
data: util.hexToUint8Array('3e99c1bb485e70a1fcef09a7ad8d38d171015243bbdd853e1a2b0e334d122ff3')
|
data: util.hexToUint8Array('3e99c1bb485e70a1fcef09a7ad8d38d171015243bbdd853e1a2b0e334d122ff3')
|
||||||
})).to.be.rejectedWith(/No encryption keys or passwords provided/);
|
})).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() {
|
describe('encrypt, decrypt, sign, verify - integration tests', function() {
|
||||||
|
@ -1042,7 +1042,7 @@ kePFjAnu9cpynKXu3usf8+FuBw2zLsg1Id1n7ttxoAte416KjBN9lFBt8mcu
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
openpgp.PacketList.fromBinary(binaryMessage, allAllowedPackets, { ...openpgp.config, ignoreUnsupportedPackets: false })
|
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() {
|
it('Throws on disallowed packet even with tolerant mode enabled', async function() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user