Release Crypto Tools 0.2.0
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-02 10:40:23 +02:00
parent 4b75f819e2
commit 82bb01b13f
37 changed files with 3673 additions and 140 deletions
+770
View File
@@ -0,0 +1,770 @@
import {
BaseBlock,
BmpString,
Integer,
ObjectIdentifier,
OctetString,
Sequence,
Set as AsnSet,
Utf8String,
fromBER,
} from "asn1js";
import {
decryptEncryptedPkcs8,
decryptPbes2Payload,
inspectEncryptedPkcs8,
inspectPbes2Payload,
inspectPkcs8,
reservePbkdf2Work,
type Pbkdf2WorkBudget,
type Pbes2Inspection,
type Pkcs8Inspection,
} from "./pbes2";
const MAX_INPUT_BYTES = 8 * 1024 * 1024;
const MAX_CONTENT_INFOS = 64;
const MAX_BAGS = 512;
const MAX_NESTING_DEPTH = 4;
const MAX_ATTRIBUTES = 32;
const MAX_MAC_ITERATIONS = 50_000;
const MAX_PASSWORD_UNITS = 4_096;
const CMS_DATA = "1.2.840.113549.1.7.1";
const CMS_ENCRYPTED_DATA = "1.2.840.113549.1.7.6";
const PBES2 = "1.2.840.113549.1.5.13";
const LEGACY_PKCS12_PBE_PREFIX = "1.2.840.113549.1.12.1.";
const KEY_BAG = "1.2.840.113549.1.12.10.1.1";
const SHROUDED_KEY_BAG = "1.2.840.113549.1.12.10.1.2";
const CERT_BAG = "1.2.840.113549.1.12.10.1.3";
const CRL_BAG = "1.2.840.113549.1.12.10.1.4";
const SECRET_BAG = "1.2.840.113549.1.12.10.1.5";
const SAFE_CONTENTS_BAG = "1.2.840.113549.1.12.10.1.6";
const X509_CERTIFICATE = "1.2.840.113549.1.9.22.1";
const FRIENDLY_NAME = "1.2.840.113549.1.9.20";
const LOCAL_KEY_ID = "1.2.840.113549.1.9.21";
type Block = BaseBlock;
const DIGESTS: Record<
string,
{ name: "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512"; u: number; v: number }
> = {
"1.3.14.3.2.26": { name: "SHA-1", u: 20, v: 64 },
"2.16.840.1.101.3.4.2.1": { name: "SHA-256", u: 32, v: 64 },
"2.16.840.1.101.3.4.2.2": { name: "SHA-384", u: 48, v: 128 },
"2.16.840.1.101.3.4.2.3": { name: "SHA-512", u: 64, v: 128 },
};
export interface Pkcs12MacInspection {
present: boolean;
status: "absent" | "password-required" | "verified" | "unsupported";
algorithm?: string;
algorithmOid?: string;
iterations?: number;
saltBytes?: number;
}
export interface Pkcs12ContentInventory {
index: number;
type: "data" | "encryptedData";
state: "parsed" | "password-required";
encryption?: Pbes2Inspection;
bagCount: number;
}
export interface Pkcs12BagInventory {
path: string;
bagType:
| "private-key"
| "shrouded-private-key"
| "certificate"
| "safe-contents"
| "crl"
| "secret"
| "unknown";
bagOid: string;
friendlyName?: string;
localKeyId?: string;
encrypted: boolean;
state: "inspected" | "password-required" | "unsupported";
key?: Pkcs8Inspection;
encryption?: Pbes2Inspection;
certificateBytes?: Uint8Array;
}
export interface Pkcs12Inspection {
recognized: true;
version: 3;
bytes: number;
authSafeContentType: typeof CMS_DATA;
mac: Pkcs12MacInspection;
contents: Pkcs12ContentInventory[];
bags: Pkcs12BagInventory[];
warnings: string[];
}
interface ParsedMacData {
digest: Uint8Array;
salt: Uint8Array;
algorithmOid: string;
algorithm?: (typeof DIGESTS)[string];
iterations: number;
}
interface InventoryContext {
password?: string;
bags: Pkcs12BagInventory[];
warnings: string[];
pbkdf2Budget: Pbkdf2WorkBudget;
}
function asBuffer(bytes: Uint8Array): ArrayBuffer {
return bytes.buffer.slice(
bytes.byteOffset,
bytes.byteOffset + bytes.byteLength,
) as ArrayBuffer;
}
function derBytes(block: Block): Uint8Array {
return new Uint8Array(block.toBER(false));
}
function parseDer(bytes: Uint8Array, name: string): Block {
if (bytes.length === 0 || bytes.length > MAX_INPUT_BYTES)
throw new Error(`${name} must contain 1 byte8 MiB.`);
const parsed = fromBER(asBuffer(bytes));
if (parsed.offset === -1 || parsed.offset !== bytes.length)
throw new Error(`${name} is malformed DER or contains trailing bytes.`);
const canonical = new Uint8Array(parsed.result.toBER(false));
if (
canonical.length !== bytes.length ||
canonical.some((value, index) => value !== bytes[index])
)
throw new Error(`${name} must use canonical definite-length DER.`);
return parsed.result;
}
function sequence(block: Block | undefined, name: string): Block[] {
if (!(block instanceof Sequence))
throw new Error(`${name} must be an ASN.1 SEQUENCE.`);
return block.valueBlock.value;
}
function set(block: Block | undefined, name: string): Block[] {
if (!(block instanceof AsnSet))
throw new Error(`${name} must be an ASN.1 SET.`);
return block.valueBlock.value;
}
function oid(block: Block | undefined, name: string): string {
if (!(block instanceof ObjectIdentifier))
throw new Error(`${name} must be an ASN.1 object identifier.`);
return block.getValue();
}
function integer(block: Block | undefined, name: string): bigint {
if (!(block instanceof Integer))
throw new Error(`${name} must be an ASN.1 INTEGER.`);
return block.toBigInt();
}
function octets(block: Block | undefined, name: string): Uint8Array {
if (!(block instanceof OctetString))
throw new Error(`${name} must be an ASN.1 OCTET STRING.`);
return new Uint8Array(block.getValue());
}
function explicit(block: Block | undefined, tag: number, name: string): Block {
if (
!block ||
block.idBlock.tagClass !== 3 ||
block.idBlock.tagNumber !== tag ||
!block.idBlock.isConstructed
)
throw new Error(`${name} must be an explicit [${tag}] value.`);
const constructed = block.valueBlock as Block["valueBlock"] & {
value?: Block[];
};
if (!Array.isArray(constructed.value) || constructed.value.length !== 1)
throw new Error(`${name} must contain exactly one value.`);
return constructed.value[0]!;
}
function implicitOctets(
block: Block | undefined,
tag: number,
name: string,
): Uint8Array {
if (
!block ||
block.idBlock.tagClass !== 3 ||
block.idBlock.tagNumber !== tag ||
block.idBlock.isConstructed
)
throw new Error(`${name} must be a primitive implicit [${tag}] value.`);
const primitive = block.valueBlock as Block["valueBlock"] & {
valueHexView?: Uint8Array;
};
return new Uint8Array(primitive.valueHexView ?? new Uint8Array());
}
function boundedPassword(password: string): void {
if (password.length > MAX_PASSWORD_UNITS)
throw new Error(
`PKCS #12 password must contain at most ${MAX_PASSWORD_UNITS.toLocaleString()} UTF-16 units.`,
);
}
function parseContentInfo(block: Block, name: string): [string, Block] {
const fields = sequence(block, name);
if (fields.length !== 2)
throw new Error(`${name} must contain a content type and [0] content.`);
return [oid(fields[0], `${name} content type`), explicit(fields[1], 0, name)];
}
function parseMacData(block: Block | undefined): ParsedMacData {
const fields = sequence(block, "PFX MacData");
if (fields.length < 2 || fields.length > 3)
throw new Error("PFX MacData has an unsupported shape.");
const digestInfo = sequence(fields[0], "PFX MacData DigestInfo");
if (digestInfo.length !== 2)
throw new Error("PFX MacData DigestInfo is incomplete.");
const algorithmIdentifier = sequence(
digestInfo[0],
"PFX MacData digest algorithm",
);
if (algorithmIdentifier.length < 1 || algorithmIdentifier.length > 2)
throw new Error("PFX MacData digest AlgorithmIdentifier is malformed.");
const algorithmOid = oid(
algorithmIdentifier[0],
"PFX MacData digest algorithm",
);
const iterationsBig = fields[2]
? integer(fields[2], "PFX MacData iterations")
: 1n;
if (iterationsBig < 1n || iterationsBig > BigInt(MAX_MAC_ITERATIONS))
throw new Error(
`PFX MacData iterations must be 1${MAX_MAC_ITERATIONS.toLocaleString()}.`,
);
const salt = octets(fields[1], "PFX MacData salt");
if (salt.length < 1 || salt.length > 1_024)
throw new Error("PFX MacData salt must contain 11,024 bytes.");
const digest = octets(digestInfo[1], "PFX MacData digest");
const algorithm = DIGESTS[algorithmOid];
if (algorithm && digest.length !== algorithm.u)
throw new Error(
`PFX MacData ${algorithm.name} digest must contain ${algorithm.u} bytes.`,
);
return {
digest,
salt,
algorithmOid,
...(algorithm ? { algorithm } : {}),
iterations: Number(iterationsBig),
};
}
function pkcs12PasswordBytes(password: string): Uint8Array {
const output = new Uint8Array((password.length + 1) * 2);
for (let index = 0; index < password.length; index += 1) {
const unit = password.charCodeAt(index);
output[index * 2] = unit >>> 8;
output[index * 2 + 1] = unit & 0xff;
}
return output;
}
function repeatToMultiple(source: Uint8Array, multiple: number): Uint8Array {
if (source.length === 0) return source;
const length = multiple * Math.ceil(source.length / multiple);
return Uint8Array.from(
{ length },
(_, index) => source[index % source.length]!,
);
}
async function pkcs12MacKey(
password: string,
salt: Uint8Array,
iterations: number,
digest: NonNullable<ParsedMacData["algorithm"]>,
): Promise<Uint8Array> {
// RFC 7292 Appendix B, diversifier ID 3 (MAC material).
const diversifier = new Uint8Array(digest.v).fill(3);
const passwordBytes = pkcs12PasswordBytes(password);
const saltBlock = repeatToMultiple(salt, digest.v);
const passwordBlock = repeatToMultiple(passwordBytes, digest.v);
const state = new Uint8Array(saltBlock.length + passwordBlock.length);
state.set(saltBlock);
state.set(passwordBlock, saltBlock.length);
const source = new Uint8Array(diversifier.length + state.length);
source.set(diversifier);
source.set(state, diversifier.length);
let derived = new Uint8Array();
const blocks = Math.ceil(digest.u / digest.u);
for (let blockIndex = 0; blockIndex < blocks; blockIndex += 1) {
let a = new Uint8Array(await crypto.subtle.digest(digest.name, source));
for (let round = 1; round < iterations; round += 1)
a = new Uint8Array(await crypto.subtle.digest(digest.name, a));
const next = new Uint8Array(derived.length + a.length);
next.set(derived);
next.set(a, derived.length);
derived = next;
if (state.length > 0) {
const b = Uint8Array.from(
{ length: digest.v },
(_, index) => a[index % a.length]!,
);
for (let offset = 0; offset < state.length; offset += digest.v) {
let carry = 1;
for (let index = digest.v - 1; index >= 0; index -= 1) {
const position = offset + index;
const sum = state[position]! + b[index]! + carry;
state[position] = sum & 0xff;
carry = sum >>> 8;
}
}
source.set(state, diversifier.length);
}
}
passwordBytes.fill(0);
state.fill(0);
source.fill(0);
return derived.slice(0, digest.u);
}
function equalConstantTime(left: Uint8Array, right: Uint8Array): boolean {
let difference = left.length ^ right.length;
const length = Math.max(left.length, right.length);
for (let index = 0; index < length; index += 1)
difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
return difference === 0;
}
async function verifyMacData(
mac: ParsedMacData,
authSafeBytes: Uint8Array,
password: string,
): Promise<void> {
if (!mac.algorithm)
throw new Error(
`PFX MacData digest ${mac.algorithmOid} is not supported by this WebCrypto workflow.`,
);
const keyBytes = await pkcs12MacKey(
password,
mac.salt,
mac.iterations,
mac.algorithm,
);
try {
const key = await crypto.subtle.importKey(
"raw",
asBuffer(keyBytes),
{ name: "HMAC", hash: mac.algorithm.name },
false,
["sign"],
);
const actual = new Uint8Array(
await crypto.subtle.sign("HMAC", key, asBuffer(authSafeBytes)),
);
if (!equalConstantTime(actual, mac.digest))
throw new Error(
"PKCS #12 password is incorrect, or MacData/authSafe bytes are damaged.",
);
} finally {
keyBytes.fill(0);
}
}
function parseAttributes(
block: Block | undefined,
path: string,
): Pick<Pkcs12BagInventory, "friendlyName" | "localKeyId"> {
if (!block) return {};
const attributes = set(block, `${path} attributes`);
if (attributes.length > MAX_ATTRIBUTES)
throw new Error(`${path} has more than ${MAX_ATTRIBUTES} attributes.`);
let friendlyName: string | undefined;
let localKeyId: string | undefined;
for (const attribute of attributes) {
const fields = sequence(attribute, `${path} attribute`);
if (fields.length !== 2)
throw new Error(`${path} contains a malformed bag attribute.`);
const attributeOid = oid(fields[0], `${path} attribute type`);
const values = set(fields[1], `${path} attribute values`);
if (values.length !== 1)
throw new Error(`${path} bag attributes must contain one value.`);
const value = values[0]!;
if (attributeOid === FRIENDLY_NAME) {
if (!(value instanceof BmpString) && !(value instanceof Utf8String))
throw new Error(
`${path} friendlyName must be BMPString or UTF8String.`,
);
const candidate = value.getValue();
if (candidate.length > 256)
throw new Error(`${path} friendlyName exceeds 256 UTF-16 units.`);
friendlyName = candidate;
} else if (attributeOid === LOCAL_KEY_ID) {
const candidate = octets(value, `${path} localKeyId`);
if (candidate.length > 64)
throw new Error(`${path} localKeyId exceeds 64 bytes.`);
localKeyId = Array.from(candidate, (byte) =>
byte.toString(16).padStart(2, "0"),
)
.join("")
.toUpperCase();
}
}
return {
...(friendlyName === undefined ? {} : { friendlyName }),
...(localKeyId === undefined ? {} : { localKeyId }),
};
}
function addBag(context: InventoryContext, bag: Pkcs12BagInventory): void {
if (context.bags.length >= MAX_BAGS)
throw new Error(`PFX contains more than ${MAX_BAGS} SafeBags.`);
context.bags.push(bag);
}
async function inspectSafeContents(
bytes: Uint8Array,
context: InventoryContext,
prefix: string,
depth: number,
): Promise<number> {
if (depth > MAX_NESTING_DEPTH)
throw new Error(
`PFX SafeContents nesting exceeds ${MAX_NESTING_DEPTH} levels.`,
);
const bags = sequence(
parseDer(bytes, `${prefix} SafeContents`),
"SafeContents",
);
const before = context.bags.length;
for (const [index, block] of bags.entries()) {
const path = `${prefix}/bag[${index}]`;
const fields = sequence(block, path);
if (fields.length < 2 || fields.length > 3)
throw new Error(`${path} has an unsupported SafeBag shape.`);
const bagOid = oid(fields[0], `${path} bagId`);
const bagValue = explicit(fields[1], 0, `${path} bagValue`);
const attributes = parseAttributes(fields[2], path);
if (bagOid === KEY_BAG) {
const keyBytes = derBytes(bagValue);
try {
addBag(context, {
path,
bagType: "private-key",
bagOid,
...attributes,
encrypted: false,
state: "inspected",
key: inspectPkcs8(keyBytes),
});
} finally {
keyBytes.fill(0);
}
continue;
}
if (bagOid === SHROUDED_KEY_BAG) {
const encryptedBytes = derBytes(bagValue);
let encryption: Pbes2Inspection;
try {
encryption = inspectEncryptedPkcs8(encryptedBytes);
} catch (reason) {
throw new Error(
`${path} uses a legacy or unsupported shrouded-key PBE: ${reason instanceof Error ? reason.message : "unsupported algorithm"}`,
{ cause: reason },
);
}
if (context.password === undefined) {
addBag(context, {
path,
bagType: "shrouded-private-key",
bagOid,
...attributes,
encrypted: true,
state: "password-required",
encryption,
});
} else {
reservePbkdf2Work(context.pbkdf2Budget, encryption.iterations, path);
let decrypted: Awaited<ReturnType<typeof decryptEncryptedPkcs8>>;
try {
decrypted = await decryptEncryptedPkcs8(
encryptedBytes,
context.password,
);
} catch (reason) {
throw new Error(
`${path} could not decrypt its shrouded PKCS #8 key: ${reason instanceof Error ? reason.message : "decryption failed"}`,
{ cause: reason },
);
}
try {
addBag(context, {
path,
bagType: "shrouded-private-key",
bagOid,
...attributes,
encrypted: true,
state: "inspected",
encryption,
key: decrypted.key,
});
} finally {
decrypted.bytes.fill(0);
}
}
continue;
}
if (bagOid === CERT_BAG) {
const certFields = sequence(bagValue, `${path} CertBag`);
if (certFields.length !== 2)
throw new Error(`${path} CertBag has an unsupported shape.`);
const certId = oid(certFields[0], `${path} certificate type`);
if (certId !== X509_CERTIFICATE)
throw new Error(`${path} certificate type ${certId} is unsupported.`);
addBag(context, {
path,
bagType: "certificate",
bagOid,
...attributes,
encrypted: false,
state: "inspected",
certificateBytes: octets(
explicit(certFields[1], 0, `${path} certificate value`),
`${path} X.509 certificate`,
),
});
continue;
}
if (bagOid === SAFE_CONTENTS_BAG) {
addBag(context, {
path,
bagType: "safe-contents",
bagOid,
...attributes,
encrypted: false,
state: "inspected",
});
const nestedBytes = derBytes(bagValue);
try {
await inspectSafeContents(nestedBytes, context, path, depth + 1);
} finally {
nestedBytes.fill(0);
}
continue;
}
const bagType =
bagOid === CRL_BAG ? "crl" : bagOid === SECRET_BAG ? "secret" : "unknown";
addBag(context, {
path,
bagType,
bagOid,
...attributes,
encrypted: false,
state: "unsupported",
});
context.warnings.push(
`${path} (${bagType}, OID ${bagOid}) is inventoried but its value is deliberately not decoded or exported.`,
);
}
return context.bags.length - before;
}
async function inspectAuthenticatedSafe(
bytes: Uint8Array,
context: InventoryContext,
): Promise<Pkcs12ContentInventory[]> {
const blocks = sequence(
parseDer(bytes, "PFX AuthenticatedSafe"),
"AuthenticatedSafe",
);
if (blocks.length > MAX_CONTENT_INFOS)
throw new Error(
`PFX AuthenticatedSafe contains more than ${MAX_CONTENT_INFOS} ContentInfo values.`,
);
const contents: Pkcs12ContentInventory[] = [];
for (const [index, block] of blocks.entries()) {
const [contentType, content] = parseContentInfo(
block,
`AuthenticatedSafe[${index}]`,
);
const prefix = `content[${index}]`;
if (contentType === CMS_DATA) {
const bagCount = await inspectSafeContents(
octets(content, `${prefix} data`),
context,
prefix,
0,
);
contents.push({ index, type: "data", state: "parsed", bagCount });
continue;
}
if (contentType !== CMS_ENCRYPTED_DATA)
throw new Error(
`AuthenticatedSafe[${index}] content type ${contentType} is unsupported; only data and encryptedData are accepted.`,
);
const encryptedData = sequence(content, `${prefix} EncryptedData`);
if (encryptedData.length < 2 || encryptedData.length > 3)
throw new Error(`${prefix} EncryptedData has an unsupported shape.`);
if (integer(encryptedData[0], `${prefix} EncryptedData version`) !== 0n)
throw new Error(`${prefix} EncryptedData version must be 0.`);
const encryptedContentInfo = sequence(
encryptedData[1],
`${prefix} EncryptedContentInfo`,
);
if (encryptedContentInfo.length !== 3)
throw new Error(`${prefix} EncryptedContentInfo is incomplete.`);
if (
oid(encryptedContentInfo[0], `${prefix} encrypted content type`) !==
CMS_DATA
)
throw new Error(`${prefix} encrypted content must contain CMS data.`);
const algorithmIdentifier = derBytes(encryptedContentInfo[1]!);
const algorithmFields = sequence(
encryptedContentInfo[1],
`${prefix} encryption algorithm`,
);
const algorithmOid = oid(
algorithmFields[0],
`${prefix} encryption algorithm`,
);
if (algorithmOid !== PBES2) {
const family = algorithmOid.startsWith(LEGACY_PKCS12_PBE_PREFIX)
? "legacy PKCS #12 PBE"
: "unsupported encryption";
throw new Error(
`${prefix} uses ${family} OID ${algorithmOid}; only PBES2/PBKDF2 with AES-CBC or AES-GCM is supported.`,
);
}
const encryptedBytes = implicitOctets(
encryptedContentInfo[2],
0,
`${prefix} encryptedContent`,
);
const encryption = inspectPbes2Payload(algorithmIdentifier, encryptedBytes);
if (context.password === undefined) {
contents.push({
index,
type: "encryptedData",
state: "password-required",
encryption,
bagCount: 0,
});
continue;
}
let plaintext: Uint8Array | undefined;
try {
reservePbkdf2Work(context.pbkdf2Budget, encryption.iterations, prefix);
plaintext = (
await decryptPbes2Payload(
algorithmIdentifier,
encryptedBytes,
context.password,
)
).bytes;
const bagCount = await inspectSafeContents(plaintext, context, prefix, 0);
contents.push({
index,
type: "encryptedData",
state: "parsed",
encryption,
bagCount,
});
} catch (reason) {
throw new Error(
`${prefix} PBES2 content could not be decrypted and parsed: ${reason instanceof Error ? reason.message : "decryption failed"}`,
{ cause: reason },
);
} finally {
plaintext?.fill(0);
}
}
return contents;
}
export function recognizesPkcs12(bytes: Uint8Array): boolean {
try {
const root = sequence(parseDer(bytes, "PKCS #12/PFX"), "PFX");
return (
root.length >= 2 &&
root.length <= 3 &&
integer(root[0], "PFX version") === 3n &&
sequence(root[1], "PFX authSafe ContentInfo").length >= 1
);
} catch {
return false;
}
}
export async function inspectPkcs12(
bytes: Uint8Array,
options: { password?: string } = {},
): Promise<Pkcs12Inspection> {
if (options.password !== undefined) boundedPassword(options.password);
const root = sequence(parseDer(bytes, "PKCS #12/PFX"), "PFX");
if (root.length < 2 || root.length > 3)
throw new Error("PFX must contain version, authSafe and optional MacData.");
if (integer(root[0], "PFX version") !== 3n)
throw new Error("PFX version must be 3.");
const [authSafeContentType, authSafeContent] = parseContentInfo(
root[1]!,
"PFX authSafe ContentInfo",
);
if (authSafeContentType !== CMS_DATA)
throw new Error(
`PFX authSafe content type ${authSafeContentType} is unsupported; RFC 7292 requires CMS data here.`,
);
const authSafeBytes = octets(authSafeContent, "PFX authSafe data");
let mac: Pkcs12MacInspection;
if (!root[2]) {
mac = { present: false, status: "absent" };
} else {
const parsedMac = parseMacData(root[2]);
if (options.password === undefined) {
mac = {
present: true,
status: parsedMac.algorithm ? "password-required" : "unsupported",
algorithm: parsedMac.algorithm?.name ?? `OID ${parsedMac.algorithmOid}`,
algorithmOid: parsedMac.algorithmOid,
iterations: parsedMac.iterations,
saltBytes: parsedMac.salt.length,
};
} else {
await verifyMacData(parsedMac, authSafeBytes, options.password);
mac = {
present: true,
status: "verified",
algorithm: parsedMac.algorithm!.name,
algorithmOid: parsedMac.algorithmOid,
iterations: parsedMac.iterations,
saltBytes: parsedMac.salt.length,
};
}
}
const context: InventoryContext = {
...(options.password === undefined ? {} : { password: options.password }),
bags: [],
warnings: [],
pbkdf2Budget: { iterations: 0 },
};
if (!root[2])
context.warnings.push(
"MacData is absent, so the AuthenticatedSafe has no verified password/integrity check.",
);
const contents = await inspectAuthenticatedSafe(authSafeBytes, context);
return {
recognized: true,
version: 3,
bytes: bytes.length,
authSafeContentType: CMS_DATA,
mac,
contents,
bags: context.bags,
warnings: context.warnings,
};
}