diff --git a/keyserver/package.json b/keyserver/package.json --- a/keyserver/package.json +++ b/keyserver/package.json @@ -118,7 +118,7 @@ ] }, "transformIgnorePatterns": [ - "/node_modules/(?!@babel/runtime)" + "/node_modules/(?!(@babel/runtime|vodozemac))" ], "setupFiles": [ "/jest-setup.js" diff --git a/keyserver/src/creators/olm-session-creator.js b/keyserver/src/creators/olm-session-creator.js --- a/keyserver/src/creators/olm-session-creator.js +++ b/keyserver/src/creators/olm-session-creator.js @@ -1,6 +1,6 @@ // @flow -import type { Account as OlmAccount } from '@commapp/olm'; +import type { Account as OlmAccount } from 'vodozemac'; import { ServerError } from 'lib/utils/errors.js'; diff --git a/keyserver/src/cron/cron.js b/keyserver/src/cron/cron.js --- a/keyserver/src/cron/cron.js +++ b/keyserver/src/cron/cron.js @@ -1,9 +1,9 @@ // @flow -import type { Account as OlmAccount } from '@commapp/olm'; import olm from '@commapp/olm'; import cluster from 'cluster'; import schedule from 'node-schedule'; +import type { Account as OlmAccount } from 'vodozemac'; import { getOlmMemory, diff --git a/keyserver/src/database/migration-config.js b/keyserver/src/database/migration-config.js --- a/keyserver/src/database/migration-config.js +++ b/keyserver/src/database/migration-config.js @@ -1,7 +1,7 @@ // @flow -import type { Account as OlmAccount } from '@commapp/olm'; import fs from 'fs'; +import type { Account as OlmAccount } from 'vodozemac'; import bots from 'lib/facts/bots.js'; import genesis from 'lib/facts/genesis.js'; diff --git a/keyserver/src/push/encrypted-notif-utils-api.js b/keyserver/src/push/encrypted-notif-utils-api.js --- a/keyserver/src/push/encrypted-notif-utils-api.js +++ b/keyserver/src/push/encrypted-notif-utils-api.js @@ -1,7 +1,6 @@ // @flow -import type { EncryptResult } from '@commapp/olm'; - +import type { EncryptResult } from 'lib/types/encrypted-type.js'; import type { EncryptedNotifUtilsAPI } from 'lib/types/notif-types.js'; import { getOlmUtility } from 'lib/utils/olm-utility.js'; diff --git a/keyserver/src/responders/keys-responders.js b/keyserver/src/responders/keys-responders.js --- a/keyserver/src/responders/keys-responders.js +++ b/keyserver/src/responders/keys-responders.js @@ -1,6 +1,6 @@ // @flow -import type { Account as OlmAccount } from '@commapp/olm'; +import type { Account as OlmAccount } from 'vodozemac'; import type { OlmSessionInitializationInfo, @@ -18,20 +18,39 @@ function retrieveSessionInitializationKeysSet( account: OlmAccount, ): SessionInitializationKeysSet { - const identityKeys = account.identity_keys(); + const identityKeys = JSON.stringify({ + ed25519: account.ed25519_key, + curve25519: account.curve25519_key, + }); const prekey = account.prekey(); - const prekeySignature = account.prekey_signature(); + if (!prekey) { + throw new ServerError('missing_prekey'); + } + + // Wrap prekey in old Olm format to match expected structure on all clients + const prekeyWrapped = JSON.stringify({ + curve25519: { AAAAAA: prekey }, + }); + const prekeySignature = account.prekey_signature(); if (!prekeySignature) { throw new ServerError('invalid_prekey'); } account.generate_one_time_keys(1); - const oneTimeKey = account.one_time_keys(); + const oneTimeKeysMap = account.one_time_keys(); + const oneTimeKeysEntries = Array.from(oneTimeKeysMap.entries()); + const oneTimeKeysObject = Object.fromEntries(oneTimeKeysEntries); + const oneTimeKey = JSON.stringify({ curve25519: oneTimeKeysObject }); account.mark_keys_as_published(); - return { identityKeys, oneTimeKey, prekey, prekeySignature }; + return { + identityKeys, + oneTimeKey, + prekey: prekeyWrapped, + prekeySignature, + }; } async function getOlmSessionInitializationDataResponder(): Promise { diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js --- a/keyserver/src/responders/user-responders.js +++ b/keyserver/src/responders/user-responders.js @@ -1,10 +1,10 @@ // @flow -import type { Utility as OlmUtility } from '@commapp/olm'; import invariant from 'invariant'; import { SiweErrorType, SiweMessage } from 'siwe'; import t, { type TInterface } from 'tcomb'; import bcrypt from 'twin-bcrypt'; +import type { Utility as OlmUtility } from 'vodozemac'; import { baseLegalPolicies, diff --git a/keyserver/src/updaters/olm-account-updater.js b/keyserver/src/updaters/olm-account-updater.js --- a/keyserver/src/updaters/olm-account-updater.js +++ b/keyserver/src/updaters/olm-account-updater.js @@ -1,6 +1,6 @@ // @flow -import type { Account as OlmAccount } from '@commapp/olm'; +import { type Account as OlmAccount } from 'vodozemac'; import { ServerError } from 'lib/utils/errors.js'; import sleep from 'lib/utils/sleep.js'; diff --git a/keyserver/src/updaters/olm-session-updater.js b/keyserver/src/updaters/olm-session-updater.js --- a/keyserver/src/updaters/olm-session-updater.js +++ b/keyserver/src/updaters/olm-session-updater.js @@ -1,7 +1,8 @@ // @flow -import type { EncryptResult, Session as OlmSession } from '@commapp/olm'; +import type { Session as OlmSession } from 'vodozemac'; +import type { EncryptResult } from 'lib/types/encrypted-type.js'; import { ServerError } from 'lib/utils/errors.js'; import sleep from 'lib/utils/sleep.js'; @@ -56,9 +57,11 @@ }, (olmSession: OlmSession) => { for (const messageName in messagesToEncrypt) { - encryptedMessages[messageName] = olmSession.encrypt( - messagesToEncrypt[messageName], - ); + const olmMessage = olmSession.encrypt(messagesToEncrypt[messageName]); + encryptedMessages[messageName] = { + type: olmMessage.message_type, + body: olmMessage.ciphertext, + }; } }, ); diff --git a/keyserver/src/user/login.js b/keyserver/src/user/login.js --- a/keyserver/src/user/login.js +++ b/keyserver/src/user/login.js @@ -1,7 +1,7 @@ // @flow -import type { Account as OlmAccount } from '@commapp/olm'; import { getRustAPI } from 'rust-node-addon'; +import type { Account as OlmAccount } from 'vodozemac'; import { getCommConfig } from 'lib/utils/comm-config.js'; import { ServerError } from 'lib/utils/errors.js'; diff --git a/keyserver/src/utils/olm-objects.js b/keyserver/src/utils/olm-objects.js --- a/keyserver/src/utils/olm-objects.js +++ b/keyserver/src/utils/olm-objects.js @@ -1,13 +1,20 @@ // @flow -import olm, { +import uuid from 'uuid'; +import { type Account as OlmAccount, + Account, + OlmMessage, type Session as OlmSession, -} from '@commapp/olm'; -import uuid from 'uuid'; +} from 'vodozemac'; import { olmEncryptedMessageTypes } from 'lib/types/crypto-types.js'; import { ServerError } from 'lib/utils/errors.js'; +import { + getVodozemacPickleKey, + unpickleVodozemacAccount, + unpickleVodozemacSession, +} from 'lib/utils/vodozemac-utils.js'; import { getMessageForException } from '../responders/utils.js'; @@ -20,16 +27,12 @@ pickledOlmAccount: PickledOlmAccount, callback: (account: OlmAccount, picklingKey: string) => Promise | T, ): Promise<{ +result: T, +pickledOlmAccount: PickledOlmAccount }> { - const { picklingKey, pickledAccount } = pickledOlmAccount; - - await olm.init(); - - const account = new olm.Account(); - account.unpickle(picklingKey, pickledAccount); + const { picklingKey } = pickledOlmAccount; + const account = unpickleVodozemacAccount(pickledOlmAccount); try { const result = await callback(account, picklingKey); - const updatedAccount = account.pickle(picklingKey); + const updatedAccount = account.pickle(getVodozemacPickleKey(picklingKey)); return { result, pickledOlmAccount: { @@ -45,14 +48,10 @@ } async function createPickledOlmAccount(): Promise { - await olm.init(); - - const account = new olm.Account(); - account.create(); + const account = new Account(); const picklingKey = uuid.v4(); - const pickledAccount = account.pickle(picklingKey); - + const pickledAccount = account.pickle(getVodozemacPickleKey(picklingKey)); account.free(); return { @@ -69,16 +68,12 @@ pickledOlmSession: PickledOlmSession, callback: (session: OlmSession) => Promise | T, ): Promise<{ +result: T, +pickledOlmSession: PickledOlmSession }> { - const { picklingKey, pickledSession } = pickledOlmSession; - - await olm.init(); - - const session = new olm.Session(); - session.unpickle(picklingKey, pickledSession); + const { picklingKey } = pickledOlmSession; + const session = unpickleVodozemacSession(pickledOlmSession); try { const result = await callback(session); - const updatedSession = session.pickle(picklingKey); + const updatedSession = session.pickle(getVodozemacPickleKey(picklingKey)); return { result, pickledOlmSession: { @@ -99,19 +94,20 @@ initialEncryptedMessage: string, theirCurve25519Key: string, ): Promise { - await olm.init(); - const session = new olm.Session(); - - session.create_inbound_from( - account, - theirCurve25519Key, + const olmMessage = new OlmMessage( + olmEncryptedMessageTypes.PREKEY, initialEncryptedMessage, ); - - account.remove_one_time_keys(session); - session.decrypt(olmEncryptedMessageTypes.PREKEY, initialEncryptedMessage); - const pickledSession = session.pickle(accountPicklingKey); - + const inboundCreationResult = account.create_inbound_session( + theirCurve25519Key, + olmMessage, + ); + // into_session() is consuming object. + // There is no need to call free() on inboundCreationResult + const session = inboundCreationResult.into_session(); + const pickledSession = session.pickle( + getVodozemacPickleKey(accountPicklingKey), + ); session.free(); return pickledSession; diff --git a/keyserver/src/utils/olm-utils.js b/keyserver/src/utils/olm-utils.js --- a/keyserver/src/utils/olm-utils.js +++ b/keyserver/src/utils/olm-utils.js @@ -1,9 +1,8 @@ // @flow -import type { Account as OlmAccount } from '@commapp/olm'; import invariant from 'invariant'; +import { type Account as OlmAccount } from 'vodozemac'; -import { getOneTimeKeyValuesFromBlob } from 'lib/shared/crypto-utils.js'; import type { IdentityNewDeviceKeyUpload } from 'lib/types/identity-service-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { @@ -134,16 +133,12 @@ await Promise.all([ fetchCallUpdateOlmAccount('content', (contentAccount: OlmAccount) => { contentAccount.generate_one_time_keys(numberOfKeys); - contentOneTimeKeys = getOneTimeKeyValuesFromBlob( - contentAccount.one_time_keys(), - ); + contentOneTimeKeys = [...contentAccount.one_time_keys().values()]; contentAccount.mark_keys_as_published(); }), fetchCallUpdateOlmAccount('notifications', (notifAccount: OlmAccount) => { notifAccount.generate_one_time_keys(numberOfKeys); - notifOneTimeKeys = getOneTimeKeyValuesFromBlob( - notifAccount.one_time_keys(), - ); + notifOneTimeKeys = [...notifAccount.one_time_keys().values()]; notifAccount.mark_keys_as_published(); }), ]); @@ -164,7 +159,7 @@ const pickledOlmAccount = await fetchPickledOlmAccount('content'); const getAccountEd25519Key: (account: OlmAccount) => string = ( account: OlmAccount, - ) => JSON.parse(account.identity_keys()).ed25519; + ) => account.ed25519_key; const { result } = await unpickleAccountAndUseCallback( pickledOlmAccount, @@ -211,7 +206,7 @@ contentAccount: OlmAccount, notifAccount: OlmAccount, ): Promise { - const deviceID = JSON.parse(contentAccount.identity_keys()).ed25519; + const deviceID = contentAccount.ed25519_key; const { prekey: contentPrekey, prekeySignature: contentPrekeySignature } = getAccountPrekeysSet(contentAccount); diff --git a/keyserver/src/utils/olm-utils.test.js b/keyserver/src/utils/olm-utils.test.js --- a/keyserver/src/utils/olm-utils.test.js +++ b/keyserver/src/utils/olm-utils.test.js @@ -2,8 +2,6 @@ import olm from '@commapp/olm'; -import { getOlmUtility } from 'lib/utils/olm-utility.js'; - describe('olm.Account', () => { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 '; @@ -220,12 +218,6 @@ return true; }; - it('should get Olm Utility', async () => { - await olm.init(); - const utility = getOlmUtility(); - expect(utility).toBeDefined(); - }); - it('should generate, regenerate, forget, and publish prekey', async () => { await olm.init(); const account = initAccount(false); diff --git a/lib/shared/crypto-utils.js b/lib/shared/crypto-utils.js --- a/lib/shared/crypto-utils.js +++ b/lib/shared/crypto-utils.js @@ -84,6 +84,10 @@ ); } +// Methods below are now considered to be deprecated. Vodozemac uses a different +// API and there is no need to parse prekey and OTKs. The only exception is +// `get_olm_session_initialization_data` which still returns keys in the old +// format to support older clients. function getOneTimeKeyValues( oneTimeKeys: OLMOneTimeKeys, ): $ReadOnlyArray { diff --git a/lib/types/encrypted-type.js b/lib/types/encrypted-type.js new file mode 100644 --- /dev/null +++ b/lib/types/encrypted-type.js @@ -0,0 +1,6 @@ +// @flow + +export type EncryptResult = { + +type: 0 | 1, // 0: PreKey, 1: Message + +body: string, +}; diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js --- a/lib/types/notif-types.js +++ b/lib/types/notif-types.js @@ -1,8 +1,8 @@ // @flow -import type { EncryptResult } from '@commapp/olm'; import t, { type TInterface, type TUnion } from 'tcomb'; +import type { EncryptResult } from './encrypted-type.js'; import type { MessageData, RawMessageInfo } from './message-types.js'; import type { ThickRawThreadInfos } from './thread-types.js'; import type { EntityText, ThreadEntity } from '../utils/entity-text.js'; diff --git a/lib/utils/olm-utility.js b/lib/utils/olm-utility.js --- a/lib/utils/olm-utility.js +++ b/lib/utils/olm-utility.js @@ -1,16 +1,15 @@ // @flow -import type { Utility as OlmUtility } from '@commapp/olm'; -import olm from '@commapp/olm'; +import { Utility } from 'vodozemac'; -let cachedOlmUtility: OlmUtility; -function getOlmUtility(): OlmUtility { +let cachedOlmUtility: Utility; +function getOlmUtility(): Utility { if (cachedOlmUtility) { return cachedOlmUtility; } - // This `olm.Utility` is created once and is cached for the entire + // This `Utility` is created once and is cached for the entire // program lifetime, there is no need to free the memory. - cachedOlmUtility = new olm.Utility(); + cachedOlmUtility = new Utility(); return cachedOlmUtility; } diff --git a/lib/utils/olm-utils.js b/lib/utils/olm-utils.js --- a/lib/utils/olm-utils.js +++ b/lib/utils/olm-utils.js @@ -1,11 +1,7 @@ // @flow -import type { Account as OlmAccount } from '@commapp/olm'; +import type { Account as VodozemacAccount } from 'vodozemac'; -import { - getOneTimeKeyValuesFromBlob, - getPrekeyValueFromBlob, -} from '../shared/crypto-utils.js'; import { ONE_TIME_KEYS_NUMBER } from '../types/identity-service-types.js'; const maxPublishedPrekeyAge = 30 * 24 * 60 * 60 * 1000; // 30 days @@ -18,7 +14,7 @@ +oneTimeKeys: $ReadOnlyArray, }; -function validateAccountPrekey(account: OlmAccount) { +function validateAccountPrekey(account: VodozemacAccount) { if (shouldRotatePrekey(account)) { account.generate_prekey(); } @@ -27,7 +23,7 @@ } } -function shouldRotatePrekey(account: OlmAccount): boolean { +function shouldRotatePrekey(account: VodozemacAccount): boolean { // Our fork of Olm only remembers two prekeys at a time. // If the new one hasn't been published, then the old one is still active. // In that scenario, we need to avoid rotating the prekey because it will @@ -45,7 +41,7 @@ ); } -function shouldForgetPrekey(account: OlmAccount): boolean { +function shouldForgetPrekey(account: VodozemacAccount): boolean { // Our fork of Olm only remembers two prekeys at a time. // We have to hold onto the old one until the new one is published. if (account.unpublished_prekey()) { @@ -60,36 +56,42 @@ ); } -function getLastPrekeyPublishTime(account: OlmAccount): Date { - const olmLastPrekeyPublishTime = account.last_prekey_publish_time(); +function getLastPrekeyPublishTime(account: VodozemacAccount): Date { + const vodozemacLastPrekeyPublishTime = account.last_prekey_publish_time(); - // Olm uses seconds, while the Date() constructor expects milliseconds. - return new Date(olmLastPrekeyPublishTime * 1000); + // Vodozemac uses seconds, while the Date() constructor expects milliseconds. + return new Date(Number(vodozemacLastPrekeyPublishTime) * 1000); } -function getAccountPrekeysSet(account: OlmAccount): { +function getAccountPrekeysSet(account: VodozemacAccount): { +prekey: string, +prekeySignature: ?string, } { - const prekey = getPrekeyValueFromBlob(account.prekey()); + const prekey = account.prekey(); + if (!prekey) { + throw Error('Prekey is missing'); + } const prekeySignature = account.prekey_signature(); return { prekey, prekeySignature }; } function getAccountOneTimeKeys( - account: OlmAccount, + account: VodozemacAccount, numberOfKeys: number = ONE_TIME_KEYS_NUMBER, ): $ReadOnlyArray { - let oneTimeKeys = getOneTimeKeyValuesFromBlob(account.one_time_keys()); + let oneTimeKeys = [...account.one_time_keys().values()]; if (oneTimeKeys.length < numberOfKeys) { account.generate_one_time_keys(numberOfKeys - oneTimeKeys.length); - oneTimeKeys = getOneTimeKeyValuesFromBlob(account.one_time_keys()); + oneTimeKeys = [...account.one_time_keys().values()]; } return oneTimeKeys; } -function retrieveAccountKeysSet(account: OlmAccount): AccountKeysSet { - const identityKeys = account.identity_keys(); +function retrieveAccountKeysSet(account: VodozemacAccount): AccountKeysSet { + const identityKeys = JSON.stringify({ + ed25519: account.ed25519_key, + curve25519: account.curve25519_key, + }); validateAccountPrekey(account); const { prekey, prekeySignature } = getAccountPrekeysSet(account); diff --git a/lib/utils/vodozemac-utils.js b/lib/utils/vodozemac-utils.js new file mode 100644 --- /dev/null +++ b/lib/utils/vodozemac-utils.js @@ -0,0 +1,61 @@ +// @flow + +import { Account, Session } from 'vodozemac'; + +// Helper function to get 32-byte pickle key for vodozemac +function getVodozemacPickleKey(picklingKey: string): Uint8Array { + const fullKeyBytes = new TextEncoder().encode(picklingKey); + // NOTE: vodozemac works only with 32-byte keys. + // We have sessions pickled with 64-byte keys. Additionally, this key + // is used in backup, so it can't simply be migrated. Instead, we're going + // to just use the first 32 bytes of the existing secret key. + return fullKeyBytes.slice(0, 32); +} + +function unpickleVodozemacAccount({ + picklingKey, + pickledAccount, +}: { + +picklingKey: string, + +pickledAccount: string, +}): Account { + const fullKeyBytes = new TextEncoder().encode(picklingKey); + const keyBytes = getVodozemacPickleKey(picklingKey); + try { + // First try vodozemac native format + console.log('Account unpickle'); + return Account.from_pickle(pickledAccount, keyBytes); + } catch (e) { + console.log(e); + // Fall back to libolm format + console.log('Account unpickle: fallback to libolm'); + return Account.from_libolm_pickle(pickledAccount, fullKeyBytes); + } +} + +function unpickleVodozemacSession({ + picklingKey, + pickledSession, +}: { + +picklingKey: string, + +pickledSession: string, +}): Session { + const fullKeyBytes = new TextEncoder().encode(picklingKey); + const keyBytes = getVodozemacPickleKey(picklingKey); + try { + // First try vodozemac native format + console.log('Session unpickle'); + return Session.from_pickle(pickledSession, keyBytes); + } catch (e) { + console.log(e); + // Fall back to libolm format + console.log('Session unpickle: fallback to libolm'); + return Session.from_libolm_pickle(pickledSession, fullKeyBytes); + } +} + +export { + getVodozemacPickleKey, + unpickleVodozemacAccount, + unpickleVodozemacSession, +}; diff --git a/web/push-notif/notif-crypto-utils.js b/web/push-notif/notif-crypto-utils.js --- a/web/push-notif/notif-crypto-utils.js +++ b/web/push-notif/notif-crypto-utils.js @@ -1,11 +1,13 @@ // @flow -import olm from '@commapp/olm'; import type { EncryptResult } from '@commapp/olm'; import invariant from 'invariant'; import localforage from 'localforage'; import uuid from 'uuid'; -import initVodozemac from 'vodozemac'; +import initVodozemac, { + OlmMessage, + type Account as VodozemacAccount, +} from 'vodozemac'; import { olmEncryptedMessageTypes, @@ -23,6 +25,11 @@ import { getMessageForException } from 'lib/utils/errors.js'; import { promiseAll } from 'lib/utils/promises.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; +import { + getVodozemacPickleKey, + unpickleVodozemacAccount, + unpickleVodozemacSession, +} from 'lib/utils/vodozemac-utils.js'; import { fetchAuthMetadata, @@ -58,7 +65,7 @@ }; export type NotificationAccountWithPicklingKey = { - +notificationAccount: olm.Account, + +notificationAccount: VodozemacAccount, +picklingKey: string, +synchronizationValue: ?string, +accountEncryptionKey?: CryptoKey, @@ -723,11 +730,10 @@ if (notificationsOlmData) { // Memory is freed below in this condition. - const session = new olm.Session(); - session.unpickle( - notificationsOlmData.picklingKey, - notificationsOlmData.pendingSessionUpdate, - ); + const session = unpickleVodozemacSession({ + picklingKey: notificationsOlmData.picklingKey, + pickledSession: notificationsOlmData.pendingSessionUpdate, + }); isSenderChainEmpty = session.is_sender_chain_empty(); hasReceivedMessage = session.has_received_message(); @@ -762,14 +768,12 @@ ); // Memory is freed below after pickling. - const account = new olm.Account(); - const session = new olm.Session(); + let account; + let session; + let decryptedNotification: T; try { - account.unpickle( - notificationAccount.picklingKey, - notificationAccount.pickledAccount, - ); + account = unpickleVodozemacAccount(notificationAccount); if (notifInboundKeys.error) { throw new Error(notifInboundKeys.error); @@ -780,20 +784,26 @@ 'curve25519 must be present in notifs inbound keys', ); - session.create_inbound_from( - account, + const olmMessage = new OlmMessage(messageType, encryptedPayload); + const inboundCreationResult = account.create_inbound_session( notifInboundKeys.curve25519, - encryptedPayload, + olmMessage, ); - account.remove_one_time_keys(session); + const decryptedString = inboundCreationResult.plaintext; + // into_session() is consuming object. + // There is no need to call free() on inboundCreationResult + session = inboundCreationResult.into_session(); + olmMessage.free(); - const decryptedNotification: T = JSON.parse( - session.decrypt(messageType, encryptedPayload), - ); + decryptedNotification = JSON.parse(decryptedString); - const pickledOlmSession = session.pickle(notificationAccount.picklingKey); - const pickledAccount = account.pickle(notificationAccount.picklingKey); + const pickledOlmSession = session.pickle( + getVodozemacPickleKey(notificationAccount.picklingKey), + ); + const pickledAccount = account.pickle( + getVodozemacPickleKey(notificationAccount.picklingKey), + ); // session reset attempt or session initialization - handled the same const sessionResetAttempt = @@ -834,8 +844,8 @@ // any session state return { decryptedNotification }; } finally { - session.free(); - account.free(); + session?.free(); + account?.free(); } } @@ -846,12 +856,16 @@ type: OlmEncryptedMessageTypes, ): DecryptionResult { // Memory is freed below after pickling. - const session = new olm.Session(); - session.unpickle(picklingKey, pickledSession); - const decryptedNotification: T = JSON.parse( - session.decrypt(type, encryptedPayload), + const session = unpickleVodozemacSession({ picklingKey, pickledSession }); + + const olmMessage = new OlmMessage(type, encryptedPayload); + const decryptedString = session.decrypt(olmMessage); + olmMessage.free(); + + const decryptedNotification: T = JSON.parse(decryptedString); + const newPendingSessionUpdate = session.pickle( + getVodozemacPickleKey(picklingKey), ); - const newPendingSessionUpdate = session.pickle(picklingKey); session.free(); const newUpdateCreationTimestamp = Date.now(); @@ -970,10 +984,22 @@ ); // Memory is freed below after pickling. - const session = new olm.Session(); - session.unpickle(picklingKey, pendingSessionUpdate); - const encryptedNotification = session.encrypt(payload); - const newPendingSessionUpdate = session.pickle(picklingKey); + + const session = unpickleVodozemacSession({ + picklingKey, + pickledSession: pendingSessionUpdate, + }); + + const olmMessage = session.encrypt(payload); + const encryptedNotification = { + body: olmMessage.ciphertext, + type: olmMessage.message_type, + }; + olmMessage.free(); + + const newPendingSessionUpdate = session.pickle( + getVodozemacPickleKey(picklingKey), + ); session.free(); const updatedOlmData: NotificationsOlmDataType = { @@ -1040,10 +1066,9 @@ validatedNotifsAccountEncryptionKey, ); - const { pickledAccount, picklingKey } = pickledOLMAccount; + const { picklingKey } = pickledOLMAccount; - const notificationAccount = new olm.Account(); - notificationAccount.unpickle(picklingKey, pickledAccount); + const notificationAccount = unpickleVodozemacAccount(pickledOLMAccount); return { notificationAccount, diff --git a/web/shared-worker/worker/worker-crypto.js b/web/shared-worker/worker/worker-crypto.js --- a/web/shared-worker/worker/worker-crypto.js +++ b/web/shared-worker/worker/worker-crypto.js @@ -1,27 +1,29 @@ // @flow -import olm, { - type Account as OlmAccount, - type Utility as OlmUtility, -} from '@commapp/olm'; import base64 from 'base-64'; import localforage from 'localforage'; import uuid from 'uuid'; -import initVodozemac from 'vodozemac'; +import initVodozemac, { + Account, + type Account as VodozemacAccount, + OlmMessage, + Session, + Utility, +} from 'vodozemac'; import { initialEncryptedMessageContent } from 'lib/shared/crypto-utils.js'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import { - type OLMIdentityKeys, - type PickledOLMAccount, + type ClientPublicKeys, + type EncryptedData, type IdentityKeysBlob, - type SignedIdentityKeysBlob, + type NotificationsOlmDataType, type OlmAPI, + type OLMIdentityKeys, type OneTimeKeysResultValues, - type ClientPublicKeys, - type NotificationsOlmDataType, - type EncryptedData, type OutboundSessionCreationResult, + type PickledOLMAccount, + type SignedIdentityKeysBlob, } from 'lib/types/crypto-types.js'; import type { PlatformDetails } from 'lib/types/device-types.js'; import type { IdentityNewDeviceKeyUpload } from 'lib/types/identity-service-types.js'; @@ -29,52 +31,56 @@ import type { InboundP2PMessage } from 'lib/types/sqlite-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { entries } from 'lib/utils/objects.js'; -import { verifyMemoryUsage } from 'lib/utils/olm-memory-utils.js'; import { getOlmUtility } from 'lib/utils/olm-utility.js'; import { - retrieveAccountKeysSet, getAccountOneTimeKeys, getAccountPrekeysSet, + OLM_ERROR_FLAG, + olmSessionErrors, + retrieveAccountKeysSet, shouldForgetPrekey, shouldRotatePrekey, - olmSessionErrors, - OLM_ERROR_FLAG, } from 'lib/utils/olm-utils.js'; +import { + getVodozemacPickleKey, + unpickleVodozemacAccount, + unpickleVodozemacSession, +} from 'lib/utils/vodozemac-utils.js'; import { getIdentityClient } from './identity-client.js'; import { getProcessingStoreOpsExceptionMessage } from './process-operations.js'; import { getDBModule, - getSQLiteQueryExecutor, getPlatformDetails, + getSQLiteQueryExecutor, } from './worker-database.js'; import { + encryptNotification, + getNotifsCryptoAccount_WITH_MANUAL_MEMORY_MANAGEMENT, getOlmDataKeyForCookie, - getOlmEncryptionKeyDBLabelForCookie, getOlmDataKeyForDeviceID, + getOlmEncryptionKeyDBLabelForCookie, getOlmEncryptionKeyDBLabelForDeviceID, - encryptNotification, type NotificationAccountWithPicklingKey, - getNotifsCryptoAccount_WITH_MANUAL_MEMORY_MANAGEMENT, persistNotifsAccountWithOlmData, } from '../../push-notif/notif-crypto-utils.js'; import { + type LegacyCryptoStore, type WorkerRequestMessage, - type WorkerResponseMessage, workerRequestMessageTypes, + type WorkerResponseMessage, workerResponseMessageTypes, - type LegacyCryptoStore, } from '../../types/worker-types.js'; import type { OlmPersistSession } from '../types/sqlite-query-executor.js'; -type OlmSession = { +session: olm.Session, +version: number }; +type OlmSession = { +session: Session, +version: number }; type OlmSessions = { [deviceID: string]: OlmSession, }; type WorkerCryptoStore = { +contentAccountPickleKey: string, - +contentAccount: olm.Account, + +contentAccount: VodozemacAccount, +contentSessions: OlmSessions, }; @@ -114,14 +120,18 @@ const pickledContentAccount: PickledOLMAccount = { picklingKey: contentAccountPickleKey, - pickledAccount: contentAccount.pickle(contentAccountPickleKey), + pickledAccount: contentAccount.pickle( + getVodozemacPickleKey(contentAccountPickleKey), + ), }; const pickledContentSessions: OlmPersistSession[] = entries( contentSessions, ).map(([targetDeviceID, sessionData]) => ({ targetDeviceID, - sessionData: sessionData.session.pickle(contentAccountPickleKey), + sessionData: sessionData.session.pickle( + getVodozemacPickleKey(contentAccountPickleKey), + ), version: sessionData.version, })); @@ -144,7 +154,9 @@ accountEncryptionKey, } = notifsCryptoAccount; - const pickledAccount = notificationAccount.pickle(picklingKey); + const pickledAccount = notificationAccount.pickle( + getVodozemacPickleKey(picklingKey), + ); const accountWithPicklingKey: PickledOLMAccount = { pickledAccount, picklingKey, @@ -190,31 +202,23 @@ const notificationsPrekey = notificationsInitializationInfo.prekey; // Memory is freed below after persisting. - const session = new olm.Session(); - if (notificationsInitializationInfo.oneTimeKey) { - session.create_outbound( - notificationAccount, - notificationsIdentityKeys.curve25519, - notificationsIdentityKeys.ed25519, - notificationsPrekey, - notificationsInitializationInfo.prekeySignature, - notificationsInitializationInfo.oneTimeKey, - ); - } else { - session.create_outbound_without_otk( - notificationAccount, - notificationsIdentityKeys.curve25519, - notificationsIdentityKeys.ed25519, - notificationsPrekey, - notificationsInitializationInfo.prekeySignature, - ); - } - const { body: message, type: messageType } = session.encrypt( + const session = notificationAccount.create_outbound_session( + notificationsIdentityKeys.curve25519, + notificationsIdentityKeys.ed25519, + notificationsInitializationInfo.oneTimeKey || '', + notificationsPrekey, + notificationsInitializationInfo.prekeySignature, + true, // olmCompatibilityMode + ); + + const encryptedMessage = session.encrypt( JSON.stringify(initialEncryptedMessageContent), ); + const message = encryptedMessage.ciphertext; + const messageType = encryptedMessage.message_type; const mainSession = session.pickle( - notificationAccountWithPicklingKey.picklingKey, + getVodozemacPickleKey(notificationAccountWithPicklingKey.picklingKey), ); const notificationsOlmData: NotificationsOlmDataType = { mainSession, @@ -223,7 +227,9 @@ picklingKey, }; - const pickledAccount = notificationAccount.pickle(picklingKey); + const pickledAccount = notificationAccount.pickle( + getVodozemacPickleKey(picklingKey), + ); const accountWithPicklingKey: PickledOLMAccount = { pickledAccount, picklingKey, @@ -247,7 +253,7 @@ async function getOrCreateOlmAccount(accountIDInDB: number): Promise<{ +picklingKey: string, - +account: olm.Account, + +account: VodozemacAccount, +synchronizationValue?: ?string, }> { const sqliteQueryExecutor = getSQLiteQueryExecutor(); @@ -289,18 +295,18 @@ }; } - // This `olm.Account` is created once and is cached for the entire + // This `Account` is created once and is cached for the entire // program lifetime. Freeing is done as part of `clearCryptoStore`. - const account = new olm.Account(); + let account; let picklingKey; if (!accountDBString) { picklingKey = uuid.v4(); - account.create(); + account = new Account(); } else { const dbAccount: PickledOLMAccount = JSON.parse(accountDBString); picklingKey = dbAccount.picklingKey; - account.unpickle(picklingKey, dbAccount.pickledAccount); + account = unpickleVodozemacAccount(dbAccount); } if (accountIDInDB === sqliteQueryExecutor.getNotifsAccountID()) { @@ -329,10 +335,12 @@ const sessionsData: OlmSessions = {}; for (const persistedSession: OlmPersistSession of dbSessionsData) { const { sessionData, version } = persistedSession; - // This `olm.Session` is created once and is cached for the entire + // This `Session` is created once and is cached for the entire // program lifetime. Freeing is done as part of `clearCryptoStore`. - const session = new olm.Session(); - session.unpickle(picklingKey, sessionData); + const session = unpickleVodozemacSession({ + picklingKey, + pickledSession: sessionData, + }); sessionsData[persistedSession.targetDeviceID] = { session, version, @@ -344,13 +352,10 @@ function unpickleInitialCryptoStoreAccount( account: PickledOLMAccount, -): olm.Account { - const { picklingKey, pickledAccount } = account; - // This `olm.Account` is created once and is cached for the entire +): VodozemacAccount { + // This `Account` is created once and is cached for the entire // program lifetime. Freeing is done as part of `clearCryptoStore`. - const olmAccount = new olm.Account(); - olmAccount.unpickle(picklingKey, pickledAccount); - return olmAccount; + return unpickleVodozemacAccount(account); } async function initializeCryptoAccount( @@ -397,7 +402,6 @@ message.vodozemacWasmPath, message.initialCryptoStore, ); - verifyMemoryUsage('INITIALIZE_CRYPTO_ACCOUNT'); } else if (message.type === workerRequestMessageTypes.CALL_OLM_API_METHOD) { const method: (...$ReadOnlyArray) => mixed = (olmAPI[ message.method @@ -405,7 +409,6 @@ // Flow doesn't allow us to bind the (stringified) method name with // the argument types so we need to pass the args as mixed. const result = await method(...message.args); - verifyMemoryUsage(message.method); return { type: workerResponseMessageTypes.CALL_OLM_API_METHOD, result, @@ -424,10 +427,14 @@ await getNotifsCryptoAccount_WITH_MANUAL_MEMORY_MANAGEMENT(); const identityKeysBlob: IdentityKeysBlob = { - notificationIdentityPublicKeys: JSON.parse( - notificationAccount.identity_keys(), - ), - primaryIdentityPublicKeys: JSON.parse(contentAccount.identity_keys()), + notificationIdentityPublicKeys: { + ed25519: notificationAccount.ed25519_key, + curve25519: notificationAccount.curve25519_key, + }, + primaryIdentityPublicKeys: { + ed25519: contentAccount.ed25519_key, + curve25519: contentAccount.curve25519_key, + }, }; const payloadToBeSigned: string = JSON.stringify(identityKeysBlob); @@ -447,13 +454,13 @@ signature: string, signingPublicKey: string, ) { - const olmUtility: OlmUtility = getOlmUtility(); + const olmUtility: Utility = getOlmUtility(); try { olmUtility.ed25519_verify(signingPublicKey, message, signature); return true; } catch (err) { const isSignatureInvalid = - getMessageForException(err)?.includes('BAD_MESSAGE_MAC'); + getMessageForException(err)?.includes('Signature'); if (isSignatureInvalid) { return false; } @@ -461,8 +468,8 @@ } } -function isPrekeySignatureValid(account: OlmAccount): boolean { - const signingPublicKey = JSON.parse(account.identity_keys()).ed25519; +function isPrekeySignatureValid(account: VodozemacAccount): boolean { + const signingPublicKey = account.ed25519_key; const { prekey, prekeySignature } = getAccountPrekeysSet(account); if (!prekey || !prekeySignature) { return false; @@ -589,10 +596,14 @@ ); const result = { - primaryIdentityPublicKeys: JSON.parse(contentAccount.identity_keys()), - notificationIdentityPublicKeys: JSON.parse( - notificationAccount.identity_keys(), - ), + primaryIdentityPublicKeys: { + ed25519: contentAccount.ed25519_key, + curve25519: contentAccount.curve25519_key, + }, + notificationIdentityPublicKeys: { + ed25519: notificationAccount.ed25519_key, + curve25519: notificationAccount.curve25519_key, + }, blobPayload: payload, signature, }; @@ -608,15 +619,17 @@ if (!olmSession) { throw new Error(olmSessionErrors.sessionDoesNotExist); } - const encryptedContent = olmSession.session.encrypt(content); + const olmMessage = olmSession.session.encrypt(content); + const encryptedData = { + message: olmMessage.ciphertext, + messageType: olmMessage.message_type, + sessionVersion: olmSession.version, + }; + olmMessage.free(); await persistCryptoStore(); - return { - message: encryptedContent.body, - messageType: encryptedContent.type, - sessionVersion: olmSession.version, - }; + return encryptedData; }, async encryptAndPersist( content: string, @@ -630,8 +643,12 @@ if (!olmSession) { throw new Error(olmSessionErrors.sessionDoesNotExist); } - - const encryptedContent = olmSession.session.encrypt(content); + const olmMessage = olmSession.session.encrypt(content); + const encryptedContent = { + body: olmMessage.ciphertext, + type: olmMessage.message_type, + }; + olmMessage.free(); const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); @@ -693,16 +710,20 @@ throw new Error(olmSessionErrors.invalidSessionVersion); } + const olmMessage = new OlmMessage( + encryptedData.messageType, + encryptedData.message, + ); + let result; try { - result = olmSession.session.decrypt( - encryptedData.messageType, - encryptedData.message, - ); + result = olmSession.session.decrypt(olmMessage); } catch (e) { + olmMessage.free(); throw new Error(`error decrypt => ${OLM_ERROR_FLAG} ` + e.message); } + olmMessage.free(); await persistCryptoStore(); return result; @@ -729,16 +750,20 @@ throw new Error(olmSessionErrors.invalidSessionVersion); } + const olmMessage = new OlmMessage( + encryptedData.messageType, + encryptedData.message, + ); + let result; try { - result = olmSession.session.decrypt( - encryptedData.messageType, - encryptedData.message, - ); + result = olmSession.session.decrypt(olmMessage); } catch (e) { + olmMessage.free(); throw new Error(`error decrypt => ${OLM_ERROR_FLAG} ` + e.message); } + olmMessage.free(); const sqliteQueryExecutor = getSQLiteQueryExecutor(); const dbModule = getDBModule(); if (!sqliteQueryExecutor || !dbModule) { @@ -787,27 +812,30 @@ } } - // This `olm.Session` is created once and is cached for the entire + // This `Session` is created once and is cached for the entire // program lifetime. Freeing is done as part of `clearCryptoStore`. - const session = new olm.Session(); - session.create_inbound_from( - contentAccount, - contentIdentityKeys.curve25519, + const olmMessage = new OlmMessage( + initialEncryptedData.messageType, initialEncryptedData.message, ); - contentAccount.remove_one_time_keys(session); - let initialEncryptedMessage; + let inboundCreationResult; try { - initialEncryptedMessage = session.decrypt( - initialEncryptedData.messageType, - initialEncryptedData.message, + inboundCreationResult = contentAccount.create_inbound_session( + contentIdentityKeys.curve25519, + olmMessage, ); } catch (e) { - session.free(); + olmMessage.free(); throw new Error(`error decrypt => ${OLM_ERROR_FLAG} ` + e.message); } + // into_session() is consuming object. + // There is no need to call free() on inboundCreationResult + const initialEncryptedMessage = inboundCreationResult.plaintext; + const session = inboundCreationResult.into_session(); + olmMessage.free(); + if (existingSession) { existingSession.session.free(); } @@ -829,30 +857,25 @@ const { contentAccount, contentSessions } = cryptoStore; const existingSession = contentSessions[contentIdentityKeys.ed25519]; - // This `olm.Session` is created once and is cached for the entire + // This `Session` is created once and is cached for the entire // program lifetime. Freeing is done as part of `clearCryptoStore`. - const session = new olm.Session(); - if (contentInitializationInfo.oneTimeKey) { - session.create_outbound( - contentAccount, - contentIdentityKeys.curve25519, - contentIdentityKeys.ed25519, - contentInitializationInfo.prekey, - contentInitializationInfo.prekeySignature, - contentInitializationInfo.oneTimeKey, - ); - } else { - session.create_outbound_without_otk( - contentAccount, - contentIdentityKeys.curve25519, - contentIdentityKeys.ed25519, - contentInitializationInfo.prekey, - contentInitializationInfo.prekeySignature, - ); - } - const initialEncryptedData = session.encrypt( + const session = contentAccount.create_outbound_session( + contentIdentityKeys.curve25519, + contentIdentityKeys.ed25519, + contentInitializationInfo.oneTimeKey || '', + contentInitializationInfo.prekey, + contentInitializationInfo.prekeySignature, + true, // olmCompatibilityMode + ); + + const olmMessage = session.encrypt( JSON.stringify(initialEncryptedMessageContent), ); + const initialEncryptedData = { + body: olmMessage.ciphertext, + type: olmMessage.message_type, + }; + olmMessage.free(); const newSessionVersion = existingSession ? existingSession.version + 1 : 1; if (existingSession) {