blob: 369f68548fa12b3051dcd2a460eec51928b3131d [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
class Cbor {
constructor(buffer) {
this.slice = new Uint8Array(buffer);
}
get data() {
return this.slice;
}
get length() {
return this.slice.length;
}
get empty() {
return this.slice.length == 0;
}
get hex() {
const hexTable = '0123456789abcdef';
let s = '';
for (let i = 0; i < this.data.length; i++) {
s += hexTable.charAt(this.data[i] >> 4);
s += hexTable.charAt(this.data[i] & 15);
}
return s;
}
base64Encode(chars, padding) {
const len3 = 3 * Math.floor(this.slice.length / 3);
var chunks = [];
for (let i = 0; i < len3; i += 3) {
const v =
(this.slice[i] << 16) + (this.slice[i + 1] << 8) + this.slice[i + 2];
chunks.push(
chars[v >> 18] + chars[(v >> 12) & 0x3f] + chars[(v >> 6) & 0x3f] +
chars[v & 0x3f]);
}
const remainder = this.slice.length - len3;
if (remainder == 1) {
const v = this.slice[len3];
chunks.push(chars[v >> 2] + chars[(v << 4) & 0x3f]);
if (padding == 1 /* Include */) {
chunks.push('==');
}
} else if (remainder == 2) {
const v = (this.slice[len3] << 8) + this.slice[len3 + 1];
chunks.push(
chars[v >> 10] + chars[(v >> 4) & 0x3f] + chars[(v << 2) & 0x3f]);
if (padding == 1 /* Include */) {
chunks.push('=');
}
}
return chunks.join('');
}
webSafeBase64() {
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
return this.base64Encode(chars, 0 /* None */);
}
base64() {
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
return this.base64Encode(chars, 1 /* Include */);
}
compare(other) {
if (this.length < other.length) {
return -1;
} else if (this.length > other.length) {
return 1;
}
for (let i = 0; i < this.length; i++) {
if (this.slice[i] < other.slice[i]) {
return -1;
} else if (this.slice[i] > other.slice[i]) {
return 1;
}
}
return 0;
}
getU8() {
if (this.empty) {
throw('Cbor: empty during getU8');
}
const byte = this.slice[0];
this.slice = this.slice.subarray(1);
return byte;
}
skip(n) {
if (this.length < n) {
throw('Cbor: too few bytes to skip');
}
this.slice = this.slice.subarray(n);
}
getBytes(n) {
if (this.length < n) {
throw('Cbor: insufficient bytes in getBytes');
}
const ret = this.slice.subarray(0, n);
this.slice = this.slice.subarray(n);
return ret;
}
getUnsigned(n) {
const bytes = this.getBytes(n);
let value = 0;
for (let i = 0; i < n; i++) {
value <<= 8;
value |= bytes[i];
}
return value;
}
getU16() {
return this.getUnsigned(2);
}
getU32() {
return this.getUnsigned(4);
}
getASN1_(expectedTag, includeHeader) {
if (this.empty) {
throw 'getASN1: empty slice, expected tag ' + expectedTag;
}
const v = this.getAnyASN1();
if (v.tag != expectedTag) {
throw 'getASN1: got tag ' + v.tag + ', want ' + expectedTag;
}
if (!includeHeader) {
v.val.skip(v.headerLen);
}
return v.val;
}
getASN1(expectedTag) {
return this.getASN1_(expectedTag, false);
}
getASN1Element(expectedTag) {
return this.getASN1_(expectedTag, true);
}
getOptionalASN1(expectedTag) {
if (this.slice.length < 1 || this.slice[0] != expectedTag) {
return null;
}
return this.getASN1(expectedTag);
}
getAnyASN1() {
const header = new Cbor(this.slice);
const tag = header.getU8();
const lengthByte = header.getU8();
if ((tag & 0x1f) == 0x1f) {
throw 'getAnyASN1: long-form tag found';
}
let len = 0;
let headerLen = 0;
if ((lengthByte & 0x80) == 0) {
// Short form length.
len = lengthByte + 2;
headerLen = 2;
} else {
// The high bit indicates that this is the long form, while the next 7
// bits encode the number of subsequent octets used to encode the length
// (ITU-T X.690 clause 8.1.3.5.b).
const numBytes = lengthByte & 0x7f;
// Bitwise operations are always on signed 32-bit two's complement
// numbers. This check ensures that we stay under this limit. We could
// do this in a better way, but there's no need to process very large
// objects.
if (numBytes == 0 || numBytes > 3) {
throw 'getAnyASN1: bad ASN.1 long-form length';
}
const lengthBytes = header.getBytes(numBytes);
for (let i = 0; i < numBytes; i++) {
len <<= 8;
len |= lengthBytes[i];
}
if (len < 128 || (len >> ((numBytes - 1) * 8)) == 0) {
throw 'getAnyASN1: incorrectly encoded ASN.1 length';
}
headerLen = 2 + numBytes;
len += headerLen;
}
if (this.slice.length < len) {
throw 'getAnyASN1: too few bytes in input';
}
const prefix = this.slice.subarray(0, len);
this.slice = this.slice.subarray(len);
return {tag: tag, headerLen: headerLen, val: new Cbor(prefix)};
}
getBase128Int() {
let lookahead = this.slice.length;
if (lookahead > 4) {
lookahead = 4;
}
let len = 0;
for (let i = 0; i < lookahead; i++) {
if (!(this.slice[i] & 0x80)) {
len = i + 1;
break;
}
}
if (len == 0) {
throw 'base128 value too large';
}
let n = 0;
let octets = this.getBytes(len);
for (let i = 0; i < len; i++) {
if ((n & 0xff000000) != 0) {
throw 'base128 value too large';
}
n <<= 7;
n |= octets[i] & 0x7f;
}
return n;
}
getASN1ObjectIdentifier() {
let b = this.getASN1(6 /* OBJECT */);
let first = b.getBase128Int();
let result = [0, 0];
result[1] = first % 40;
result[0] = (first - result[1]) / 40;
while (!b.empty) {
result.push(b.getBase128Int());
}
return result;
}
getCBORHeader() {
const copy = new Cbor(this.slice);
const a = this.getU8();
const majorType = a >> 5;
const info = a & 31;
if (info < 24) {
return [majorType, info, new Cbor(copy.getBytes(1))];
} else if (info < 28) {
const lengthLength = 1 << (info - 24);
let data = this.getBytes(lengthLength);
let value = 0;
for (let i = 0; i < lengthLength; i++) {
// Javascript has problems handling uint64s given the limited range of
// a double.
if (value > 35184372088831) {
throw('Cbor: cannot represent CBOR number');
}
// Not using bitwise operations to avoid truncating to 32 bits.
value *= 256;
value += data[i];
}
switch (lengthLength) {
case 1:
if (value < 24) {
throw('Cbor: value should have been encoded in single byte');
}
break;
case 2:
if (value < 256) {
throw('Cbor: non-minimal integer');
}
break;
case 4:
if (value < 65536) {
throw('Cbor: non-minimal integer');
}
break;
case 8:
if (value < 4294967296) {
throw('Cbor: non-minimal integer');
}
break;
}
return [majorType, value, new Cbor(copy.getBytes(1 + lengthLength))];
} else {
throw('Cbor: CBOR contains unhandled info value ' + info);
}
}
getCBOR() {
const [major, value] = this.getCBORHeader();
switch (major) {
case 0:
return value;
case 1:
return 0 - (1 + value);
case 2:
return this.getBytes(value);
case 3:
return this.getBytes(value);
case 4: {
let ret = new Array(value);
for (let i = 0; i < value; i++) {
ret[i] = this.getCBOR();
}
return ret;
}
case 5:
if (value == 0) {
return {};
}
let copy = new Cbor(this.data);
const [firstKeyMajor] = copy.getCBORHeader();
if (firstKeyMajor == 3) {
// String-keyed map.
let lastKeyHeader = new Cbor(new Uint8Array(0));
let lastKeyBytes = new Cbor(new Uint8Array(0));
let ret = {};
for (let i = 0; i < value; i++) {
const [keyMajor, keyLength, keyHeader] = this.getCBORHeader();
if (keyMajor != 3) {
throw('Cbor: non-string in string-valued map');
}
const keyBytes = new Cbor(this.getBytes(keyLength));
if (i > 0) {
const headerCmp = lastKeyHeader.compare(keyHeader);
if (headerCmp > 0 ||
(headerCmp == 0 && lastKeyBytes.compare(keyBytes) >= 0)) {
throw(
'Cbor: map keys in wrong order: ' + lastKeyHeader.hex +
'/' + lastKeyBytes.hex + ' ' + keyHeader.hex + '/' +
keyBytes.hex);
}
}
lastKeyHeader = keyHeader;
lastKeyBytes = keyBytes;
ret[keyBytes.parseUTF8()] = this.getCBOR();
}
return ret;
} else if (firstKeyMajor == 0 || firstKeyMajor == 1) {
// Number-keyed map.
let lastKeyHeader = new Cbor(new Uint8Array(0));
let ret = {};
for (let i = 0; i < value; i++) {
let [keyMajor, keyValue, keyHeader] = this.getCBORHeader();
if (keyMajor != 0 && keyMajor != 1) {
throw('Cbor: non-number in number-valued map');
}
if (i > 0 && lastKeyHeader.compare(keyHeader) >= 0) {
throw(
'Cbor: map keys in wrong order: ' + lastKeyHeader.hex + ' ' +
keyHeader.hex);
}
lastKeyHeader = keyHeader;
if (keyMajor == 1) {
keyValue = 0 - (1 + keyValue);
}
ret[keyValue] = this.getCBOR();
}
return ret;
} else {
throw('Cbor: map keyed by invalid major type ' + firstKeyMajor);
}
default:
throw('Cbor: unhandled major type ' + major);
}
}
parseUTF8() {
return (new TextDecoder('utf-8')).decode(this.slice);
}
}