openpgpjs/src/util.js

590 lines
16 KiB
JavaScript

// GPG4Browsers - An OpenPGP implementation in javascript
// Copyright (C) 2011 Recurity Labs GmbH
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3.0 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
/* eslint-disable no-console */
/**
* This object contains utility functions
* @module util
* @private
*/
import * as stream from '@openpgp/web-stream-tools';
import { getBigInteger } from './biginteger';
const debugMode = globalThis.process && globalThis.process.env.NODE_ENV === 'development';
const util = {
isString: function(data) {
return typeof data === 'string' || String.prototype.isPrototypeOf(data);
},
isArray: function(data) {
return Array.prototype.isPrototypeOf(data);
},
isUint8Array: stream.isUint8Array,
isStream: stream.isStream,
readNumber: function (bytes) {
let n = 0;
for (let i = 0; i < bytes.length; i++) {
n += (256 ** i) * bytes[bytes.length - 1 - i];
}
return n;
},
writeNumber: function (n, bytes) {
const b = new Uint8Array(bytes);
for (let i = 0; i < bytes; i++) {
b[i] = (n >> (8 * (bytes - i - 1))) & 0xFF;
}
return b;
},
readDate: function (bytes) {
const n = util.readNumber(bytes);
const d = new Date(n * 1000);
return d;
},
writeDate: function (time) {
const numeric = Math.floor(time.getTime() / 1000);
return util.writeNumber(numeric, 4);
},
normalizeDate: function (time = Date.now()) {
return time === null || time === Infinity ? time : new Date(Math.floor(+time / 1000) * 1000);
},
/**
* Read one MPI from bytes in input
* @param {Uint8Array} bytes - Input data to parse
* @returns {Uint8Array} Parsed MPI.
*/
readMPI: function (bytes) {
const bits = (bytes[0] << 8) | bytes[1];
const bytelen = (bits + 7) >>> 3;
return bytes.subarray(2, 2 + bytelen);
},
/**
* Left-pad Uint8Array to length by adding 0x0 bytes
* @param {Uint8Array} bytes - Data to pad
* @param {Number} length - Padded length
* @returns {Uint8Array} Padded bytes.
*/
leftPad(bytes, length) {
const padded = new Uint8Array(length);
const offset = length - bytes.length;
padded.set(bytes, offset);
return padded;
},
/**
* Convert a Uint8Array to an MPI-formatted Uint8Array.
* @param {Uint8Array} bin - An array of 8-bit integers to convert
* @returns {Uint8Array} MPI-formatted Uint8Array.
*/
uint8ArrayToMPI: function (bin) {
const bitSize = util.uint8ArrayBitLength(bin);
if (bitSize === 0) {
throw new Error('Zero MPI');
}
const stripped = bin.subarray(bin.length - Math.ceil(bitSize / 8));
const prefix = new Uint8Array([(bitSize & 0xFF00) >> 8, bitSize & 0xFF]);
return util.concatUint8Array([prefix, stripped]);
},
/**
* Return bit length of the input data
* @param {Uint8Array} bin input data (big endian)
* @returns bit length
*/
uint8ArrayBitLength: function (bin) {
let i; // index of leading non-zero byte
for (i = 0; i < bin.length; i++) if (bin[i] !== 0) break;
if (i === bin.length) {
return 0;
}
const stripped = bin.subarray(i);
return (stripped.length - 1) * 8 + util.nbits(stripped[0]);
},
/**
* Convert a hex string to an array of 8-bit integers
* @param {String} hex - A hex string to convert
* @returns {Uint8Array} An array of 8-bit integers.
*/
hexToUint8Array: function (hex) {
const result = new Uint8Array(hex.length >> 1);
for (let k = 0; k < hex.length >> 1; k++) {
result[k] = parseInt(hex.substr(k << 1, 2), 16);
}
return result;
},
/**
* Convert an array of 8-bit integers to a hex string
* @param {Uint8Array} bytes - Array of 8-bit integers to convert
* @returns {String} Hexadecimal representation of the array.
*/
uint8ArrayToHex: function (bytes) {
const r = [];
const e = bytes.length;
let c = 0;
let h;
while (c < e) {
h = bytes[c++].toString(16);
while (h.length < 2) {
h = '0' + h;
}
r.push('' + h);
}
return r.join('');
},
/**
* Convert a string to an array of 8-bit integers
* @param {String} str - String to convert
* @returns {Uint8Array} An array of 8-bit integers.
*/
stringToUint8Array: function (str) {
return stream.transform(str, str => {
if (!util.isString(str)) {
throw new Error('stringToUint8Array: Data must be in the form of a string');
}
const result = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
result[i] = str.charCodeAt(i);
}
return result;
});
},
/**
* Convert an array of 8-bit integers to a string
* @param {Uint8Array} bytes - An array of 8-bit integers to convert
* @returns {String} String representation of the array.
*/
uint8ArrayToString: function (bytes) {
bytes = new Uint8Array(bytes);
const result = [];
const bs = 1 << 14;
const j = bytes.length;
for (let i = 0; i < j; i += bs) {
result.push(String.fromCharCode.apply(String, bytes.subarray(i, i + bs < j ? i + bs : j)));
}
return result.join('');
},
/**
* Convert a native javascript string to a Uint8Array of utf8 bytes
* @param {String|ReadableStream} str - The string to convert
* @returns {Uint8Array|ReadableStream} A valid squence of utf8 bytes.
*/
encodeUTF8: function (str) {
const encoder = new TextEncoder('utf-8');
// eslint-disable-next-line no-inner-declarations
function process(value, lastChunk = false) {
return encoder.encode(value, { stream: !lastChunk });
}
return stream.transform(str, process, () => process('', true));
},
/**
* Convert a Uint8Array of utf8 bytes to a native javascript string
* @param {Uint8Array|ReadableStream} utf8 - A valid squence of utf8 bytes
* @returns {String|ReadableStream} A native javascript string.
*/
decodeUTF8: function (utf8) {
const decoder = new TextDecoder('utf-8');
// eslint-disable-next-line no-inner-declarations
function process(value, lastChunk = false) {
return decoder.decode(value, { stream: !lastChunk });
}
return stream.transform(utf8, process, () => process(new Uint8Array(), true));
},
/**
* Concat a list of Uint8Arrays, Strings or Streams
* The caller must not mix Uint8Arrays with Strings, but may mix Streams with non-Streams.
* @param {Array<Uint8Array|String|ReadableStream>} Array - Of Uint8Arrays/Strings/Streams to concatenate
* @returns {Uint8Array|String|ReadableStream} Concatenated array.
*/
concat: stream.concat,
/**
* Concat Uint8Arrays
* @param {Array<Uint8Array>} Array - Of Uint8Arrays to concatenate
* @returns {Uint8Array} Concatenated array.
*/
concatUint8Array: stream.concatUint8Array,
/**
* Check Uint8Array equality
* @param {Uint8Array} array1 - First array
* @param {Uint8Array} array2 - Second array
* @returns {Boolean} Equality.
*/
equalsUint8Array: function (array1, array2) {
if (!util.isUint8Array(array1) || !util.isUint8Array(array2)) {
throw new Error('Data must be in the form of a Uint8Array');
}
if (array1.length !== array2.length) {
return false;
}
for (let i = 0; i < array1.length; i++) {
if (array1[i] !== array2[i]) {
return false;
}
}
return true;
},
/**
* Calculates a 16bit sum of a Uint8Array by adding each character
* codes modulus 65535
* @param {Uint8Array} Uint8Array - To create a sum of
* @returns {Uint8Array} 2 bytes containing the sum of all charcodes % 65535.
*/
writeChecksum: function (text) {
let s = 0;
for (let i = 0; i < text.length; i++) {
s = (s + text[i]) & 0xFFFF;
}
return util.writeNumber(s, 2);
},
/**
* Helper function to print a debug message. Debug
* messages are only printed if
* @param {String} str - String of the debug message
*/
printDebug: function (str) {
if (debugMode) {
console.log(str);
}
},
/**
* Helper function to print a debug error. Debug
* messages are only printed if
* @param {String} str - String of the debug message
*/
printDebugError: function (error) {
if (debugMode) {
console.error(error);
}
},
// returns bit length of the integer x
nbits: function (x) {
let r = 1;
let t = x >>> 16;
if (t !== 0) {
x = t;
r += 16;
}
t = x >> 8;
if (t !== 0) {
x = t;
r += 8;
}
t = x >> 4;
if (t !== 0) {
x = t;
r += 4;
}
t = x >> 2;
if (t !== 0) {
x = t;
r += 2;
}
t = x >> 1;
if (t !== 0) {
x = t;
r += 1;
}
return r;
},
/**
* If S[1] == 0, then double(S) == (S[2..128] || 0);
* otherwise, double(S) == (S[2..128] || 0) xor
* (zeros(120) || 10000111).
*
* Both OCB and EAX (through CMAC) require this function to be constant-time.
*
* @param {Uint8Array} data
*/
double: function(data) {
const doubleVar = new Uint8Array(data.length);
const last = data.length - 1;
for (let i = 0; i < last; i++) {
doubleVar[i] = (data[i] << 1) ^ (data[i + 1] >> 7);
}
doubleVar[last] = (data[last] << 1) ^ ((data[0] >> 7) * 0x87);
return doubleVar;
},
/**
* Shift a Uint8Array to the right by n bits
* @param {Uint8Array} array - The array to shift
* @param {Integer} bits - Amount of bits to shift (MUST be smaller
* than 8)
* @returns {String} Resulting array.
*/
shiftRight: function (array, bits) {
if (bits) {
for (let i = array.length - 1; i >= 0; i--) {
array[i] >>= bits;
if (i > 0) {
array[i] |= (array[i - 1] << (8 - bits));
}
}
}
return array;
},
/**
* Get native Web Cryptography api, only the current version of the spec.
* @returns {Object} The SubtleCrypto api or 'undefined'.
*/
getWebCrypto: function() {
return typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.subtle;
},
/**
* Detect Node.js runtime.
*/
detectNode: function() {
return typeof globalThis.process === 'object' &&
typeof globalThis.process.versions === 'object';
},
/**
* Detect native BigInt support
*/
detectBigInt: () => typeof BigInt !== 'undefined',
/**
* Get BigInteger class
* It wraps the native BigInt type if it's available
* Otherwise it relies on bn.js
* @returns {BigInteger}
* @async
*/
getBigInteger,
/**
* Get native Node.js crypto api.
* @returns {Object} The crypto module or 'undefined'.
*/
getNodeCrypto: function() {
return require('crypto');
},
getNodeZlib: function() {
return require('zlib');
},
/**
* Get native Node.js Buffer constructor. This should be used since
* Buffer is not available under browserify.
* @returns {Function} The Buffer constructor or 'undefined'.
*/
getNodeBuffer: function() {
return (require('buffer') || {}).Buffer;
},
getHardwareConcurrency: function() {
if (util.detectNode()) {
const os = require('os');
return os.cpus().length;
}
return navigator.hardwareConcurrency || 1;
},
isEmailAddress: function(data) {
if (!util.isString(data)) {
return false;
}
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+([a-zA-Z]{2,}|xn--[a-zA-Z\-0-9]+)))$/;
return re.test(data);
},
/**
* Normalize line endings to <CR><LF>
* Support any encoding where CR=0x0D, LF=0x0A
*/
canonicalizeEOL: function(data) {
const CR = 13;
const LF = 10;
let carryOverCR = false;
return stream.transform(data, bytes => {
if (carryOverCR) {
bytes = util.concatUint8Array([new Uint8Array([CR]), bytes]);
}
if (bytes[bytes.length - 1] === CR) {
carryOverCR = true;
bytes = bytes.subarray(0, -1);
} else {
carryOverCR = false;
}
let index;
const indices = [];
for (let i = 0; ; i = index) {
index = bytes.indexOf(LF, i) + 1;
if (index) {
if (bytes[index - 2] !== CR) indices.push(index);
} else {
break;
}
}
if (!indices.length) {
return bytes;
}
const normalized = new Uint8Array(bytes.length + indices.length);
let j = 0;
for (let i = 0; i < indices.length; i++) {
const sub = bytes.subarray(indices[i - 1] || 0, indices[i]);
normalized.set(sub, j);
j += sub.length;
normalized[j - 1] = CR;
normalized[j] = LF;
j++;
}
normalized.set(bytes.subarray(indices[indices.length - 1] || 0), j);
return normalized;
}, () => (carryOverCR ? new Uint8Array([CR]) : undefined));
},
/**
* Convert line endings from canonicalized <CR><LF> to native <LF>
* Support any encoding where CR=0x0D, LF=0x0A
*/
nativeEOL: function(data) {
const CR = 13;
const LF = 10;
let carryOverCR = false;
return stream.transform(data, bytes => {
if (carryOverCR && bytes[0] !== LF) {
bytes = util.concatUint8Array([new Uint8Array([CR]), bytes]);
} else {
bytes = new Uint8Array(bytes); // Don't mutate passed bytes
}
if (bytes[bytes.length - 1] === CR) {
carryOverCR = true;
bytes = bytes.subarray(0, -1);
} else {
carryOverCR = false;
}
let index;
let j = 0;
for (let i = 0; i !== bytes.length; i = index) {
index = bytes.indexOf(CR, i) + 1;
if (!index) index = bytes.length;
const last = index - (bytes[index] === LF ? 1 : 0);
if (i) bytes.copyWithin(j, i, last);
j += last - i;
}
return bytes.subarray(0, j);
}, () => (carryOverCR ? new Uint8Array([CR]) : undefined));
},
/**
* Remove trailing spaces and tabs from each line
*/
removeTrailingSpaces: function(text) {
return text.split('\n').map(line => {
let i = line.length - 1;
for (; i >= 0 && (line[i] === ' ' || line[i] === '\t'); i--);
return line.substr(0, i + 1);
}).join('\n');
},
wrapError: function(message, error) {
if (!error) {
return new Error(message);
}
// update error message
try {
error.message = message + ': ' + error.message;
} catch (e) {}
return error;
},
/**
* Map allowed packet tags to corresponding classes
* Meant to be used to format `allowedPacket` for Packetlist.read
* @param {Array<Object>} allowedClasses
* @returns {Object} map from enum.packet to corresponding *Packet class
*/
constructAllowedPackets: function(allowedClasses) {
const map = {};
allowedClasses.forEach(PacketClass => {
if (!PacketClass.tag) {
throw new Error('Invalid input: expected a packet class');
}
map[PacketClass.tag] = PacketClass;
});
return map;
},
/**
* Return a Promise that will resolve as soon as one of the promises in input resolves
* or will reject if all input promises all rejected
* (similar to Promise.any, but with slightly different error handling)
* @param {Array<Promise>} promises
* @return {Promise<Any>} Promise resolving to the result of the fastest fulfilled promise
* or rejected with the Error of the last resolved Promise (if all promises are rejected)
*/
anyPromise: function(promises) {
return new Promise(async (resolve, reject) => {
let exception;
await Promise.all(promises.map(async promise => {
try {
resolve(await promise);
} catch (e) {
exception = e;
}
}));
reject(exception);
});
}
};
export default util;