413 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 { transform as streamTransform, transformPair as streamTransformPair, getReader as streamGetReader, getWriter as streamGetWriter, isArrayStream, readToEnd as streamReadToEnd, passiveClone as streamPassiveClone } from '@openpgp/web-stream-tools';
import { encode as encodeBase64, decode as decodeBase64 } from './base64';
import enums from '../enums';
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;
}
// 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;
}
// BEGIN PGP SIGNED MESSAGE
if (/SIGNED MESSAGE/.test(header[1])) {
return enums.armor.signed;
}
// BEGIN PGP MESSAGE
// Used for signed, encrypted, or compressed files.
if (/MESSAGE/.test(header[1])) {
return enums.armor.message;
}
// BEGIN PGP PUBLIC KEY BLOCK
// Used for armoring public keys.
if (/PUBLIC KEY BLOCK/.test(header[1])) {
return enums.armor.publicKey;
}
// BEGIN PGP PRIVATE KEY BLOCK
// Used for armoring private keys.
if (/PRIVATE KEY BLOCK/.test(header[1])) {
return enums.armor.privateKey;
}
// 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 encodeBase64(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 streamTransform(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. crypto-refresh-06, section 6.2:
* "An OpenPGP implementation may consider improperly formatted Armor
* Headers to be corruption of the ASCII Armor, but SHOULD make an
* effort to recover."
* @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])) {
util.printDebugError(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]));
}
}
}
/**
* Remove the (optional) checksum from an armored message.
* @param {String} text - OpenPGP armored message
* @returns {String} The body of the armored message.
* @private
*/
function removeChecksum(text) {
let body = text;
const lastEquals = text.lastIndexOf('=');
if (lastEquals >= 0 && lastEquals !== text.length - 1) { // '=' as the last char means no checksum
body = text.slice(0, lastEquals);
}
return body;
}
/**
* 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) {
// eslint-disable-next-line no-async-promise-executor
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;
const data = decodeBase64(streamTransformPair(input, async (readable, writable) => {
const reader = streamGetReader(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 !== enums.armor.signed) {
resolve({ text, data, headers, type });
break;
}
}
} else if (!textDone && type === enums.armor.signed) {
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 = streamGetWriter(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 body = removeChecksum(parts[0].slice(0, -1));
await writer.write(body);
break;
}
}
await writer.ready;
await writer.close();
} catch (e) {
await writer.abort(e);
}
}));
} catch (e) {
reject(e);
}
}).then(async result => {
if (isArrayStream(result.data)) {
result.data = await streamReadToEnd(result.data);
}
return result;
});
}
/**
* 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
* @param {Boolean} [emitChecksum] - Whether to compute and include the CRC checksum
* (NB: some types of data must not include it, but compliance is left as responsibility of the caller: this function does not carry out any checks)
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {String | ReadableStream<String>} Armored text.
* @static
*/
export function armor(messageType, body, partIndex, partTotal, customComment, emitChecksum = false, config = defaultConfig) {
let text;
let hash;
if (messageType === enums.armor.signed) {
text = body.text;
hash = body.hash;
body = body.data;
}
// unless explicitly forbidden by the spec, we need to include the checksum to work around a GnuPG bug
// where data fails to be decoded if the base64 ends with no padding chars (=) (see https://dev.gnupg.org/T7071)
const maybeBodyClone = emitChecksum && streamPassiveClone(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(encodeBase64(body));
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
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(encodeBase64(body));
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
result.push('-----END PGP MESSAGE, PART ' + partIndex + '-----\n');
break;
case enums.armor.signed:
result.push('-----BEGIN PGP SIGNED MESSAGE-----\n');
result.push(hash ? `Hash: ${hash}\n\n` : '\n');
result.push(text.replace(/^-/mg, '- -'));
result.push('\n-----BEGIN PGP SIGNATURE-----\n');
result.push(addheader(customComment, config));
result.push(encodeBase64(body));
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
result.push('-----END PGP SIGNATURE-----\n');
break;
case enums.armor.message:
result.push('-----BEGIN PGP MESSAGE-----\n');
result.push(addheader(customComment, config));
result.push(encodeBase64(body));
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
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(encodeBase64(body));
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
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(encodeBase64(body));
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
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(encodeBase64(body));
maybeBodyClone && result.push('=', getCheckSum(maybeBodyClone));
result.push('-----END PGP SIGNATURE-----\n');
break;
}
return util.concat(result);
}