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 {
11 ModificationTime?: string;
13 BlockSizes?: number[];
26 SubjectCoordinates?: {
40 export interface ParsedExtendedAttributes {
42 ModificationTime?: number;
44 BlockSizes?: number[];
57 SubjectCoordinates?: {
71 type MaybeExtendedAttributes = DeepPartial<ExtendedAttributes>;
73 export async function encryptFolderExtendedAttributes(
74 modificationTime: Date,
75 nodePrivateKey: PrivateKeyReference,
76 addressPrivateKey: PrivateKeyReference
78 const xattr = createFolderExtendedAttributes(modificationTime);
79 return encryptExtendedAttributes(xattr, nodePrivateKey, addressPrivateKey);
82 export function createFolderExtendedAttributes(modificationTime: Date): ExtendedAttributes {
85 ModificationTime: dateToIsoString(modificationTime),
90 export type XAttrCreateParams = {
105 captureTime?: string;
107 orientation?: number;
108 subjectCoordinates?: {
117 export async function encryptFileExtendedAttributes(
118 params: XAttrCreateParams,
119 nodePrivateKey: PrivateKeyReference,
120 addressPrivateKey: PrivateKeyReference
122 const xattr = createFileExtendedAttributes(params);
123 return encryptExtendedAttributes(xattr, nodePrivateKey, addressPrivateKey);
126 export function createFileExtendedAttributes({
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);
139 ModificationTime: dateToIsoString(new Date(file.lastModified)),
141 BlockSizes: blockSizes,
151 Height: media.height,
152 Duration: media.duration,
157 Latitude: location.latitude,
158 Longitude: location.longitude,
163 CaptureTime: camera.captureTime,
164 Device: camera.device,
165 Orientation: camera.orientation,
166 SubjectCoordinates: camera.subjectCoordinates
168 Top: camera.subjectCoordinates.top,
169 Left: camera.subjectCoordinates.left,
170 Bottom: camera.subjectCoordinates.bottom,
171 Right: camera.subjectCoordinates.right,
180 * Creates and encryptes XAttr for documents.
182 export async function encryptDocumentExtendedAttributes(
183 nodePrivateKey: PrivateKeyReference,
184 addressPrivateKey: PrivateKeyReference
186 const xattr = createDocumentExtendedAttributes();
187 return encryptExtendedAttributes(xattr, nodePrivateKey, addressPrivateKey);
190 export function createDocumentExtendedAttributes() {
196 async function encryptExtendedAttributes(
197 xattr: ExtendedAttributes,
198 nodePrivateKey: PrivateKeyReference,
199 addressPrivateKey: PrivateKeyReference
202 const xattrString = JSON.stringify(xattr);
204 const { message } = await CryptoProxy.encryptMessage({
205 textData: xattrString,
206 encryptionKeys: nodePrivateKey,
207 signingKeys: addressPrivateKey,
213 throw new EnrichedError('Failed to encrypt extended attributes', {
215 addressKeyId: addressPrivateKey.getKeyID(),
224 export async function decryptExtendedAttributes(
225 encryptedXAttr: string,
226 nodePrivateKey: PrivateKeyReference,
227 addressPublicKey: PublicKeyReference | PublicKeyReference[]
228 ): Promise<{ xattrs: ParsedExtendedAttributes; verified: VERIFICATION_STATUS }> {
230 const { data: xattrString, verified } = await decryptSigned({
231 armoredMessage: encryptedXAttr,
232 privateKey: nodePrivateKey,
233 publicKey: addressPublicKey,
237 xattrs: parseExtendedAttributes(xattrString),
241 throw new EnrichedError('Failed to decrypt extended attributes', {
244 addressKeyIds: (Array.isArray(addressPublicKey) ? addressPublicKey : [addressPublicKey]).map((key) =>
252 export function parseExtendedAttributes(xattrString: string): ParsedExtendedAttributes {
253 let xattr: MaybeExtendedAttributes = {};
255 xattr = JSON.parse(xattrString) as MaybeExtendedAttributes;
257 console.warn(`XAttr "${xattrString}" is not valid JSON`);
261 ModificationTime: parseModificationTime(xattr),
262 Size: parseSize(xattr),
263 BlockSizes: parseBlockSizes(xattr),
264 Digests: parseDigests(xattr),
266 Media: parseMedia(xattr),
270 function parseModificationTime(xattr: MaybeExtendedAttributes): number | undefined {
271 const modificationTime = xattr?.Common?.ModificationTime;
272 if (modificationTime === undefined) {
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`);
281 const modificationTimestamp = Math.trunc(modificationDate.getTime() / 1000);
282 if (Number.isNaN(modificationTimestamp)) {
283 console.warn(`XAttr modification time "${modificationTime}" is not valid`);
286 return modificationTimestamp;
289 function parseSize(xattr: MaybeExtendedAttributes): number | undefined {
290 const size = xattr?.Common?.Size;
291 if (size === undefined) {
294 if (typeof size !== 'number') {
295 console.warn(`XAttr file size "${size}" is not valid`);
301 function parseBlockSizes(xattr: MaybeExtendedAttributes): number[] | undefined {
302 const blockSizes = xattr?.Common?.BlockSizes;
303 if (blockSizes === undefined) {
306 if (!Array.isArray(blockSizes)) {
307 console.warn(`XAttr block sizes "${blockSizes}" is not valid`);
310 if (!blockSizes.every((item) => typeof item === 'number')) {
311 console.warn(`XAttr block sizes "${blockSizes}" is not valid`);
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) {
322 const width = media.Width;
323 if (typeof width !== 'number') {
324 console.warn(`XAttr media width "${width}" is not valid`);
327 const height = media.Height;
328 if (typeof height !== 'number') {
329 console.warn(`XAttr media height "${height}" is not valid`);
332 const duration = media.Duration;
333 if (duration !== undefined && typeof duration !== 'number') {
334 console.warn(`XAttr media duration "${duration}" is not valid`);
344 function parseDigests(xattr: MaybeExtendedAttributes): { SHA1: string } | undefined {
345 const digests = xattr?.Common?.Digests;
346 if (digests === undefined || digests.SHA1 === undefined) {
350 const sha1 = digests.SHA1;
351 if (typeof sha1 !== 'string') {
352 console.warn(`XAttr digest SHA1 "${sha1}" is not valid`);
361 function dateToIsoString(date: Date) {
362 const isDateValid = !Number.isNaN(date.getTime());
363 return isDateValid ? date.toISOString() : undefined;