diff options
author | larabr <7375870+larabr@users.noreply.github.com> | 2022-08-19 15:49:24 +0300 |
---|---|---|
committer | Eduardo Conde Pena <econdepe@proton.ch> | 2022-11-04 17:22:04 +0300 |
commit | a9d148d6a877bff6a87afc97b0a5780e9b4795a4 (patch) | |
tree | 83037b59f5512a8da3a8b890b515009f4dcdcaec | |
parent | 42102fab656be0f170b194083538b070105779db (diff) |
Migrate messages page by page and use constant-time decryption
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---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---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); + }); +}); |