@indrajala/asn1der
Version:
parse ASN.1 DER structures
247 lines (180 loc) • 7.2 kB
text/typescript
import { bitsAreSet, getStandAloneBitsValue } from "./bitwise";
import { Asn1Node } from "./model/Asn1Node";
import { Asn1Construction } from "./model/enums";
import { IAsn1Identifier } from "./model/interfaces";
const parseIdentifer = (raw: Buffer): {
identifier: IAsn1Identifier,
lengthValueRemainder: Buffer
} => {
const b0 = raw.readUInt8(0);
//console.log(`b0: ${b0.toString()}d 0x${b0.toString(16)} ${b0.toString(2).padStart(8, '0')}b`);
const klass = getStandAloneBitsValue(b0, [7,8]);
const construction = getStandAloneBitsValue(b0, [6]);
const initialOctetTagNumber = getStandAloneBitsValue(b0, [1,2,3,4,5]);
let remainder = raw.subarray(1);
const isLongFormTag = initialOctetTagNumber == 31;
// long-form tag
//
let longFormTagNumber = 0;
if (isLongFormTag) {
let tagByteCount = 1;
while (bitsAreSet(remainder.readUInt8(tagByteCount - 1), [8])) {
tagByteCount = tagByteCount + 1;
if (tagByteCount > remainder.length) {
throw 'badly encoded long-form tag, infinite subsequent tag bytes';
}
}
//console.log(`tag byte count: ${tagByteCount}`);
const tagBytes = remainder.subarray(0, tagByteCount);
//console.log(`tagBytes.length: ${tagBytes.length}`);
remainder = remainder.subarray(tagByteCount);
// TODO common with parseOID ?
const sevenBitStrings = [...tagBytes]
.map(it => it.toString(2))
.map(it => it.padStart(8, '0'))
.map(it => it.substring(1));
//console.log(`sevenBitStrings: ${sevenBitStrings}`);
const joined = sevenBitStrings.join('');
//console.log(`joined: ${joined}`);
const padded = joined.padStart(joined.length + ( 8 - (joined.length % 8)), '0');
//console.log(`padded: ${padded}`);
longFormTagNumber = parseInt(padded, 2);
//console.log(`long-form tag number: ${longFormTagNumber}`);
}
const tagNumber = (isLongFormTag)
? longFormTagNumber
: initialOctetTagNumber;
//console.log(`final tag number: ${tagNumber} => ${tagDescription}`);
return {
identifier:{
raw: Buffer.from([b0]),
class: klass,
construction,
tagNumber,
},
lengthValueRemainder: remainder
};
};
const parseLength = (
raw: Buffer
): {
length: number,
contentRemainder: Buffer
} => {
const firstLengthByte = raw.readUInt8(0);
let remainder = raw.subarray(1);
const isLongFormLength = bitsAreSet(firstLengthByte, [8]);
//console.log(isLongFormLength ? 'long form length' : 'short form length');
let longFormLength = 0;
if (isLongFormLength) {
const numberOfSubsequentLengthBytes = getStandAloneBitsValue(firstLengthByte, [1,2,3,4,5,6,7]);
//console.log(`content length is coded on ${numberOfSubsequentLengthBytes} subsequent bytes`);
const lengthBytes = remainder.subarray(0, numberOfSubsequentLengthBytes);
remainder = remainder.subarray(numberOfSubsequentLengthBytes);
longFormLength = parseInt(lengthBytes.toString('hex'), 16);
//console.log(`long form length: ${longFormLength}`);
}
const length = (isLongFormLength)
? longFormLength
: firstLengthByte;
//console.log(`length ${length}`);
return { length, contentRemainder: remainder };
};
export const pemToDER = (pem: string): Buffer => {
const toRemove = [
'-----BEGIN CERTIFICATE-----',
'-----END CERTIFICATE-----',
'-----BEGIN RSA PRIVATE KEY-----',
'-----END RSA PRIVATE KEY-----',
'-----BEGIN CERTIFICATE REQUEST-----',
'-----END CERTIFICATE REQUEST-----',
'\n',
' '
];
const b64 = toRemove
.reduce((ac, token) => ac.replace(new RegExp(token, 'g'), ''), pem);
return Buffer.from(b64, 'base64');
};
export const parseOID = (b: Buffer): string => {
const firstByte = b.readInt8(0);
const firstNodeValue = Math.floor(firstByte / 40);
const secondNodeValue = firstByte - (firstNodeValue * 40);
const nodes = [firstNodeValue, secondNodeValue];
let remainder = b.subarray(1);
while (remainder.length > 0) {
const isLongForm = bitsAreSet(remainder.readUInt8(0), [8]);
if (!isLongForm) {
nodes.push(remainder.readUInt8(0));
remainder = remainder.subarray(1);
} else {
const head = [];
while (bitsAreSet(remainder.readUInt8(0), [8])) {
head.push(remainder.readUInt8(0));
remainder = remainder.subarray(1);
}
head.push(remainder.readUInt8(0));
remainder = remainder.subarray(1);
const sevenBitStrings = [...head]
.map(it => it.toString(2))
.map(it => it.padStart(8, '0'))
.map(it => it.substring(1));
//console.log(`sevenBitStrings: ${sevenBitStrings}`);
const joined = sevenBitStrings.join('');
//console.log(`joined: ${joined}`);
const padded = joined.padStart(joined.length + ( 8 - (joined.length % 8)), '0');
//console.log(`padded: ${padded}`);
const longFormOID = parseInt(padded, 2);
nodes.push(longFormOID);
}
}
return nodes.join('.');
};
export const parseDER = (
der: Buffer
): Array<Asn1Node> => {
// console.log(der.toString('hex').substring(0, 20), "...");
const { identifier, lengthValueRemainder } = parseIdentifer(der);
// console.log(`${' '.repeat(indent)} ${identifier.classDescription
// } ${identifier.constructionDescription
// } #${identifier.tagNumber
// } (${identifier.tagDescription})`);
// const tagDescription = asn1TagDescription.has(identifier.tagNumber)
// ? asn1TagDescription.get(identifier.tagNumber)
// : null;
// const label = (tagDescription != null)
// ? tagDescription
// : `${identifier.tagNumber}`;
// parse length
const { length, contentRemainder } = parseLength(lengthValueRemainder);
const content = contentRemainder.subarray(0, length);
const residualRemainder = contentRemainder.subarray(length);
// console.log(`${' '.repeat(indent)}[${label}] ${content.toString('hex').substring(0, 60)}`);
// console.log(`content: ${content.toString('hex')}`);
const children = (identifier.construction == Asn1Construction.Primitive)
? []
: (content.length > 0)
? parseDER(content)
: [];
// if (
// (identifier.construction == Asn1Construction.Constructed)
// &&
// (content.length == 0)
// ) {
// console.log(`constructed element with zero length content:`);
// console.log(identifier);
// }
const peers = (residualRemainder.length > 0)
? parseDER(residualRemainder)
: [];
const self = new Asn1Node(
der,
identifier,
length,
content,
children,
);
return [
self,
...peers
];
};