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;
15 return status === VERIFICATION_STATUS.SIGNED_AND_VALID
16 ? EVENT_VERIFICATION_STATUS.SUCCESSFUL
17 : EVENT_VERIFICATION_STATUS.FAILED;
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.
24 export const getAggregatedEventVerificationStatus = (arr: (EVENT_VERIFICATION_STATUS | undefined)[]) => {
26 return EVENT_VERIFICATION_STATUS.NOT_VERIFIED;
28 if (arr.some((verification) => verification === EVENT_VERIFICATION_STATUS.FAILED)) {
29 return EVENT_VERIFICATION_STATUS.FAILED;
31 if (arr.every((verification) => verification === EVENT_VERIFICATION_STATUS.SUCCESSFUL)) {
32 return EVENT_VERIFICATION_STATUS.SUCCESSFUL;
34 return EVENT_VERIFICATION_STATUS.NOT_VERIFIED;
37 export const getDecryptedSessionKey = async (
39 privateKeys: PrivateKeyReference | PrivateKeyReference[]
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) {
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.
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 ({
66 publicKeys: PublicKeyReference | PublicKeyReference[];
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.
72 * We try to verify the signature in the legacy way and log the fact in Sentry
74 captureMessage('Fallback to legacy signature verification of calendar event', { level: 'info' });
75 const { verified: verifiedLegacy } = await CryptoProxy.verifyMessage({
77 stripTrailingSpaces: true,
78 verificationKeys: publicKeys,
79 armoredSignature: signature,
82 return verifiedLegacy;
85 export const verifySignedCard = async (
88 publicKeys: PublicKeyReference | PublicKeyReference[]
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,
95 const verified = getNeedsLegacyVerification(verifiedBinary, dataToVerify)
96 ? await getVerifiedLegacy({ textData: dataToVerify, signature, publicKeys })
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
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],
117 const decryptedText = utf8ArrayToString(decryptedData);
119 signature && getNeedsLegacyVerification(verifiedBinary, decryptedText)
120 ? await getVerifiedLegacy({ textData: decryptedText, signature, publicKeys })
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 });
137 if (Type === CALENDAR_CARD_TYPE.ENCRYPTED) {
139 throw new Error('Cannot decrypt without session key');
141 return decryptCard(base64StringToUint8Array(Data), Signature, [], sessionKey);
143 if (Type === CALENDAR_CARD_TYPE.SIGNED) {
145 throw new Error('Signed card is missing signature');
147 return verifySignedCard(Data, Signature, publicKeys);
149 if (Type === CALENDAR_CARD_TYPE.ENCRYPTED_AND_SIGNED) {
151 throw new Error('Encrypted and signed card is missing signature');
154 throw new Error('Cannot decrypt without session key');
157 return decryptCard(base64StringToUint8Array(Data), Signature, publicKeys, sessionKey);
159 throw new Error('Unknow event card type');