UNPKG

asn1-ts

Version:

ASN.1 encoding and decoding, including BER, CER, and DER.

661 lines (660 loc) 26.6 kB
import ASN1Element from "../asn1.mjs"; import * as errors from "../errors.mjs"; import { ASN1Construction, ASN1TagClass, ASN1UniversalType, } from "../values.mjs"; import CharacterString from "../types/CharacterString.mjs"; import convertBytesToText from "../utils/convertBytesToText.mjs"; import convertTextToBytes from "../utils/convertTextToBytes.mjs"; import sortCanonically from "../utils/sortCanonically.mjs"; import ObjectIdentifier from "../types/ObjectIdentifier.mjs"; import encodeBoolean from "./x690/encoders/encodeBoolean.mjs"; import decodeBoolean from "./der/decoders/decodeBoolean.mjs"; import encodeBitString from "./x690/encoders/encodeBitString.mjs"; import decodeBitString from "./der/decoders/decodeBitString.mjs"; import encodeReal from "./x690/encoders/encodeReal.mjs"; import decodeReal from "./der/decoders/decodeReal.mjs"; import encodeSequence from "./x690/encoders/encodeSequence.mjs"; import decodeSequence from "./der/decoders/decodeSequence.mjs"; import encodeUTCTime from "./x690/encoders/encodeUTCTime.mjs"; import decodeUTCTime from "./der/decoders/decodeUTCTime.mjs"; import encodeGeneralizedTime from "./x690/encoders/encodeGeneralizedTime.mjs"; import decodeGeneralizedTime from "./der/decoders/decodeGeneralizedTime.mjs"; import encodeExternal from "../codecs/x690/encoders/encodeExternal.mjs"; import encodeEmbeddedPDV from "../codecs/x690/encoders/encodeEmbeddedPDV.mjs"; import encodeCharacterString from "../codecs/x690/encoders/encodeCharacterString.mjs"; import decodeExternal from "../codecs/x690/decoders/decodeExternal.mjs"; import decodeEmbeddedPDV from "../codecs/x690/decoders/decodeEmbeddedPDV.mjs"; import decodeCharacterString from "../codecs/x690/decoders/decodeCharacterString.mjs"; import encodeGraphicString from "../codecs/ber/encoders/encodeGraphicString.mjs"; import encodeNumericString from "../codecs/ber/encoders/encodeNumericString.mjs"; import encodeObjectDescriptor from "../codecs/ber/encoders/encodeObjectDescriptor.mjs"; import encodePrintableString from "../codecs/ber/encoders/encodePrintableString.mjs"; import encodeVisibleString from "../codecs/ber/encoders/encodeVisibleString.mjs"; import encodeGeneralString from "../codecs/ber/encoders/encodeGeneralString.mjs"; import decodeGraphicString from "../codecs/x690/decoders/decodeGraphicString.mjs"; import decodeNumericString from "../codecs/x690/decoders/decodeNumericString.mjs"; import decodeObjectDescriptor from "../codecs/x690/decoders/decodeObjectDescriptor.mjs"; import decodePrintableString from "../codecs/x690/decoders/decodePrintableString.mjs"; import decodeVisibleString from "../codecs/x690/decoders/decodeVisibleString.mjs"; import decodeGeneralString from "../codecs/x690/decoders/decodeGeneralString.mjs"; import encodeDuration from "../codecs/x690/encoders/encodeDuration.mjs"; import decodeDuration from "../codecs/der/decoders/decodeDuration.mjs"; import X690Element from "../x690.mjs"; import { isUniquelyTagged } from "../utils/index.mjs"; import { Buffer } from "node:buffer"; export default class DERElement extends X690Element { get value() { if (this._value instanceof Uint8Array) { return this._value; } const bytes = encodeSequence(this._value); this._value = bytes; return bytes; } set value(v) { this._currentValueLength = v.length; this._value = v; } construct(els) { this._currentValueLength = undefined; this._value = els; } set boolean(value) { this.value = encodeBoolean(value); } get boolean() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("BOOLEAN cannot be constructed.", this); } return decodeBoolean(this.value); } set bitString(value) { this.value = encodeBitString(value); } get bitString() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("BIT STRING cannot be constructed.", this); } return decodeBitString(this.value); } set octetString(value) { this.value = new Uint8Array(value); } get octetString() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("OCTET STRING cannot be constructed.", this); } return new Uint8Array(this.value); } set objectDescriptor(value) { this.value = encodeObjectDescriptor(value); } get objectDescriptor() { return decodeObjectDescriptor(this.value); } set external(value) { this.value = encodeExternal(value); this.construction = ASN1Construction.constructed; } get external() { if (this.construction !== ASN1Construction.constructed) { throw new errors.ASN1ConstructionError("EXTERNAL cannot be primitively-constructed.", this); } return decodeExternal(this.value); } set real(value) { this.value = encodeReal(value); } get real() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("REAL cannot be constructed.", this); } return decodeReal(this.value); } set embeddedPDV(value) { this.value = encodeEmbeddedPDV(value); this.construction = ASN1Construction.constructed; } get embeddedPDV() { if (this.construction !== ASN1Construction.constructed) { throw new errors.ASN1ConstructionError("EMBEDDED PDV cannot be primitively-constructed.", this); } return decodeEmbeddedPDV(this.value); } set utf8String(value) { this.value = convertTextToBytes(value); } get utf8String() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("UTF8String cannot be constructed.", this); } return convertBytesToText(this.value); } set sequence(value) { this.construct(value); this.construction = ASN1Construction.constructed; } get sequence() { if (this.construction !== ASN1Construction.constructed) { throw new errors.ASN1ConstructionError("SET or SEQUENCE cannot be primitively constructed.", this); } if (Array.isArray(this._value)) { return this._value; } return decodeSequence(this.value); } set set(value) { sortCanonically(value); this.sequence = value; } get set() { const ret = this.sequence; if (!isUniquelyTagged(ret)) { throw new errors.ASN1ConstructionError("Duplicate tag in SET.", this); } return ret; } set sequenceOf(value) { this.construct(value); this.construction = ASN1Construction.constructed; } get sequenceOf() { if (this.construction !== ASN1Construction.constructed) { throw new errors.ASN1ConstructionError("SET or SEQUENCE cannot be primitively constructed.", this); } if (Array.isArray(this._value)) { return this._value; } return decodeSequence(this.value); } set setOf(value) { this.sequence = value; } get setOf() { return this.sequence; } set numericString(value) { this.value = encodeNumericString(value); } get numericString() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("NumericString cannot be constructed.", this); } return decodeNumericString(this.value); } set printableString(value) { this.value = encodePrintableString(value); } get printableString() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("PrintableString cannot be constructed.", this); } return decodePrintableString(this.value); } set teletexString(value) { this.value = new Uint8Array(value); } get teletexString() { return this.octetString; } set videotexString(value) { this.value = new Uint8Array(value); } get videotexString() { return this.octetString; } set ia5String(value) { this.value = convertTextToBytes(value); } get ia5String() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("IA5String cannot be constructed.", this); } return convertBytesToText(this.value); } set utcTime(value) { this.value = encodeUTCTime(value); } get utcTime() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("UTCTime cannot be constructed.", this); } return decodeUTCTime(this.value); } set generalizedTime(value) { this.value = encodeGeneralizedTime(value); } get generalizedTime() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("GeneralizedTime cannot be constructed.", this); } return decodeGeneralizedTime(this.value); } set graphicString(value) { this.value = encodeGraphicString(value); } get graphicString() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("GraphicString cannot be constructed.", this); } return decodeGraphicString(this.value); } set visibleString(value) { this.value = encodeVisibleString(value); } get visibleString() { return decodeVisibleString(this.value); } set generalString(value) { this.value = encodeGeneralString(value); } get generalString() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("GeneralString cannot be constructed.", this); } return decodeGeneralString(this.value); } set characterString(value) { this.value = encodeCharacterString(value); this.construction = ASN1Construction.constructed; } get characterString() { if (this.construction !== ASN1Construction.constructed) { throw new errors.ASN1ConstructionError("CHARACTER STRING cannot be primitively-constructed.", this); } return decodeCharacterString(this.value); } set universalString(value) { const buf = new Uint8Array(value.length << 2); for (let i = 0; i < value.length; i++) { buf[(i << 2)] = value.charCodeAt(i) >>> 24; buf[(i << 2) + 1] = value.charCodeAt(i) >>> 16; buf[(i << 2) + 2] = value.charCodeAt(i) >>> 8; buf[(i << 2) + 3] = value.charCodeAt(i); } this.value = buf; } get universalString() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("UniversalString cannot be constructed.", this); } if (this.value.length % 4) { throw new errors.ASN1Error("UniversalString encoded on non-mulitple of four bytes.", this); } let ret = ""; for (let i = 0; i < this.value.length; i += 4) { ret += String.fromCharCode((this.value[i + 0] << 24) + (this.value[i + 1] << 16) + (this.value[i + 2] << 8) + (this.value[i + 3] << 0)); } return ret; } set bmpString(value) { const buf = new Uint8Array(value.length << 1); for (let i = 0, strLen = value.length; i < strLen; i++) { buf[(i << 1)] = value.charCodeAt(i) >>> 8; buf[(i << 1) + 1] = value.charCodeAt(i); } this.value = buf; } get bmpString() { if (this.construction !== ASN1Construction.primitive) { throw new errors.ASN1ConstructionError("BMPString cannot be constructed.", this); } if (this.value.length % 2) throw new errors.ASN1Error("BMPString encoded on non-mulitple of two bytes.", this); if (typeof Buffer !== "undefined") { const swappedEndianness = Buffer.allocUnsafe(this.value.length); for (let i = 0; i < this.value.length; i += 2) { swappedEndianness[i] = this.value[i + 1]; swappedEndianness[i + 1] = this.value[i]; } return swappedEndianness.toString("utf16le"); } else if (typeof TextEncoder !== "undefined") { return (new TextDecoder("utf-16be")).decode(new Uint8Array(this.value)); } else { throw new errors.ASN1Error("Neither TextDecoder nor Buffer are defined to decode bytes into text.", this); } } set duration(value) { this.value = encodeDuration(value); } get duration() { return decodeDuration(this.value); } encode(value) { switch (typeof value) { case ("undefined"): { this.value = new Uint8Array(0); break; } case ("boolean"): { this.tagNumber = ASN1UniversalType.boolean; this.boolean = value; break; } case ("number"): { if (Number.isInteger(value)) { this.tagNumber = ASN1UniversalType.integer; this.integer = value; } else { this.tagNumber = ASN1UniversalType.realNumber; this.real = value; } break; } case ("bigint"): { this.tagNumber = ASN1UniversalType.integer; this.integer = value; break; } case ("string"): { this.tagNumber = ASN1UniversalType.utf8String; this.utf8String = value; break; } case ("object"): { if (!value) { this.tagNumber = ASN1UniversalType.nill; this.value = new Uint8Array(0); } else if (value instanceof Uint8Array) { this.tagNumber = ASN1UniversalType.octetString; this.octetString = value; } else if (value instanceof Uint8ClampedArray) { this.tagNumber = ASN1UniversalType.bitString; this.bitString = value; } else if (value instanceof ASN1Element) { this.construction = ASN1Construction.constructed; this.sequence = [value]; } else if (value instanceof Set) { this.construction = ASN1Construction.constructed; this.set = Array.from(value).map((v) => { if (typeof v === "object" && v instanceof ASN1Element) { return v; } else { const e = new DERElement(); e.encode(v); return e; } }); } else if ((value instanceof ObjectIdentifier) || (value.constructor?.name === "ObjectIdentifier")) { this.tagNumber = ASN1UniversalType.objectIdentifier; this.objectIdentifier = value; } else if (Array.isArray(value)) { this.construction = ASN1Construction.constructed; this.tagNumber = ASN1UniversalType.sequence; this.sequence = value.map((sub) => { const ret = new DERElement(); ret.encode(sub); return ret; }); } else if (value instanceof Date) { this.generalizedTime = value; } else { throw new errors.ASN1Error(`Cannot encode value of type ${value.constructor.name}.`, this); } break; } default: { throw new errors.ASN1Error(`Cannot encode value of type ${typeof value}.`, this); } } } static fromSequence(sequence) { const ret = new DERElement(ASN1TagClass.universal, ASN1Construction.constructed, ASN1UniversalType.sequence); ret.sequence = sequence.filter((element) => Boolean(element)); return ret; } static fromSet(set) { const ret = new DERElement(ASN1TagClass.universal, ASN1Construction.constructed, ASN1UniversalType.set); ret.set = set.filter((element) => Boolean(element)); return ret; } static fromSetOf(set) { const ret = new DERElement(ASN1TagClass.universal, ASN1Construction.constructed, ASN1UniversalType.set); ret.setOf = set.filter((element) => Boolean(element)); return ret; } get inner() { if (this.construction !== ASN1Construction.constructed) { throw new errors.ASN1ConstructionError("An explicitly-encoded element cannot be encoded using " + "primitive construction.", this); } if (Array.isArray(this._value)) { if (this._value.length !== 1) { throw new errors.ASN1ConstructionError(`An explicitly-encoding element contained ${this._value.length} encoded elements.`, this); } return this._value[0]; } const ret = new DERElement(); const readBytes = ret.fromBytes(this._value); if (readBytes !== this._value.length) { throw new errors.ASN1ConstructionError("An explicitly-encoding element contained more than one single " + "encoded element. The tag number of the first decoded " + `element was ${ret.tagNumber}, and it was encoded on ` + `${readBytes} bytes.`, this); } return ret; } set inner(value) { this.construction = ASN1Construction.constructed; this._value = [value]; } constructor(tagClass = ASN1TagClass.universal, construction = ASN1Construction.primitive, tagNumber = ASN1UniversalType.endOfContent, value = undefined) { super(); this._value = new Uint8Array(0); this.encode(value); this.tagClass = tagClass; this.construction = construction; this.tagNumber = tagNumber; } fromBytes(bytes) { if (bytes.length < 2) { throw new errors.ASN1TruncationError("Tried to decode a DER element that is less than two bytes.", this); } let cursor = 0; switch (bytes[cursor] & 0b11000000) { case (0b00000000): this.tagClass = ASN1TagClass.universal; break; case (0b01000000): this.tagClass = ASN1TagClass.application; break; case (0b10000000): this.tagClass = ASN1TagClass.context; break; case (0b11000000): this.tagClass = ASN1TagClass.private; break; default: this.tagClass = ASN1TagClass.universal; } this.construction = ((bytes[cursor] & 0b00100000) ? ASN1Construction.constructed : ASN1Construction.primitive); this.tagNumber = (bytes[cursor] & 0b00011111); cursor++; if (this.tagNumber >= 31) { if (bytes[cursor] === 0b10000000) { throw new errors.ASN1PaddingError("Leading padding byte on long tag number encoding.", this); } this.tagNumber = 0; const limit = (((bytes.length - 1) >= 4) ? 4 : (bytes.length - 1)); while (cursor < limit) { if (!(bytes[cursor++] & 0b10000000)) break; } if (bytes[cursor - 1] & 0b10000000) { if (limit === (bytes.length - 1)) { throw new errors.ASN1TruncationError("ASN.1 tag number appears to have been truncated.", this); } else { throw new errors.ASN1OverflowError("ASN.1 tag number too large.", this); } } for (let i = 1; i < cursor; i++) { this.tagNumber <<= 7; this.tagNumber |= (bytes[i] & 0x7F); } if (this.tagNumber < 31) { throw new errors.ASN1Error("ASN.1 tag number could have been encoded in short form.", this); } } if ((bytes[cursor] & 0b10000000) === 0b10000000) { const numberOfLengthOctets = (bytes[cursor] & 0x7F); if (numberOfLengthOctets === 0b01111111) { throw new errors.ASN1UndefinedError("Length byte with undefined meaning encountered.", this); } if (numberOfLengthOctets > 4) { throw new errors.ASN1OverflowError("Element length too long to decode to an integer.", this); } if (cursor + numberOfLengthOctets >= bytes.length) { throw new errors.ASN1TruncationError("Element length bytes appear to have been truncated.", this); } cursor++; const lengthNumberOctets = new Uint8Array(4); for (let i = numberOfLengthOctets; i > 0; i--) { lengthNumberOctets[(4 - i)] = bytes[(cursor + numberOfLengthOctets - i)]; } let length = 0; for (const octet of lengthNumberOctets) { length <<= 8; length += octet; } if ((cursor + length) < cursor) { throw new errors.ASN1OverflowError("ASN.1 element too large.", this); } cursor += (numberOfLengthOctets); if ((cursor + length) > bytes.length) { throw new errors.ASN1TruncationError("ASN.1 element truncated.", this); } if (((length <= 127 && length >= -128) && numberOfLengthOctets > 1) || ((length <= 32767 && length >= -32768) && numberOfLengthOctets > 2) || ((length <= 8388607 && length >= -8388608) && numberOfLengthOctets > 3)) { throw new errors.ASN1PaddingError("DER-encoded long-form length encoded on more octets than necessary", this); } this.value = bytes.slice(cursor, (cursor + length)); return (cursor + length); } else { const length = (bytes[cursor++] & 0x7F); if ((cursor + length) > bytes.length) { throw new errors.ASN1TruncationError("ASN.1 element was truncated.", this); } this.value = bytes.slice(cursor, (cursor + length)); return (cursor + length); } } tagAndLengthBytes() { const tagBytes = [0x00]; tagBytes[0] |= (this.tagClass << 6); tagBytes[0] |= (this.construction << 5); if (this.tagNumber < 31) { tagBytes[0] |= this.tagNumber; } else { tagBytes[0] |= 0b00011111; let number = this.tagNumber; const encodedNumber = []; while (number !== 0) { encodedNumber.unshift(number & 0x7F); number >>>= 7; encodedNumber[0] |= 0b10000000; } encodedNumber[encodedNumber.length - 1] &= 0b01111111; tagBytes.push(...encodedNumber); } let lengthOctets = [0x00]; if (this.value.length < 127) { lengthOctets[0] = this.value.length; } else { const length = this.value.length; lengthOctets = [0, 0, 0, 0]; for (let i = 0; i < 4; i++) { lengthOctets[i] = ((length >>> ((3 - i) << 3)) & 0xFF); } let startOfNonPadding = 0; for (let i = 0; i < (lengthOctets.length - 1); i++) { if (lengthOctets[i] === 0x00) startOfNonPadding++; } lengthOctets = lengthOctets.slice(startOfNonPadding); lengthOctets.unshift(0b10000000 | lengthOctets.length); } const ret = new Uint8Array(tagBytes.length + lengthOctets.length); ret.set(tagBytes, 0); ret.set(lengthOctets, tagBytes.length); return ret; } toBuffers() { return [ this.tagAndLengthBytes(), ...(Array.isArray(this._value) ? this._value.flatMap((el) => el.toBuffers()) : [this._value]), ]; } deconstruct() { return new Uint8Array(this.value); } get components() { if (Array.isArray(this._value)) { return this._value; } const encodedElements = []; let i = 0; while (i < this._value.length) { const next = new DERElement(); i += next.fromBytes(this.value.subarray(i)); encodedElements.push(next); } return encodedElements; } lengthLength(valueLength) { const len = valueLength ?? this.valueLength(); if (len < 127) { return 1; } let lengthOctets = [0, 0, 0, 0]; for (let i = 0; i < 4; i++) { lengthOctets[i] = ((len >>> ((3 - i) << 3)) & 0xFF); } let startOfNonPadding = 0; for (let i = 0; i < (lengthOctets.length - 1); i++) { if (lengthOctets[i] === 0x00) startOfNonPadding++; } return 5 - startOfNonPadding; } valueLength() { if (this._currentValueLength !== undefined) { return this._currentValueLength; } if (!Array.isArray(this._value)) { return this._value.length; } let len = 0; for (const el of this._value) { len += el.tlvLength(); } this._currentValueLength = len; return len; } tlvLength() { const value_len = this.valueLength(); return (this.tagLength() + this.lengthLength(value_len) + value_len); } }