Skip to content

Commit

Permalink
crypto-refresh: add support for Argon2 S2K (#1597)
Browse files Browse the repository at this point in the history
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`;
  • Loading branch information
larabr committed Sep 1, 2023
1 parent a8b3599 commit 1a6901d
Show file tree
Hide file tree
Showing 14 changed files with 387 additions and 32 deletions.
10 changes: 10 additions & 0 deletions openpgp.d.ts
Expand Up @@ -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[];
Expand Down Expand Up @@ -903,4 +905,12 @@ export namespace enums {
utf8 = 117,
mime = 109
}

enum s2k {
simple = 0,
salted = 1,
iterated = 3,
argon2 = 4,
gnu = 101
}
}
38 changes: 38 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -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",
Expand Down
31 changes: 25 additions & 6 deletions rollup.config.js
Expand Up @@ -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} - ` +
Expand Down Expand Up @@ -50,7 +65,8 @@ export default Object.assign([
'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`,
'require(': 'void(',
delimiters: ['', '']
})
}),
wasm(wasmOptions.browser)
]
},
{
Expand All @@ -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: [
Expand All @@ -89,7 +106,8 @@ export default Object.assign([
'OpenPGP.js VERSION': `OpenPGP.js ${pkg.version}`,
'require(': 'void(',
delimiters: ['', '']
})
}),
wasm(wasmOptions.browser)
]
},
{
Expand All @@ -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 => {
Expand Down
33 changes: 31 additions & 2 deletions src/config/config.js
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/enums.js
Expand Up @@ -87,6 +87,7 @@ export default {
simple: 0,
salted: 1,
iterated: 3,
argon2: 4,
gnu: 101
},

Expand Down
4 changes: 4 additions & 0 deletions src/message.js
Expand Up @@ -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';
Expand Down Expand Up @@ -183,6 +184,9 @@ export class Message {
decryptedSessionKeyPackets.push(skeskPacket);
} catch (err) {
util.printDebugError(err);
if (err instanceof Argon2OutOfMemoryError) {
exception = err;
}
}
}));
}));
Expand Down
11 changes: 6 additions & 5 deletions src/packet/secret_key.js
Expand Up @@ -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';
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 5 additions & 4 deletions src/packet/sym_encrypted_session_key.js
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 1a6901d

Please sign in to comment.