Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / shared / lib / keys / signedKeyList.ts
blob858004a72e28c0d7cf50d6972017a28620d36373
1 import { CryptoProxy } from '@proton/crypto';
2 import { KT_SKL_SIGNING_CONTEXT } from '@proton/key-transparency/lib';
3 import isTruthy from '@proton/utils/isTruthy';
5 import { getIsAddressDisabled } from '../helpers/address';
6 import type {
7     ActiveAddressKeysByVersion,
8     Address,
9     Api,
10     DecryptedKey,
11     KeyMigrationKTVerifier,
12     KeyTransparencyVerify,
13     SignedKeyList,
14     SignedKeyListItem,
15 } from '../interfaces';
16 import type { SimpleMap } from '../interfaces/utils';
17 import { getActiveAddressKeys, getNormalizedActiveAddressKeys } from './getActiveKeys';
18 import type { PrimaryAddressKeys} from './getPrimaryKey';
19 import { getPrimaryAddressKeysForSigningByVersion } from './getPrimaryKey';
21 export const getSignedKeyListSignature = async (data: string, signingKeys: PrimaryAddressKeys, date?: Date) => {
22     const signature = await CryptoProxy.signMessage({
23         textData: data,
24         stripTrailingSpaces: true,
25         signingKeys,
26         detached: true,
27         context: KT_SKL_SIGNING_CONTEXT,
28         date,
29     });
30     return signature;
33 export type OnSKLPublishSuccess = () => Promise<void>;
35 /**
36  * Generate the signed key list data and verify it for later commit to Key Transparency.
37  * The SKL is only considered in the later commit call if the returned OnSKLPublishSuccess closure
38  * has been called beforehand.
39  */
40 export const getSignedKeyListWithDeferredPublish = async (
41     keys: ActiveAddressKeysByVersion,
42     address: Address,
43     keyTransparencyVerify: KeyTransparencyVerify
44 ): Promise<[SignedKeyList, OnSKLPublishSuccess]> => {
45     // the v6 primary key (if present) must come after the v4 one
46     const list = [...keys.v4, ...keys.v6].sort((a, b) => b.primary - a.primary);
47     const transformedKeys = (
48         await Promise.all(
49             list.map(async ({ privateKey, flags, primary, sha256Fingerprints, fingerprint }) => {
50                 const result = await CryptoProxy.isE2EEForwardingKey({ key: privateKey });
52                 if (result) {
53                     return false;
54                 }
56                 return {
57                     Primary: primary,
58                     Flags: flags,
59                     Fingerprint: fingerprint,
60                     SHA256Fingerprints: sha256Fingerprints,
61                 };
62             })
63         )
64     ).filter(isTruthy);
65     const data = JSON.stringify(transformedKeys);
66     const signingKeys = getPrimaryAddressKeysForSigningByVersion(keys);
67     if (!signingKeys.length) {
68         throw new Error('Missing primary signing key');
69     }
71     // TODO: Could be filtered as well
72     const publicKeys = list.map((key) => key.publicKey);
74     const signedKeyList: SignedKeyList = {
75         Data: data,
76         Signature: await getSignedKeyListSignature(data, signingKeys),
77     };
78     const onSKLPublish = async () => {
79         if (!getIsAddressDisabled(address)) {
80             await keyTransparencyVerify(address, signedKeyList, publicKeys);
81         }
82     };
83     return [signedKeyList, onSKLPublish];
86 /**
87  * Generate the signed key list data and verify it for later commit to Key Transparency
88  */
89 export const getSignedKeyList = async (
90     keys: ActiveAddressKeysByVersion,
91     address: Address,
92     keyTransparencyVerify: KeyTransparencyVerify
93 ): Promise<SignedKeyList> => {
94     const activeKeysWithoutForwarding = {
95         v4: (
96             await Promise.all(
97                 keys.v4.map(async (key) => {
98                     const result = await CryptoProxy.isE2EEForwardingKey({ key: key.privateKey });
99                     return result ? false : key;
100                 })
101             )
102         ).filter(isTruthy),
103         v6: keys.v6, // forwarding not supported by v6 keys
104     };
106     const [signedKeyList, onSKLPublishSuccess] = await getSignedKeyListWithDeferredPublish(
107         activeKeysWithoutForwarding,
108         address,
109         keyTransparencyVerify
110     );
111     await onSKLPublishSuccess();
112     return signedKeyList;
115 export const createSignedKeyListForMigration = async ({
116     address,
117     decryptedKeys,
118     keyMigrationKTVerifier,
119     keyTransparencyVerify,
120     api,
121 }: {
122     api: Api;
123     address: Address;
124     decryptedKeys: DecryptedKey[];
125     keyTransparencyVerify: KeyTransparencyVerify;
126     keyMigrationKTVerifier: KeyMigrationKTVerifier;
127 }): Promise<[SignedKeyList | undefined, OnSKLPublishSuccess | undefined]> => {
128     let signedKeyList: SignedKeyList | undefined;
129     let onSKLPublishSuccess: OnSKLPublishSuccess | undefined;
130     if (!address.SignedKeyList || address.SignedKeyList.ObsolescenceToken) {
131         // Only create a new signed key list if the address does not have one already
132         // or the signed key list is obsolete.
133         await keyMigrationKTVerifier({ email: address.Email, signedKeyList: address.SignedKeyList, api });
134         const activeKeys = getNormalizedActiveAddressKeys(
135             address,
136             await getActiveAddressKeys(address, address.SignedKeyList, address.Keys, decryptedKeys)
137         );
138         if (activeKeys.v4.length > 0) {
139             // v4 keys always presents, no need to check for v6 ones
140             [signedKeyList, onSKLPublishSuccess] = await getSignedKeyListWithDeferredPublish(
141                 activeKeys,
142                 address,
143                 keyTransparencyVerify
144             );
145         }
146     }
147     return [signedKeyList, onSKLPublishSuccess];
150 const signedKeyListItemParser = ({ Primary, Flags, Fingerprint, SHA256Fingerprints }: any) =>
151     (Primary === 0 || Primary === 1) &&
152     typeof Flags === 'number' &&
153     typeof Fingerprint === 'string' &&
154     Array.isArray(SHA256Fingerprints) &&
155     SHA256Fingerprints.every((fingerprint) => typeof fingerprint === 'string');
157 export const getParsedSignedKeyList = (data?: string | null): SignedKeyListItem[] | undefined => {
158     if (!data) {
159         return;
160     }
161     try {
162         const parsedData = JSON.parse(data);
163         if (!Array.isArray(parsedData)) {
164             return;
165         }
166         if (!parsedData.every(signedKeyListItemParser)) {
167             return;
168         }
169         return parsedData;
170     } catch (e: any) {
171         return undefined;
172     }
175 export const getSignedKeyListMap = (signedKeyListData?: SignedKeyListItem[]): SimpleMap<SignedKeyListItem> => {
176     if (!signedKeyListData) {
177         return {};
178     }
179     return signedKeyListData.reduce<SimpleMap<SignedKeyListItem>>((acc, cur) => {
180         acc[cur.Fingerprint] = cur;
181         return acc;
182     }, {});