diff --git a/modules/integration-node/src/decrypt_materials_manager_node.ts b/modules/integration-node/src/decrypt_materials_manager_node.ts index 334838f0b..49f8e7d95 100644 --- a/modules/integration-node/src/decrypt_materials_manager_node.ts +++ b/modules/integration-node/src/decrypt_materials_manager_node.ts @@ -20,7 +20,8 @@ import { RawAesKeyringNode, WrappingSuiteIdentifier, // eslint-disable-line no-unused-vars RawAesWrappingSuiteIdentifier, - RawRsaKeyringNode + RawRsaKeyringNode, + oaepHashSupported } from '@aws-crypto/client-node' import { RsaKeyInfo, // eslint-disable-line no-unused-vars @@ -82,18 +83,16 @@ export function rsaKeyring (keyInfo: RsaKeyInfo, key: RSAKey) { const rsaKey = key.type === 'private' ? { privateKey: key.material } : { publicKey: key.material } - const padding = rsaPadding(keyInfo) - return new RawRsaKeyringNode({ keyName, keyNamespace, rsaKey, padding }) + const { padding, oaepHash } = rsaPadding(keyInfo) + return new RawRsaKeyringNode({ keyName, keyNamespace, rsaKey, padding, oaepHash }) } export function rsaPadding (keyInfo: RsaKeyInfo) { - const paddingAlgorithm = keyInfo['padding-algorithm'] - const paddingHash = keyInfo['padding-hash'] - - if (paddingAlgorithm === 'pkcs1') return constants.RSA_PKCS1_PADDING - needs(paddingHash === 'sha1', 'Not supported at this time.') - - return constants.RSA_PKCS1_OAEP_PADDING + if (keyInfo['padding-algorithm'] === 'pkcs1') return { padding: constants.RSA_PKCS1_PADDING } + const padding = constants.RSA_PKCS1_OAEP_PADDING + const oaepHash = keyInfo['padding-hash'] + needs(oaepHashSupported || oaepHash === 'sha1', 'Not supported at this time.') + return { padding, oaepHash } } export class NotSupported extends Error { diff --git a/modules/raw-rsa-keyring-node/src/index.ts b/modules/raw-rsa-keyring-node/src/index.ts index d9d9d4336..718cda030 100644 --- a/modules/raw-rsa-keyring-node/src/index.ts +++ b/modules/raw-rsa-keyring-node/src/index.ts @@ -14,3 +14,4 @@ */ export * from './raw_rsa_keyring_node' +export * from './oaep_hash_supported' diff --git a/modules/raw-rsa-keyring-node/src/oaep_hash_supported.ts b/modules/raw-rsa-keyring-node/src/oaep_hash_supported.ts new file mode 100644 index 000000000..9eb8d1494 --- /dev/null +++ b/modules/raw-rsa-keyring-node/src/oaep_hash_supported.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* oaepHash support was added in Node.js v12.9.1 (https://github.com/nodejs/node/pull/28335) + * However, the integration tests need to be able to verify functionality on other versions. + * There are no constants to sniff, + * and looking at the version would not catch back-ports. + * So I simply try the function. + * However there is a rub as the test might seem backwards. + * Sending an invalid hash to the version that supports oaepHash will throw an error. + * But sending an invalid hash to a version that does not support oaepHash will be ignored. + */ + +import { + needs +} from '@aws-crypto/material-management-node' + +import { + constants, + publicEncrypt +} from 'crypto' + +export const oaepHashSupported = (function () { + 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-----' + + const oaepHash = 'i_am_not_valid' + try { + // @ts-ignore + publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash }, Buffer.from([1, 2, 3, 4])) + /* See note above, + * only versions that support oaepHash will respond. + * So the only way I can get here is if the option was ignored. + */ + return false + } catch (ex) { + needs(ex.code === 'ERR_OSSL_EVP_INVALID_DIGEST', 'Unexpected error testing oaepHash.') + return true + } +})() diff --git a/modules/raw-rsa-keyring-node/src/raw_rsa_keyring_node.ts b/modules/raw-rsa-keyring-node/src/raw_rsa_keyring_node.ts index 475285cc4..c5ceff3e3 100644 --- a/modules/raw-rsa-keyring-node/src/raw_rsa_keyring_node.ts +++ b/modules/raw-rsa-keyring-node/src/raw_rsa_keyring_node.ts @@ -32,7 +32,9 @@ import { constants, publicEncrypt, privateDecrypt, - randomBytes + randomBytes, + RsaPublicKey, // eslint-disable-line no-unused-vars + RsaPrivateKey // eslint-disable-line no-unused-vars } from 'crypto' import { @@ -42,6 +44,8 @@ import { UnwrapKey // eslint-disable-line no-unused-vars } from '@aws-crypto/raw-keyring' +import { oaepHashSupported } from './oaep_hash_supported' + /* Interface question: * When creating a keyring being able to define * if the keyring can be used for encrypt/decrypt/both @@ -57,18 +61,17 @@ interface RsaKey { privateKey?: string | Buffer | AwsEsdkKeyObject } +export type OaepHash = 'sha1'|'sha256'|'sha384'|'sha512'|undefined +const supportedOaepHash: OaepHash[] = ['sha1', 'sha256', 'sha384', 'sha512', undefined] + export type RawRsaKeyringNodeInput = { keyNamespace: string keyName: string rsaKey: RsaKey padding?: number + oaepHash?: OaepHash } -/* Node supports RSA_OAEP_SHA1_MFG1 by default. - * It does not support RSA_OAEP_SHA256_MFG1 at this time. - * Passing RSA_PKCS1_OAEP_PADDING implies RSA_OAEP_SHA1_MFG1. - */ - export class RawRsaKeyringNode extends KeyringNode { public keyNamespace!: string public keyName!: string @@ -78,19 +81,25 @@ export class RawRsaKeyringNode extends KeyringNode { constructor (input: RawRsaKeyringNodeInput) { super() - const { rsaKey, keyName, keyNamespace, padding = constants.RSA_PKCS1_OAEP_PADDING } = input + const { rsaKey, keyName, keyNamespace, padding = constants.RSA_PKCS1_OAEP_PADDING, oaepHash } = input const { publicKey, privateKey } = rsaKey /* Precondition: RsaKeyringNode needs either a public or a private key to operate. */ needs(publicKey || privateKey, 'No Key provided.') /* Precondition: RsaKeyringNode needs identifying information for encrypt and decrypt. */ needs(keyName && keyNamespace, 'Identifying information must be defined.') + /* Precondition: The AWS ESDK only supports specific hash values for OAEP padding. */ + needs(padding === constants.RSA_PKCS1_OAEP_PADDING + ? oaepHashSupported + ? supportedOaepHash.includes(oaepHash) + : !oaepHash || oaepHash === 'sha1' + : !oaepHash, 'Unsupported oaepHash') const _wrapKey = async (material: NodeEncryptionMaterial) => { /* Precondition: Public key must be defined to support encrypt. */ if (!publicKey) throw new Error('No public key defined in constructor. Encrypt disabled.') const { buffer, byteOffset, byteLength } = unwrapDataKey(material.getUnencryptedDataKey()) const encryptedDataKey = publicEncrypt( - { key: publicKey, padding }, + { key: publicKey, padding, oaepHash } as RsaPublicKey, Buffer.from(buffer, byteOffset, byteLength)) const providerInfo = this.keyName const providerId = this.keyNamespace @@ -112,7 +121,7 @@ export class RawRsaKeyringNode extends KeyringNode { const { buffer, byteOffset, byteLength } = edk.encryptedDataKey const encryptedDataKey = Buffer.from(buffer, byteOffset, byteLength) const unencryptedDataKey = privateDecrypt( - { key: privateKey, padding }, + { key: privateKey, padding, oaepHash } as RsaPrivateKey, encryptedDataKey) return material.setUnencryptedDataKey(unencryptedDataKey, trace) } diff --git a/modules/raw-rsa-keyring-node/test/raw_rsa_keyring_node.test.ts b/modules/raw-rsa-keyring-node/test/raw_rsa_keyring_node.test.ts index 5ade2cdf3..5ef84955d 100644 --- a/modules/raw-rsa-keyring-node/test/raw_rsa_keyring_node.test.ts +++ b/modules/raw-rsa-keyring-node/test/raw_rsa_keyring_node.test.ts @@ -19,7 +19,8 @@ import * as chai from 'chai' import chaiAsPromised from 'chai-as-promised' import 'mocha' import { - RawRsaKeyringNode + RawRsaKeyringNode, + OaepHash // eslint-disable-line no-unused-vars } from '../src/index' import { KeyringNode, @@ -30,6 +31,7 @@ import { NodeDecryptionMaterial, unwrapDataKey } from '@aws-crypto/material-management-node' +import { oaepHashSupported } from '../src/oaep_hash_supported' chai.use(chaiAsPromised) const { expect } = chai @@ -108,6 +110,16 @@ describe('RawRsaKeyringNode::constructor', () => { })).to.throw() }) + it('Precondition: The AWS ESDK only supports specific hash values for OAEP padding.', () => { + expect(() => new RawRsaKeyringNode({ + keyName, + keyNamespace, + // @ts-ignore Valid hash, but not supported by the ESDK + oaepHash: 'rmd160', + rsaKey: { privateKey: privatePem } + })).to.throw('Unsupported oaepHash') + }) + it('Precondition: RsaKeyringNode needs identifying information for encrypt and decrypt.', () => { // @ts-ignore Typescript is trying to save us. expect(() => new RawRsaKeyringNode({ @@ -126,58 +138,61 @@ describe('RawRsaKeyringNode::constructor', () => { }) }) -describe('RawRsaKeyringNode encrypt/decrypt', () => { - const keyNamespace = 'keyNamespace' - const keyName = 'keyName' - const keyring = new RawRsaKeyringNode({ - rsaKey: { privateKey: privatePem, publicKey: publicPem }, - keyName, - keyNamespace - }) - let encryptedDataKey: EncryptedDataKey - - it('can encrypt and create unencrypted data key', async () => { - const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256) - const material = new NodeEncryptionMaterial(suite, {}) - const test = await keyring.onEncrypt(material) - expect(test.hasValidKey()).to.equal(true) - const udk = unwrapDataKey(test.getUnencryptedDataKey()) - expect(udk).to.have.lengthOf(suite.keyLengthBytes) - expect(test.encryptedDataKeys).to.have.lengthOf(1) - const [edk] = test.encryptedDataKeys - expect(edk.providerId).to.equal(keyNamespace) - encryptedDataKey = edk - }) - - it('can decrypt an EncryptedDataKey', async () => { - const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256) - const material = new NodeDecryptionMaterial(suite, {}) - const test = await keyring.onDecrypt(material, [encryptedDataKey]) - expect(test.hasValidKey()).to.equal(true) - }) - - it('Precondition: Public key must be defined to support encrypt.', async () => { +const oaepHashOptions: OaepHash[] = [undefined, 'sha1', 'sha256', 'sha384', 'sha512'] +oaepHashOptions + .filter(oaepHash => oaepHashSupported || [undefined, 'sha1'].includes(oaepHash)) + .forEach(oaepHash => describe(`RawRsaKeyringNode encrypt/decrypt for oaepHash=${oaepHash || 'undefined'}`, () => { + const keyNamespace = 'keyNamespace' + const keyName = 'keyName' const keyring = new RawRsaKeyringNode({ - rsaKey: { privateKey: privatePem }, + rsaKey: { privateKey: privatePem, publicKey: publicPem }, keyName, - keyNamespace + keyNamespace, + oaepHash + }) + let encryptedDataKey: EncryptedDataKey + + it('can encrypt and create unencrypted data key', async () => { + const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256) + const material = new NodeEncryptionMaterial(suite, {}) + const test = await keyring.onEncrypt(material) + expect(test.hasValidKey()).to.equal(true) + const udk = unwrapDataKey(test.getUnencryptedDataKey()) + expect(udk).to.have.lengthOf(suite.keyLengthBytes) + expect(test.encryptedDataKeys).to.have.lengthOf(1) + const [edk] = test.encryptedDataKeys + expect(edk.providerId).to.equal(keyNamespace) + encryptedDataKey = edk }) - const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256) - const material = new NodeEncryptionMaterial(suite, {}) - expect(keyring.onEncrypt(material)).to.rejectedWith(Error) - }) + it('can decrypt an EncryptedDataKey', async () => { + const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256) + const material = new NodeDecryptionMaterial(suite, {}) + const test = await keyring.onDecrypt(material, [encryptedDataKey]) + expect(test.hasValidKey()).to.equal(true) + }) - it('Precondition: Private key must be defined to support decrypt.', async () => { - const keyring = new RawRsaKeyringNode({ - rsaKey: { publicKey: publicPem }, - keyName, - keyNamespace + it('Precondition: Public key must be defined to support encrypt.', async () => { + const keyring = new RawRsaKeyringNode({ + rsaKey: { privateKey: privatePem }, + keyName, + keyNamespace + }) + + const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256) + const material = new NodeEncryptionMaterial(suite, {}) + return expect(keyring.onEncrypt(material)).to.rejectedWith(Error) }) - const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256) - const material = new NodeDecryptionMaterial(suite, {}) - await keyring.onDecrypt(material, [encryptedDataKey]) - expect(keyring.onDecrypt(material, [encryptedDataKey])).to.rejectedWith(Error) - }) -}) + it('Precondition: Private key must be defined to support decrypt.', async () => { + const keyring = new RawRsaKeyringNode({ + rsaKey: { publicKey: publicPem }, + keyName, + keyNamespace + }) + + const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256) + const material = new NodeDecryptionMaterial(suite, {}) + return expect(keyring._unwrapKey(material, encryptedDataKey)).to.rejectedWith(Error) + }) + }))