fork-openpgpjs/src/encoding/armor.js
Daniel Huigens e3cfa4f9dd Revert "Don't mark async function as returning a Promise explicitly"
This reverts commit 9e85f75519.

It made VS Code / TypeScript complain about unnecessary `await`s.
2021-03-28 15:39:19 +02:00

424 lines
15 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
import * as stream from '@openpgp/web-stream-tools';
import * as base64 from './base64.js';
import enums from '../enums.js';
import util from '../util';
import defaultConfig from '../config';
/**
* Finds out which Ascii Armoring type is used. Throws error if unknown type.
* @param {String} text - ascii armored text
* @returns {Integer} 0 = MESSAGE PART n of m.
* 1 = MESSAGE PART n
* 2 = SIGNED MESSAGE
* 3 = PGP MESSAGE
* 4 = PUBLIC KEY BLOCK
* 5 = PRIVATE KEY BLOCK
* 6 = SIGNATURE
* @private
*/
function getType(text) {
const reHeader = /^-----BEGIN PGP (MESSAGE, PART \d+\/\d+|MESSAGE, PART \d+|SIGNED MESSAGE|MESSAGE|PUBLIC KEY BLOCK|PRIVATE KEY BLOCK|SIGNATURE)-----$/m;
const header = text.match(reHeader);
if (!header) {
throw new Error('Unknown ASCII armor type');
}
// BEGIN PGP MESSAGE, PART X/Y
// Used for multi-part messages, where the armor is split amongst Y
// parts, and this is the Xth part out of Y.
if (/MESSAGE, PART \d+\/\d+/.test(header[1])) {
return enums.armor.multipartSection;
} else
// BEGIN PGP MESSAGE, PART X
// Used for multi-part messages, where this is the Xth part of an
// unspecified number of parts. Requires the MESSAGE-ID Armor
// Header to be used.
if (/MESSAGE, PART \d+/.test(header[1])) {
return enums.armor.multipartLast;
} else
// BEGIN PGP SIGNED MESSAGE
if (/SIGNED MESSAGE/.test(header[1])) {
return enums.armor.signed;
} else
// BEGIN PGP MESSAGE
// Used for signed, encrypted, or compressed files.
if (/MESSAGE/.test(header[1])) {
return enums.armor.message;
} else
// BEGIN PGP PUBLIC KEY BLOCK
// Used for armoring public keys.
if (/PUBLIC KEY BLOCK/.test(header[1])) {
return enums.armor.publicKey;
} else
// BEGIN PGP PRIVATE KEY BLOCK
// Used for armoring private keys.
if (/PRIVATE KEY BLOCK/.test(header[1])) {
return enums.armor.privateKey;
} else
// BEGIN PGP SIGNATURE
// Used for detached signatures, OpenPGP/MIME signatures, and
// cleartext signatures. Note that PGP 2.x uses BEGIN PGP MESSAGE
// for detached signatures.
if (/SIGNATURE/.test(header[1])) {
return enums.armor.signature;
}
}
/**
* Add additional information to the armor version of an OpenPGP binary
* packet block.
* @author Alex
* @version 2011-12-16
* @param {String} [customComment] - Additional comment to add to the armored string
* @returns {String} The header information.
* @private
*/
function addheader(customComment, config) {
let result = "";
if (config.showVersion) {
result += "Version: " + config.versionString + '\n';
}
if (config.showComment) {
result += "Comment: " + config.commentString + '\n';
}
if (customComment) {
result += "Comment: " + customComment + '\n';
}
result += '\n';
return result;
}
/**
* Calculates a checksum over the given data and returns it base64 encoded
* @param {String | ReadableStream<String>} data - Data to create a CRC-24 checksum for
* @returns {String | ReadableStream<String>} Base64 encoded checksum.
* @private
*/
function getCheckSum(data) {
const crc = createcrc24(data);
return base64.encode(crc);
}
// https://create.stephan-brumme.com/crc32/#slicing-by-8-overview
const crc_table = [
new Array(0xFF),
new Array(0xFF),
new Array(0xFF),
new Array(0xFF)
];
for (let i = 0; i <= 0xFF; i++) {
let crc = i << 16;
for (let j = 0; j < 8; j++) {
crc = (crc << 1) ^ ((crc & 0x800000) !== 0 ? 0x864CFB : 0);
}
crc_table[0][i] =
((crc & 0xFF0000) >> 16) |
(crc & 0x00FF00) |
((crc & 0x0000FF) << 16);
}
for (let i = 0; i <= 0xFF; i++) {
crc_table[1][i] = (crc_table[0][i] >> 8) ^ crc_table[0][crc_table[0][i] & 0xFF];
}
for (let i = 0; i <= 0xFF; i++) {
crc_table[2][i] = (crc_table[1][i] >> 8) ^ crc_table[0][crc_table[1][i] & 0xFF];
}
for (let i = 0; i <= 0xFF; i++) {
crc_table[3][i] = (crc_table[2][i] >> 8) ^ crc_table[0][crc_table[2][i] & 0xFF];
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView#Endianness
const isLittleEndian = (function() {
const buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 0xFF, true /* littleEndian */);
// Int16Array uses the platform's endianness.
return new Int16Array(buffer)[0] === 0xFF;
}());
/**
* Internal function to calculate a CRC-24 checksum over a given string (data)
* @param {String | ReadableStream<String>} input - Data to create a CRC-24 checksum for
* @returns {Uint8Array | ReadableStream<Uint8Array>} The CRC-24 checksum.
* @private
*/
function createcrc24(input) {
let crc = 0xCE04B7;
return stream.transform(input, value => {
const len32 = isLittleEndian ? Math.floor(value.length / 4) : 0;
const arr32 = new Uint32Array(value.buffer, value.byteOffset, len32);
for (let i = 0; i < len32; i++) {
crc ^= arr32[i];
crc =
crc_table[0][(crc >> 24) & 0xFF] ^
crc_table[1][(crc >> 16) & 0xFF] ^
crc_table[2][(crc >> 8) & 0xFF] ^
crc_table[3][(crc >> 0) & 0xFF];
}
for (let i = len32 * 4; i < value.length; i++) {
crc = (crc >> 8) ^ crc_table[0][(crc & 0xFF) ^ value[i]];
}
}, () => new Uint8Array([crc, crc >> 8, crc >> 16]));
}
/**
* Verify armored headers. RFC4880, section 6.3: "OpenPGP should consider improperly formatted
* Armor Headers to be corruption of the ASCII Armor."
* @private
* @param {Array<String>} headers - Armor headers
*/
function verifyHeaders(headers) {
for (let i = 0; i < headers.length; i++) {
if (!/^([^\s:]|[^\s:][^:]*[^\s:]): .+$/.test(headers[i])) {
throw new Error('Improperly formatted armor header: ' + headers[i]);
}
if (!/^(Version|Comment|MessageID|Hash|Charset): .+$/.test(headers[i])) {
util.printDebugError(new Error('Unknown header: ' + headers[i]));
}
}
}
/**
* Splits a message into two parts, the body and the checksum. This is an internal function
* @param {String} text - OpenPGP armored message part
* @returns {Object} An object with attribute "body" containing the body.
* and an attribute "checksum" containing the checksum.
* @private
*/
function splitChecksum(text) {
let body = text;
let checksum = "";
const lastEquals = text.lastIndexOf("=");
if (lastEquals >= 0 && lastEquals !== text.length - 1) { // '=' as the last char means no checksum
body = text.slice(0, lastEquals);
checksum = text.slice(lastEquals + 1).substr(0, 4);
}
return { body: body, checksum: checksum };
}
/**
* Dearmor an OpenPGP armored message; verify the checksum and return
* the encoded bytes
* @param {String} input - OpenPGP armored message
* @returns {Promise<Object>} An object with attribute "text" containing the message text,
* an attribute "data" containing a stream of bytes and "type" for the ASCII armor type
* @async
* @static
*/
export function unarmor(input, config = defaultConfig) {
return new Promise(async (resolve, reject) => {
try {
const reSplit = /^-----[^-]+-----$/m;
const reEmptyLine = /^[ \f\r\t\u00a0\u2000-\u200a\u202f\u205f\u3000]*$/;
let type;
const headers = [];
let lastHeaders = headers;
let headersDone;
let text = [];
let textDone;
let checksum;
let data = base64.decode(stream.transformPair(input, async (readable, writable) => {
const reader = stream.getReader(readable);
try {
while (true) {
let line = await reader.readLine();
if (line === undefined) {
throw new Error('Misformed armored text');
}
// remove trailing whitespace at end of lines
line = util.removeTrailingSpaces(line.replace(/[\r\n]/g, ''));
if (!type) {
if (reSplit.test(line)) {
type = getType(line);
}
} else if (!headersDone) {
if (reSplit.test(line)) {
reject(new Error('Mandatory blank line missing between armor headers and armor data'));
}
if (!reEmptyLine.test(line)) {
lastHeaders.push(line);
} else {
verifyHeaders(lastHeaders);
headersDone = true;
if (textDone || type !== 2) {
resolve({ text, data, headers, type });
break;
}
}
} else if (!textDone && type === 2) {
if (!reSplit.test(line)) {
// Reverse dash-escaping for msg
text.push(line.replace(/^- /, ''));
} else {
text = text.join('\r\n');
textDone = true;
verifyHeaders(lastHeaders);
lastHeaders = [];
headersDone = false;
}
}
}
} catch (e) {
reject(e);
return;
}
const writer = stream.getWriter(writable);
try {
while (true) {
await writer.ready;
const { done, value } = await reader.read();
if (done) {
throw new Error('Misformed armored text');
}
const line = value + '';
if (line.indexOf('=') === -1 && line.indexOf('-') === -1) {
await writer.write(line);
} else {
let remainder = await reader.readToEnd();
if (!remainder.length) remainder = '';
remainder = line + remainder;
remainder = util.removeTrailingSpaces(remainder.replace(/\r/g, ''));
const parts = remainder.split(reSplit);
if (parts.length === 1) {
throw new Error('Misformed armored text');
}
const split = splitChecksum(parts[0].slice(0, -1));
checksum = split.checksum;
await writer.write(split.body);
break;
}
}
await writer.ready;
await writer.close();
} catch (e) {
await writer.abort(e);
}
}));
data = stream.transformPair(data, async (readable, writable) => {
const checksumVerified = stream.readToEnd(getCheckSum(stream.passiveClone(readable)));
checksumVerified.catch(() => {});
await stream.pipe(readable, writable, {
preventClose: true
});
const writer = stream.getWriter(writable);
try {
const checksumVerifiedString = (await checksumVerified).replace('\n', '');
if (checksum !== checksumVerifiedString && (checksum || config.checksumRequired)) {
throw new Error("Ascii armor integrity check on message failed: '" + checksum + "' should be '" +
checksumVerifiedString + "'");
}
await writer.ready;
await writer.close();
} catch (e) {
await writer.abort(e);
}
});
} catch (e) {
reject(e);
}
});
}
/**
* Armor an OpenPGP binary packet block
* @param {module:enums.armor} messageType - Type of the message
* @param {Uint8Array | ReadableStream<Uint8Array>} body - The message body to armor
* @param {Integer} [partIndex]
* @param {Integer} [partTotal]
* @param {String} [customComment] - Additional comment to add to the armored string
* @returns {String | ReadableStream<String>} Armored text.
* @static
*/
export function armor(messageType, body, partIndex, partTotal, customComment, config = defaultConfig) {
let text;
let hash;
if (messageType === enums.armor.signed) {
text = body.text;
hash = body.hash;
body = body.data;
}
const bodyClone = stream.passiveClone(body);
const result = [];
switch (messageType) {
case enums.armor.multipartSection:
result.push("-----BEGIN PGP MESSAGE, PART " + partIndex + "/" + partTotal + "-----\n");
result.push(addheader(customComment, config));
result.push(base64.encode(body));
result.push("=", getCheckSum(bodyClone));
result.push("-----END PGP MESSAGE, PART " + partIndex + "/" + partTotal + "-----\n");
break;
case enums.armor.multipartLast:
result.push("-----BEGIN PGP MESSAGE, PART " + partIndex + "-----\n");
result.push(addheader(customComment, config));
result.push(base64.encode(body));
result.push("=", getCheckSum(bodyClone));
result.push("-----END PGP MESSAGE, PART " + partIndex + "-----\n");
break;
case enums.armor.signed:
result.push("\n-----BEGIN PGP SIGNED MESSAGE-----\n");
result.push("Hash: " + hash + "\n\n");
result.push(text.replace(/^-/mg, "- -"));
result.push("\n-----BEGIN PGP SIGNATURE-----\n");
result.push(addheader(customComment, config));
result.push(base64.encode(body));
result.push("=", getCheckSum(bodyClone));
result.push("-----END PGP SIGNATURE-----\n");
break;
case enums.armor.message:
result.push("-----BEGIN PGP MESSAGE-----\n");
result.push(addheader(customComment, config));
result.push(base64.encode(body));
result.push("=", getCheckSum(bodyClone));
result.push("-----END PGP MESSAGE-----\n");
break;
case enums.armor.publicKey:
result.push("-----BEGIN PGP PUBLIC KEY BLOCK-----\n");
result.push(addheader(customComment, config));
result.push(base64.encode(body));
result.push("=", getCheckSum(bodyClone));
result.push("-----END PGP PUBLIC KEY BLOCK-----\n");
break;
case enums.armor.privateKey:
result.push("-----BEGIN PGP PRIVATE KEY BLOCK-----\n");
result.push(addheader(customComment, config));
result.push(base64.encode(body));
result.push("=", getCheckSum(bodyClone));
result.push("-----END PGP PRIVATE KEY BLOCK-----\n");
break;
case enums.armor.signature:
result.push("-----BEGIN PGP SIGNATURE-----\n");
result.push(addheader(customComment, config));
result.push(base64.encode(body));
result.push("=", getCheckSum(bodyClone));
result.push("-----END PGP SIGNATURE-----\n");
break;
}
return util.concat(result);
}