Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/ProtonMail/WebClients.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlarabr <7375870+larabr@users.noreply.github.com>2022-08-19 15:49:24 +0300
committerEduardo Conde Pena <econdepe@proton.ch>2022-11-04 17:22:04 +0300
commita9d148d6a877bff6a87afc97b0a5780e9b4795a4 (patch)
tree83037b59f5512a8da3a8b890b515009f4dcdcaec
parent42102fab656be0f170b194083538b070105779db (diff)
Migrate messages page by page and use constant-time decryption
-rw-r--r--applications/mail/src/app/containers/LegacyMessagesMigrationContainer.tsx22
-rw-r--r--packages/shared/lib/mail/legacyMessagesMigration/helpers.ts189
-rw-r--r--packages/shared/test/mail/legacyMigration.data.ts85
-rw-r--r--packages/shared/test/mail/legacyMigration.spec.ts113
4 files changed, 314 insertions, 95 deletions
diff --git a/applications/mail/src/app/containers/LegacyMessagesMigrationContainer.tsx b/applications/mail/src/app/containers/LegacyMessagesMigrationContainer.tsx
index 6e942b8de1..baff341b6e 100644
--- a/applications/mail/src/app/containers/LegacyMessagesMigrationContainer.tsx
+++ b/applications/mail/src/app/containers/LegacyMessagesMigrationContainer.tsx
@@ -1,7 +1,9 @@
-import { useApi, useGetAddressKeys } from '@proton/components';
-import { migrateAll } from '@proton/shared/lib/mail/legacyMessagesMigration/helpers';
import { useEffect } from 'react';
+import { useApi, useGetAddressKeys } from '@proton/components';
+import { makeLegacyMessageIDsFetcher, migrateMultiple } from '@proton/shared/lib/mail/legacyMessagesMigration/helpers';
+import noop from '@proton/utils/noop';
+
const LegacyMessagesMigrationContainer = () => {
const api = useApi();
const getAddressKeys = useGetAddressKeys();
@@ -9,9 +11,21 @@ const LegacyMessagesMigrationContainer = () => {
useEffect(() => {
const abortController = new AbortController();
const { signal } = abortController;
- const apiWithAbort: <T>(config: object) => Promise<T> = (config) => api({ ...config, signal });
+ // Since the migration happens in the background with the user unaware of it,
+ // we silence API to avoid displaying possible errors in the UI
+ const apiWithAbort: <T>(config: object) => Promise<T> = (config) => api({ ...config, signal, silence: true });
- migrateAll({ api: apiWithAbort, getAddressKeys });
+ const run = async () => {
+ const it = makeLegacyMessageIDsFetcher(apiWithAbort);
+
+ let result = await it.next();
+ while (!result.done) {
+ const messageIDs = result.value.map(({ ID }) => ID);
+ await migrateMultiple({ messageIDs, api: apiWithAbort, getAddressKeys }).catch(noop);
+ result = await it.next();
+ }
+ };
+ void run();
return () => {
abortController.abort();
diff --git a/packages/shared/lib/mail/legacyMessagesMigration/helpers.ts b/packages/shared/lib/mail/legacyMessagesMigration/helpers.ts
index d9c017565c..44644b6bdf 100644
--- a/packages/shared/lib/mail/legacyMessagesMigration/helpers.ts
+++ b/packages/shared/lib/mail/legacyMessagesMigration/helpers.ts
@@ -1,6 +1,6 @@
import { fromUnixTime } from 'date-fns';
-import { decryptMessageLegacy, encryptMessage } from 'pmcrypto';
+import { CryptoProxy, enums } from '@proton/crypto';
import chunk from '@proton/utils/chunk';
import { getMessage, markAsBroken, queryMessageMetadata, updateBody } from '../../api/messages';
@@ -8,12 +8,7 @@ import { API_CODES, MINUTE, SECOND } from '../../constants';
import { wait } from '../../helpers/promise';
import { Api, SimpleMap } from '../../interfaces';
import { GetAddressKeys } from '../../interfaces/hooks/GetAddressKeys';
-import {
- GetMessageResponse,
- MarkAsBrokenResponse,
- Message,
- QueryMessageMetadataResponse,
-} from '../../interfaces/mail/Message';
+import { GetMessageResponse, MarkAsBrokenResponse, QueryMessageMetadataResponse } from '../../interfaces/mail/Message';
import { getPrimaryKey, splitKeys } from '../../keys';
const LABEL_LEGACY_MESSAGE = '11';
@@ -22,6 +17,19 @@ const LEGACY_MESSAGES_CHUNK_SIZE = 5; // How many messages we want to decrypt an
const RELAX_TIME = 5 * SECOND; // 5s . Time to wait (for other operations) after a batch of legacy messages has been migrated
const MAX_RETRIES = 20; // Maximum number of retries allowed for the migration to restart after an unexpected error
+// For constant-time decryption we need to specify which ciphers might have been used to encrypt the legacy message.
+// It is likely that only AES was used, but we are not 100% certain, so we go all the way.
+const SUPPORTED_CIPHERS: Set<enums.symmetric> = new Set([
+ // idea = 1,
+ 2, // tripledes
+ 3, // cast5
+ // blowfish = 4,
+ 7, // aes128
+ 8, // aes192
+ 9, // aes256
+ // twofish = 10,
+]);
+
enum MIGRATION_STATUS {
NONE,
SUCCESS,
@@ -30,9 +38,10 @@ enum MIGRATION_STATUS {
}
/**
- * Given a list of legacy message IDs, fetch, decrypt, re-encrypt and send them to API
+ * Given a list of legacy message IDs, fetch, decrypt, re-encrypt and send them to API.
+ * If decryption fails, mark the message as broken.
*/
-const migrateSingle = async ({
+export const migrateSingle = async ({
id,
api,
getAddressKeys,
@@ -53,117 +62,85 @@ const migrateSingle = async ({
const addressKeys = await getAddressKeys(AddressID);
const { privateKeys } = splitKeys(addressKeys);
const { publicKey: primaryPublicKey } = getPrimaryKey(addressKeys) || {};
+
if (!primaryPublicKey) {
throw new Error('Failed to decrypt primary address key');
}
- // Decrypt message
- let newBody = '';
+ let decryptedBody: string;
try {
- let decryptionError: Error | undefined;
- // decryptMessageLegacy is not constant-time yet, force it by hand
- const [{ data: decryptedMessage }] = await Promise.all([
- decryptMessageLegacy({
- message: Body,
- messageDate: fromUnixTime(Time),
- privateKeys,
- }).catch((error: any) => {
- decryptionError = error instanceof Error ? error : new Error('Decryption failed');
- return { data: '' };
- }),
- wait(SECOND),
- ]);
- if (decryptionError) {
- throw decryptionError;
- }
- // Re-encrypt message body. Use the primary key (first in the array) for re-encryption
- // We do not sign the message. Any original signature is lost
- const { data } = await encryptMessage({
- data: decryptedMessage,
- publicKeys: primaryPublicKey,
+ const decryptionResult = await CryptoProxy.decryptMessageLegacy({
+ armoredMessage: Body,
+ messageDate: fromUnixTime(Time),
+ decryptionKeys: privateKeys,
+ config: {
+ constantTimePKCS1Decryption: true,
+ constantTimePKCS1DecryptionSupportedSymmetricAlgorithms: SUPPORTED_CIPHERS,
+ },
});
- newBody = data;
+
+ // Simple sanity check to prevent migrating standard messages.
+ // We cannot simply look at the armoring headers before decryption because the backend has marked messages as "legacy"
+ // as long as they failed to parse, and we need to report them as "broken" if we fail to decrypt them
+ if (decryptionResult.signatures.length > 0) {
+ throw new Error('Legacy message expected');
+ }
+
+ decryptedBody = decryptionResult.data;
} catch {
// mark as broken
const { Code, Error: error } = await api<MarkAsBrokenResponse>(markAsBroken(id));
if (error || Code !== API_CODES.SINGLE_SUCCESS) {
throw new Error('Failed to mark message as broken');
}
+ // nothing more to do
+ return;
}
+ // Re-encrypt message body. Use the primary key (first in the array) for re-encryption
+ // We cannot sign the message, since we cannot determine its authenticity. Any original signature is lost.
+ const { message: newEncryptedBody } = await CryptoProxy.encryptMessage({
+ textData: decryptedBody,
+ encryptionKeys: primaryPublicKey,
+ });
+
// Send re-encrypted message to API
- void (await api({
- ...updateBody(ID, { Body: newBody }),
+ await api({
+ ...updateBody(ID, { Body: newEncryptedBody }),
// lowest priority
headers: { Priority: 'u=7' },
- }));
+ });
} catch {
statusMap[id] = MIGRATION_STATUS.ERROR;
}
};
/**
- * Query all legacy messages
- */
-const queryAllLegacyMessages = async (api: Api) => {
- const result: Message[] = [];
- let page = 0;
-
- while (true) {
- const { Messages = [] } = await api<QueryMessageMetadataResponse>({
- ...queryMessageMetadata({
- LabelID: [LABEL_LEGACY_MESSAGE],
- Page: page,
- PageSize: QUERY_LEGACY_MESSAGES_MAX_PAGESIZE,
- }),
- // lowest priority
- headers: { Priority: 'u=7' },
- });
- if (!Messages.length) {
- break;
- }
- result.push(...Messages);
- page++;
- }
-
- return result;
-};
-
-/**
- * Fetch legacy messages, re-encrypt and send them to API
+ * Re-encrypt the given legacy messages in batches, and send them to API
*/
-export const migrateAll = async ({
+export const migrateMultiple = async ({
+ messageIDs: originalMessageIDs,
api,
getAddressKeys,
- retryNumber = 0,
- messageIDs,
}: {
+ messageIDs: string[];
api: Api;
getAddressKeys: GetAddressKeys;
- retryNumber?: number;
- messageIDs?: string[];
}): Promise<void> => {
- if (retryNumber > MAX_RETRIES) {
- // end the process
- return;
- }
- try {
- // fetch all legacy messages if no messageIDs were passed
- const ids = messageIDs || (await queryAllLegacyMessages(api)).map(({ ID }) => ID);
- if (!ids.length) {
- return;
- }
- const statusMap = ids.reduce<SimpleMap<MIGRATION_STATUS>>((acc, id) => {
+ let messageIDs = [...originalMessageIDs];
+
+ for (let retryNumber = 0; retryNumber < MAX_RETRIES; retryNumber++) {
+ const statusMap = messageIDs.reduce<SimpleMap<MIGRATION_STATUS>>((acc, id) => {
acc[id] = MIGRATION_STATUS.NONE;
return acc;
}, {});
// proceed to migrate in batches of messages, waiting some time in between each batch,
// we are not in a hurry and we don't want to burn the user's machine decrypting and re-encrypting
- const batches = chunk(Object.keys(statusMap), LEGACY_MESSAGES_CHUNK_SIZE);
+ const batches = chunk(messageIDs, LEGACY_MESSAGES_CHUNK_SIZE);
for (const batch of batches) {
- void (await Promise.all(
+ await Promise.all(
batch.map((id) =>
migrateSingle({
id,
@@ -172,18 +149,48 @@ export const migrateAll = async ({
statusMap,
})
)
- ));
+ );
await wait(RELAX_TIME);
}
- return await migrateAll({
- api,
- getAddressKeys,
- retryNumber: retryNumber + 1,
- messageIDs: Object.keys(statusMap).filter((id) => statusMap[id] === MIGRATION_STATUS.ERROR),
- });
- } catch {
+ messageIDs = messageIDs.filter((id) => statusMap[id] === MIGRATION_STATUS.ERROR);
+
+ // no more messages to migrate
+ if (!messageIDs.length) {
+ return;
+ }
+
+ // if some messages failed to be migrated (most likely due to API reachability issues),
+ // we wait before retrying
await wait(MINUTE);
- return migrateAll({ api, getAddressKeys, retryNumber: retryNumber + 1 });
}
};
+
+/**
+ * Fetch legacy message IDs from the API. This is only meant to be used as part of the migration.
+ * @returns iterator-like object fetching a new page of results at every `next()` call.
+ */
+export const makeLegacyMessageIDsFetcher = (api: Api, pageSize = QUERY_LEGACY_MESSAGES_MAX_PAGESIZE) => {
+ const pageIterator = {
+ async next() {
+ const { Messages = [] } = await api<QueryMessageMetadataResponse>({
+ ...queryMessageMetadata({
+ LabelID: [LABEL_LEGACY_MESSAGE],
+ // we always fetch page 0 because we assume the returned message IDs are migrated
+ // between one `next()` call and the following one.
+ Page: 0,
+ PageSize: pageSize,
+ }),
+ // lowest priority
+ headers: { Priority: 'u=7' },
+ });
+
+ if (Messages.length > 0) {
+ return { value: Messages, done: false };
+ }
+
+ return { value: [], done: true };
+ },
+ };
+ return pageIterator;
+};
diff --git a/packages/shared/test/mail/legacyMigration.data.ts b/packages/shared/test/mail/legacyMigration.data.ts
new file mode 100644
index 0000000000..81f7c96f61
--- /dev/null
+++ b/packages/shared/test/mail/legacyMigration.data.ts
@@ -0,0 +1,85 @@
+export const testPrivateKeyLegacy = `-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: OpenPGP.js v0.9.0
+Comment: http://openpgpjs.org
+
+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
+PPekG4ttm6z3/BqqVplanIRSXlsqyp6J8A==
+=Pyb1
+-----END PGP PRIVATE KEY BLOCK-----
+`;
+
+export const testMessageEncryptedLegacy = `---BEGIN ENCRYPTED MESSAGE---esK5w7TCgVnDj8KQHBvDvhJObcOvw6/Cv2/CjMOpw5UES8KQwq/CiMOpI3MrexLDimzDmsKqVmwQw7vDkcKlRgXCosOpwoJgV8KEBCslSGbDtsOlw5gow7NxG8OSw6JNPlYuwrHCg8K5w6vDi8Kww5V5wo/Dl8KgwpnCi8Kww7nChMKdw5FHwoxmCGbCm8O6wpDDmRVEWsO7wqnCtVnDlMKORDbDnjbCqcOnNMKEwoPClFlaw6k1w5TDpcOGJsOUw5Unw5fCrcK3XnLCoRBBwo/DpsKAJiTDrUHDuGEQXz/DjMOhTCN7esO5ZjVIQSoFZMOyF8Kgw6nChcKmw6fCtcOBcW7Ck8KJwpTDnCzCnz3DjFY7wp5jUsOhw7XDosKQNsOUBmLDksKzPcO4fE/Dmw1GecKew4/CmcOJTFXDsB5uMcOFd1vDmX9ow4bDpCPDoU3Drw8oScKOXznDisKfYF3DvMKoEy0DDmzDhlHDjwIyC8OzRS/CnEZ4woM9w5cnw51fw6MZMAzDk8O3CDXDoyHDvzlFwqDCg8KsTnAiaMOsIyfCmUEaw6nChMK5TMOxG8KEHUNIwo1seMOXw5HDhyVawrzCr8KmFWHDpMO3asKpwrQbbMOlwoMew4t1Jz51wp9Jw6kGWcOzc8KgwpLCpsOHOMOgYB3DiMOxLcOQB8K7AcOyWF3CmnwfK8Kxw6XDm2TCiT/CnVTCg8Omw7Ngwp3CuUAHw6/CjRLDgcKsU8O/w6gXJ0cIw6pZMcOxEWETwpd4w58Mwr5SBMKORQjCi3FYcULDgx09w5M7SH7DrMKrw4gnXMKjwqUrBMOLwqQyF0nDhcKuwqTDqsO2w7LCnGjCvkbDgDgcw54xAkEiQMKUFlzDkMOew73CmkU4wrnCjw3DvsKaW8K0InA+w4sPSXfDuhbClMKgUcKeCMORw5ZYJcKnNEzDoMOhw7MYCX4DwqIQwoHCvsOaB1UAI8KVw6LCvcOTw53CuSgow4kZdHw5aRkYw7ZyV8OsP0LCh8KnwpIuw4p1NisoEcKcwrjDhcOtMzdvw5rDmsK3IAdAw7M4J8K+w6zCmR3CuMKUw4lqw6osPMObw53Dg8K3wqLCrsKZwr8mPcK4w4QWw5LCnwZeH1bDgwwiXcKbUhHDk1DDk0MLwoDDqMKXw5skNsKAAcOFw77Di8KNGCBzP8OcwrI5wodQQwQyw5V0wrInwrPDt8O+T8KbNsKVw7Mzw7HCsMOjwpcewoPCuMOUEsOow6QZVDjDpgbDlMOBGDXCtMOmw6jDuMKfw4nDlWTDq8Kqd0TDvwPCpSzDlA4JO3EHwrlBWcK5w7DCscOwCMK2wpsvwrYNIcOgBBXChMK0w6nCosKWEVd+w7cEal5hIcO4SWrCu0TDrW5Yw4XCmBgCwpc7YVwIwqPCi8OlGDzDmyJ/woHCscOtw4zDuC7CpUXCrDAJwp7Cj8KxPX3CrhDCvVB2w7PCosKbw7F+V11hY8Omwq1eQcO8w4wcRMKBJ2LDgW/DomXDhwkgAlxmQcKew6HDq8Ouw6ASeG/DlcKgUcKmLMOowpQWNcKJJcKDa3XDksK/woHCo3d6wrHDpMOqwqs/UUXCjUpnwrHCmsOyJx4bwoHChAnDi0TCpjLDrBvCvEghw5VtfhPCk8K5KsKIw75FCsOyDsKtV17CicOjwqAnF8OHHC0qMsOEwrgEwr13c8KZw4fDn8KXw73CksKAw4QTGRgIG8KMMXwpwrRBT2DDq8K3AsOQXl/DqMKYMivClsKiXcOhGkvDmsK9w77Cmmpvwrhsd8Kaw7bDgQ/DuCU2CyTDtjnCgn/DiMOtSyPDnsOfVTstccO6EVXDrj03MUHDvDDCgsO7BFQFEX3DszIyw7Rsw7pNwpjCs8OCLR9UbsOlw5USw73DiWJqVXTCl2tFw7FaAcKaw7l5a3Mvw5TCpMKCwpbDi3fCi8KHwrfDugUZwo5hw7fChsKDw5ZhPjA7w7HDjcO9wrrCjUbDoy4JXA1JICRDw49UNsOYOsK9FGE5wqhAw67DumnDqW0cwqbCu8OedEbDqcOfw50MVH8twpVLH8O3LsKvacKJw75xTMKkOcOJw4/DvsOYwqRwZcOnwqfCm2XCnRJFwqEgX8KLPsKfwpQWw6nChm82w6hME10KTRhGw5LCj1stPiXClsO8w7rCocOLw6lFw7tAZ8K0O3wswpZ4wqvCmMOFwpzDhMKVRRQjw53CikECPMOKZcOOwoAKcMK7WMO3K8Okw4bCjgrCisKLRsKewqzDvmtnw584wrtiw6RFVsKPecOpIhx7TsKzw4TCisKyw6nCqcK+w6fChsKxw5kWSsOgfD7CkRfCncKGKMOubsKoBA9Fe2YHwrx4aQNSG8Kpw5zDrMO1FMOPZcKSIVnDrHxOBsKyBcKmYwQMOl7CiRvCnDNVw7NaesOoPR3CrnQEwr9Xw600BSFYECnDgi1OFS7DoFYJw4M6wrzCog09WFPCmiHDogjDpQFjdsKKIsOWFsKXd0TDjXU3CsONRX3DssOrw4HDmX0Mw7rDiENvwpPCghsXacK2w6XCkMOICcKVw4nCkMO8RcOUw4zCn1VJw752RAUawqhdw5dEwqbDh0wAMH/DlTrChC/DosOoGsOPw5nClTcyw5XDlsKhNsKAcBINwpxUAi8Rw5Jvwpckwq4uBy0nw51dP2UGbidATX1FLMKFw5zDsQxewp3DlMKwwo3CrhBPJGR7cVHCnTUnwrDDksO0AcO5T3jCm245OnUVUT8WD1HDhTnCqnbCt8OjMDvCsAzCjsKSwoDDlDhtw7cFwpsDaS7CvVLDu0zDnlvDlMOEwrnCgVzCgcOZN8Oxwp0LSMKswq/DrMK9fcKTL1zDgcOvwofCtWAoL0IKR8OWwqpPw6QfVsKcwqxTXGEPKCFydX4Mw5jDmcOEWlPCgMKDPcOJw7HDgcOMahzCjMO7HyPDo8K3Y8OswqPDgSQ+w6wfw67Cr8O/w61oMsO+woTDrnECI2TDuMK5wrzDusOHw5/CosKFwrciQF3Csj5aw7DDpMKwZMK3Z8KlRBIcLcKvM2/CtBk8JMKWwqVyw6RNwoUhwoDCsXbCrD04wpQ4F8KOcMKIw7PDtMKqZRTCjsKSOMOKCMKYQ8OhwqZ1dGrChcKXLSnDiT7CrEjCihckNcOXw63CkUYpT8KTwq7CgMKiw7PCqmBzwq/Crz50XcKEGlLCrUBjw6ASVsObD8K9wpZ6eBHCi2FTMVcDSzvDgwtxw5ZJHlF5woDDtsKTwovChMOyYMKOSCt7w7hGDDsFaMOewrrCjRbDrGPDg2rCpsO3wo8IEMO9wqjCrG0mRXHDocKJwqQYdsKOw7UUwqIUwq/CqUlKW8ObwpcZGizCpgd4dAZBXMOYw5s5w6HDvkEgw6sbRxAwwoBSOyXCjDPDpsKlwrPCrl/DqsOswoJJDWzDp8Ocw5nDrE5FWm3DncKVwpnCqMKiwoDDmMONQcOEwpwRwonCsh0Tw7FCw6Nfw7U7wp7DnMKnfMOHCMOnw4TClcOVwrzCiiddUj3CmsOgwqvDhxfDjsOMWcKDZnvDocObw77Do1rDgMKHVsKCLcOXRMOHD0RNwpEdwozCrBnDqBYWwojCiVzCjTTCqcO5wqgAwqhhw7tnw5ZuOcOYNGTDiR1GAEzDuE0PeErDnlQlfsOjw6UGWUUNw6TCmgx8NMKzDMKgL8O3esKDwprDoTl8wrbDvVDCvU4Iw5sAwr/DugcoR8KMw4hNeMKSw7Jmw4rDjG8NbcO8w7jCs8OvfFXCoBBNfcOqNsK0EQLCncKPw53DrsOiwolvwqjCr8OZDsORw47DiyA+VcOMSg5wworDgGx0w7sgKMOyDMOyZRkgw43CqUHDicKfwpDCo8OII8KvKsOxDcKoFsOaw7HCgXTDssK7B8KIwoNcw4zCu8KBw4vCvFjDkWLDl8OyB8O/w4oYw5DCslzDk2kDw7jDgcOJw4jComXDkwdfw61xw53Cv8KPf11iwq0kKsKDw7nCmiVNF0NqLMKvwqvDjhQ3ZXbDomvDs8OKQQ7CocOnwr1Fw7xZRMK6w41cw5DDgzzCthIoAMOBQcOPbcOPVx/Cm8OYw7pHwo/CvCxhCcKVw7vChShnw6rClUQ7w6dbZMOrw4hpw7lZXMOxw5pnUXHDiMOLDxrDiA/DtMKqw6zDjXRJwp07BsKEwoTClBHCritDYXgzT3RWDcOlw4lfw4Vbw7fCj8K0w4AnwqjCrxPDpCVXF8KbY8OMPwQvwqdaw6E8w4AHPcKbNGl8wpQMX2PDp0pJfcOyGsOUXkNww5jCg8Obwo7DryjCisKeYiQ/XUzDvRvDncOtCMKJwqxHw6LDh8KwwrV7LGPCkcKOIXbCv8KHwpnDi1keQkLDssOSw7XCk8K+w7YdSMKAQmbDo8KPw7xywpnCsgANNTJYScKkNAvDo8KZw6Ayw6tmC8KaTsKEbcOZTx3DilrDtUjDi8OWV8K/wrocwpNKLlYbbcOmPcKPwrvCsTpLey5Xw58XJBPCo8KEPWJrwqZJX1fCncKDw4AZw4hWw5pTw7pidlzDtMO6w7t9DcK+R8KefMOfETvCskgjOgHCqcK7UgHCgsOfwrt8bcKQw5FeZcOiw4Faw7hRTjDDocOuEMOoEm04NQTCrCjDvMOaNDV6V8OHc8OTdMOndCh7HMOqw7HDnlzCl3MqwpjDiiDDtcKmCknCuBcQwobDvcOUN2LDmsOeHMOmPMKeH0nCt0nDgsO8w73CkRDDmMOuacO9w5J1KsKswqY7UMKyHHzDjMOjw5QOSWUhw4jCpMKJw4DCtcKNdcKPLcOFJsOqQ14=---END ENCRYPTED MESSAGE---||---BEGIN ENCRYPTED RANDOM KEY--------BEGIN PGP MESSAGE-----
+Version: OpenPGP.js v0.9.0
+Comment: http://openpgpjs.org
+
+wcBMA2tjJVxNCRhtAQf/YzkQoUqaqa3NJ/c1apIF/dsl7yJ4GdVrC3/w7lxE
+2CO5ioQD4s6QMWP2Y9dOdVl2INwz8eXOds9NS+1nMs4SoMbrpJnAjx8Cthti
+1Z/8eWMU023LYahds8BYM0T435K/2tTB5GTA4uTl2y8Xzz2PbptQ4PrUDaII
++egeQQyPA0yuoRDwpaeTiaBYOSa06YYuK5Agr0buQAxRIMCxI2o+fucjoabv
+FsQHKGu20U5GlJroSIyIVVkaH3evhNti/AnYX1HuokcGEQNsF5vo4SjWcH23
+2P86EIV+w5lUWC1FN9vZCyvbvyuqLHQMtqKVn4GBOkIc3bYQ0jru3a0FG4Cx
+bNJ0ASps2+p3Vxe0d+so2iFV92ByQ+0skyCUwCNUlwOV5V5f2fy1ImXk4mXI
+cO/bcbqRxx3pG9gkPIh43FoQktTT+tsJ5vS53qfaLGdhCYfkrWjsKu+2P9Xg
++Cr8clh6NTblhfkoAS1gzjA3XgsgEFrtP+OGqwg=
+=c5WU
+-----END PGP MESSAGE-----
+---END ENCRYPTED RANDOM KEY---
+`;
+
+export const testMessageContentLegacy =
+ '<div>flkasjfkjasdklfjasd<br></div><div>fasd<br></div><div>jfasjdfjasd<br></div><div>fj<br></div><div>asdfj<br></div><div>sadjf<br></div><div>sadjf<br></div><div>asjdf<br></div><div>jasd<br></div><div>fj<br></div><div>asdjf<br></div><div>asdjfsad<br></div><div>fasdlkfjasdjfkljsadfljsdfjsdljflkdsjfkljsdlkfjsdlk<br></div><div>jasfd<br></div><div>jsd<br></div><div>jf<br></div><div>sdjfjsdf<br></div><div><br></div><div>djfskjsladf<br></div><div>asd<br></div><div>fja<br></div><div>sdjfajsf<br></div><div>jas<br></div><div>fas<br></div><div>fj<br></div><div>afj<br></div><div>ajf<br></div><div>af<br></div><div>asdfasdfasd<br></div><div>Sent from <a href="https://protonmail.ch">ProtonMail</a>, encrypted email based in Switzerland.<br></div><div>dshfljsadfasdf<br></div><div>as<br></div><div>df<br></div><div>asd<br></div><div>fasd<br></div><div>f<br></div><div>asd<br></div><div>fasdflasdklfjsadlkjf</div><div>asd<br></div><div>fasdlkfjasdlkfjklasdjflkasjdflaslkfasdfjlasjflkasflksdjflkjasdf<br></div><div>asdflkasdjflajsfljaslkflasf<br></div><div>asdfkas<br></div><div>dfjas<br></div><div>djf<br></div><div>asjf<br></div><div>asj<br></div><div>faj<br></div><div>f<br></div><div>afj<br></div><div>sdjaf<br></div><div>jas<br></div><div>sdfj<br></div><div>ajf<br></div><div>aj<br></div><div>ajsdafafdaaf<br></div><div>a<br></div><div>f<br></div><div>lasl;ga<br></div><div>sags<br></div><div>ad<br></div><div>gags<br></div><div>g<br></div><div>ga<br></div><div>a<br></div><div>gg<br></div><div>a<br></div><div>ag<br></div><div>ag<br></div><div>agga.g.ga,ag.ag./ga<br></div><div><br></div><div>dsga<br></div><div>sg<br></div><div><br></div><div>gasga\\g\\g\\g\\g\\g\\n\\y\\t\\r\\\\r\\r\\\\n\\n\\n\\<br></div><div><br></div><div><br></div><div>sd<br></div><div>asdf<br></div><div>asdf<br></div><div>dsa<br></div><div>fasd<br></div><div>f</div>';
diff --git a/packages/shared/test/mail/legacyMigration.spec.ts b/packages/shared/test/mail/legacyMigration.spec.ts
new file mode 100644
index 0000000000..6c2fe8eae5
--- /dev/null
+++ b/packages/shared/test/mail/legacyMigration.spec.ts
@@ -0,0 +1,113 @@
+import { CryptoProxy } from '@proton/crypto';
+import { Api } from '@proton/shared/lib/interfaces';
+import { migrateSingle } from '@proton/shared/lib/mail/legacyMessagesMigration/helpers';
+
+import { testMessageContentLegacy, testMessageEncryptedLegacy, testPrivateKeyLegacy } from './legacyMigration.data';
+
+describe('legacy message migration helpers', () => {
+ it('should re-encrypt a legacy message', async () => {
+ const messageID = 'testID';
+ const decryptionKey = await CryptoProxy.importPrivateKey({
+ armoredKey: testPrivateKeyLegacy,
+ passphrase: '123',
+ });
+ const primaryKey = await CryptoProxy.generateKey({ userIDs: { email: 'test@test.it' } });
+
+ let wasMigrated = false;
+ let validReEncryption = false;
+ let wasMarkedBroken = false;
+ const mockApi = async ({ url, data }: { url: string; data: any }) => {
+ if (url.endsWith(`messages/${messageID}`)) {
+ const Message = {
+ Body: testMessageEncryptedLegacy,
+ Time: 1420070400000,
+ ID: messageID,
+ AddressID: 'keyID',
+ };
+
+ return { Message };
+ }
+
+ if (url.endsWith(`messages/${messageID}/body`)) {
+ wasMigrated = true;
+
+ const { Body: reEncryptedBody } = data;
+ // we should be able to decrypt the migrated message
+ const { data: decryptedData, signatures } = await CryptoProxy.decryptMessage({
+ armoredMessage: reEncryptedBody,
+ decryptionKeys: primaryKey,
+ });
+
+ validReEncryption = decryptedData === testMessageContentLegacy && !signatures.length;
+ }
+
+ if (url.endsWith(`messages/${messageID}/mark/broken`)) {
+ wasMarkedBroken = true;
+ }
+ };
+
+ await migrateSingle({
+ id: messageID,
+ statusMap: {},
+ api: mockApi as Api,
+ getAddressKeys: async () => [
+ {
+ ID: 'keyID',
+ privateKey: decryptionKey,
+ publicKey: primaryKey,
+ },
+ ],
+ });
+
+ expect(wasMigrated).toBe(true);
+ expect(validReEncryption).toBe(true);
+ expect(wasMarkedBroken).toBe(false);
+ });
+
+ it('should mark an undecryptable message as broken', async () => {
+ const messageID = 'testID';
+ const decryptionKey = await CryptoProxy.importPrivateKey({
+ armoredKey: testPrivateKeyLegacy,
+ passphrase: '123',
+ });
+
+ let wasMigrated = false;
+ let wasMarkedBroken = false;
+ const mockApi = async ({ url }: { url: string; data: any }) => {
+ if (url.endsWith(`messages/${messageID}`)) {
+ const Message = {
+ Body: '---BEGIN INVALID MESSAGE---',
+ Time: 1420070400000,
+ ID: messageID,
+ AddressID: 'keyID',
+ };
+
+ return { Message };
+ }
+
+ if (url.endsWith(`messages/${messageID}/body`)) {
+ wasMigrated = true;
+ }
+
+ if (url.endsWith(`messages/${messageID}/mark/broken`)) {
+ wasMarkedBroken = true;
+ }
+ };
+
+ await migrateSingle({
+ id: messageID,
+ statusMap: {},
+ api: mockApi as Api,
+ getAddressKeys: async () => [
+ {
+ ID: 'keyID',
+ privateKey: decryptionKey,
+ publicKey: decryptionKey,
+ },
+ ],
+ });
+
+ expect(wasMigrated).toBe(false);
+ expect(wasMarkedBroken).toBe(true);
+ });
+});