Skip to content

Commit 8cd6b26

Browse files
committed
fix: oaepHash feature detection
It is important to be perscriptive 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 initializaion. Both the tests, and the integration tests have been updated honor `oaepHashSupported` values
1 parent d8caf36 commit 8cd6b26

File tree

5 files changed

+137
-58
lines changed

5 files changed

+137
-58
lines changed

modules/integration-node/src/decrypt_materials_manager_node.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
*/
1515

1616
import {
17+
needs,
1718
MultiKeyringNode,
1819
KmsKeyringNode,
1920
RawAesKeyringNode,
2021
WrappingSuiteIdentifier, // eslint-disable-line no-unused-vars
2122
RawAesWrappingSuiteIdentifier,
22-
RawRsaKeyringNode
23+
RawRsaKeyringNode,
24+
oaepHashSupported
2325
} from '@aws-crypto/client-node'
2426
import {
2527
RsaKeyInfo, // eslint-disable-line no-unused-vars
@@ -81,16 +83,16 @@ export function rsaKeyring (keyInfo: RsaKeyInfo, key: RSAKey) {
8183
const rsaKey = key.type === 'private'
8284
? { privateKey: key.material }
8385
: { publicKey: key.material }
84-
const padding = rsaPadding(keyInfo)
85-
const oaepHash = keyInfo['padding-hash']
86+
const { padding, oaepHash } = rsaPadding(keyInfo)
8687
return new RawRsaKeyringNode({ keyName, keyNamespace, rsaKey, padding, oaepHash })
8788
}
8889

8990
export function rsaPadding (keyInfo: RsaKeyInfo) {
90-
const paddingAlgorithm = keyInfo['padding-algorithm']
91-
return paddingAlgorithm === 'pkcs1'
92-
? constants.RSA_PKCS1_PADDING
93-
: 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 }
9496
}
9597

9698
export class NotSupported extends Error {

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

Lines changed: 1 addition & 0 deletions
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'
Lines changed: 51 additions & 0 deletions
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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import {
4444
UnwrapKey // eslint-disable-line no-unused-vars
4545
} from '@aws-crypto/raw-keyring'
4646

47+
import { oaepHashSupported } from './oaep_hash_supported'
48+
4749
/* Interface question:
4850
* When creating a keyring being able to define
4951
* if the keyring can be used for encrypt/decrypt/both
@@ -59,12 +61,15 @@ interface RsaKey {
5961
privateKey?: string | Buffer | AwsEsdkKeyObject
6062
}
6163

64+
export type OaepHash = 'sha1'|'sha256'|'sha384'|'sha512'|undefined
65+
const supportedOaepHash: OaepHash[] = ['sha1', 'sha256', 'sha384', 'sha512', undefined]
66+
6267
export type RawRsaKeyringNodeInput = {
6368
keyNamespace: string
6469
keyName: string
6570
rsaKey: RsaKey
6671
padding?: number
67-
oaepHash?: 'sha1'|'sha256'|'sha384'|'sha512'
72+
oaepHash?: OaepHash
6873
}
6974

7075
export class RawRsaKeyringNode extends KeyringNode {
@@ -82,6 +87,12 @@ export class RawRsaKeyringNode extends KeyringNode {
8287
needs(publicKey || privateKey, 'No Key provided.')
8388
/* Precondition: RsaKeyringNode needs identifying information for encrypt and decrypt. */
8489
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')
8596

8697
const _wrapKey = async (material: NodeEncryptionMaterial) => {
8798
/* Precondition: Public key must be defined to support encrypt. */

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

Lines changed: 64 additions & 50 deletions
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,59 +138,61 @@ describe('RawRsaKeyringNode::constructor', () => {
126138
})
127139
})
128140

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

168-
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256)
169-
const material = new NodeEncryptionMaterial(suite, {})
170-
return expect(keyring.onEncrypt(material)).to.rejectedWith(Error)
171-
})
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+
})
172174

173-
it('Precondition: Private key must be defined to support decrypt.', async () => {
174-
const keyring = new RawRsaKeyringNode({
175-
rsaKey: { publicKey: publicPem },
176-
keyName,
177-
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)
178185
})
179186

180-
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256)
181-
const material = new NodeDecryptionMaterial(suite, {})
182-
return expect(keyring._unwrapKey(material, encryptedDataKey)).to.rejectedWith(Error)
183-
})
184-
}))
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)