Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / applications / drive / src / app / store / _links / extendedAttributes.ts
blob89973170a09b31e807eb547d78c8bce6c4e2ecf4
1 import type { PrivateKeyReference, PublicKeyReference, VERIFICATION_STATUS } from '@proton/crypto';
2 import { CryptoProxy } from '@proton/crypto';
3 import { FILE_CHUNK_SIZE } from '@proton/shared/lib/drive/constants';
4 import { decryptSigned } from '@proton/shared/lib/keys/driveKeys';
6 import { EnrichedError } from '../../utils/errorHandling/EnrichedError';
7 import type { DeepPartial } from '../../utils/type/DeepPartial';
9 export interface ExtendedAttributes {
10     Common: {
11         ModificationTime?: string;
12         Size?: number;
13         BlockSizes?: number[];
14         Digests?: {
15             SHA1: string;
16         };
17     };
18     Location?: {
19         Latitude: number;
20         Longitude: number;
21     };
22     Camera?: {
23         CaptureTime?: string;
24         Device?: string;
25         Orientation?: number;
26         SubjectCoordinates?: {
27             Top: number;
28             Left: number;
29             Bottom: number;
30             Right: number;
31         };
32     };
33     Media?: {
34         Width: number;
35         Height: number;
36         Duration?: number;
37     };
40 export interface ParsedExtendedAttributes {
41     Common: {
42         ModificationTime?: number;
43         Size?: number;
44         BlockSizes?: number[];
45         Digests?: {
46             SHA1: string;
47         };
48     };
49     Location?: {
50         Latitude: number;
51         Longitude: number;
52     };
53     Camera?: {
54         CaptureTime?: string;
55         Device?: string;
56         Orientation?: number;
57         SubjectCoordinates?: {
58             Top: number;
59             Left: number;
60             Bottom: number;
61             Right: number;
62         };
63     };
64     Media?: {
65         Width: number;
66         Height: number;
67         Duration?: number;
68     };
71 type MaybeExtendedAttributes = DeepPartial<ExtendedAttributes>;
73 export async function encryptFolderExtendedAttributes(
74     modificationTime: Date,
75     nodePrivateKey: PrivateKeyReference,
76     addressPrivateKey: PrivateKeyReference
77 ) {
78     const xattr = createFolderExtendedAttributes(modificationTime);
79     return encryptExtendedAttributes(xattr, nodePrivateKey, addressPrivateKey);
82 export function createFolderExtendedAttributes(modificationTime: Date): ExtendedAttributes {
83     return {
84         Common: {
85             ModificationTime: dateToIsoString(modificationTime),
86         },
87     };
90 export type XAttrCreateParams = {
91     file: File;
92     media?: {
93         width: number;
94         height: number;
95         duration?: number;
96     };
97     digests?: {
98         sha1: string;
99     };
100     location?: {
101         latitude: number;
102         longitude: number;
103     };
104     camera?: {
105         captureTime?: string;
106         device?: string;
107         orientation?: number;
108         subjectCoordinates?: {
109             top: number;
110             left: number;
111             bottom: number;
112             right: number;
113         };
114     };
117 export async function encryptFileExtendedAttributes(
118     params: XAttrCreateParams,
119     nodePrivateKey: PrivateKeyReference,
120     addressPrivateKey: PrivateKeyReference
121 ) {
122     const xattr = createFileExtendedAttributes(params);
123     return encryptExtendedAttributes(xattr, nodePrivateKey, addressPrivateKey);
126 export function createFileExtendedAttributes({
127     file,
128     digests,
129     media,
130     camera,
131     location,
132 }: XAttrCreateParams): ExtendedAttributes {
133     const blockSizes = new Array(Math.floor(file.size / FILE_CHUNK_SIZE));
134     blockSizes.fill(FILE_CHUNK_SIZE);
135     blockSizes.push(file.size % FILE_CHUNK_SIZE);
137     return {
138         Common: {
139             ModificationTime: dateToIsoString(new Date(file.lastModified)),
140             Size: file.size,
141             BlockSizes: blockSizes,
142             Digests: digests
143                 ? {
144                       SHA1: digests.sha1,
145                   }
146                 : undefined,
147         },
148         Media: media
149             ? {
150                   Width: media.width,
151                   Height: media.height,
152                   Duration: media.duration,
153               }
154             : undefined,
155         Location: location
156             ? {
157                   Latitude: location.latitude,
158                   Longitude: location.longitude,
159               }
160             : undefined,
161         Camera: camera
162             ? {
163                   CaptureTime: camera.captureTime,
164                   Device: camera.device,
165                   Orientation: camera.orientation,
166                   SubjectCoordinates: camera.subjectCoordinates
167                       ? {
168                             Top: camera.subjectCoordinates.top,
169                             Left: camera.subjectCoordinates.left,
170                             Bottom: camera.subjectCoordinates.bottom,
171                             Right: camera.subjectCoordinates.right,
172                         }
173                       : undefined,
174               }
175             : undefined,
176     };
180  * Creates and encryptes XAttr for documents.
181  */
182 export async function encryptDocumentExtendedAttributes(
183     nodePrivateKey: PrivateKeyReference,
184     addressPrivateKey: PrivateKeyReference
185 ) {
186     const xattr = createDocumentExtendedAttributes();
187     return encryptExtendedAttributes(xattr, nodePrivateKey, addressPrivateKey);
190 export function createDocumentExtendedAttributes() {
191     return {
192         Common: {},
193     };
196 async function encryptExtendedAttributes(
197     xattr: ExtendedAttributes,
198     nodePrivateKey: PrivateKeyReference,
199     addressPrivateKey: PrivateKeyReference
200 ) {
201     try {
202         const xattrString = JSON.stringify(xattr);
204         const { message } = await CryptoProxy.encryptMessage({
205             textData: xattrString,
206             encryptionKeys: nodePrivateKey,
207             signingKeys: addressPrivateKey,
208             compress: true,
209         });
211         return message;
212     } catch (e) {
213         throw new EnrichedError('Failed to encrypt extended attributes', {
214             tags: {
215                 addressKeyId: addressPrivateKey.getKeyID(),
216             },
217             extra: {
218                 e,
219             },
220         });
221     }
224 export async function decryptExtendedAttributes(
225     encryptedXAttr: string,
226     nodePrivateKey: PrivateKeyReference,
227     addressPublicKey: PublicKeyReference | PublicKeyReference[]
228 ): Promise<{ xattrs: ParsedExtendedAttributes; verified: VERIFICATION_STATUS }> {
229     try {
230         const { data: xattrString, verified } = await decryptSigned({
231             armoredMessage: encryptedXAttr,
232             privateKey: nodePrivateKey,
233             publicKey: addressPublicKey,
234         });
236         return {
237             xattrs: parseExtendedAttributes(xattrString),
238             verified,
239         };
240     } catch (e) {
241         throw new EnrichedError('Failed to decrypt extended attributes', {
242             extra: {
243                 e,
244                 addressKeyIds: (Array.isArray(addressPublicKey) ? addressPublicKey : [addressPublicKey]).map((key) =>
245                     key.getKeyID()
246                 ),
247             },
248         });
249     }
252 export function parseExtendedAttributes(xattrString: string): ParsedExtendedAttributes {
253     let xattr: MaybeExtendedAttributes = {};
254     try {
255         xattr = JSON.parse(xattrString) as MaybeExtendedAttributes;
256     } catch (err) {
257         console.warn(`XAttr "${xattrString}" is not valid JSON`);
258     }
259     return {
260         Common: {
261             ModificationTime: parseModificationTime(xattr),
262             Size: parseSize(xattr),
263             BlockSizes: parseBlockSizes(xattr),
264             Digests: parseDigests(xattr),
265         },
266         Media: parseMedia(xattr),
267     };
270 function parseModificationTime(xattr: MaybeExtendedAttributes): number | undefined {
271     const modificationTime = xattr?.Common?.ModificationTime;
272     if (modificationTime === undefined) {
273         return undefined;
274     }
275     const modificationDate = new Date(modificationTime);
276     // This is the best way to check if date is "Invalid Date". :shrug:
277     if (JSON.stringify(modificationDate) === 'null') {
278         console.warn(`XAttr modification time "${modificationTime}" is not valid`);
279         return undefined;
280     }
281     const modificationTimestamp = Math.trunc(modificationDate.getTime() / 1000);
282     if (Number.isNaN(modificationTimestamp)) {
283         console.warn(`XAttr modification time "${modificationTime}" is not valid`);
284         return undefined;
285     }
286     return modificationTimestamp;
289 function parseSize(xattr: MaybeExtendedAttributes): number | undefined {
290     const size = xattr?.Common?.Size;
291     if (size === undefined) {
292         return undefined;
293     }
294     if (typeof size !== 'number') {
295         console.warn(`XAttr file size "${size}" is not valid`);
296         return undefined;
297     }
298     return size;
301 function parseBlockSizes(xattr: MaybeExtendedAttributes): number[] | undefined {
302     const blockSizes = xattr?.Common?.BlockSizes;
303     if (blockSizes === undefined) {
304         return undefined;
305     }
306     if (!Array.isArray(blockSizes)) {
307         console.warn(`XAttr block sizes "${blockSizes}" is not valid`);
308         return undefined;
309     }
310     if (!blockSizes.every((item) => typeof item === 'number')) {
311         console.warn(`XAttr block sizes "${blockSizes}" is not valid`);
312         return undefined;
313     }
314     return blockSizes as number[];
317 function parseMedia(xattr: MaybeExtendedAttributes): { Width: number; Height: number; Duration?: number } | undefined {
318     const media = xattr?.Media;
319     if (media === undefined || media.Width === undefined || media.Height === undefined) {
320         return undefined;
321     }
322     const width = media.Width;
323     if (typeof width !== 'number') {
324         console.warn(`XAttr media width "${width}" is not valid`);
325         return undefined;
326     }
327     const height = media.Height;
328     if (typeof height !== 'number') {
329         console.warn(`XAttr media height "${height}" is not valid`);
330         return undefined;
331     }
332     const duration = media.Duration;
333     if (duration !== undefined && typeof duration !== 'number') {
334         console.warn(`XAttr media duration "${duration}" is not valid`);
335         return undefined;
336     }
337     return {
338         Width: width,
339         Height: height,
340         Duration: duration,
341     };
344 function parseDigests(xattr: MaybeExtendedAttributes): { SHA1: string } | undefined {
345     const digests = xattr?.Common?.Digests;
346     if (digests === undefined || digests.SHA1 === undefined) {
347         return undefined;
348     }
350     const sha1 = digests.SHA1;
351     if (typeof sha1 !== 'string') {
352         console.warn(`XAttr digest SHA1 "${sha1}" is not valid`);
353         return undefined;
354     }
356     return {
357         SHA1: sha1,
358     };
361 function dateToIsoString(date: Date) {
362     const isDateValid = !Number.isNaN(date.getTime());
363     return isDateValid ? date.toISOString() : undefined;