Merge branch 'renovate/all-minor-patch' into 'main'
[ProtonMail-WebClient.git] / packages / pass / lib / export / export.ts
blobb00a267a8bfdecffc27b954b252253590a12e209
1 import JSZip from 'jszip';
2 import Papa from 'papaparse';
4 import { CryptoProxy } from '@proton/crypto';
5 import { decodeBase64, encodeBase64, encodeUtf8Base64 } from '@proton/crypto/lib/utils';
6 import type { TransferableFile } from '@proton/pass/utils/file/transferable-file';
7 import { prop } from '@proton/pass/utils/fp/lens';
8 import { PASS_APP_NAME } from '@proton/shared/lib/constants';
9 import { uint8ArrayToBase64String } from '@proton/shared/lib/helpers/encoding';
11 import type { ExportCSVItem } from './types';
12 import { type ExportData, ExportFormat, type ExportOptions } from './types';
14 const EXPORT_AS_JSON_TYPES = ['creditCard', 'identity'];
16 /** Exporting data from the extension uses the .zip format
17  * for future-proofing : we will support integrating additional
18  * files when exporting */
19 export const createPassExportZip = async (payload: ExportData): Promise<Uint8Array> => {
20     const zip = new JSZip();
21     zip.file(`${PASS_APP_NAME}/data.json`, JSON.stringify(payload));
22     return zip.generateAsync({ type: 'uint8array' });
25 /** FIXME: ideally we should also support exporting
26  * `extraFields` to notes when exporting to CSV */
27 export const createPassExportCSV = (payload: ExportData): string => {
28     const items = Object.values(payload.vaults)
29         .flatMap(prop('items'))
30         .map<ExportCSVItem>(({ data, aliasEmail, createTime, modifyTime, shareId }) => ({
31             type: data.type,
32             name: data.metadata.name,
33             url: 'urls' in data.content ? data.content.urls.join(', ') : '',
34             email: (() => {
35                 switch (data.type) {
36                     case 'login':
37                         return data.content.itemEmail;
38                     case 'alias':
39                         return aliasEmail ?? '';
40                     default:
41                         return '';
42                 }
43             })(),
44             username: data.type === 'login' ? data.content.itemUsername : '',
45             password: 'password' in data.content ? data.content.password : '',
46             note: EXPORT_AS_JSON_TYPES.includes(data.type)
47                 ? JSON.stringify({ ...data.content, note: data.metadata.note })
48                 : data.metadata.note,
49             totp: 'totpUri' in data.content ? data.content.totpUri : '',
50             createTime: createTime.toString(),
51             modifyTime: modifyTime.toString(),
52             vault: payload.vaults[shareId].name,
53         }));
55     return Papa.unparse(items);
58 /**
59  * Encrypts an `Uint8Array` representation of an export zip to a base64.
60  * Once support for argon2 is released, then we should pass a config to use
61  * that instead - as long as the export feature is alpha, it’s okay to release
62  * without argon2, but let’s pass config: { s2kIterationCountByte: 255 } to
63  * encryptMessage with the highest security we have atm */
64 export const encryptPassExport = async (data: Uint8Array, passphrase: string): Promise<string> =>
65     encodeBase64(
66         (
67             await CryptoProxy.encryptMessage({
68                 binaryData: data,
69                 passwords: [passphrase],
70                 config: { s2kIterationCountByte: 255 },
71                 format: 'armored',
72             })
73         ).message
74     );
76 export const decryptPassExport = async (base64: string, passphrase: string): Promise<Uint8Array> =>
77     (
78         await CryptoProxy.decryptMessage({
79             armoredMessage: decodeBase64(base64),
80             passwords: [passphrase],
81             format: 'binary',
82         })
83     ).data;
85 const getMimeType = (format: ExportFormat) => {
86     switch (format) {
87         case ExportFormat.ZIP:
88             return 'application/zip';
89         case ExportFormat.PGP:
90             return 'application/pgp-encrypted';
91         case ExportFormat.CSV:
92             return 'text/csv;charset=utf-8;';
93     }
96 const createBase64Export = async (payload: ExportData, options: ExportOptions): Promise<string> => {
97     switch (options.format) {
98         case ExportFormat.ZIP:
99             return uint8ArrayToBase64String(await createPassExportZip(payload));
100         case ExportFormat.PGP:
101             return encryptPassExport(await createPassExportZip(payload), options.passphrase);
102         case ExportFormat.CSV:
103             return encodeUtf8Base64(createPassExportCSV(payload));
104     }
107 /** If data is encrypted, will export as PGP file instead of a ZIP.
108  * Returns a `TransferableFile` in case the data must be passed around
109  * different contexts (ie: from extension component to service worker) */
110 export const createPassExport = async (payload: ExportData, options: ExportOptions): Promise<TransferableFile> => {
111     const base64 = await createBase64Export(payload, options);
112     const type = getMimeType(options.format);
113     const timestamp = new Date().toISOString().split('T')[0];
114     const name = `${PASS_APP_NAME}_export_${timestamp}.${options.format}`;
116     return { base64, name, type };