modules/serialize/src/serialize_factory.ts (246 lines of code) (raw):
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
/*
* This public interface for serializing the AWS Encryption SDK Message Header Format
* is provided for the use of the Encryption SDK for JavaScript only. It can be used
* as a reference but is not intended to be use by any packages other than the
* Encryption SDK for JavaScript.
*
* See: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html#header-structure
*/
import { concatBuffers } from './concat_buffers'
import {
IvLength,
EncryptionContext,
needs,
EncryptedDataKey,
MessageFormat,
AlgorithmSuite,
} from '@aws-crypto/material-management'
import {
ContentType,
ObjectType,
SequenceIdentifier,
SerializationVersion,
} from './identifiers'
import { uInt16BE, uInt8, uInt32BE } from './uint_util'
import {
MessageHeader,
MessageHeaderV1,
MessageHeaderV2,
SerializeOptions,
} from './types'
export function serializeFactory(
fromUtf8: (input: any) => Uint8Array,
sorting: SerializeOptions
) {
return {
frameIv,
nonFramedBodyIv,
headerAuthIv,
frameHeader,
finalFrameHeader,
encodeEncryptionContext,
serializeEncryptionContext,
serializeEncryptedDataKeys,
serializeEncryptedDataKey,
serializeMessageHeader,
buildMessageHeader,
}
function frameIv(ivLength: IvLength, sequenceNumber: number) {
/* Precondition: sequenceNumber must conform to the specification. i.e. 1 - (2^32 - 1)
* The sequence number starts at 1
* https://github.com/awslabs/aws-encryption-sdk-specification/blob/master/data-format/message-body.md#sequence-number
*/
needs(
sequenceNumber > 0 &&
SequenceIdentifier.SEQUENCE_NUMBER_END >= sequenceNumber,
'sequenceNumber out of bounds'
)
const buff = new Uint8Array(ivLength)
const view = new DataView(buff.buffer, buff.byteOffset, buff.byteLength)
view.setUint32(ivLength - 4, sequenceNumber, false) // big-endian
return buff
}
function nonFramedBodyIv(ivLength: IvLength) {
return frameIv(ivLength, 1)
}
function headerAuthIv(ivLength: IvLength) {
return new Uint8Array(ivLength) // new Uint8Array is 0 filled by default
}
function frameHeader(sequenceNumber: number, iv: Uint8Array) {
return concatBuffers(uInt32BE(sequenceNumber), iv)
}
function finalFrameHeader(
sequenceNumber: number,
iv: Uint8Array,
contentLength: number
) {
return concatBuffers(
uInt32BE(SequenceIdentifier.SEQUENCE_NUMBER_END), // Final Frame identifier
uInt32BE(sequenceNumber),
iv,
uInt32BE(contentLength)
)
}
function encodeEncryptionContext(
encryptionContext: EncryptionContext
): Uint8Array[] {
// use closure value from the serializeFactory
// If the encryption context contains high order
// utf8 code points the "old" implementation would sort these values
// based on their values, see the false branch of this function.
// This led to different sorting if using these high order utf8 code points,
// which led to decryption failures from other ESDK language implementations
// that correctly sorted the encryption context by sorting based on the utf8
// values as opposed to the string value.
// See, https://github.com/aws/aws-encryption-sdk-javascript/issues/428
// for mote details
const { utf8Sorting } = sorting
if (utf8Sorting) {
return Object.entries(encryptionContext)
.map((entries) => entries.map(fromUtf8))
.sort(([aKey], [bKey]) => compare(aKey, bKey))
.map(([key, value]) =>
concatBuffers(
uInt16BE(key.byteLength),
key,
uInt16BE(value.byteLength),
value
)
)
} else {
return (
Object.entries(encryptionContext)
/* Precondition: The serialized encryption context entries must be sorted by UTF-8 key value. */
.sort(([aKey], [bKey]) => aKey.localeCompare(bKey))
.map((entries) => entries.map(fromUtf8))
.map(([key, value]) =>
concatBuffers(
uInt16BE(key.byteLength),
key,
uInt16BE(value.byteLength),
value
)
)
)
}
}
function compare(a: Uint8Array, b: Uint8Array): number {
for (let i = 0; i < a.byteLength; i++) {
if (a[i] < b[i]) {
return -1
}
if (a[i] > b[i]) {
return 1
}
}
if (a.byteLength > b.byteLength) {
return 1
}
if (a.byteLength < b.byteLength) {
return -1
}
return 0
}
function serializeEncryptionContext(encryptionContext: EncryptionContext) {
const encryptionContextElements = encodeEncryptionContext(encryptionContext)
/* Check for early return (Postcondition): If there is no context then the length of the _whole_ serialized portion is 0.
* This is part of the specification of the AWS Encryption SDK Message Format.
* It is not 0 for length and 0 for count. The count element is omitted.
*/
if (!encryptionContextElements.length) return uInt16BE(0)
const aadData = concatBuffers(
uInt16BE(encryptionContextElements.length),
...encryptionContextElements
)
const aadLength = uInt16BE(aadData.byteLength)
return concatBuffers(aadLength, aadData)
}
function serializeEncryptedDataKeys(
encryptedDataKeys: ReadonlyArray<EncryptedDataKey>
) {
const encryptedKeyInfo = encryptedDataKeys.map(serializeEncryptedDataKey)
return concatBuffers(
uInt16BE(encryptedDataKeys.length),
...encryptedKeyInfo
)
}
function serializeEncryptedDataKey(edk: EncryptedDataKey) {
const { providerId, providerInfo, encryptedDataKey, rawInfo } = edk
const providerIdBytes = fromUtf8(providerId)
// The providerInfo is technically a binary field, so I prefer rawInfo
const providerInfoBytes = rawInfo || fromUtf8(providerInfo)
return concatBuffers(
uInt16BE(providerIdBytes.byteLength),
providerIdBytes,
uInt16BE(providerInfoBytes.byteLength),
providerInfoBytes,
uInt16BE(encryptedDataKey.byteLength),
encryptedDataKey
)
}
function serializeMessageHeader(messageHeader: MessageHeader) {
/* Precondition: Must be a version that can be serialized. */
needs(SerializationVersion[messageHeader.version], 'Unsupported version.')
if (messageHeader.version === 1) {
return serializeMessageHeaderV1(messageHeader as MessageHeaderV1)
} else {
return serializeMessageHeaderV2(messageHeader as MessageHeaderV2)
}
}
function serializeMessageHeaderV1(messageHeader: MessageHeaderV1) {
return concatBuffers(
uInt8(messageHeader.version),
uInt8(messageHeader.type),
uInt16BE(messageHeader.suiteId),
messageHeader.messageId,
serializeEncryptionContext(messageHeader.encryptionContext),
serializeEncryptedDataKeys(messageHeader.encryptedDataKeys),
new Uint8Array([messageHeader.contentType]),
new Uint8Array([0, 0, 0, 0]),
uInt8(messageHeader.headerIvLength),
uInt32BE(messageHeader.frameLength)
)
}
function serializeMessageHeaderV2(messageHeader: MessageHeaderV2) {
return concatBuffers(
uInt8(messageHeader.version),
uInt16BE(messageHeader.suiteId),
messageHeader.messageId,
serializeEncryptionContext(messageHeader.encryptionContext),
serializeEncryptedDataKeys(messageHeader.encryptedDataKeys),
new Uint8Array([messageHeader.contentType]),
uInt32BE(messageHeader.frameLength),
messageHeader.suiteData
)
}
/* This _could_ take the material directly.
* But I don't do that on purpose.
* It may be overly paranoid,
* but this way once the material is created,
* it has a minimum of egress.
*/
function buildMessageHeader({
encryptionContext,
encryptedDataKeys,
suite,
messageId,
frameLength,
suiteData,
}: {
encryptionContext: Readonly<EncryptionContext>
encryptedDataKeys: ReadonlyArray<EncryptedDataKey>
suite: AlgorithmSuite
messageId: Uint8Array
frameLength: number
suiteData?: Uint8Array
}): MessageHeader {
const { messageFormat: version, id: suiteId } = suite
const contentType = ContentType.FRAMED_DATA
if (version === MessageFormat.V1) {
const type = ObjectType.CUSTOMER_AE_DATA
const { ivLength: headerIvLength } = suite
return {
version,
type,
suiteId,
messageId,
encryptionContext,
encryptedDataKeys,
contentType,
headerIvLength,
frameLength,
} as MessageHeaderV1
} else if (version === MessageFormat.V2) {
return {
version,
suiteId,
messageId,
encryptionContext: encryptionContext,
encryptedDataKeys: encryptedDataKeys,
contentType,
frameLength,
suiteData,
} as MessageHeaderV2
}
needs(false, 'Unsupported message format version.')
}
}
export function serializeMessageHeaderAuth({
headerIv,
headerAuthTag,
messageHeader,
}: {
headerIv: Uint8Array
headerAuthTag: Uint8Array
messageHeader: MessageHeader
}) {
if (messageHeader.version === MessageFormat.V1) {
return concatBuffers(headerIv, headerAuthTag)
}
return headerAuthTag
}