-
Notifications
You must be signed in to change notification settings - Fork 46
/
attachment.ts
299 lines (275 loc) · 10.4 KB
/
attachment.ts
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
/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */
'use strict';
import { Buf } from './buf.js';
import { Str } from './common.js';
export type Attachment$treatAs =
| 'publicKey'
| 'privateKey'
| 'encryptedMsg' /* may be signed-only (known as 'signedMsg' in MsgBlockType) as well,
should probably be renamed to 'cryptoMsg' to not be confused with 'encryptedMsg' in MsgBlockType */
| 'hidden'
| 'signature'
| 'encryptedFile'
| 'plainFile'
| 'inlineImage'
| 'needChunk'
| 'maybePgp';
type ContentTransferEncoding = '7bit' | 'quoted-printable' | 'base64';
export type AttachmentId = { id: string; msgId: string } | { url: string }; // a way to extract data
export type AttachmentProperties = {
type?: string;
name?: string;
length?: number;
inline?: boolean;
treatAs?: Attachment$treatAs;
cid?: string;
contentDescription?: string;
contentTransferEncoding?: ContentTransferEncoding;
};
export type AttachmentMeta = (AttachmentId | { data: Uint8Array }) & AttachmentProperties;
export type FcAttachmentLinkData = { name: string; type: string; size: number };
export type TransferableAttachment = (AttachmentId | { data: /* base64 see #2587 */ string }) & AttachmentProperties;
export class Attachment {
// Regex to trigger message download and processing based on attachment file names
// todo: it'd be better to compile this regex based on the data we have in `treatAs` method
public static readonly webmailNamePattern =
/^(((cryptup|flowcrypt)-backup-[a-z0-9]+\.(key|asc))|(.+\.pgp)|(.+\.gpg)|(.+\.asc)|(OpenPGP_signature(.asc)?)|(noname)|(message)|(PGPMIME version identification)|(ATT[0-9]{5})|())$/m;
public static readonly encryptedMsgNames = [
'msg.asc',
'message.asc',
'encrypted.asc',
'encrypted.eml.pgp',
'Message.pgp',
'openpgp-encrypted-message.asc',
'.asc.pgp',
];
public length = NaN;
public type: string;
public name: string;
public url: string | undefined;
public id: string | undefined;
public msgId: string | undefined;
public inline: boolean;
public cid: string | undefined;
public contentDescription: string | undefined;
public contentTransferEncoding?: ContentTransferEncoding;
private bytes: Uint8Array | undefined;
private treatAsValue: Attachment$treatAs | undefined; // this field is to disable on-the-fly detection by this.treatAs()
public constructor(attachmentMeta: AttachmentMeta) {
if ('data' in attachmentMeta) {
this.bytes = attachmentMeta.data;
this.length = attachmentMeta.data.length;
} else {
this.length = Number(attachmentMeta.length);
}
this.name = attachmentMeta.name || '';
this.type = attachmentMeta.type || 'application/octet-stream';
this.url = 'url' in attachmentMeta ? attachmentMeta.url : undefined;
this.inline = !!attachmentMeta.inline;
this.id = 'id' in attachmentMeta ? attachmentMeta.id : undefined;
this.msgId = 'msgId' in attachmentMeta ? attachmentMeta.msgId : undefined;
this.treatAsValue = attachmentMeta.treatAs;
this.cid = attachmentMeta.cid;
this.contentDescription = attachmentMeta.contentDescription;
this.contentTransferEncoding = attachmentMeta.contentTransferEncoding;
}
public static treatAsForPgpEncryptedAttachments = (mimeType: string | undefined, pgpEncryptedIndex: number | undefined) => {
let treatAs: 'hidden' | 'encryptedMsg' | undefined;
if (mimeType === 'application/pgp-encrypted' && pgpEncryptedIndex === 0) {
treatAs = 'hidden';
}
if (mimeType === 'application/octet-stream' && pgpEncryptedIndex === 1) {
treatAs = 'encryptedMsg';
}
return treatAs;
};
public static keyinfoAsPubkeyAttachment = (ki: { public: string; longid: string }) => {
const data = Buf.fromUtfStr(ki.public);
return new Attachment({
data,
type: 'application/pgp-keys',
contentTransferEncoding: Str.is7bit(data) ? '7bit' : 'quoted-printable',
name: `0x${ki.longid}.asc`,
});
};
public static sanitizeName = (name: string): string => {
const trimmed = name.trim();
if (trimmed === '') {
return '_';
}
return trimmed.replace(/[\u0000\u002f\u005c]/g, '_').replace(/__+/g, '_');
};
public static attachmentId = (): string => {
return `f_${Str.sloppyRandom(30)}@flowcrypt`;
};
public static toTransferableAttachment = (attachmentMeta: AttachmentMeta): TransferableAttachment => {
return 'data' in attachmentMeta
? {
...attachmentMeta,
data: Buf.fromUint8(attachmentMeta.data).toBase64Str(), // should we better convert to url?
}
: attachmentMeta;
};
public static fromTransferableAttachment = (t: TransferableAttachment): Attachment => {
return new Attachment(
'data' in t
? {
...t,
data: Buf.fromBase64Str(t.data),
}
: t
);
};
public isPublicKey = (): boolean => {
if (this.treatAsValue) {
return this.treatAsValue === 'publicKey';
}
return (
(this.type === 'application/pgp-keys' && !this.isPrivateKey()) ||
/^(0|0x)?([A-F0-9]{16}|[A-F0-9]{8}([A-F0-9]{8})?)\.asc(\.pgp)?$/i.test(this.name) || // Key ID (8 or 16 characters) with .asc extension (optional .pgp)
(this.name.toLowerCase().includes('public') && /[A-F0-9]{8}.*\.asc$/g.test(this.name)) || // name contains the word "public", any key id and ends with .asc
(this.name.endsWith('.asc') && this.hasData() && Buf.with(this.getData().subarray(0, 100)).toUtfStr().includes('-----BEGIN PGP PUBLIC KEY BLOCK-----'))
);
};
public isPrivateKey = (): boolean => {
return (
Boolean(this.name.match(/(cryptup|flowcrypt)-backup-([a-z0-9]+(?:\-[A-F0-9]{40})?)\.(key|asc)$/g)) ||
(/\.(asc|key)$/.test(this.name) &&
this.hasData() &&
Buf.with(this.getData().subarray(0, 100)).toUtfStr().includes('-----BEGIN PGP PRIVATE KEY BLOCK-----'))
);
};
public hasData = () => {
return this.bytes instanceof Uint8Array;
};
public setData = (bytes: Uint8Array) => {
if (this.hasData()) {
throw new Error('Attachment bytes already set');
}
this.bytes = bytes;
};
public getData = (): Buf => {
if (this.bytes instanceof Buf) {
return this.bytes;
}
if (this.bytes instanceof Uint8Array) {
return new Buf(this.bytes);
}
throw new Error('Attachment has no data set');
};
public isAttachmentAnImage = () => {
return this.type.startsWith('image/') || this.type.startsWith('img/');
};
public treatAs = (attachments: Attachment[], isBodyEmpty = false): Attachment$treatAs => {
if (this.treatAsValue) {
// pre-set
return this.treatAsValue;
} else if (['PGPexch.htm.pgp', 'PGPMIME version identification', 'Version.txt', 'PGPMIME Versions Identification'].includes(this.name)) {
return 'hidden'; // PGPexch.htm.pgp is html alternative of textual body content produced by PGP Desktop and GPG4o
} else if (this.name === 'signature.asc') {
return 'signature';
} else if (this.type === 'application/pgp-signature') {
// this may be a signature for an attachment following these patterns:
// sample.name.sig for sample.name.pgp #3448
// or sample.name.sig for sample.name
if (attachments.length > 1) {
const nameWithoutExtension = Str.getFilenameWithoutExtension(this.name);
if (attachments.some(a => a !== this && (a.name === nameWithoutExtension || Str.getFilenameWithoutExtension(a.name) === nameWithoutExtension))) {
return 'hidden';
}
}
return 'signature';
} else if (this.inline && this.isAttachmentAnImage()) {
return 'inlineImage';
} else if (!this.name && !this.isAttachmentAnImage() && this.type !== 'application/octet-stream' && this.type !== 'multipart/mixed') {
// this.name may be '' or undefined - catch either
return this.length < 100 ? 'hidden' : 'encryptedMsg';
} else if (this.name === 'msg.asc' && this.length < 100 && this.type === 'application/pgp-encrypted') {
return 'hidden'; // mail.ch does this - although it looks like encrypted msg, it will just contain PGP version eg "Version: 1"
} else if (Attachment.encryptedMsgNames.includes(this.name)) {
return 'encryptedMsg';
} else if (this.name === 'message' && isBodyEmpty) {
// treat message as encryptedMsg when empty body for the 'message' attachment
return 'encryptedMsg';
} else if (this.name.match(/(\.pgp$)|(\.gpg$)|(\.[a-zA-Z0-9]{3,4}\.asc$)/g)) {
// ends with one of .gpg, .pgp, .???.asc, .????.asc
return 'encryptedFile';
// todo: after #4906 is done we should "decrypt" the encryptedFile here to see if it's a binary 'publicKey' (as in message 1869220e0c8f16dd)
} else if (this.isPublicKey()) {
return 'publicKey';
} else if (this.isPrivateKey()) {
return 'privateKey';
} else {
// && !Attachment.encryptedMsgNames.includes(this.name) -- already checked above
const isAmbiguousAscFile = this.name.endsWith('.asc'); // ambiguous .asc name
const isAmbiguousNonameFile = !this.name || this.name === 'noname'; // may not even be OpenPGP related
if (!this.inline && this.length < 100000 && (isAmbiguousAscFile || isAmbiguousNonameFile) && !this.isAttachmentAnImage()) {
if (isAmbiguousNonameFile && this.type === 'application/octet-stream') {
return 'plainFile';
}
return this.hasData() ? 'maybePgp' : 'needChunk';
}
return 'plainFile';
}
};
public isPgpMimeVersion = () => {
return this.type === 'application/pgp-encrypted' && this.name.length === 0 && this.getData().toUtfStr() === 'Version: 1';
};
public isExecutableFile = () => {
return [
'ade',
'adp',
'apk',
'appx',
'appxbundle',
'bat',
'cab',
'chm',
'cmd',
'com',
'cpl',
'diagcab',
'diagcfg',
'diagpack',
'dll',
'dmg',
'ex',
'ex_',
'exe',
'hta',
'img',
'ins',
'iso',
'isp',
'jar',
'jnlp',
'js',
'jse',
'lib',
'lnk',
'mde',
'msc',
'msi',
'msix',
'msixbundle',
'msp',
'mst',
'nsh',
'pif',
'ps1',
'scr',
'sct',
'shb',
'sys',
'vb',
'vbe',
'vbs',
'vhd',
'vxd',
'wsc',
'wsf',
'wsh',
'xll',
].some(exeFileExtension => this.name.endsWith('.' + exeFileExtension));
};
}