Skip to content

Commit 81b4562

Browse files
richardTowersseebees
authored andcommitted
feat: Support sha256, sha384, and sha512 for OAEP padding (#240)
Resolves #198 This is particularly useful because CloudFront's Field Level Encryption uses RSA_OAEP_SHA256_MGF1, which this library doesn't support yet. Support for oaepHash was added in node 12.9 (nodejs/node#28335), so this won't work for older node versions. It's still a backwards compatible change because by default `oaepHash` will be undefined, as before. Added oaepHash feature detection. It is important to be prescriptive in what options will work. Node.js versions that do not support `oaepHash` will silently encrypt data. This means that the encrypted data key would not have the security properties requested. So, `oaep_hash_supported.ts` will attempt to encrypt and report the success. This will happen only once, on initialization. The integration tests have also been updated to verify OAEP test vectors based on OAEP hash support in Node.js.
1 parent 4478069 commit 81b4562

File tree

5 files changed

+143
-68
lines changed

5 files changed

+143
-68
lines changed

modules/integration-node/src/decrypt_materials_manager_node.ts

+9-10
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
RawAesKeyringNode,
2121
WrappingSuiteIdentifier, // eslint-disable-line no-unused-vars
2222
RawAesWrappingSuiteIdentifier,
23-
RawRsaKeyringNode
23+
RawRsaKeyringNode,
24+
oaepHashSupported
2425
} from '@aws-crypto/client-node'
2526
import {
2627
RsaKeyInfo, // eslint-disable-line no-unused-vars
@@ -82,18 +83,16 @@ export function rsaKeyring (keyInfo: RsaKeyInfo, key: RSAKey) {
8283
const rsaKey = key.type === 'private'
8384
? { privateKey: key.material }
8485
: { publicKey: key.material }
85-
const padding = rsaPadding(keyInfo)
86-
return new RawRsaKeyringNode({ keyName, keyNamespace, rsaKey, padding })
86+
const { padding, oaepHash } = rsaPadding(keyInfo)
87+
return new RawRsaKeyringNode({ keyName, keyNamespace, rsaKey, padding, oaepHash })
8788
}
8889

8990
export function rsaPadding (keyInfo: RsaKeyInfo) {
90-
const paddingAlgorithm = keyInfo['padding-algorithm']
91-
const paddingHash = keyInfo['padding-hash']
92-
93-
if (paddingAlgorithm === 'pkcs1') return constants.RSA_PKCS1_PADDING
94-
needs(paddingHash === 'sha1', 'Not supported at this time.')
95-
96-
return constants.RSA_PKCS1_OAEP_PADDING
91+
if (keyInfo['padding-algorithm'] === 'pkcs1') return { padding: constants.RSA_PKCS1_PADDING }
92+
const padding = constants.RSA_PKCS1_OAEP_PADDING
93+
const oaepHash = keyInfo['padding-hash']
94+
needs(oaepHashSupported || oaepHash === 'sha1', 'Not supported at this time.')
95+
return { padding, oaepHash }
9796
}
9897

9998
export class NotSupported extends Error {

modules/raw-rsa-keyring-node/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414
*/
1515

1616
export * from './raw_rsa_keyring_node'
17+
export * from './oaep_hash_supported'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use
5+
* this file except in compliance with the License. A copy of the License is
6+
* located at
7+
*
8+
* http://aws.amazon.com/apache2.0/
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed on an
11+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
* implied. See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
/* oaepHash support was added in Node.js v12.9.1 (https://github.com/nodejs/node/pull/28335)
17+
* However, the integration tests need to be able to verify functionality on other versions.
18+
* There are no constants to sniff,
19+
* and looking at the version would not catch back-ports.
20+
* So I simply try the function.
21+
* However there is a rub as the test might seem backwards.
22+
* Sending an invalid hash to the version that supports oaepHash will throw an error.
23+
* But sending an invalid hash to a version that does not support oaepHash will be ignored.
24+
*/
25+
26+
import {
27+
needs
28+
} from '@aws-crypto/material-management-node'
29+
30+
import {
31+
constants,
32+
publicEncrypt
33+
} from 'crypto'
34+
35+
export const oaepHashSupported = (function () {
36+
const key = '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAs7RoNYEPAIws89VV+kra\nrVv/4wbdmUAaAKWgWuxZi5na9GJSmnhCkqyLRm7wPbQY4LCoa5/IMUxkHLsYDPdu\nudY0Qm0GcoxOlvJKHYo4RjF7HyiS34D6dvyO4Gd3aq0mZHoxSGCxW/7hf03wEMzc\niVJXWHXhaI0lD6nrzIEgLrE4L+3V2LeAQjvZsTKd+bYMqeZOL2syiVVIAU8POwAG\nGVBroJoveFm/SUp6lCiN0M2kTeyQA2ax3QTtZSAa8nwrI7U52XOzVmdMicJsy2Pg\nuW98te3MuODdK24yNkHIkYameP/Umf/SJshUJQd5a/TUp3XE+HhOWAumx22tIDlC\nvZS11cuk2fp0WeHUnXaC19N5qWKfvHEKSugzty/z3lGP7ItFhrF2X1qJHeAAsL11\nkjo6Lc48KsE1vKvbnW4VLyB3wdNiVvmUNO29tPXwaR0Q5Gbr3jk3nUzdkEHouHWQ\n41lubOHCCBN3V13mh/MgtNhESHjfmmOnh54ErD9saA1d7CjTf8g2wqmjEqvGSW6N\nq7zhcWR2tp1olflS7oHzul4/I3hnkfL6Kb2xAWWaQKvg3mtsY2OPlzFEP0tR5UcH\nPfp5CeS1Xzg7hN6vRICW6m4l3u2HJFld2akDMm1vnSz8RCbPW7jp7YBxUkWJmypM\ntG7Yv2aGZXGbUtM8o1cZarECAwEAAQ==\n-----END PUBLIC KEY-----'
37+
38+
const oaepHash = 'i_am_not_valid'
39+
try {
40+
// @ts-ignore
41+
publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash }, Buffer.from([1, 2, 3, 4]))
42+
/* See note above,
43+
* only versions that support oaepHash will respond.
44+
* So the only way I can get here is if the option was ignored.
45+
*/
46+
return false
47+
} catch (ex) {
48+
needs(ex.code === 'ERR_OSSL_EVP_INVALID_DIGEST', 'Unexpected error testing oaepHash.')
49+
return true
50+
}
51+
})()

