Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / shared / lib / calendar / crypto / decrypt.ts
blob5b004c21e8df991fef30b5a91a6ed66cbfb5d8df
1 import type { PrivateKeyReference, PublicKeyReference, SessionKey } from '@proton/crypto';
2 import { CryptoProxy, VERIFICATION_STATUS } from '@proton/crypto';
3 import { stringToUtf8Array, utf8ArrayToString } from '@proton/crypto/lib/utils';
4 import { captureMessage } from '@proton/shared/lib/helpers/sentry';
6 import { base64StringToUint8Array } from '../../helpers/encoding';
7 import type { CalendarEventData } from '../../interfaces/calendar';
8 import type { SimpleMap } from '../../interfaces/utils';
9 import { CALENDAR_CARD_TYPE, EVENT_VERIFICATION_STATUS } from '../constants';
11 export const getEventVerificationStatus = (status: VERIFICATION_STATUS | undefined, hasPublicKeys: boolean) => {
12     if (!hasPublicKeys || status === undefined) {
13         return EVENT_VERIFICATION_STATUS.NOT_VERIFIED;
14     }
15     return status === VERIFICATION_STATUS.SIGNED_AND_VALID
16         ? EVENT_VERIFICATION_STATUS.SUCCESSFUL
17         : EVENT_VERIFICATION_STATUS.FAILED;
20 /**
21  * Given an array with signature verification status values, which correspond to verifying different parts of a component,
22  * return an aggregated signature verification status for the component.
23  */
24 export const getAggregatedEventVerificationStatus = (arr: (EVENT_VERIFICATION_STATUS | undefined)[]) => {
25     if (!arr.length) {
26         return EVENT_VERIFICATION_STATUS.NOT_VERIFIED;
27     }
28     if (arr.some((verification) => verification === EVENT_VERIFICATION_STATUS.FAILED)) {
29         return EVENT_VERIFICATION_STATUS.FAILED;
30     }
31     if (arr.every((verification) => verification === EVENT_VERIFICATION_STATUS.SUCCESSFUL)) {
32         return EVENT_VERIFICATION_STATUS.SUCCESSFUL;
33     }
34     return EVENT_VERIFICATION_STATUS.NOT_VERIFIED;
37 export const getDecryptedSessionKey = async (
38     data: Uint8Array,
39     privateKeys: PrivateKeyReference | PrivateKeyReference[]
40 ) => {
41     return CryptoProxy.decryptSessionKey({ binaryMessage: data, decryptionKeys: privateKeys });
44 export const getNeedsLegacyVerification = (verifiedBinary: VERIFICATION_STATUS, textData: string) => {
45     if (verifiedBinary !== VERIFICATION_STATUS.SIGNED_AND_INVALID) {
46         return false;
47     }
48     if (/ \r\n/.test(textData)) {
49         // if there are trailing spaces using the RFC-compliant line separator, those got stripped by clients signing
50         // as-text with the stripTrailingSpaces option enabled. We need legacy verification.
51         return true;
52     }
53     const textDataWithoutCRLF = textData.replaceAll(`\r\n`, '');
55     // if there are "\n" end-of-lines we need legacy verification as those got normalized by clients signing as-text
56     return /\n/.test(textDataWithoutCRLF);
59 const getVerifiedLegacy = async ({
60     textData,
61     signature,
62     publicKeys,
63 }: {
64     textData: string;
65     signature: string;
66     publicKeys: PublicKeyReference | PublicKeyReference[];
67 }) => {
68     /**
69      * Verification of an ical card may have failed because the signature is a legacy one,
70      * done as text and therefore using OpenPGP normalization (\n -> \r\n) + stripping trailing spaces.
71      *
72      * We try to verify the signature in the legacy way and log the fact in Sentry
73      */
74     captureMessage('Fallback to legacy signature verification of calendar event', { level: 'info' });
75     const { verified: verifiedLegacy } = await CryptoProxy.verifyMessage({
76         textData,
77         stripTrailingSpaces: true,
78         verificationKeys: publicKeys,
79         armoredSignature: signature,
80     });
82     return verifiedLegacy;
85 export const verifySignedCard = async (
86     dataToVerify: string,
87     signature: string,
88     publicKeys: PublicKeyReference | PublicKeyReference[]
89 ) => {
90     const { verified: verifiedBinary } = await CryptoProxy.verifyMessage({
91         binaryData: stringToUtf8Array(dataToVerify), // not 'utf8' to avoid issues with trailing spaces and automatic normalisation of EOLs to \n
92         verificationKeys: publicKeys,
93         armoredSignature: signature,
94     });
95     const verified = getNeedsLegacyVerification(verifiedBinary, dataToVerify)
96         ? await getVerifiedLegacy({ textData: dataToVerify, signature, publicKeys })
97         : verifiedBinary;
98     const hasPublicKeys = Array.isArray(publicKeys) ? !!publicKeys.length : !!publicKeys;
99     const verificationStatus = getEventVerificationStatus(verified, hasPublicKeys);
101     return { data: dataToVerify, verificationStatus };
104 export const decryptCard = async (
105     dataToDecrypt: Uint8Array,
106     signature: string | null,
107     publicKeys: PublicKeyReference | PublicKeyReference[],
108     sessionKey: SessionKey
109 ) => {
110     const { data: decryptedData, verified: verifiedBinary } = await CryptoProxy.decryptMessage({
111         binaryMessage: dataToDecrypt,
112         format: 'binary', // even though we convert to utf8 later, we can't use 'utf8' here as that would entail automatic normalisation of EOLs to \n
113         verificationKeys: publicKeys,
114         armoredSignature: signature || undefined,
115         sessionKeys: [sessionKey],
116     });
117     const decryptedText = utf8ArrayToString(decryptedData);
118     const verified =
119         signature && getNeedsLegacyVerification(verifiedBinary, decryptedText)
120             ? await getVerifiedLegacy({ textData: decryptedText, signature, publicKeys })
121             : verifiedBinary;
122     const hasPublicKeys = Array.isArray(publicKeys) ? !!publicKeys.length : !!publicKeys;
123     const verificationStatus = getEventVerificationStatus(verified, hasPublicKeys);
125     return { data: utf8ArrayToString(decryptedData), verificationStatus };
128 export const decryptAndVerifyCalendarEvent = (
129     { Type, Data, Signature, Author }: CalendarEventData,
130     publicKeysMap: SimpleMap<PublicKeyReference | PublicKeyReference[]>,
131     sessionKey: SessionKey | undefined
132 ): Promise<{ data: string; verificationStatus: EVENT_VERIFICATION_STATUS }> => {
133     const publicKeys = publicKeysMap[Author] || [];
134     if (Type === CALENDAR_CARD_TYPE.CLEAR_TEXT) {
135         return Promise.resolve({ data: Data, verificationStatus: EVENT_VERIFICATION_STATUS.NOT_VERIFIED });
136     }
137     if (Type === CALENDAR_CARD_TYPE.ENCRYPTED) {
138         if (!sessionKey) {
139             throw new Error('Cannot decrypt without session key');
140         }
141         return decryptCard(base64StringToUint8Array(Data), Signature, [], sessionKey);
142     }
143     if (Type === CALENDAR_CARD_TYPE.SIGNED) {
144         if (!Signature) {
145             throw new Error('Signed card is missing signature');
146         }
147         return verifySignedCard(Data, Signature, publicKeys);
148     }
149     if (Type === CALENDAR_CARD_TYPE.ENCRYPTED_AND_SIGNED) {
150         if (!Signature) {
151             throw new Error('Encrypted and signed card is missing signature');
152         }
153         if (!sessionKey) {
154             throw new Error('Cannot decrypt without session key');
155         }
157         return decryptCard(base64StringToUint8Array(Data), Signature, publicKeys, sessionKey);
158     }
159     throw new Error('Unknow event card type');