modules/material-management/src/multi_keyring.ts (116 lines of code) (raw):
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { immutableClass, readOnlyProperty } from './immutable_class'
import { Keyring, KeyringNode, KeyringWebCrypto } from './keyring'
import {
SupportedAlgorithmSuites,
EncryptionMaterial,
DecryptionMaterial,
Catchable,
} from './types'
import { needs } from './needs'
import { EncryptedDataKey } from './encrypted_data_key'
import { NodeAlgorithmSuite } from './node_algorithms'
import { WebCryptoAlgorithmSuite } from './web_crypto_algorithms'
export class MultiKeyringNode
extends KeyringNode
implements MultiKeyring<NodeAlgorithmSuite>
{
public readonly generator?: KeyringNode
public readonly children!: ReadonlyArray<KeyringNode>
constructor(input: MultiKeyringInput<NodeAlgorithmSuite>) {
super()
decorateProperties(this, KeyringNode, input)
}
_onEncrypt = buildPrivateOnEncrypt<NodeAlgorithmSuite>()
_onDecrypt = buildPrivateOnDecrypt<NodeAlgorithmSuite>()
}
immutableClass(MultiKeyringNode)
export class MultiKeyringWebCrypto
extends KeyringWebCrypto
implements MultiKeyring<WebCryptoAlgorithmSuite>
{
public declare readonly generator?: KeyringWebCrypto
public declare readonly children: ReadonlyArray<KeyringWebCrypto>
constructor(input: MultiKeyringInput<WebCryptoAlgorithmSuite>) {
super()
decorateProperties(this, KeyringWebCrypto, input)
}
_onEncrypt = buildPrivateOnEncrypt<WebCryptoAlgorithmSuite>()
_onDecrypt = buildPrivateOnDecrypt<WebCryptoAlgorithmSuite>()
}
immutableClass(MultiKeyringWebCrypto)
function decorateProperties<S extends SupportedAlgorithmSuites>(
obj: MultiKeyring<S>,
BaseKeyring: any,
{ generator, children = [] }: MultiKeyringInput<S>
) {
/* Precondition: MultiKeyring must have keyrings. */
needs(generator || children.length, 'Noop MultiKeyring is not supported.')
/* Precondition: generator must be a Keyring. */
needs(
!!generator === generator instanceof BaseKeyring,
'Generator must be a Keyring'
)
/* Precondition: All children must be Keyrings. */
needs(
children.every((kr) => kr instanceof BaseKeyring),
'Child must be a Keyring'
)
readOnlyProperty(obj, 'children', Object.freeze(children.slice()))
readOnlyProperty(obj, 'generator', generator)
}
function buildPrivateOnEncrypt<S extends SupportedAlgorithmSuites>() {
return async function _onEncrypt(
this: MultiKeyring<S>,
material: EncryptionMaterial<S>
): Promise<EncryptionMaterial<S>> {
/* Precondition: Only Keyrings explicitly designated as generators can generate material.
* Technically, the precondition below will handle this.
* Since if I do not have an unencrypted data key,
* and I do not have a generator,
* then generated.hasUnencryptedDataKey === false will throw.
* But this is a much more meaningful error.
*/
needs(
!material.hasUnencryptedDataKey ? this.generator : true,
'Only Keyrings explicitly designated as generators can generate material.'
)
const generated = this.generator
? await this.generator.onEncrypt(material)
: material
/* Precondition: A Generator Keyring *must* ensure generated material. */
needs(
generated.hasUnencryptedDataKey,
'Generator Keyring has not generated material.'
)
/* By default this is a serial operation. A keyring _may_ perform an expensive operation
* or create resource constraints such that encrypting with multiple keyrings could
* fail in unexpected ways.
* Additionally, "downstream" keyrings may make choices about the EncryptedDataKeys they
* append based on already appended EDK's.
*/
for (const keyring of this.children) {
await keyring.onEncrypt(generated)
}
// Keyrings are required to not create new EncryptionMaterial instances, but
// only append EncryptedDataKey. Therefore the generated material has all
// the data I want.
return generated
}
}
function buildPrivateOnDecrypt<S extends SupportedAlgorithmSuites>() {
return async function _onDecrypt(
this: MultiKeyring<S>,
material: DecryptionMaterial<S>,
encryptedDataKeys: EncryptedDataKey[]
): Promise<DecryptionMaterial<S>> {
const children = this.children.slice()
if (this.generator) children.unshift(this.generator)
const childKeyringErrors: Catchable[] = []
for (const keyring of children) {
/* Check for early return (Postcondition): Do not attempt to decrypt once I have a valid key. */
if (material.hasValidKey()) return material
try {
await keyring.onDecrypt(material, encryptedDataKeys)
} catch (e) {
/* Failures onDecrypt should not short-circuit the process
* If the caller does not have access they may have access
* through another Keyring.
*/
childKeyringErrors.push({ errPlus: e })
}
}
/* Postcondition: A child keyring must provide a valid data key or no child keyring must have raised an error.
* If I have a data key,
* decrypt errors can be ignored.
* However, if I was unable to decrypt a data key AND I have errors,
* these errors should bubble up.
* Otherwise, the only error customers will see is that
* the material does not have an unencrypted data key.
* So I return a concatenated Error message
*/
needs(
material.hasValidKey() ||
(!material.hasValidKey() && !childKeyringErrors.length),
childKeyringErrors.reduce(
(m, e, i) => `${m} Error #${i + 1} \n ${e.errPlus.stack} \n`,
'Unable to decrypt data key and one or more child keyrings had an error. \n '
)
)
return material
}
}
interface MultiKeyringInput<S extends SupportedAlgorithmSuites> {
generator?: Keyring<S>
children?: Keyring<S>[]
}
export interface MultiKeyring<S extends SupportedAlgorithmSuites>
extends Keyring<S> {
generator?: Keyring<S>
children: ReadonlyArray<Keyring<S>>
}