Skip to content

Commit 54197ea

Browse files
tniessentargos
authored andcommitted
crypto: extend RSA-OAEP support with oaepHash
This adds an oaepHash option to asymmetric encryption which allows users to specify a hash function when using OAEP padding. This feature is required for interoperability with WebCrypto applications. PR-URL: #28335 Fixes: #25756 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Sam Roberts <[email protected]>
1 parent 000999c commit 54197ea

File tree

5 files changed

+117
-5
lines changed

5 files changed

+117
-5
lines changed

doc/api/crypto.md

+10
Original file line numberDiff line numberDiff line change
@@ -2307,11 +2307,16 @@ An array of supported digest functions can be retrieved using
23072307
<!-- YAML
23082308
added: v0.11.14
23092309
changes:
2310+
- version: REPLACEME
2311+
pr-url: https://github.com/nodejs/node/pull/28335
2312+
description: The `oaepHash` option was added.
23102313
- version: v11.6.0
23112314
pr-url: https://github.com/nodejs/node/pull/24234
23122315
description: This function now supports key objects.
23132316
-->
23142317
* `privateKey` {Object | string | Buffer | KeyObject}
2318+
- `oaepHash` {string} The hash function to use for OAEP padding.
2319+
**Default:** `'sha1'`
23152320
- `padding` {crypto.constants} An optional padding value defined in
23162321
`crypto.constants`, which may be: `crypto.constants.RSA_NO_PADDING`,
23172322
`crypto.constants.RSA_PKCS1_PADDING`, or
@@ -2383,12 +2388,17 @@ be passed instead of a public key.
23832388
<!-- YAML
23842389
added: v0.11.14
23852390
changes:
2391+
- version: REPLACEME
2392+
pr-url: https://github.com/nodejs/node/pull/28335
2393+
description: The `oaepHash` option was added.
23862394
- version: v11.6.0
23872395
pr-url: https://github.com/nodejs/node/pull/24234
23882396
description: This function now supports key objects.
23892397
-->
23902398
* `key` {Object | string | Buffer | KeyObject}
23912399
- `key` {string | Buffer | KeyObject} A PEM encoded public or private key.
2400+
- `oaepHash` {string} The hash function to use for OAEP padding.
2401+
**Default:** `'sha1'`
23922402
- `passphrase` {string | Buffer} An optional passphrase for the private key.
23932403
- `padding` {crypto.constants} An optional padding value defined in
23942404
`crypto.constants`, which may be: `crypto.constants.RSA_NO_PADDING`,

lib/internal/crypto/cipher.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ function rsaFunctionFor(method, defaultPadding, keyType) {
5050
preparePrivateKey(options) :
5151
preparePublicOrPrivateKey(options);
5252
const padding = options.padding || defaultPadding;
53-
return method(data, format, type, passphrase, buffer, padding);
53+
const { oaepHash } = options;
54+
if (oaepHash !== undefined && typeof oaepHash !== 'string')
55+
throw new ERR_INVALID_ARG_TYPE('options.oaepHash', 'string', oaepHash);
56+
return method(data, format, type, passphrase, buffer, padding, oaepHash);
5457
};
5558
}
5659

src/node_crypto.cc

+11
Original file line numberDiff line numberDiff line change
@@ -5225,6 +5225,7 @@ template <PublicKeyCipher::Operation operation,
52255225
bool PublicKeyCipher::Cipher(Environment* env,
52265226
const ManagedEVPPKey& pkey,
52275227
int padding,
5228+
const char* oaep_hash,
52285229
const unsigned char* data,
52295230
int len,
52305231
AllocatedBuffer* out) {
@@ -5236,6 +5237,12 @@ bool PublicKeyCipher::Cipher(Environment* env,
52365237
if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), padding) <= 0)
52375238
return false;
52385239

