/
cleartext.js
200 lines (186 loc) · 7.9 KB
/
cleartext.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
// 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 { armor, unarmor } from './encoding/armor';
import enums from './enums';
import util from './util';
import { PacketList, LiteralDataPacket, SignaturePacket } from './packet';
import { Signature } from './signature';
import { createVerificationObjects, createSignaturePackets } from './message';
import defaultConfig from './config';
/**
* Class that represents an OpenPGP cleartext signed message.
* See {@link https://tools.ietf.org/html/rfc4880#section-7}
*/
export class CleartextMessage {
/**
* @param {String} text - The cleartext of the signed message
* @param {Signature} signature - The detached signature or an empty signature for unsigned messages
*/
constructor(text, signature) {
// normalize EOL to canonical form <CR><LF>
this.text = util.removeTrailingSpaces(text).replace(/\r?\n/g, '\r\n');
if (signature && !(signature instanceof Signature)) {
throw new Error('Invalid signature input');
}
this.signature = signature || new Signature(new PacketList());
}
/**
* Returns the key IDs of the keys that signed the cleartext message
* @returns {Array<module:type/keyid~Keyid>} Array of keyid objects.
*/
getSigningKeyIds() {
const keyIds = [];
const signatureList = this.signature.packets;
signatureList.forEach(function(packet) {
keyIds.push(packet.issuerKeyId);
});
return keyIds;
}
/**
* Sign the cleartext message
* @param {Array<Key>} privateKeys - private keys with decrypted secret key data for signing
* @param {Signature} [signature] - Any existing detached signature
* @param {Array<module:type/keyid~Keyid>} [signingKeyIds] - Array of key IDs to use for signing. Each signingKeyIds[i] corresponds to privateKeys[i]
* @param {Date} [date] - The creation time of the signature that should be created
* @param {Array} [userIds] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }]
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {CleartextMessage} New cleartext message with signed content.
* @async
*/
async sign(privateKeys, signature = null, signingKeyIds = [], date = new Date(), userIds = [], config = defaultConfig) {
const literalDataPacket = new LiteralDataPacket();
literalDataPacket.setText(this.text);
const newSignature = new Signature(await createSignaturePackets(literalDataPacket, privateKeys, signature, signingKeyIds, date, userIds, true, undefined, config));
return new CleartextMessage(this.text, newSignature);
}
/**
* Verify signatures of cleartext signed message
* @param {Array<Key>} keys - Array of keys to verify signatures
* @param {Date} [date] - Verify the signature against the given date, i.e. check signature creation time < date < expiration time
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Array<{keyid: module:type/keyid~Keyid,
* signature: Promise<Signature>,
* verified: Promise<Boolean>}>} List of signer's keyid and validity of signature.
* @async
*/
verify(keys, date = new Date(), config = defaultConfig) {
const signatureList = this.signature.packets;
const literalDataPacket = new LiteralDataPacket();
// we assume that cleartext signature is generated based on UTF8 cleartext
literalDataPacket.setText(this.text);
return createVerificationObjects(signatureList, [literalDataPacket], keys, date, true, undefined, config);
}
/**
* Get cleartext
* @returns {String} Cleartext of message.
*/
getText() {
// normalize end of line to \n
return this.text.replace(/\r\n/g, '\n');
}
/**
* Returns ASCII armored text of cleartext signed message
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {String | ReadableStream<String>} ASCII armor.
*/
armor(config = defaultConfig) {
let hashes = this.signature.packets.map(function(packet) {
return enums.read(enums.hash, packet.hashAlgorithm).toUpperCase();
});
hashes = hashes.filter(function(item, i, ar) { return ar.indexOf(item) === i; });
const body = {
hash: hashes.join(),
text: this.text,
data: this.signature.packets.write()
};
return armor(enums.armor.signed, body, undefined, undefined, undefined, config);
}
/**
* Creates a new CleartextMessage object from text
* @param {String} text
* @static
*/
static fromText(text) {
return new CleartextMessage(text);
}
}
/**
* Reads an OpenPGP cleartext signed message and returns a CleartextMessage object
* @param {Object} options
* @param {String | ReadableStream<String>} options.cleartextMessage - Text to be parsed
* @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config}
* @returns {CleartextMessage} New cleartext message object.
* @async
* @static
*/
export async function readCleartextMessage({ cleartextMessage, config }) {
config = { ...defaultConfig, ...config };
if (!cleartextMessage) {
throw new Error('readCleartextMessage: must pass options object containing `cleartextMessage`');
}
const input = await unarmor(cleartextMessage);
if (input.type !== enums.armor.signed) {
throw new Error('No cleartext signed message.');
}
const packetlist = new PacketList();
await packetlist.read(input.data, { SignaturePacket }, undefined, config);
verifyHeaders(input.headers, packetlist);
const signature = new Signature(packetlist);
return new CleartextMessage(input.text, signature);
}
/**
* Compare hash algorithm specified in the armor header with signatures
* @param {Array<String>} headers - Armor headers
* @param {PacketList} packetlist - The packetlist with signature packets
* @private
*/
function verifyHeaders(headers, packetlist) {
const checkHashAlgos = function(hashAlgos) {
const check = packet => algo => packet.hashAlgorithm === algo;
for (let i = 0; i < packetlist.length; i++) {
if (packetlist[i].tag === enums.packet.signature && !hashAlgos.some(check(packetlist[i]))) {
return false;
}
}
return true;
};
let oneHeader = null;
let hashAlgos = [];
headers.forEach(function(header) {
oneHeader = header.match(/Hash: (.+)/); // get header value
if (oneHeader) {
oneHeader = oneHeader[1].replace(/\s/g, ''); // remove whitespace
oneHeader = oneHeader.split(',');
oneHeader = oneHeader.map(function(hash) {
hash = hash.toLowerCase();
try {
return enums.write(enums.hash, hash);
} catch (e) {
throw new Error('Unknown hash algorithm in armor header: ' + hash);
}
});
hashAlgos = hashAlgos.concat(oneHeader);
} else {
throw new Error('Only "Hash" header allowed in cleartext signed message');
}
});
if (!hashAlgos.length && !checkHashAlgos([enums.hash.md5])) {
throw new Error('If no "Hash" header in cleartext signed message, then only MD5 signatures allowed');
} else if (hashAlgos.length && !checkHashAlgos(hashAlgos)) {
throw new Error('Hash algorithm mismatch in armor header and signature');
}
}