1 import { c } from 'ttag';
3 import type { PrivateKeyReference, PublicKeyReference, SessionKey } from '@proton/crypto';
4 import { CryptoProxy, VERIFICATION_STATUS, toPublicKeyReference } from '@proton/crypto';
5 import { SIGNATURE_CONTEXT } from '@proton/shared/lib/calendar/crypto/constants';
6 import isTruthy from '@proton/utils/isTruthy';
8 import { KEYGEN_CONFIGS, KEYGEN_TYPES } from '../../../constants';
9 import { uint8ArrayToBase64String } from '../../../helpers/encoding';
10 import type { KeyGenConfig, SimpleMap } from '../../../interfaces';
12 CreateOrResetCalendarPayload,
15 } from '../../../interfaces/calendar';
16 import { getEncryptedSessionKey } from '../encrypt';
18 export const generatePassphrase = () => {
19 const value = crypto.getRandomValues(new Uint8Array(32));
20 return uint8ArrayToBase64String(value);
24 * The calendar key is generated with less user info to not confuse if the key is exported.
26 export const generateCalendarKey = async ({
31 keyGenConfig: KeyGenConfig;
33 const privateKey = await CryptoProxy.generateKey({
34 userIDs: [{ name: 'Calendar key' }],
38 const privateKeyArmored = await CryptoProxy.exportPrivateKey({ privateKey: privateKey, passphrase });
40 return { privateKey, privateKeyArmored };
43 export const signPassphrase = ({ passphrase, privateKey }: { passphrase: string; privateKey: PrivateKeyReference }) => {
44 return CryptoProxy.signMessage({ textData: passphrase, signingKeys: privateKey, detached: true });
47 export const encryptPassphrase = async ({
53 privateKey: PrivateKeyReference;
54 publicKey: PublicKeyReference;
60 const sessionKey = await CryptoProxy.generateSessionKey({ recipientKeys: publicKey });
61 // we encrypt using `sessionKey` directly instead of `encryptionKeys` so that returned message only includes
62 // symmetrically encrypted data
63 const { message: encryptedData, signature: binarySignature } = await CryptoProxy.encryptMessage({
64 textData: passphrase, // stripTrailingSpaces: false
66 signingKeys: privateKey,
71 // encrypt to the public key separately to get a separate serialized session key
72 const encryptedSessionKey = await CryptoProxy.encryptSessionKey({
74 encryptionKeys: publicKey,
79 keyPacket: uint8ArrayToBase64String(encryptedSessionKey),
80 dataPacket: uint8ArrayToBase64String(encryptedData),
81 signature: await CryptoProxy.getArmoredSignature({ binarySignature }),
85 export function encryptPassphraseSessionKey({
90 sessionKey: SessionKey;
91 publicKey: PublicKeyReference;
92 signingKey: PrivateKeyReference;
93 }): Promise<{ encryptedSessionKey: string; armoredSignature: string }>;
94 export function encryptPassphraseSessionKey({
99 sessionKey: SessionKey;
100 memberPublicKeys: SimpleMap<PublicKeyReference>;
101 signingKey: PrivateKeyReference;
102 }): Promise<{ encryptedSessionKeyMap: SimpleMap<string>; armoredSignature: string }>;
103 export function encryptPassphraseSessionKey({
108 sessionKey: SessionKey;
109 memberPublicKeys: SimpleMap<PublicKeyReference>;
110 signingKey: PrivateKeyReference;
111 }): Promise<{ encryptedSessionKey?: string; encryptedSessionKeyMap?: SimpleMap<string>; armoredSignature: string }>;
113 export async function encryptPassphraseSessionKey({
119 sessionKey: SessionKey;
120 publicKey?: PublicKeyReference;
121 memberPublicKeys?: SimpleMap<PublicKeyReference>;
122 signingKey: PrivateKeyReference;
124 const armoredSignaturePromise = CryptoProxy.signMessage({
125 binaryData: sessionKey.data,
126 signingKeys: signingKey,
129 context: { critical: true, value: SIGNATURE_CONTEXT.SHARE_CALENDAR_INVITE },
133 const [armoredSignature, encryptedSessionKey] = await Promise.all([
134 armoredSignaturePromise,
135 uint8ArrayToBase64String(await getEncryptedSessionKey(sessionKey, publicKey)),
142 } else if (memberPublicKeys) {
143 const encryptedSessionKeysPromise = Promise.all(
144 Object.entries(memberPublicKeys).map(async ([id, publicKey]) => {
146 throw new Error('Missing public key for member');
148 const keyPacket = uint8ArrayToBase64String(await getEncryptedSessionKey(sessionKey, publicKey));
150 return [id, keyPacket];
154 const [armoredSignature, encryptedSessionKeys] = await Promise.all([
155 armoredSignaturePromise,
156 encryptedSessionKeysPromise,
160 encryptedSessionKeyMap: Object.fromEntries(encryptedSessionKeys),
165 throw new Error('Missing parameters to encrypt session key');
169 * Decrypts a calendar passphrase with either some private keys or a session key
171 export const decryptPassphrase = async ({
178 armoredPassphrase: string;
179 sessionKey?: SessionKey;
180 armoredSignature?: string;
181 privateKeys?: PrivateKeyReference[];
182 publicKeys?: PublicKeyReference[];
184 const { data: decryptedPassphrase, verified } = await CryptoProxy.decryptMessage({
185 armoredMessage: armoredPassphrase,
187 decryptionKeys: privateKeys,
188 verificationKeys: publicKeys,
189 sessionKeys: sessionKey,
192 if (publicKeys?.length && verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
193 const error = new Error(c('Error').t`Signature verification failed`);
194 error.name = 'SignatureError';
198 return decryptedPassphrase;
202 * Retrieves the decrypted session key that encrypts a calendar passphrase with a private key
204 export const decryptPassphraseSessionKey = ({
208 armoredPassphrase: string;
209 privateKeys: PrivateKeyReference[];
211 return CryptoProxy.decryptSessionKey({
212 armoredMessage: armoredPassphrase,
213 decryptionKeys: privateKeys,
218 * Decrypt the calendar keys.
219 * @param Keys - the calendar keys as coming from the API
220 * @param passphrasesMap - The decrypted passphrases map
222 export const getDecryptedCalendarKeys = async (
224 passphrasesMap: { [key: string]: string | undefined } = {}
225 ): Promise<DecryptedCalendarKey[]> => {
226 const process = async (Key: tsKey) => {
228 const { PrivateKey, PassphraseID } = Key;
229 const passphrase = passphrasesMap[PassphraseID] || '';
230 const privateKey = await CryptoProxy.importPrivateKey({ armoredKey: PrivateKey, passphrase });
231 const publicKey = await toPublicKeyReference(privateKey);
241 return Promise.all(Keys.map(process)).then((result) => {
242 return result.filter(isTruthy);
246 export const generateCalendarKeyPayload = async ({
252 privateKey: PrivateKeyReference;
253 publicKey: PublicKeyReference;
254 }): Promise<CreateOrResetCalendarPayload> => {
255 const passphrase = generatePassphrase();
256 const keyGenConfig = KEYGEN_CONFIGS[KEYGEN_TYPES.CURVE25519];
257 const [{ privateKeyArmored: PrivateKey }, { dataPacket: DataPacket, keyPacket: KeyPacket, signature: Signature }] =
259 generateCalendarKey({ passphrase, keyGenConfig }),
260 encryptPassphrase({ passphrase, privateKey, publicKey }),
264 AddressID: addressID,