5240+
if (oaep_hash != nullptr) {
5241+
if (!EVP_PKEY_CTX_md(ctx.get(), EVP_PKEY_OP_TYPE_CRYPT,
5242+
EVP_PKEY_CTRL_RSA_OAEP_MD, oaep_hash))
5243+
return false;
5244+
}
5245+
52395246
size_t out_len = 0;
52405247
if (EVP_PKEY_cipher(ctx.get(), nullptr, &out_len, data, len) <= 0)
52415248
return false;
@@ -5272,6 +5279,9 @@ void PublicKeyCipher::Cipher(const FunctionCallbackInfo<Value>& args) {
52725279
uint32_t padding;
52735280
if (!args[offset + 1]->Uint32Value(env->context()).To(&padding)) return;
52745281

5282+
const node::Utf8Value oaep_str(env->isolate(), args[offset + 2]);
5283+
const char* oaep_hash = args[offset + 2]->IsString() ? *oaep_str : nullptr;
5284+
52755285
AllocatedBuffer out;
52765286

52775287
ClearErrorOnReturn clear_error_on_return;
@@ -5280,6 +5290,7 @@ void PublicKeyCipher::Cipher(const FunctionCallbackInfo<Value>& args) {
52805290
env,
52815291
pkey,
52825292
padding,
5293+
oaep_hash,
52835294
buf.data(),
52845295
buf.length(),
52855296
&out);

src/node_crypto.h

+1
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ class PublicKeyCipher {
713713
static bool Cipher(Environment* env,
714714
const ManagedEVPPKey& pkey,
715715
int padding,
716+
const char* oaep_hash,
716717
const unsigned char* data,
717718
int len,
718719
AllocatedBuffer* out);

test/parallel/test-crypto-rsa-dsa.js

+91-4
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ const decryptError = {
154154
}, decryptError);
155155
}
156156

