-
Notifications
You must be signed in to change notification settings - Fork 796
/
message.js
932 lines (857 loc) · 43.1 KB
/
message.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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
// 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 { armor, unarmor } from './encoding/armor';
import KeyID from './type/keyid';
import defaultConfig from './config';
import crypto from './crypto';
import enums from './enums';
import util from './util';
import { Signature } from './signature';
import { getPreferredHashAlgo, getPreferredAlgo, isAEADSupported, createSignaturePacket } from './key';
import {
PacketList,
LiteralDataPacket,
CompressedDataPacket,
AEADEncryptedDataPacket,
SymEncryptedIntegrityProtectedDataPacket,
SymmetricallyEncryptedDataPacket,
PublicKeyEncryptedSessionKeyPacket,
SymEncryptedSessionKeyPacket,
OnePassSignaturePacket,
SignaturePacket
} from './packet';
// A Message can contain the following packets
const allowedMessagePackets = /*#__PURE__*/ util.constructAllowedPackets([
LiteralDataPacket,
CompressedDataPacket,
AEADEncryptedDataPacket,
SymEncryptedIntegrityProtectedDataPacket,
SymmetricallyEncryptedDataPacket,
PublicKeyEncryptedSessionKeyPacket,
SymEncryptedSessionKeyPacket,
OnePassSignaturePacket,
SignaturePacket
]);
// A SKESK packet can contain the following packets
const allowedSymSessionKeyPackets = /*#__PURE__*/ util.constructAllowedPackets([SymEncryptedSessionKeyPacket]);
// A detached signature can contain the following packets
const allowedDetachedSignaturePackets = /*#__PURE__*/ util.constructAllowedPackets([SignaturePacket]);
/**
* Class that represents an OpenPGP message.
* Can be an encrypted message, signed message, compressed message or literal message
* See {@link https://tools.ietf.org/html/rfc4880#section-11.3}
*/
export class Message {
/**
* @param {PacketList} packetlist - The packets that form this message
*/
constructor(packetlist) {
this.packets = packetlist || new PacketList();
}
/**
* Returns the key IDs of the keys to which the session key is encrypted
* @returns {Array<module:type/keyid~KeyID>} Array of keyID objects.
*/
getEncryptionKeyIDs() {
const keyIDs = [];
const pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey);
pkESKeyPacketlist.forEach(function(packet) {
keyIDs.push(packet.publicKeyID);
});
return keyIDs;
}
/**
* Returns the key IDs of the keys that signed the message
* @returns {Array<module:type/keyid~KeyID>} Array of keyID objects.
*/
getSigningKeyIDs() {
const msg = this.unwrapCompressed();
// search for one pass signatures
const onePassSigList = msg.packets.filterByTag(enums.packet.onePassSignature);
if (onePassSigList.length > 0) {
return onePassSigList.map(packet => packet.issuerKeyID);
}
// if nothing found look for signature packets
const signatureList = msg.packets.filterByTag(enums.packet.signature);
return signatureList.map(packet => packet.issuerKeyID);
}
/**
* Decrypt the message. Either a private key, a session key, or a password must be specified.
* @param {Array<PrivateKey>} [decryptionKeys] - Private keys with decrypted secret data
* @param {Array<String>} [passwords] - Passwords used to decrypt
* @param {Array<Object>} [sessionKeys] - Session keys in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] }
* @param {Date} [date] - Use the given date for key verification instead of the current time
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<Message>} New message with decrypted content.
* @async
*/
async decrypt(decryptionKeys, passwords, sessionKeys, date = new Date(), config = defaultConfig) {
const sessionKeyObjects = sessionKeys || await this.decryptSessionKeys(decryptionKeys, passwords, date, config);
const symEncryptedPacketlist = this.packets.filterByTag(
enums.packet.symmetricallyEncryptedData,
enums.packet.symEncryptedIntegrityProtectedData,
enums.packet.aeadEncryptedData
);
if (symEncryptedPacketlist.length === 0) {
throw new Error('No encrypted data found');
}
const symEncryptedPacket = symEncryptedPacketlist[0];
let exception = null;
const decryptedPromise = Promise.all(sessionKeyObjects.map(async ({ algorithm: algorithmName, data }) => {
if (!util.isUint8Array(data) || !util.isString(algorithmName)) {
throw new Error('Invalid session key for decryption.');
}
try {
const algo = enums.write(enums.symmetric, algorithmName);
await symEncryptedPacket.decrypt(algo, data, config);
} catch (e) {
util.printDebugError(e);
exception = e;
}
}));
// We don't await stream.cancel here because it only returns when the other copy is canceled too.
stream.cancel(symEncryptedPacket.encrypted); // Don't keep copy of encrypted data in memory.
symEncryptedPacket.encrypted = null;
await decryptedPromise;
if (!symEncryptedPacket.packets || !symEncryptedPacket.packets.length) {
throw exception || new Error('Decryption failed.');
}
const resultMsg = new Message(symEncryptedPacket.packets);
symEncryptedPacket.packets = new PacketList(); // remove packets after decryption
return resultMsg;
}
/**
* Decrypt encrypted session keys either with private keys or passwords.
* @param {Array<PrivateKey>} [decryptionKeys] - Private keys with decrypted secret data
* @param {Array<String>} [passwords] - Passwords used to decrypt
* @param {Date} [date] - Use the given date for key verification, instead of current time
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<Array<{
* data: Uint8Array,
* algorithm: String
* }>>} array of object with potential sessionKey, algorithm pairs
* @async
*/
async decryptSessionKeys(decryptionKeys, passwords, date = new Date(), config = defaultConfig) {
let decryptedSessionKeyPackets = [];
let exception;
if (passwords) {
const skeskPackets = this.packets.filterByTag(enums.packet.symEncryptedSessionKey);
if (skeskPackets.length === 0) {
throw new Error('No symmetrically encrypted session key packet found.');
}
await Promise.all(passwords.map(async function(password, i) {
let packets;
if (i) {
packets = await PacketList.fromBinary(skeskPackets.write(), allowedSymSessionKeyPackets, config);
} else {
packets = skeskPackets;
}
await Promise.all(packets.map(async function(skeskPacket) {
try {
await skeskPacket.decrypt(password);
decryptedSessionKeyPackets.push(skeskPacket);
} catch (err) {
util.printDebugError(err);
}
}));
}));
} else if (decryptionKeys) {
const pkeskPackets = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey);
if (pkeskPackets.length === 0) {
throw new Error('No public key encrypted session key packet found.');
}
await Promise.all(pkeskPackets.map(async function(pkeskPacket) {
await Promise.all(decryptionKeys.map(async function(decryptionKey) {
let algos = [
enums.symmetric.aes256, // Old OpenPGP.js default fallback
enums.symmetric.aes128, // RFC4880bis fallback
enums.symmetric.tripledes, // RFC4880 fallback
enums.symmetric.cast5 // Golang OpenPGP fallback
];
try {
const primaryUser = await decryptionKey.getPrimaryUser(date, undefined, config); // TODO: Pass userID from somewhere.
if (primaryUser.selfCertification.preferredSymmetricAlgorithms) {
algos = algos.concat(primaryUser.selfCertification.preferredSymmetricAlgorithms);
}
} catch (e) {}
// do not check key expiration to allow decryption of old messages
const decryptionKeyPackets = (await decryptionKey.getDecryptionKeys(pkeskPacket.publicKeyID, null, undefined, config)).map(key => key.keyPacket);
await Promise.all(decryptionKeyPackets.map(async function(decryptionKeyPacket) {
if (!decryptionKeyPacket || decryptionKeyPacket.isDummy()) {
return;
}
if (!decryptionKeyPacket.isDecrypted()) {
throw new Error('Decryption key is not decrypted.');
}
// To hinder CCA attacks against PKCS1, we carry out a constant-time decryption flow if the `constantTimePKCS1Decryption` config option is set.
const doConstantTimeDecryption = config.constantTimePKCS1Decryption && (
pkeskPacket.publicKeyAlgorithm === enums.publicKey.rsaEncrypt ||
pkeskPacket.publicKeyAlgorithm === enums.publicKey.rsaEncryptSign ||
pkeskPacket.publicKeyAlgorithm === enums.publicKey.rsaSign ||
pkeskPacket.publicKeyAlgorithm === enums.publicKey.elgamal
);
if (doConstantTimeDecryption) {
// The goal is to not reveal whether PKESK decryption (specifically the PKCS1 decoding step) failed, hence, we always proceed to decrypt the message,
// either with the successfully decrypted session key, or with a randomly generated one.
// Since the SEIP/AEAD's symmetric algorithm and key size are stored in the encrypted portion of the PKESK, and the execution flow cannot depend on
// the decrypted payload, we always assume the message to be encrypted with one of the symmetric algorithms specified in `config.constantTimePKCS1DecryptionSupportedSymmetricAlgorithms`:
// - If the PKESK decryption succeeds, and the session key cipher is in the supported set, then we try to decrypt the data with the decrypted session key as well as with the
// randomly generated keys of the remaining key types.
// - If the PKESK decryptions fails, or if it succeeds but support for the cipher is not enabled, then we discard the session key and try to decrypt the data using only the randomly
// generated session keys.
// NB: as a result, if the data is encrypted with a non-suported cipher, decryption will always fail.
const serialisedPKESK = pkeskPacket.write(); // make copies to be able to decrypt the PKESK packet multiple times
await Promise.all(Array.from(config.constantTimePKCS1DecryptionSupportedSymmetricAlgorithms).map(async sessionKeyAlgorithm => {
const pkeskPacketCopy = new PublicKeyEncryptedSessionKeyPacket();
pkeskPacketCopy.read(serialisedPKESK);
const randomSessionKey = {
sessionKeyAlgorithm,
sessionKey: crypto.generateSessionKey(sessionKeyAlgorithm)
};
try {
await pkeskPacketCopy.decrypt(decryptionKeyPacket, randomSessionKey);
decryptedSessionKeyPackets.push(pkeskPacketCopy);
} catch (err) {
// `decrypt` can still throw some non-security-sensitive errors
util.printDebugError(err);
exception = err;
}
}));
} else {
try {
await pkeskPacket.decrypt(decryptionKeyPacket);
if (!algos.includes(enums.write(enums.symmetric, pkeskPacket.sessionKeyAlgorithm))) {
throw new Error('A non-preferred symmetric algorithm was used.');
}
decryptedSessionKeyPackets.push(pkeskPacket);
} catch (err) {
util.printDebugError(err);
exception = err;
}
}
}));
}));
stream.cancel(pkeskPacket.encrypted); // Don't keep copy of encrypted data in memory.
pkeskPacket.encrypted = null;
}));
} else {
throw new Error('No key or password specified.');
}
if (decryptedSessionKeyPackets.length > 0) {
// Return only unique session keys
if (decryptedSessionKeyPackets.length > 1) {
const seen = new Set();
decryptedSessionKeyPackets = decryptedSessionKeyPackets.filter(item => {
const k = item.sessionKeyAlgorithm + util.uint8ArrayToString(item.sessionKey);
if (seen.has(k)) {
return false;
}
seen.add(k);
return true;
});
}
return decryptedSessionKeyPackets.map(packet => ({
data: packet.sessionKey,
algorithm: enums.read(enums.symmetric, packet.sessionKeyAlgorithm)
}));
}
throw exception || new Error('Session key decryption failed.');
}
/**
* Get literal data that is the body of the message
* @returns {(Uint8Array|null)} Literal body of the message as Uint8Array.
*/
getLiteralData() {
const msg = this.unwrapCompressed();
const literal = msg.packets.findPacket(enums.packet.literalData);
return (literal && literal.getBytes()) || null;
}
/**
* Get filename from literal data packet
* @returns {(String|null)} Filename of literal data packet as string.
*/
getFilename() {
const msg = this.unwrapCompressed();
const literal = msg.packets.findPacket(enums.packet.literalData);
return (literal && literal.getFilename()) || null;
}
/**
* Get literal data as text
* @returns {(String|null)} Literal body of the message interpreted as text.
*/
getText() {
const msg = this.unwrapCompressed();
const literal = msg.packets.findPacket(enums.packet.literalData);
if (literal) {
return literal.getText();
}
return null;
}
/**
* Generate a new session key object, taking the algorithm preferences of the passed encryption keys into account, if any.
* @param {Array<PublicKey>} [encryptionKeys] - Public key(s) to select algorithm preferences for
* @param {Date} [date] - Date to select algorithm preferences at
* @param {Array<Object>} [userIDs] - User IDs to select algorithm preferences for
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<{ data: Uint8Array, algorithm: String, aeadAlgorithm: undefined|String }>} Object with session key data and algorithms.
* @async
*/
static async generateSessionKey(encryptionKeys = [], date = new Date(), userIDs = [], config = defaultConfig) {
const algo = await getPreferredAlgo('symmetric', encryptionKeys, date, userIDs, config);
const algorithmName = enums.read(enums.symmetric, algo);
const aeadAlgorithmName = config.aeadProtect && await isAEADSupported(encryptionKeys, date, userIDs, config) ?
enums.read(enums.aead, await getPreferredAlgo('aead', encryptionKeys, date, userIDs, config)) :
undefined;
const sessionKeyData = crypto.generateSessionKey(algo);
return { data: sessionKeyData, algorithm: algorithmName, aeadAlgorithm: aeadAlgorithmName };
}
/**
* Encrypt the message either with public keys, passwords, or both at once.
* @param {Array<PublicKey>} [encryptionKeys] - Public key(s) for message encryption
* @param {Array<String>} [passwords] - Password(s) for message encryption
* @param {Object} [sessionKey] - Session key in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] }
* @param {Boolean} [wildcard] - Use a key ID of 0 instead of the public key IDs
* @param {Array<module:type/keyid~KeyID>} [encryptionKeyIDs] - Array of key IDs to use for encryption. Each encryptionKeyIDs[i] corresponds to keys[i]
* @param {Date} [date] - Override the creation date of the literal package
* @param {Array<Object>} [userIDs] - User IDs to encrypt for, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }]
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<Message>} New message with encrypted content.
* @async
*/
async encrypt(encryptionKeys, passwords, sessionKey, wildcard = false, encryptionKeyIDs = [], date = new Date(), userIDs = [], config = defaultConfig) {
if (sessionKey) {
if (!util.isUint8Array(sessionKey.data) || !util.isString(sessionKey.algorithm)) {
throw new Error('Invalid session key for encryption.');
}
} else if (encryptionKeys && encryptionKeys.length) {
sessionKey = await Message.generateSessionKey(encryptionKeys, date, userIDs, config);
} else if (passwords && passwords.length) {
sessionKey = await Message.generateSessionKey(undefined, undefined, undefined, config);
} else {
throw new Error('No keys, passwords, or session key provided.');
}
const { data: sessionKeyData, algorithm: algorithmName, aeadAlgorithm: aeadAlgorithmName } = sessionKey;
const msg = await Message.encryptSessionKey(sessionKeyData, algorithmName, aeadAlgorithmName, encryptionKeys, passwords, wildcard, encryptionKeyIDs, date, userIDs, config);
let symEncryptedPacket;
if (aeadAlgorithmName) {
symEncryptedPacket = new AEADEncryptedDataPacket();
symEncryptedPacket.aeadAlgorithm = enums.write(enums.aead, aeadAlgorithmName);
} else {
symEncryptedPacket = new SymEncryptedIntegrityProtectedDataPacket();
}
symEncryptedPacket.packets = this.packets;
const algorithm = enums.write(enums.symmetric, algorithmName);
await symEncryptedPacket.encrypt(algorithm, sessionKeyData, config);
msg.packets.push(symEncryptedPacket);
symEncryptedPacket.packets = new PacketList(); // remove packets after encryption
return msg;
}
/**
* Encrypt a session key either with public keys, passwords, or both at once.
* @param {Uint8Array} sessionKey - session key for encryption
* @param {String} algorithmName - session key algorithm
* @param {String} [aeadAlgorithmName] - AEAD algorithm, e.g. 'eax' or 'ocb'
* @param {Array<PublicKey>} [encryptionKeys] - Public key(s) for message encryption
* @param {Array<String>} [passwords] - For message encryption
* @param {Boolean} [wildcard] - Use a key ID of 0 instead of the public key IDs
* @param {Array<module:type/keyid~KeyID>} [encryptionKeyIDs] - Array of key IDs to use for encryption. Each encryptionKeyIDs[i] corresponds to encryptionKeys[i]
* @param {Date} [date] - Override the date
* @param {Array} [userIDs] - User IDs to encrypt for, e.g. [{ name:'Robert Receiver', email:'robert@openpgp.org' }]
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<Message>} New message with encrypted content.
* @async
*/
static async encryptSessionKey(sessionKey, algorithmName, aeadAlgorithmName, encryptionKeys, passwords, wildcard = false, encryptionKeyIDs = [], date = new Date(), userIDs = [], config = defaultConfig) {
const packetlist = new PacketList();
const algorithm = enums.write(enums.symmetric, algorithmName);
const aeadAlgorithm = aeadAlgorithmName && enums.write(enums.aead, aeadAlgorithmName);
if (encryptionKeys) {
const results = await Promise.all(encryptionKeys.map(async function(primaryKey, i) {
const encryptionKey = await primaryKey.getEncryptionKey(encryptionKeyIDs[i], date, userIDs, config);
const pkESKeyPacket = new PublicKeyEncryptedSessionKeyPacket();
pkESKeyPacket.publicKeyID = wildcard ? KeyID.wildcard() : encryptionKey.getKeyID();
pkESKeyPacket.publicKeyAlgorithm = encryptionKey.keyPacket.algorithm;
pkESKeyPacket.sessionKey = sessionKey;
pkESKeyPacket.sessionKeyAlgorithm = algorithm;
await pkESKeyPacket.encrypt(encryptionKey.keyPacket);
delete pkESKeyPacket.sessionKey; // delete plaintext session key after encryption
return pkESKeyPacket;
}));
packetlist.push(...results);
}
if (passwords) {
const testDecrypt = async function(keyPacket, password) {
try {
await keyPacket.decrypt(password);
return 1;
} catch (e) {
return 0;
}
};
const sum = (accumulator, currentValue) => accumulator + currentValue;
const encryptPassword = async function(sessionKey, algorithm, aeadAlgorithm, password) {
const symEncryptedSessionKeyPacket = new SymEncryptedSessionKeyPacket(config);
symEncryptedSessionKeyPacket.sessionKey = sessionKey;
symEncryptedSessionKeyPacket.sessionKeyAlgorithm = algorithm;
if (aeadAlgorithm) {
symEncryptedSessionKeyPacket.aeadAlgorithm = aeadAlgorithm;
}
await symEncryptedSessionKeyPacket.encrypt(password, config);
if (config.passwordCollisionCheck) {
const results = await Promise.all(passwords.map(pwd => testDecrypt(symEncryptedSessionKeyPacket, pwd)));
if (results.reduce(sum) !== 1) {
return encryptPassword(sessionKey, algorithm, password);
}
}
delete symEncryptedSessionKeyPacket.sessionKey; // delete plaintext session key after encryption
return symEncryptedSessionKeyPacket;
};
const results = await Promise.all(passwords.map(pwd => encryptPassword(sessionKey, algorithm, aeadAlgorithm, pwd)));
packetlist.push(...results);
}
return new Message(packetlist);
}
/**
* Sign the message (the literal data packet of the message)
* @param {Array<PrivateKey>} signingKeys - private keys with decrypted secret key data for signing
* @param {Signature} [signature] - Any existing detached signature to add to the message
* @param {Array<module:type/keyid~KeyID>} [signingKeyIDs] - Array of key IDs to use for signing. Each signingKeyIDs[i] corresponds to signingKeys[i]
* @param {Date} [date] - Override the creation time of the signature
* @param {Array} [userIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }]
* @param {Array} [notations] - Notation Data to add to the signatures, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }]
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<Message>} New message with signed content.
* @async
*/
async sign(signingKeys = [], signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], config = defaultConfig) {
const packetlist = new PacketList();
const literalDataPacket = this.packets.findPacket(enums.packet.literalData);
if (!literalDataPacket) {
throw new Error('No literal data packet to sign.');
}
let i;
let existingSigPacketlist;
// If data packet was created from Uint8Array, use binary, otherwise use text
const signatureType = literalDataPacket.text === null ?
enums.signature.binary : enums.signature.text;
if (signature) {
existingSigPacketlist = signature.packets.filterByTag(enums.packet.signature);
for (i = existingSigPacketlist.length - 1; i >= 0; i--) {
const signaturePacket = existingSigPacketlist[i];
const onePassSig = new OnePassSignaturePacket();
onePassSig.signatureType = signaturePacket.signatureType;
onePassSig.hashAlgorithm = signaturePacket.hashAlgorithm;
onePassSig.publicKeyAlgorithm = signaturePacket.publicKeyAlgorithm;
onePassSig.issuerKeyID = signaturePacket.issuerKeyID;
if (!signingKeys.length && i === 0) {
onePassSig.flags = 1;
}
packetlist.push(onePassSig);
}
}
await Promise.all(Array.from(signingKeys).reverse().map(async function (primaryKey, i) {
if (!primaryKey.isPrivate()) {
throw new Error('Need private key for signing');
}
const signingKeyID = signingKeyIDs[signingKeys.length - 1 - i];
const signingKey = await primaryKey.getSigningKey(signingKeyID, date, userIDs, config);
const onePassSig = new OnePassSignaturePacket();
onePassSig.signatureType = signatureType;
onePassSig.hashAlgorithm = await getPreferredHashAlgo(primaryKey, signingKey.keyPacket, date, userIDs, config);
onePassSig.publicKeyAlgorithm = signingKey.keyPacket.algorithm;
onePassSig.issuerKeyID = signingKey.getKeyID();
if (i === signingKeys.length - 1) {
onePassSig.flags = 1;
}
return onePassSig;
})).then(onePassSignatureList => {
onePassSignatureList.forEach(onePassSig => packetlist.push(onePassSig));
});
packetlist.push(literalDataPacket);
packetlist.push(...(await createSignaturePackets(literalDataPacket, signingKeys, signature, signingKeyIDs, date, userIDs, notations, false, config)));
return new Message(packetlist);
}
/**
* Compresses the message (the literal and -if signed- signature data packets of the message)
* @param {module:enums.compression} algo - compression algorithm
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Message} New message with compressed content.
*/
compress(algo, config = defaultConfig) {
if (algo === enums.compression.uncompressed) {
return this;
}
const compressed = new CompressedDataPacket(config);
compressed.algorithm = algo;
compressed.packets = this.packets;
const packetList = new PacketList();
packetList.push(compressed);
return new Message(packetList);
}
/**
* Create a detached signature for the message (the literal data packet of the message)
* @param {Array<PrivateKey>} signingKeys - 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 signingKeys[i]
* @param {Date} [date] - Override the creation time of the signature
* @param {Array} [userIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }]
* @param {Array} [notations] - Notation Data to add to the signatures, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }]
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<Signature>} New detached signature of message content.
* @async
*/
async signDetached(signingKeys = [], signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], config = defaultConfig) {
const literalDataPacket = this.packets.findPacket(enums.packet.literalData);
if (!literalDataPacket) {
throw new Error('No literal data packet to sign.');
}
return new Signature(await createSignaturePackets(literalDataPacket, signingKeys, signature, signingKeyIDs, date, userIDs, notations, true, config));
}
/**
* Verify message signatures
* @param {Array<PublicKey>} verificationKeys - Array of public 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 {Promise<Array<{
* keyID: module:type/keyid~KeyID,
* signature: Promise<Signature>,
* verified: Promise<true>
* }>>} List of signer's keyID and validity of signatures.
* @async
*/
async verify(verificationKeys, date = new Date(), config = defaultConfig) {
const msg = this.unwrapCompressed();
const literalDataList = msg.packets.filterByTag(enums.packet.literalData);
if (literalDataList.length !== 1) {
throw new Error('Can only verify message with one literal data packet.');
}
if (stream.isArrayStream(msg.packets.stream)) {
msg.packets.push(...await stream.readToEnd(msg.packets.stream, _ => _ || []));
}
const onePassSigList = msg.packets.filterByTag(enums.packet.onePassSignature).reverse();
const signatureList = msg.packets.filterByTag(enums.packet.signature);
if (onePassSigList.length && !signatureList.length && util.isStream(msg.packets.stream) && !stream.isArrayStream(msg.packets.stream)) {
await Promise.all(onePassSigList.map(async onePassSig => {
onePassSig.correspondingSig = new Promise((resolve, reject) => {
onePassSig.correspondingSigResolve = resolve;
onePassSig.correspondingSigReject = reject;
});
onePassSig.signatureData = stream.fromAsync(async () => (await onePassSig.correspondingSig).signatureData);
onePassSig.hashed = stream.readToEnd(await onePassSig.hash(onePassSig.signatureType, literalDataList[0], undefined, false));
onePassSig.hashed.catch(() => {});
}));
msg.packets.stream = stream.transformPair(msg.packets.stream, async (readable, writable) => {
const reader = stream.getReader(readable);
const writer = stream.getWriter(writable);
try {
for (let i = 0; i < onePassSigList.length; i++) {
const { value: signature } = await reader.read();
onePassSigList[i].correspondingSigResolve(signature);
}
await reader.readToEnd();
await writer.ready;
await writer.close();
} catch (e) {
onePassSigList.forEach(onePassSig => {
onePassSig.correspondingSigReject(e);
});
await writer.abort(e);
}
});
return createVerificationObjects(onePassSigList, literalDataList, verificationKeys, date, false, config);
}
return createVerificationObjects(signatureList, literalDataList, verificationKeys, date, false, config);
}
/**
* Verify detached message signature
* @param {Array<PublicKey>} verificationKeys - Array of public keys to verify signatures
* @param {Signature} signature
* @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 {Promise<Array<{
* keyID: module:type/keyid~KeyID,
* signature: Promise<Signature>,
* verified: Promise<true>
* }>>} List of signer's keyID and validity of signature.
* @async
*/
verifyDetached(signature, verificationKeys, date = new Date(), config = defaultConfig) {
const msg = this.unwrapCompressed();
const literalDataList = msg.packets.filterByTag(enums.packet.literalData);
if (literalDataList.length !== 1) {
throw new Error('Can only verify message with one literal data packet.');
}
const signatureList = signature.packets;
return createVerificationObjects(signatureList, literalDataList, verificationKeys, date, true, config);
}
/**
* Unwrap compressed message
* @returns {Message} Message Content of compressed message.
*/
unwrapCompressed() {
const compressed = this.packets.filterByTag(enums.packet.compressedData);
if (compressed.length) {
return new Message(compressed[0].packets);
}
return this;
}
/**
* Append signature to unencrypted message object
* @param {String|Uint8Array} detachedSignature - The detached ASCII-armored or Uint8Array PGP signature
* @param {Object} [config] - Full configuration, defaults to openpgp.config
*/
async appendSignature(detachedSignature, config = defaultConfig) {
await this.packets.read(
util.isUint8Array(detachedSignature) ? detachedSignature : (await unarmor(detachedSignature)).data,
allowedDetachedSignaturePackets,
config
);
}
/**
* Returns binary encoded message
* @returns {ReadableStream<Uint8Array>} Binary message.
*/
write() {
return this.packets.write();
}
/**
* Returns ASCII armored text of message
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {ReadableStream<String>} ASCII armor.
*/
armor(config = defaultConfig) {
return armor(enums.armor.message, this.write(), null, null, null, config);
}
}
/**
* Create signature packets for the message
* @param {LiteralDataPacket} literalDataPacket - the literal data packet to sign
* @param {Array<PrivateKey>} [signingKeys] - private keys with decrypted secret key data for signing
* @param {Signature} [signature] - Any existing detached signature to append
* @param {Array<module:type/keyid~KeyID>} [signingKeyIDs] - Array of key IDs to use for signing. Each signingKeyIDs[i] corresponds to signingKeys[i]
* @param {Date} [date] - Override the creationtime of the signature
* @param {Array} [userIDs] - User IDs to sign with, e.g. [{ name:'Steve Sender', email:'steve@openpgp.org' }]
* @param {Array} [notations] - Notation Data to add to the signatures, e.g. [{ name: 'test@example.org', value: new TextEncoder().encode('test'), humanReadable: true, critical: false }]
* @param {Boolean} [detached] - Whether to create detached signature packets
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<PacketList>} List of signature packets.
* @async
* @private
*/
export async function createSignaturePackets(literalDataPacket, signingKeys, signature = null, signingKeyIDs = [], date = new Date(), userIDs = [], notations = [], detached = false, config = defaultConfig) {
const packetlist = new PacketList();
// If data packet was created from Uint8Array, use binary, otherwise use text
const signatureType = literalDataPacket.text === null ?
enums.signature.binary : enums.signature.text;
await Promise.all(signingKeys.map(async (primaryKey, i) => {
const userID = userIDs[i];
if (!primaryKey.isPrivate()) {
throw new Error('Need private key for signing');
}
const signingKey = await primaryKey.getSigningKey(signingKeyIDs[i], date, userID, config);
return createSignaturePacket(literalDataPacket, primaryKey, signingKey.keyPacket, { signatureType }, date, userID, notations, detached, config);
})).then(signatureList => {
packetlist.push(...signatureList);
});
if (signature) {
const existingSigPacketlist = signature.packets.filterByTag(enums.packet.signature);
packetlist.push(...existingSigPacketlist);
}
return packetlist;
}
/**
* Create object containing signer's keyID and validity of signature
* @param {SignaturePacket} signature - Signature packet
* @param {Array<LiteralDataPacket>} literalDataList - Array of literal data packets
* @param {Array<PublicKey>} verificationKeys - Array of public keys to verify signatures
* @param {Date} [date] - Check signature validity with respect to the given date
* @param {Boolean} [detached] - Whether to verify detached signature packets
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<{
* keyID: module:type/keyid~KeyID,
* signature: Promise<Signature>,
* verified: Promise<true>
* }>} signer's keyID and validity of signature
* @async
* @private
*/
async function createVerificationObject(signature, literalDataList, verificationKeys, date = new Date(), detached = false, config = defaultConfig) {
let primaryKey;
let unverifiedSigningKey;
for (const key of verificationKeys) {
const issuerKeys = key.getKeys(signature.issuerKeyID);
if (issuerKeys.length > 0) {
primaryKey = key;
unverifiedSigningKey = issuerKeys[0];
break;
}
}
const isOnePassSignature = signature instanceof OnePassSignaturePacket;
const signaturePacketPromise = isOnePassSignature ? signature.correspondingSig : signature;
const verifiedSig = {
keyID: signature.issuerKeyID,
verified: (async () => {
if (!unverifiedSigningKey) {
throw new Error(`Could not find signing key with key ID ${signature.issuerKeyID.toHex()}`);
}
await signature.verify(unverifiedSigningKey.keyPacket, signature.signatureType, literalDataList[0], date, detached, config);
const signaturePacket = await signaturePacketPromise;
if (unverifiedSigningKey.getCreationTime() > signaturePacket.created) {
throw new Error('Key is newer than the signature');
}
// We pass the signature creation time to check whether the key was expired at the time of signing.
// We check this after signature verification because for streamed one-pass signatures, the creation time is not available before
try {
await primaryKey.getSigningKey(unverifiedSigningKey.getKeyID(), signaturePacket.created, undefined, config);
} catch (e) {
// If a key was reformatted then the self-signatures of the signing key might be in the future compared to the message signature,
// making the key invalid at the time of signing.
// However, if the key is valid at the given `date`, we still allow using it provided the relevant `config` setting is enabled.
// Note: we do not support the edge case of a key that was reformatted and it has expired.
if (config.allowInsecureVerificationWithReformattedKeys && e.message.match(/Signature creation time is in the future/)) {
await primaryKey.getSigningKey(unverifiedSigningKey.getKeyID(), date, undefined, config);
} else {
throw e;
}
}
return true;
})(),
signature: (async () => {
const signaturePacket = await signaturePacketPromise;
const packetlist = new PacketList();
signaturePacket && packetlist.push(signaturePacket);
return new Signature(packetlist);
})()
};
// Mark potential promise rejections as "handled". This is needed because in
// some cases, we reject them before the user has a reasonable chance to
// handle them (e.g. `await readToEnd(result.data); await result.verified` and
// the data stream errors).
verifiedSig.signature.catch(() => {});
verifiedSig.verified.catch(() => {});
return verifiedSig;
}
/**
* Create list of objects containing signer's keyID and validity of signature
* @param {Array<SignaturePacket>} signatureList - Array of signature packets
* @param {Array<LiteralDataPacket>} literalDataList - Array of literal data packets
* @param {Array<PublicKey>} verificationKeys - Array of public keys to verify signatures
* @param {Date} date - Verify the signature against the given date,
* i.e. check signature creation time < date < expiration time
* @param {Boolean} [detached] - Whether to verify detached signature packets
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<Array<{
* keyID: module:type/keyid~KeyID,
* signature: Promise<Signature>,
* verified: Promise<true>
* }>>} list of signer's keyID and validity of signatures (one entry per signature packet in input)
* @async
* @private
*/
export async function createVerificationObjects(signatureList, literalDataList, verificationKeys, date = new Date(), detached = false, config = defaultConfig) {
return Promise.all(signatureList.filter(function(signature) {
return ['text', 'binary'].includes(enums.read(enums.signature, signature.signatureType));
}).map(async function(signature) {
return createVerificationObject(signature, literalDataList, verificationKeys, date, detached, config);
}));
}
/**
* Reads an (optionally armored) OpenPGP message and returns a Message object
* @param {Object} options
* @param {String | ReadableStream<String>} [options.armoredMessage] - Armored message to be parsed
* @param {Uint8Array | ReadableStream<Uint8Array>} [options.binaryMessage] - Binary to be parsed
* @param {Object} [options.config] - Custom configuration settings to overwrite those in [config]{@link module:config}
* @returns {Promise<Message>} New message object.
* @async
* @static
*/
export async function readMessage({ armoredMessage, binaryMessage, config, ...rest }) {
config = { ...defaultConfig, ...config };
let input = armoredMessage || binaryMessage;
if (!input) {
throw new Error('readMessage: must pass options object containing `armoredMessage` or `binaryMessage`');
}
if (armoredMessage && !util.isString(armoredMessage) && !util.isStream(armoredMessage)) {
throw new Error('readMessage: options.armoredMessage must be a string or stream');
}
if (binaryMessage && !util.isUint8Array(binaryMessage) && !util.isStream(binaryMessage)) {
throw new Error('readMessage: options.binaryMessage must be a Uint8Array or stream');
}
const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw new Error(`Unknown option: ${unknownOptions.join(', ')}`);
const streamType = util.isStream(input);
if (streamType) {
await stream.loadStreamsPonyfill();
input = stream.toStream(input);
}
if (armoredMessage) {
const { type, data } = await unarmor(input, config);
if (type !== enums.armor.message) {
throw new Error('Armored text not of type message');
}
input = data;
}
const packetlist = await PacketList.fromBinary(input, allowedMessagePackets, config);
const message = new Message(packetlist);
message.fromStream = streamType;
return message;
}
/**
* Creates new message object from text or binary data.
* @param {Object} options
* @param {String | ReadableStream<String>} [options.text] - The text message contents
* @param {Uint8Array | ReadableStream<Uint8Array>} [options.binary] - The binary message contents
* @param {String} [options.filename=""] - Name of the file (if any)
* @param {Date} [options.date=current date] - Date of the message, or modification date of the file
* @param {'utf8'|'binary'|'text'|'mime'} [options.format='utf8' if text is passed, 'binary' otherwise] - Data packet type
* @returns {Promise<Message>} New message object.
* @async
* @static
*/
export async function createMessage({ text, binary, filename, date = new Date(), format = text !== undefined ? 'utf8' : 'binary', ...rest }) {
let input = text !== undefined ? text : binary;
if (input === undefined) {
throw new Error('createMessage: must pass options object containing `text` or `binary`');
}
if (text && !util.isString(text) && !util.isStream(text)) {
throw new Error('createMessage: options.text must be a string or stream');
}
if (binary && !util.isUint8Array(binary) && !util.isStream(binary)) {
throw new Error('createMessage: options.binary must be a Uint8Array or stream');
}
const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw new Error(`Unknown option: ${unknownOptions.join(', ')}`);
const streamType = util.isStream(input);
if (streamType) {
await stream.loadStreamsPonyfill();
input = stream.toStream(input);
}
const literalDataPacket = new LiteralDataPacket(date);
if (text !== undefined) {
literalDataPacket.setText(input, enums.write(enums.literal, format));
} else {
literalDataPacket.setBytes(input, enums.write(enums.literal, format));
}
if (filename !== undefined) {
literalDataPacket.setFilename(filename);
}
const literalDataPacketlist = new PacketList();
literalDataPacketlist.push(literalDataPacket);
const message = new Message(literalDataPacketlist);
message.fromStream = streamType;
return message;
}