modules/raw-rsa-keyring-node/src/raw_rsa_keyring_node.ts

+18-9
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ import {
3232
constants,
3333
publicEncrypt,
3434
privateDecrypt,
35-
randomBytes
35+
randomBytes,
36+
RsaPublicKey, // eslint-disable-line no-unused-vars
37+
RsaPrivateKey // eslint-disable-line no-unused-vars
3638
} from 'crypto'
3739

3840
import {
@@ -42,6 +44,8 @@ import {
4244
UnwrapKey // eslint-disable-line no-unused-vars
4345
} from '@aws-crypto/raw-keyring'
4446

47+
import { oaepHashSupported } from './oaep_hash_supported'
48+
4549
/* Interface question:
4650
* When creating a keyring being able to define
4751
* if the keyring can be used for encrypt/decrypt/both
@@ -57,18 +61,17 @@ interface RsaKey {
5761
privateKey?: string | Buffer | AwsEsdkKeyObject
5862
}
5963

64+
export type OaepHash = 'sha1'|'sha256'|'sha384'|'sha512'|undefined
65+
const supportedOaepHash: OaepHash[] = ['sha1', 'sha256', 'sha384', 'sha512', undefined]
66+
6067
export type RawRsaKeyringNodeInput = {
6168
keyNamespace: string
6269
keyName: string
6370
rsaKey: RsaKey
6471
padding?: number
72+
oaepHash?: OaepHash
6573
}
6674

67-
/* Node supports RSA_OAEP_SHA1_MFG1 by default.
68-
* It does not support RSA_OAEP_SHA256_MFG1 at this time.
69-
* Passing RSA_PKCS1_OAEP_PADDING implies RSA_OAEP_SHA1_MFG1.
70-
*/
71-
7275
export class RawRsaKeyringNode extends KeyringNode {
7376
public keyNamespace!: string
7477
public keyName!: string
@@ -78,19 +81,25 @@ export class RawRsaKeyringNode extends KeyringNode {
7881
constructor (input: RawRsaKeyringNodeInput) {
7982
super()
8083

81-
const { rsaKey, keyName, keyNamespace, padding = constants.RSA_PKCS1_OAEP_PADDING } = input
84+
const { rsaKey, keyName, keyNamespace, padding = constants.RSA_PKCS1_OAEP_PADDING, oaepHash } = input
8285
const { publicKey, privateKey } = rsaKey
8386
/* Precondition: RsaKeyringNode needs either a public or a private key to operate. */
8487
needs(publicKey || privateKey, 'No Key provided.')
8588
/* Precondition: RsaKeyringNode needs identifying information for encrypt and decrypt. */
8689
needs(keyName && keyNamespace, 'Identifying information must be defined.')
90+
/* Precondition: The AWS ESDK only supports specific hash values for OAEP padding. */
91+
needs(padding === constants.RSA_PKCS1_OAEP_PADDING
92+
? oaepHashSupported
93+
? supportedOaepHash.includes(oaepHash)
94+
: !oaepHash || oaepHash === 'sha1'
95+
: !oaepHash, 'Unsupported oaepHash')
8796

8897
const _wrapKey = async (material: NodeEncryptionMaterial) => {
8998
/* Precondition: Public key must be defined to support encrypt. */
9099
if (!publicKey) throw new Error('No public key defined in constructor. Encrypt disabled.')
91100
const { buffer, byteOffset, byteLength } = unwrapDataKey(material.getUnencryptedDataKey())
92101
const encryptedDataKey = publicEncrypt(
93-
{ key: publicKey, padding },
102+
{ key: publicKey, padding, oaepHash } as RsaPublicKey,
94103
Buffer.from(buffer, byteOffset, byteLength))
95104
const providerInfo = this.keyName
96105
const providerId = this.keyNamespace
@@ -112,7 +121,7 @@ export class RawRsaKeyringNode extends KeyringNode {
112121
const { buffer, byteOffset, byteLength } = edk.encryptedDataKey
113122
const encryptedDataKey = Buffer.from(buffer, byteOffset, byteLength)
114123
const unencryptedDataKey = privateDecrypt(
115-
{ key: privateKey, padding },
124+
{ key: privateKey, padding, oaepHash } as RsaPrivateKey,
116125
encryptedDataKey)
117126
return material.setUnencryptedDataKey(unencryptedDataKey, trace)
118127
}

modules/raw-rsa-keyring-node/test/raw_rsa_keyring_node.test.ts

+64-49
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import * as chai from 'chai'
1919
import chaiAsPromised from 'chai-as-promised'
2020
import 'mocha'
2121
import {
22-
RawRsaKeyringNode
22+
RawRsaKeyringNode,
23+
OaepHash // eslint-disable-line no-unused-vars
2324
} from '../src/index'
2425
import {
2526
KeyringNode,
@@ -30,6 +31,7 @@ import {
3031
NodeDecryptionMaterial,
3132
unwrapDataKey
3233
} from '@aws-crypto/material-management-node'
34+
import { oaepHashSupported } from '../src/oaep_hash_supported'
3335

3436
chai.use(chaiAsPromised)
3537
const { expect } = chai
@@ -108,6 +110,16 @@ describe('RawRsaKeyringNode::constructor', () => {
108110
})).to.throw()
109111
})
110112

113+
it('Precondition: The AWS ESDK only supports specific hash values for OAEP padding.', () => {
114+
expect(() => new RawRsaKeyringNode({
115+
keyName,
116+
keyNamespace,
117+
// @ts-ignore Valid hash, but not supported by the ESDK
118+
oaepHash: 'rmd160',
119+
rsaKey: { privateKey: privatePem }
120+
})).to.throw('Unsupported oaepHash')
121+
})
122+
111123
it('Precondition: RsaKeyringNode needs identifying information for encrypt and decrypt.', () => {
112124
// @ts-ignore Typescript is trying to save us.
113125
expect(() => new RawRsaKeyringNode({
@@ -126,58 +138,61 @@ describe('RawRsaKeyringNode::constructor', () => {
126138
})
127139
})
128140

129-
describe('RawRsaKeyringNode encrypt/decrypt', () => {
130-
const keyNamespace = 'keyNamespace'
131-
const keyName = 'keyName'
132-
const keyring = new RawRsaKeyringNode({
133-
rsaKey: { privateKey: privatePem, publicKey: publicPem },
134-
keyName,
135-
keyNamespace
136-
})
137-
let encryptedDataKey: EncryptedDataKey
138-
139-
it('can encrypt and create unencrypted data key', async () => {
140-
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256)
141-
const material = new NodeEncryptionMaterial(suite, {})
142-
const test = await keyring.onEncrypt(material)
143-
expect(test.hasValidKey()).to.equal(true)
144-
const udk = unwrapDataKey(test.getUnencryptedDataKey())
145-
expect(udk).to.have.lengthOf(suite.keyLengthBytes)
146-
expect(test.encryptedDataKeys).to.have.lengthOf(1)
147-
const [edk] = test.encryptedDataKeys
148-
expect(edk.providerId).to.equal(keyNamespace)
149-
encryptedDataKey = edk
150-
})
151-
152-
it('can decrypt an EncryptedDataKey', async () => {
153-
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256)
154-
const material = new NodeDecryptionMaterial(suite, {})
155-
const test = await keyring.onDecrypt(material, [encryptedDataKey])
156-
expect(test.hasValidKey()).to.equal(true)
157-
})
158-
159-
it('Precondition: Public key must be defined to support encrypt.', async () => {
141+
const oaepHashOptions: OaepHash[] = [undefined, 'sha1', 'sha256', 'sha384', 'sha512']
142+
oaepHashOptions
143+
.filter(oaepHash => oaepHashSupported || [undefined, 'sha1'].includes(oaepHash))
144+
.forEach(oaepHash => describe(`RawRsaKeyringNode encrypt/decrypt for oaepHash=${oaepHash || 'undefined'}`, () => {
145+
const keyNamespace = 'keyNamespace'
146+
const keyName = 'keyName'
160147
const keyring = new RawRsaKeyringNode({
161-
rsaKey: { privateKey: privatePem },
148+
rsaKey: { privateKey: privatePem, publicKey: publicPem },
162149
keyName,
163-
keyNamespace
150+
keyNamespace,
151+
oaepHash
152+
})
153+
let encryptedDataKey: EncryptedDataKey
154+
155+
it('can encrypt and create unencrypted data key', async () => {
156+
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256)
157+
const material = new NodeEncryptionMaterial(suite, {})
158+
const test = await keyring.onEncrypt(material)
159+
expect(test.hasValidKey()).to.equal(true)
160+
const udk = unwrapDataKey(test.getUnencryptedDataKey())
161+
expect(udk).to.have.lengthOf(suite.keyLengthBytes)
162+
expect(test.encryptedDataKeys).to.have.lengthOf(1)
163+
const [edk] = test.encryptedDataKeys
164+
expect(edk.providerId).to.equal(keyNamespace)
165+
encryptedDataKey = edk
164166
})
165167

166-
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256)
167-
const material = new NodeEncryptionMaterial(suite, {})
168-
expect(keyring.onEncrypt(material)).to.rejectedWith(Error)
169-
})
168+
it('can decrypt an EncryptedDataKey', async () => {
169+
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256)
170+
const material = new NodeDecryptionMaterial(suite, {})
171+
const test = await keyring.onDecrypt(material, [encryptedDataKey])
172+
expect(test.hasValidKey()).to.equal(true)
173+
})
170174

171-
it('Precondition: Private key must be defined to support decrypt.', async () => {
172-
const keyring = new RawRsaKeyringNode({
173-
rsaKey: { publicKey: publicPem },
174-
keyName,
175-
keyNamespace
175+
it('Precondition: Public key must be defined to support encrypt.', async () => {
176+
const keyring = new RawRsaKeyringNode({
177+
rsaKey: { privateKey: privatePem },
178+
keyName,
179+
keyNamespace
180+
})
181+
182+
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256)
183+
const material = new NodeEncryptionMaterial(suite, {})
184+
return expect(keyring.onEncrypt(material)).to.rejectedWith(Error)
176185
})
177186

178-
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256)
179-
const material = new NodeDecryptionMaterial(suite, {})
180-
await keyring.onDecrypt(material, [encryptedDataKey])
181-
expect(keyring.onDecrypt(material, [encryptedDataKey])).to.rejectedWith(Error)
182-
})
183-
})
187+
it('Precondition: Private key must be defined to support decrypt.', async () => {
188+
const keyring = new RawRsaKeyringNode({
189+
rsaKey: { publicKey: publicPem },
190+
keyName,
191+
keyNamespace
192+
})
193+
194+
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256)
195+
const material = new NodeDecryptionMaterial(suite, {})
196+
return expect(keyring._unwrapKey(material, encryptedDataKey)).to.rejectedWith(Error)
197+
})
198+
}))

0 commit comments

Comments
 (0)