Files
crypto-tools/src/crypto/pkcs12.ts
T
zemion 82bb01b13f
Verify / verify (push) Canceled after 0s
Release Crypto Tools 0.2.0
2026-09-02 10:40:23 +02:00

771 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
};
}