157-
function test_rsa(padding) {
157+
function test_rsa(padding, encryptOaepHash, decryptOaepHash) {
158158
const size = (padding === 'RSA_NO_PADDING') ? rsaKeySize / 8 : 32;
159159
const input = Buffer.allocUnsafe(size);
160160
for (let i = 0; i < input.length; i++)
@@ -165,18 +165,21 @@ function test_rsa(padding) {
165165

166166
const encryptedBuffer = crypto.publicEncrypt({
167167
key: rsaPubPem,
168-
padding: padding
168+
padding: padding,
169+
oaepHash: encryptOaepHash
169170
}, bufferToEncrypt);
170171

171172
let decryptedBuffer = crypto.privateDecrypt({
172173
key: rsaKeyPem,
173-
padding: padding
174+
padding: padding,
175+
oaepHash: decryptOaepHash
174176
}, encryptedBuffer);
175177
assert.deepStrictEqual(decryptedBuffer, input);
176178

177179
decryptedBuffer = crypto.privateDecrypt({
178180
key: rsaPkcs8KeyPem,
179-
padding: padding
181+
padding: padding,
182+
oaepHash: decryptOaepHash
180183
}, encryptedBuffer);
181184
assert.deepStrictEqual(decryptedBuffer, input);
182185
}
@@ -185,6 +188,90 @@ test_rsa('RSA_NO_PADDING');
185188
test_rsa('RSA_PKCS1_PADDING');
186189
test_rsa('RSA_PKCS1_OAEP_PADDING');
187190

191+
// Test OAEP with different hash functions.
192+
test_rsa('RSA_PKCS1_OAEP_PADDING', undefined, 'sha1');
193+
test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha1', undefined);
194+
test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha256', 'sha256');
195+
test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha512', 'sha512');
196+
197+
// The following RSA-OAEP test cases were created using the WebCrypto API to
198+
// ensure compatibility when using non-SHA1 hash functions.
199+
{
200+
function testDecrypt(oaepHash, ciphertext) {
201+
const decrypted = crypto.privateDecrypt({
202+
key: rsaPkcs8KeyPem,
203+
oaepHash
204+
}, Buffer.from(ciphertext, 'hex'));
205+
206+
assert.strictEqual(decrypted.toString('utf8'), 'Hello Node.js');
207+
}
208+
209+
testDecrypt(undefined, '16ece59cf985a8cf1a3434e4b9707c922c20638fdf9abf7e5dc' +
210+
'7943f4136899348c54116d15b2c17563b9c7143f9d5b85b4561' +
211+
'5ad0598ea6d21c900f3957b65400612306a9bebae441f005646' +
212+
'f7a7c97129a103ab54e777168ef966514adb17786b968ea0ff4' +
213+
'30a524904c4a11c683764b7c8dbb60df0952768381cdba4d665' +
214+
'e5006034393a10d56d33e75b2714db824a18da46441ef7f94a3' +
215+
'4a7058c0bbad0394083a038558bcc6dd370f8e518e1bd8d73b2' +
216+
'96fc51d77da44799e4ee774926ded7910e8768f92db76f63107' +
217+
'338d33354b735d3ad094240dbd7ffdfda27ef0255306dcf4a64' +
218+
'62849492abd1a97fdd37743ff87c4d2ec89866c5cdbb696bd2b' +
219+
'30');
220+
testDecrypt('sha1', '16ece59cf985a8cf1a3434e4b9707c922c20638fdf9abf7e5dc794' +
221+
'3f4136899348c54116d15b2c17563b9c7143f9d5b85b45615ad059' +
222+
'8ea6d21c900f3957b65400612306a9bebae441f005646f7a7c9712' +
223+
'9a103ab54e777168ef966514adb17786b968ea0ff430a524904c4a' +
224+
'11c683764b7c8dbb60df0952768381cdba4d665e5006034393a10d' +
225+
'56d33e75b2714db824a18da46441ef7f94a34a7058c0bbad039408' +
226+
'3a038558bcc6dd370f8e518e1bd8d73b296fc51d77da44799e4ee7' +
227+
'74926ded7910e8768f92db76f63107338d33354b735d3ad094240d' +
228+
'bd7ffdfda27ef0255306dcf4a6462849492abd1a97fdd37743ff87' +
229+
'c4d2ec89866c5cdbb696bd2b30');
230+
testDecrypt('sha256', '16ccf09afe5eb0130182b9fc1ca4af61a38e772047cac42146bf' +
231+
'a0fa5879aa9639203e4d01442d212ff95bddfbe4661222215a2e' +
232+
'91908c37ab926edea7cfc53f83357bc27f86af0f5f2818ae141f' +
233+
'4e9e934d4e66189aff30f062c9c3f6eb9bc495a59082cb978f99' +
234+
'b56ce5fa530a8469e46129258e5c42897cb194b6805e936e5cbb' +
235+
'eaa535bad6b1d3cdfc92119b7dd325a2e6d2979e316bdacc9f80' +
236+
'e29c7bbdf6846d738e380deadcb48df8c1e8aabf7a9dd2f8c71d' +
237+
'6681dbec7dcadc01887c51288674268796bc77fdf8f1c94c9ca5' +
238+
'0b1cc7cddbaf4e56cb151d23e2c699d2844c0104ee2e7e9dcdb9' +
239+
'07cfab43339120a40c59ca54f32b8d21b48a29656c77');
240+
testDecrypt('sha512', '831b72e8dd91841729ecbddf2647d6f19dc0094734f8803d8c65' +
241+
'1b5655a12ae6156b74d9b594bcc0eacd002728380b94f46e8657' +
242+
'f130f354e03b6e7815ee257eda78dba296d67d24410c31c48e58' +
243+
'75cc79e4bde594b412be5f357f57a7ac1f1d18b718e408df162d' +
244+
'1795508e6a0616192b647ad942ea068a44fb2b323d35a3a61b92' +
245+
'6feb105d6c0b2a8fc8050222d1cf4a9e44da1f95bbc677fd6437' +
246+
'49c6c89ac551d072f04cd9320c97a8d94755c8a804954c082bed' +
247+
'7fa59199a00aca154c14a7b584b63c538daf9b9c7c90abfca193' +
248+
'87d2131f9d9b9ecfc8672249c33144d1be3bfc41558a13f99466' +
249+
'3661a3af24fd0a97619d508db36f5fc131af86fc68cf');
250+
}
251+
252+
// Test invalid oaepHash options.
253+
for (const fn of [crypto.publicEncrypt, crypto.privateDecrypt]) {
254+
assert.throws(() => {
255+
fn({
256+
key: rsaPubPem,
257+
oaepHash: 'Hello world'
258+
}, Buffer.alloc(10));
259+
}, {
260+
code: 'ERR_OSSL_EVP_INVALID_DIGEST'
261+
});
262+
263+
for (const oaepHash of [0, false, null, Symbol(), () => {}]) {
264+
common.expectsError(() => {
265+
fn({
266+
key: rsaPubPem,
267+
oaepHash
268+
}, Buffer.alloc(10));
269+
}, {
270+
code: 'ERR_INVALID_ARG_TYPE'
271+
});
272+
}
273+
}
274+
188275
// Test RSA key signing/verification
189276
let rsaSign = crypto.createSign('SHA1');
190277
let rsaVerify = crypto.createVerify('SHA1');

0 commit comments

Comments
 (0)