diff --git a/keyserver/package.json b/keyserver/package.json --- a/keyserver/package.json +++ b/keyserver/package.json @@ -49,6 +49,7 @@ "@babel/runtime": "^7.28.3", "@commapp/olm": "0.2.6", "@hono/node-server": "^1.13.7", + "@commapp/vodozemac": "0.1.0", "@parse/node-apn": "^3.2.0", "@vingle/bmp-js": "^0.2.5", "bad-words": "^3.0.4", @@ -117,7 +118,7 @@ ] }, "transformIgnorePatterns": [ - "/node_modules/(?!@babel/runtime)" + "/node_modules/(?!(@babel/runtime|@commapp/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 '@commapp/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,15 +1,9 @@ // @flow -import type { Account as OlmAccount } from '@commapp/olm'; -import olm from '@commapp/olm'; +import type { Account as OlmAccount } from '@commapp/vodozemac'; import cluster from 'cluster'; import schedule from 'node-schedule'; -import { - getOlmMemory, - compareAndLogOlmMemory, -} from 'lib/utils/olm-memory-utils.js'; - import { backupDB } from './backups.js'; import { createDailyUpdatesThread } from './daily-updates.js'; import { postMetrics } from './metrics.js'; @@ -88,7 +82,6 @@ schedule.scheduleJob( '0 0 * * *', // every day at midnight in the keyserver's timezone async () => { - const memBefore = getOlmMemory(); try { await fetchCallUpdateOlmAccount( 'content', @@ -101,15 +94,12 @@ ); } catch (e) { console.warn('encountered error while trying to validate prekeys', e); - } finally { - compareAndLogOlmMemory(memBefore, 'prekey upload cronjob'); } }, ); schedule.scheduleJob( '0 2 * * *', // every day at 2:00 AM in the keyserver's timezone async () => { - const memBefore = getOlmMemory(); try { await synchronizeInviteLinksWithBlobs(); } catch (e) { @@ -117,24 +107,6 @@ 'encountered an error while trying to synchronize invite links with blobs', e, ); - } finally { - compareAndLogOlmMemory(memBefore, 'invite links cronjob'); - } - }, - ); - schedule.scheduleJob( - '0,15,30,45 * * * *', // every 15 minutes - async () => { - const memBefore = getOlmMemory(); - try { - await olm.init(); - } catch (e) { - console.warn( - 'encountered an error while executing olm init cron job', - e, - ); - } finally { - compareAndLogOlmMemory(memBefore, 'olm init cronjob'); } }, ); 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,6 +1,6 @@ // @flow -import type { Account as OlmAccount } from '@commapp/olm'; +import type { Account as OlmAccount } from '@commapp/vodozemac'; import fs from 'fs'; import bots from 'lib/facts/bots.js'; diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js --- a/keyserver/src/keyserver.js +++ b/keyserver/src/keyserver.js @@ -1,6 +1,6 @@ // @flow -import olm from '@commapp/olm'; +import initVodozemac from '@commapp/vodozemac'; import cluster from 'cluster'; import compression from 'compression'; import cookieParser from 'cookie-parser'; @@ -72,7 +72,7 @@ void (async () => { const [webAppCorsConfig] = await Promise.all([ getWebAppCorsConfig(), - olm.init(), + initVodozemac(), prefetchAllURLFacts(), initENSCache(), initFCCache(), 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 '@commapp/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,6 +1,6 @@ // @flow -import type { Utility as OlmUtility } from '@commapp/olm'; +import type { Utility as OlmUtility } from '@commapp/vodozemac'; import invariant from 'invariant'; import { SiweErrorType, SiweMessage } from 'siwe'; import t, { type TInterface } from 'tcomb'; diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js --- a/keyserver/src/responders/website-responders.js +++ b/keyserver/src/responders/website-responders.js @@ -41,6 +41,7 @@ +fontsURL: string, +cssInclude: string, +olmFilename: string, + +vodozemacFilename: string, +commQueryExecutorFilename: string, +backupClientFilename: string, +webworkersOpaqueFilename: string, @@ -57,6 +58,7 @@ fontsURL, cssInclude: '', olmFilename: '', + vodozemacFilename: '', commQueryExecutorFilename: '', backupClientFilename: '', webworkersOpaqueFilename: '', @@ -82,6 +84,7 @@ /> `, olmFilename: manifest['olm.wasm'], + vodozemacFilename: manifest['vodozemac.wasm'], commQueryExecutorFilename: webworkersManifest['comm-query-executor.wasm'], backupClientFilename: webworkersManifest['backup-client-wasm_bg.wasm'], webworkersOpaqueFilename: webworkersManifest['comm_opaque2_wasm_bg.wasm'], @@ -136,6 +139,7 @@ fontsURL, cssInclude, olmFilename, + vodozemacFilename, commQueryExecutorFilename, backupClientFilename, webworkersOpaqueFilename, @@ -195,6 +199,7 @@ var keyserverURL = "${keyserverURL}"; var baseURL = "${baseURL}"; var olmFilename = "${olmFilename}"; + var vodozemacFilename = "${vodozemacFilename}"; var commQueryExecutorFilename = "${commQueryExecutorFilename}"; var backupClientFilename = "${backupClientFilename}"; var webworkersOpaqueFilename = "${webworkersOpaqueFilename}" diff --git a/keyserver/src/socket/tunnelbroker.js b/keyserver/src/socket/tunnelbroker.js --- a/keyserver/src/socket/tunnelbroker.js +++ b/keyserver/src/socket/tunnelbroker.js @@ -44,10 +44,6 @@ convertObjToBytes, } from 'lib/utils/conversion-utils.js'; import { getMessageForException } from 'lib/utils/errors.js'; -import { - compareAndLogOlmMemory, - getOlmMemory, -} from 'lib/utils/olm-memory-utils.js'; import sleep from 'lib/utils/sleep.js'; import { @@ -391,14 +387,11 @@ refreshOneTimeKeys: (numberOfKeys: number) => void = numberOfKeys => { const oldOneTimeKeysPromise = this.oneTimeKeysPromise; this.oneTimeKeysPromise = (async () => { - const memBefore = getOlmMemory(); try { await oldOneTimeKeysPromise; await uploadNewOneTimeKeys(numberOfKeys); } catch (e) { console.error('Encountered error when trying to upload new OTKs:', e); - } finally { - compareAndLogOlmMemory(memBefore, 'otk refresh'); } })(); }; 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 '@commapp/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 '@commapp/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,6 +1,6 @@ // @flow -import type { Account as OlmAccount } from '@commapp/olm'; +import type { Account as OlmAccount } from '@commapp/vodozemac'; import { getRustAPI } from 'rust-node-addon'; import { getCommConfig } from 'lib/utils/comm-config.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 initVodozemac, { type Account as OlmAccount, + Account, + OlmMessage, type Session as OlmSession, -} from '@commapp/olm'; +} from '@commapp/vodozemac'; import uuid from 'uuid'; 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,14 @@ pickledOlmAccount: PickledOlmAccount, callback: (account: OlmAccount, picklingKey: string) => Promise | T, ): Promise<{ +result: T, +pickledOlmAccount: PickledOlmAccount }> { - const { picklingKey, pickledAccount } = pickledOlmAccount; + await initVodozemac(); - 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 +50,12 @@ } async function createPickledOlmAccount(): Promise { - await olm.init(); + await initVodozemac(); - 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 +72,14 @@ pickledOlmSession: PickledOlmSession, callback: (session: OlmSession) => Promise | T, ): Promise<{ +result: T, +pickledOlmSession: PickledOlmSession }> { - const { picklingKey, pickledSession } = pickledOlmSession; - - await olm.init(); + await initVodozemac(); - 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 +100,22 @@ initialEncryptedMessage: string, theirCurve25519Key: string, ): Promise { - await olm.init(); - const session = new olm.Session(); + await initVodozemac(); - 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 { type Account as OlmAccount } from '@commapp/vodozemac'; import invariant from 'invariant'; -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 deleted file mode 100644 --- a/keyserver/src/utils/olm-utils.test.js +++ /dev/null @@ -1,337 +0,0 @@ -// @flow - -import olm from '@commapp/olm'; - -import { getOlmUtility } from 'lib/utils/olm-utility.js'; - -describe('olm.Account', () => { - const alphabet = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 '; - - const randomString = (length: number) => - Array.from( - { length }, - () => alphabet[Math.floor(Math.random() * alphabet.length)], - ).join(''); - - const initAccount = (mark_prekey_published: boolean = true) => { - const account = new olm.Account(); - account.create(); - account.generate_prekey(); - account.generate_one_time_keys(1); - if (mark_prekey_published) { - account.mark_prekey_as_published(); - } - return account; - }; - - const createSession = ( - aliceSession: olm.Session, - aliceAccount: olm.Account, - bobAccount: olm.Account, - regen: boolean = false, - forget: boolean = false, - invalid_sign: boolean = false, - ) => { - const bobOneTimeKeys = JSON.parse(bobAccount.one_time_keys()).curve25519; - bobAccount.mark_keys_as_published(); - const otk_id = Object.keys(bobOneTimeKeys)[0]; - - if (regen) { - bobAccount.generate_prekey(); - if (forget) { - bobAccount.forget_old_prekey(); - } - } - - if (invalid_sign) { - try { - aliceSession.create_outbound( - aliceAccount, - JSON.parse(bobAccount.identity_keys()).curve25519, - JSON.parse(bobAccount.identity_keys()).ed25519, - String(Object.values(JSON.parse(bobAccount.prekey()).curve25519)[0]), - bobAccount.sign(randomString(32)), - bobOneTimeKeys[otk_id], - ); - } catch (error) { - expect(error.message).toBe('OLM.BAD_SIGNATURE'); - return false; - } - - try { - aliceSession.create_outbound( - aliceAccount, - JSON.parse(bobAccount.identity_keys()).curve25519, - JSON.parse(bobAccount.identity_keys()).ed25519, - String(Object.values(JSON.parse(bobAccount.prekey()).curve25519)[0]), - randomString(43), - bobOneTimeKeys[otk_id], - ); - } catch (error) { - expect(error.message).toBe('OLM.INVALID_BASE64'); - return false; - } - } - - aliceSession.create_outbound( - aliceAccount, - JSON.parse(bobAccount.identity_keys()).curve25519, - JSON.parse(bobAccount.identity_keys()).ed25519, - String(Object.values(JSON.parse(bobAccount.prekey()).curve25519)[0]), - String(bobAccount.prekey_signature()), - bobOneTimeKeys[otk_id], - ); - - return aliceSession; - }; - - const createSessionWithoutOTK = ( - aliceSession: olm.Session, - aliceAccount: olm.Account, - bobAccount: olm.Account, - ) => { - aliceSession.create_outbound_without_otk( - aliceAccount, - JSON.parse(bobAccount.identity_keys()).curve25519, - JSON.parse(bobAccount.identity_keys()).ed25519, - String(Object.values(JSON.parse(bobAccount.prekey()).curve25519)[0]), - String(bobAccount.prekey_signature()), - ); - - return aliceSession; - }; - - const testRatchet = ( - aliceSession: olm.Session, - bobSession: olm.Session, - bobAccount: olm.Account, - num_msg: number = 1, - ) => { - let test_text = randomString(40); - let encrypted = aliceSession.encrypt(test_text); - expect(encrypted.type).toEqual(0); - - try { - bobSession.create_inbound(bobAccount, encrypted.body); - } catch (error) { - expect(error.message).toBe('OLM.BAD_MESSAGE_KEY_ID'); - return false; - } - - bobAccount.remove_one_time_keys(bobSession); - let decrypted = bobSession.decrypt(encrypted.type, encrypted.body); - expect(decrypted).toEqual(test_text); - - test_text = randomString(40); - encrypted = bobSession.encrypt(test_text); - expect(encrypted.type).toEqual(1); - decrypted = aliceSession.decrypt(encrypted.type, encrypted.body); - expect(decrypted).toEqual(test_text); - - const aliceEncrypted = aliceSession.encrypt(test_text); - expect(() => - aliceSession.decrypt(aliceEncrypted.type, aliceEncrypted.body), - ).toThrow('OLM.BAD_MESSAGE_MAC'); - - for (let index = 1; index < num_msg; index++) { - test_text = randomString(40); - encrypted = aliceSession.encrypt(test_text); - expect(encrypted.type).toEqual(1); - decrypted = bobSession.decrypt(encrypted.type, encrypted.body); - expect(decrypted).toEqual(test_text); - - test_text = randomString(40); - encrypted = bobSession.encrypt(test_text); - expect(encrypted.type).toEqual(1); - decrypted = aliceSession.decrypt(encrypted.type, encrypted.body); - expect(decrypted).toEqual(test_text); - } - - expect(() => - aliceSession.decrypt_sequential(encrypted.type, encrypted.body), - ).toThrow('OLM.OLM_ALREADY_DECRYPTED_OR_KEYS_SKIPPED'); - - return true; - }; - - const testRatchetSequential = ( - aliceSession: olm.Session, - bobSession: olm.Session, - bobAccount: olm.Account, - ) => { - let test_text = randomString(40); - let encrypted = aliceSession.encrypt(test_text); - expect(encrypted.type).toEqual(0); - - try { - bobSession.create_inbound(bobAccount, encrypted.body); - } catch (error) { - expect(error.message).toBe('OLM.BAD_MESSAGE_KEY_ID'); - return false; - } - - bobAccount.remove_one_time_keys(bobSession); - let decrypted = bobSession.decrypt(encrypted.type, encrypted.body); - expect(decrypted).toEqual(test_text); - - test_text = randomString(40); - encrypted = bobSession.encrypt(test_text); - expect(encrypted.type).toEqual(1); - decrypted = aliceSession.decrypt(encrypted.type, encrypted.body); - expect(decrypted).toEqual(test_text); - - const testText1 = 'message1'; - const encrypted1 = bobSession.encrypt(testText1); - const testText2 = 'message2'; - const encrypted2 = bobSession.encrypt(testText2); - - // encrypt message using alice session and trying to decrypt with - // the same session => `BAD_MESSAGE_MAC` - const aliceEncrypted = aliceSession.encrypt(test_text); - expect(() => - aliceSession.decrypt_sequential(aliceEncrypted.type, aliceEncrypted.body), - ).toThrow('OLM.BAD_MESSAGE_MAC'); - - // decrypting encrypted2 before encrypted1 using - // decrypt_sequential() => OLM_MESSAGE_OUT_OF_ORDER - expect(() => - aliceSession.decrypt_sequential(encrypted2.type, encrypted2.body), - ).toThrow('OLM.OLM_MESSAGE_OUT_OF_ORDER'); - - // test correct order - const decrypted1 = aliceSession.decrypt_sequential( - encrypted1.type, - encrypted1.body, - ); - expect(decrypted1).toEqual(testText1); - const decrypted2 = aliceSession.decrypt_sequential( - encrypted2.type, - encrypted2.body, - ); - expect(decrypted2).toEqual(testText2); - - // try to decrypt second time - // the same message => OLM_ALREADY_DECRYPTED_OR_KEYS_SKIPPED - expect(() => - aliceSession.decrypt_sequential(encrypted2.type, encrypted2.body), - ).toThrow('OLM.OLM_ALREADY_DECRYPTED_OR_KEYS_SKIPPED'); - - 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); - - expect(account.last_prekey_publish_time()).toEqual(0); - expect(account.prekey()).toBeDefined(); - expect(account.unpublished_prekey()).toBeDefined(); - account.mark_prekey_as_published(); - const last_published = account.last_prekey_publish_time(); - expect(last_published).toBeGreaterThan(0); - - try { - console.log(account.unpublished_prekey()); - } catch (error) { - expect(error.message).toContain('NO_UNPUBLISHED_PREKEY'); - } - account.forget_old_prekey(); - - account.generate_prekey(); - expect(account.prekey()).toBeDefined(); - expect(account.unpublished_prekey()).toBeDefined(); - - expect(account.last_prekey_publish_time()).toEqual(last_published); - account.mark_prekey_as_published(); - expect(account.last_prekey_publish_time()).toBeGreaterThanOrEqual( - last_published, - ); - account.forget_old_prekey(); - }); - - it('should encrypt and decrypt', async () => { - await olm.init(); - const aliceAccount = initAccount(); - const bobAccount = initAccount(); - const aliceSession = new olm.Session(); - const bobSession = new olm.Session(); - - createSession(aliceSession, aliceAccount, bobAccount); - expect(testRatchet(aliceSession, bobSession, bobAccount)).toBeTrue; - }); - - it('should encrypt and decrypt sequential', async () => { - await olm.init(); - const aliceAccount = initAccount(); - const bobAccount = initAccount(); - const aliceSession = new olm.Session(); - const bobSession = new olm.Session(); - - createSession(aliceSession, aliceAccount, bobAccount); - expect(testRatchetSequential(aliceSession, bobSession, bobAccount)) - .toBeTrue; - }); - - it('should encrypt and decrypt, even after a prekey is rotated', async () => { - await olm.init(); - const aliceAccount = initAccount(); - const bobAccount = initAccount(); - const aliceSession = new olm.Session(); - const bobSession = new olm.Session(); - - createSession(aliceSession, aliceAccount, bobAccount, true); - expect(testRatchet(aliceSession, bobSession, bobAccount)).toBeTrue; - }); - - it('should not encrypt and decrypt, after the old prekey is forgotten', async () => { - await olm.init(); - const aliceAccount = initAccount(); - const bobAccount = initAccount(); - const aliceSession = new olm.Session(); - const bobSession = new olm.Session(); - - createSession(aliceSession, aliceAccount, bobAccount, true, true); - expect(testRatchet(aliceSession, bobSession, bobAccount)).toBeFalse; - }); - - it('should encrypt and decrypt repeatedly', async () => { - await olm.init(); - const aliceAccount = initAccount(); - const bobAccount = initAccount(); - const aliceSession = new olm.Session(); - const bobSession = new olm.Session(); - - createSession(aliceSession, aliceAccount, bobAccount, false, false); - expect(testRatchet(aliceSession, bobSession, bobAccount, 100)).toBeTrue; - }); - - it('should not encrypt and decrypt if prekey is not signed correctly', async () => { - await olm.init(); - const aliceAccount = initAccount(); - const bobAccount = initAccount(); - const aliceSession = new olm.Session(); - - expect( - createSession(aliceSession, aliceAccount, bobAccount, false, false, true), - ).toBeFalse; - }); - - it('should create session without one-time key', async () => { - await olm.init(); - const aliceAccount = initAccount(); - const bobAccount = initAccount(); - const aliceSession = new olm.Session(); - const bobSession = new olm.Session(); - - expect(createSessionWithoutOTK(aliceSession, aliceAccount, bobAccount)) - .toBeTrue; - expect(testRatchet(aliceSession, bobSession, bobAccount, 100)).toBeTrue; - }); -}); diff --git a/lib/flow-typed/npm/@commapp/vodozemac_vx.x.x.js b/lib/flow-typed/npm/@commapp/vodozemac_vx.x.x.js new file mode 100644 --- /dev/null +++ b/lib/flow-typed/npm/@commapp/vodozemac_vx.x.x.js @@ -0,0 +1,91 @@ +// flow-typed signature: 39f28f3c9faafcadb70d2bbe7f61d07e +// flow-typed version: <>/@commapp/vodozemac_v0.1.0/flow_v0.269.1 + +declare module '@commapp/vodozemac' { + declare export default function init(opts?: Object): Promise; + declare export function initSync(options: { module: Buffer | Uint8Array }): void; + + declare export class Account { + constructor(): Account; + free(): void; + + +ed25519_key: string; + +curve25519_key: string; + sign(message: string): string; + + one_time_keys(): Map; + mark_keys_as_published(): void; + max_number_of_one_time_keys(): number; + generate_one_time_keys(count: number): void; + + generate_prekey(): void; + prekey(): ?string; + unpublished_prekey(): ?string; + prekey_signature(): ?string; + forget_old_prekey(): void; + + mark_prekey_as_published(): boolean; + last_prekey_publish_time(): bigint; + + pickle(pickle_key: Uint8Array): string; + static from_pickle(pickle: string, pickle_key: Uint8Array): Account; + static from_libolm_pickle(pickle: string, pickle_key: Uint8Array): Account; + + create_outbound_session( + identity_key: string, + signing_key: string, + one_time_key: ?string, + pre_key: string, + pre_key_signature: string, + olm_compatibility_mode: boolean, + ): Session; + + create_inbound_session( + identity_key: string, + message: OlmMessage, + ): InboundCreationResult; + } + + declare export class Session { + free(): void; + + pickle(pickle_key: Uint8Array): string; + static from_pickle(pickle: string, pickle_key: Uint8Array): Session; + static from_libolm_pickle(pickle: string, pickle_key: Uint8Array): Session; + + +session_id: string; + has_received_message(): boolean; + is_sender_chain_empty(): boolean; + session_matches(message: OlmMessage): boolean; + + encrypt(plaintext: string): OlmMessage; + decrypt(message: OlmMessage): string; + } + + declare export class OlmMessage { + constructor(message_type: number, ciphertext: string): OlmMessage; + free(): void; + + +ciphertext: string; + +message_type: 0 | 1, // 0: PreKey, 1: Message + } + + declare export class InboundCreationResult { + free(): void; + + +plaintext: string; + into_session(): Session; + } + + declare export class Utility { + constructor(): void; + free(): void; + + sha256(input: string | Uint8Array): string; + ed25519_verify( + key: string, + message: string | Uint8Array, + signature: string, + ): void; + } +} diff --git a/lib/package.json b/lib/package.json --- a/lib/package.json +++ b/lib/package.json @@ -38,6 +38,7 @@ "dependencies": { "@commapp/olm": "0.2.6", "@ensdomains/ensjs": "^4.0.1", + "@commapp/vodozemac": "0.1.0", "@khanacademy/simple-markdown": "^2.1.0", "@rainbow-me/rainbowkit": "^2.0.7", "base-64": "^0.1.0", 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-memory-utils.js b/lib/utils/olm-memory-utils.js deleted file mode 100644 --- a/lib/utils/olm-memory-utils.js +++ /dev/null @@ -1,75 +0,0 @@ -// @flow - -import olm from '@commapp/olm'; - -let olmTotalMemory = null, - olmUsedMemory = null; - -function verifyMemoryUsage(method: string) { - try { - if (olmTotalMemory === null && olmUsedMemory === null) { - olmTotalMemory = olm.get_total_memory(); - olmUsedMemory = olm.get_used_memory(); - console.error( - `Olm first time memory check - Total: ${olmTotalMemory ?? -1}, Used: ${ - olmUsedMemory ?? -1 - }`, - ); - return; - } - - const currentTotalMemory = olm.get_total_memory(); - if (currentTotalMemory !== olmTotalMemory) { - console.error( - `Olm's total memory changed from ${olmTotalMemory ?? -1} ` + - `to ${currentTotalMemory} after executing ${method} method`, - ); - olmTotalMemory = currentTotalMemory; - } - - const currentUsedMemory = olm.get_used_memory(); - if (currentUsedMemory !== olmUsedMemory) { - console.error( - `Olm's used memory changed from ${olmUsedMemory ?? -1} ` + - `to ${currentUsedMemory} after executing ${method} method`, - ); - olmUsedMemory = currentUsedMemory; - } - } catch (e) { - console.error('Encountered error while trying log Olm memory', e); - } -} - -type OlmMemory = { - +total: ?number, - +used: ?number, -}; - -function getOlmMemory(): OlmMemory { - try { - const total = olm.get_total_memory(); - const used = olm.get_used_memory(); - return { total, used }; - } catch (e) { - console.error('Encountered error in getOlmMemory:', e); - return { total: null, used: null }; - } -} - -function compareAndLogOlmMemory(previous: OlmMemory, method: string) { - const current = getOlmMemory(); - if (current.total !== previous.total) { - console.error( - `Olm's total memory changed from ${previous.total ?? -1} ` + - `to ${current.total ?? -1} during execution of ${method} method`, - ); - } - if (current.used !== previous.used) { - console.error( - `Olm's used memory changed from ${previous.used ?? -1} ` + - `to ${current.used ?? -1} during execution of ${method} method`, - ); - } -} - -export { verifyMemoryUsage, getOlmMemory, compareAndLogOlmMemory }; 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 '@commapp/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 '@commapp/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); @@ -128,8 +130,7 @@ // the corresponding .cpp file // at `native/cpp/CommonCpp/CryptoTools/CryptoModule.cpp`. invalidSessionVersion: 'INVALID_SESSION_VERSION', - alreadyDecrypted: - OLM_SESSION_ERROR_PREFIX + `ALREADY_DECRYPTED_OR_KEYS_SKIPPED`, + alreadyDecrypted: `The message key with the given key can't be created`, }); function hasHigherDeviceID( 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 '@commapp/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 + return Account.from_pickle(pickledAccount, keyBytes); + } catch (e) { + console.log( + 'Failed to unpickle account with vodozemac format, falling back to libolm:', + e.message, + ); + 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 + return Session.from_pickle(pickledSession, keyBytes); + } catch (e) { + console.log( + 'Failed to unpickle session with vodozemac format, falling back to libolm:', + e.message, + ); + return Session.from_libolm_pickle(pickledSession, fullKeyBytes); + } +} + +export { + getVodozemacPickleKey, + unpickleVodozemacAccount, + unpickleVodozemacSession, +}; diff --git a/native/native_rust_library/Cargo.lock b/native/native_rust_library/Cargo.lock --- a/native/native_rust_library/Cargo.lock +++ b/native/native_rust_library/Cargo.lock @@ -1418,6 +1418,7 @@ name = "native_rust_library" version = "0.1.0" dependencies = [ + "anyhow", "argon2", "backup_client", "base64 0.21.7", @@ -1431,6 +1432,7 @@ "regex", "serde", "serde_json", + "sha2", "siwe", "tokio", "tokio-util", @@ -2870,7 +2872,7 @@ [[package]] name = "vodozemac" version = "0.9.0" -source = "git+https://github.com/CommE2E/vodozemac#3c142a35f114fcc9edb9dc766b051716c5160150" +source = "git+https://github.com/CommE2E/vodozemac#040c9875016241680cb89387a3d6aa1af93b1214" dependencies = [ "aes", "arrayvec", diff --git a/native/native_rust_library/Cargo.toml b/native/native_rust_library/Cargo.toml --- a/native/native_rust_library/Cargo.toml +++ b/native/native_rust_library/Cargo.toml @@ -22,6 +22,8 @@ base64 = "0.21" regex = "1.10" vodozemac = { git = "https://github.com/CommE2E/vodozemac", features = ["libolm-compat"] } +anyhow = "1.0.97" +sha2 = "0.10" [target.'cfg(target_os = "android")'.dependencies] backup_client = { path = "../../shared/backup_client", default-features = false, features = [ diff --git a/native/native_rust_library/build.rs b/native/native_rust_library/build.rs --- a/native/native_rust_library/build.rs +++ b/native/native_rust_library/build.rs @@ -197,7 +197,7 @@ .expect("Couldn't write backup service config"); println!("cargo:rerun-if-changed=src/lib.rs"); - println!("cargo:rerun-if-changed=src/session.rs"); + println!("cargo:rerun-if-changed=src/vodozemac.rs"); println!("cargo:rerun-if-changed={}", IdentityServiceConfig::FILEPATH); println!("cargo:rerun-if-changed={}", BackupServiceConfig::FILEPATH); } diff --git a/native/native_rust_library/src/lib.rs b/native/native_rust_library/src/lib.rs --- a/native/native_rust_library/src/lib.rs +++ b/native/native_rust_library/src/lib.rs @@ -10,12 +10,16 @@ mod backup; mod constants; mod identity; -mod session; mod utils; +mod vodozemac; use crate::argon2_tools::compute_backup_key_str; use crate::utils::jsi_callbacks::handle_string_result_as_callback; -use session::{session_from_pickle, EncryptResult, VodozemacSession}; +use vodozemac::{ + account_from_pickle, account_new, encrypt_result_new, session_from_pickle, + sha256, verify_ed25519_signature, verify_prekey_signature, EncryptResult, + InboundCreationResult, VodozemacAccount, VodozemacSession, +}; mod generated { // We get the CODE_VERSION from this generated file @@ -568,36 +572,100 @@ } // Vodozemac crypto functions + // NOTE: We use `not(target_os = "ios")` to target Android instead of + // checking for Android directly due to problems with setting the Android + // target OS on CI. + #[cfg(not(target_os = "ios"))] extern "Rust" { // NOTE: Keep in sync with Vodozemac crypto functions block // in native/vodozemac_bindings/src/lib.rs. - #[cfg(target_os = "android")] - type VodozemacSession; - #[cfg(target_os = "android")] + + // EncryptResult type type EncryptResult; - #[cfg(target_os = "android")] - fn pickle(self: &VodozemacSession, pickle_key: &[u8; 32]) -> String; - #[cfg(target_os = "android")] + fn encrypt_result_new( + encrypted_message: String, + message_type: u32, + ) -> Box; fn encrypted_message(self: &EncryptResult) -> String; - #[cfg(target_os = "android")] fn message_type(self: &EncryptResult) -> u32; - #[cfg(target_os = "android")] + + // VodozemacSession type + type VodozemacSession; + fn pickle(self: &VodozemacSession, pickle_key: &[u8; 32]) -> String; fn encrypt( self: &mut VodozemacSession, plaintext: &str, ) -> Result>; - #[cfg(target_os = "android")] fn decrypt( self: &mut VodozemacSession, encrypted_message: String, message_type: u32, ) -> Result; + fn has_received_message(self: &VodozemacSession) -> bool; + fn is_sender_chain_empty(self: &VodozemacSession) -> bool; - #[cfg(target_os = "android")] pub fn session_from_pickle( session_state: String, session_key: String, ) -> Result>; + + // VodozemacAccount type + type VodozemacAccount; + fn pickle(self: &VodozemacAccount, pickle_key: &[u8; 32]) -> String; + fn ed25519_key(self: &VodozemacAccount) -> String; + fn curve25519_key(self: &VodozemacAccount) -> String; + fn sign(self: &VodozemacAccount, message: &str) -> String; + fn generate_one_time_keys(self: &mut VodozemacAccount, count: usize); + fn one_time_keys(self: &VodozemacAccount) -> Vec; + fn mark_keys_as_published(self: &mut VodozemacAccount); + fn max_number_of_one_time_keys(self: &VodozemacAccount) -> usize; + fn mark_prekey_as_published(self: &mut VodozemacAccount) -> bool; + fn generate_prekey(self: &mut VodozemacAccount); + fn forget_old_prekey(self: &mut VodozemacAccount); + fn last_prekey_publish_time(self: &mut VodozemacAccount) -> u64; + fn prekey(self: &VodozemacAccount) -> String; + fn unpublished_prekey(self: &VodozemacAccount) -> String; + fn prekey_signature(self: &VodozemacAccount) -> String; + fn create_outbound_session( + self: &VodozemacAccount, + identity_key: &str, + signing_key: &str, + one_time_key: &str, + pre_key: &str, + pre_key_signature: &str, + olm_compatibility_mode: bool, + ) -> Result>; + fn create_inbound_session( + self: &mut VodozemacAccount, + identity_key: &str, + message: &EncryptResult, + ) -> Result>; + + pub fn account_new() -> Box; + + pub fn account_from_pickle( + account_state: String, + session_key: String, + ) -> Result>; + + pub fn verify_ed25519_signature( + public_key: &str, + message: &str, + signature: &str, + ) -> Result<()>; + + pub fn verify_prekey_signature( + public_key: &str, + prekey_base64: &str, + signature: &str, + ) -> Result<()>; + + pub fn sha256(input: &[u8]) -> String; + + // InboundCreationResult type + type InboundCreationResult; + fn plaintext(self: &InboundCreationResult) -> String; + fn take_session(self: &mut InboundCreationResult) -> Box; } } diff --git a/native/native_rust_library/src/session.rs b/native/native_rust_library/src/session.rs deleted file mode 100644 --- a/native/native_rust_library/src/session.rs +++ /dev/null @@ -1,114 +0,0 @@ -use vodozemac::olm::{Session, SessionPickle}; -use vodozemac::{olm, PickleError}; - -pub struct VodozemacSession(pub(crate) vodozemac::olm::Session); - -impl From for VodozemacSession { - fn from(session: Session) -> Self { - VodozemacSession(session) - } -} - -pub struct EncryptResult { - pub encrypted_message: String, - pub message_type: u32, -} - -impl EncryptResult { - pub fn encrypted_message(&self) -> String { - self.encrypted_message.clone() - } - - pub fn message_type(&self) -> u32 { - self.message_type - } -} - -impl VodozemacSession { - pub fn pickle(&self, pickle_key: &[u8; 32]) -> String { - self.0.pickle().encrypt(pickle_key) - } - - pub fn encrypt( - &mut self, - plaintext: &str, - ) -> Result, String> { - let olm_message = self.0.encrypt(plaintext.as_bytes()); - - let (message_type, encrypted_message) = match olm_message { - olm::OlmMessage::Normal(msg) => (1, msg.to_base64()), - olm::OlmMessage::PreKey(msg) => (0, msg.to_base64()), - }; - - Ok(Box::from(EncryptResult { - encrypted_message, - message_type: message_type as u32, - })) - } - - pub fn decrypt( - &mut self, - encrypted_message: String, - message_type: u32, - ) -> Result { - let olm_message: vodozemac::olm::OlmMessage = match message_type { - 0 => olm::PreKeyMessage::from_base64(encrypted_message.as_str()) - .map_err(|e| e.to_string())? - .into(), - 1 => olm::Message::from_base64(encrypted_message.as_str()) - .map_err(|e| e.to_string())? - .into(), - _ => return Err("wrong message type".to_string()), - }; - - let result = self.0.decrypt(&olm_message).map_err(|e| e.to_string())?; - let plaintext = String::from_utf8(result).expect("Invalid UTF-8"); - - Ok(plaintext) - } -} - -pub fn session_from_pickle( - session_state: String, - session_key: String, -) -> Result, String> { - let key_bytes = session_key.as_bytes(); - - //NOTE: vvodozemac 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. - let key: &[u8; 32] = &key_bytes[0..32] - .try_into() - .expect("String must be at least 32 bytes"); - - let session_pickle = - match SessionPickle::from_encrypted(session_state.as_str(), key) { - Ok(pickle) => Some(pickle), - Err(e) => { - match e { - PickleError::Base64(base64_error) => { - return Err(base64_error.to_string()); - } - //TODO: Use only specific error type - PickleError::Decryption(_) => { - println!("Decryption error, will try from_libolm_pickle"); - None - } - PickleError::Serialization(serialization_error) => { - return Err(serialization_error.to_string()); - } - } - } - }; - - let session: VodozemacSession = if let Some(pickle) = session_pickle { - Session::from_pickle(pickle).into() - } else { - Session::from_libolm_pickle(&session_state, session_key.as_bytes()) - .map_err(|e| e.to_string())? - .into() - }; - - Ok(Box::from(session)) -} diff --git a/native/native_rust_library/src/vodozemac.rs b/native/native_rust_library/src/vodozemac.rs new file mode 100644 --- /dev/null +++ b/native/native_rust_library/src/vodozemac.rs @@ -0,0 +1,409 @@ +use sha2::{Digest, Sha256}; +use vodozemac::olm::{Account, AccountPickle, Session, SessionPickle}; +use vodozemac::{olm, PickleError}; + +pub struct VodozemacSession(pub(crate) vodozemac::olm::Session); + +impl From for VodozemacSession { + fn from(session: Session) -> Self { + VodozemacSession(session) + } +} + +pub struct EncryptResult { + pub encrypted_message: String, + pub message_type: u32, +} + +pub fn encrypt_result_new( + encrypted_message: String, + message_type: u32, +) -> Box { + Box::new(EncryptResult { + encrypted_message, + message_type, + }) +} + +impl EncryptResult { + pub fn encrypted_message(&self) -> String { + self.encrypted_message.clone() + } + + pub fn message_type(&self) -> u32 { + self.message_type + } +} + +impl TryFrom<&EncryptResult> for olm::OlmMessage { + type Error = anyhow::Error; + + fn try_from(message: &EncryptResult) -> Result { + match message.message_type { + 0 => { + let prekey = + olm::PreKeyMessage::from_base64(&message.encrypted_message)?; + Ok(prekey.into()) + } + 1 => { + let msg = olm::Message::from_base64(&message.encrypted_message)?; + Ok(msg.into()) + } + _ => anyhow::bail!("Invalid message type: {}", message.message_type), + } + } +} + +impl VodozemacSession { + pub fn pickle(&self, pickle_key: &[u8; 32]) -> String { + self.0.pickle().encrypt(pickle_key) + } + + pub fn encrypt( + &mut self, + plaintext: &str, + ) -> Result, String> { + let olm_message = self.0.encrypt(plaintext.as_bytes()); + + let (message_type, encrypted_message) = match olm_message { + olm::OlmMessage::Normal(msg) => (1, msg.to_base64()), + olm::OlmMessage::PreKey(msg) => (0, msg.to_base64()), + }; + + Ok(Box::from(EncryptResult { + encrypted_message, + message_type: message_type as u32, + })) + } + + pub fn decrypt( + &mut self, + encrypted_message: String, + message_type: u32, + ) -> Result { + let encrypted_result = EncryptResult { + encrypted_message, + message_type, + }; + let olm_message: olm::OlmMessage = (&encrypted_result) + .try_into() + .map_err(|e: anyhow::Error| e.to_string())?; + + let result = self.0.decrypt(&olm_message).map_err(|e| e.to_string())?; + let plaintext = String::from_utf8(result).expect("Invalid UTF-8"); + + Ok(plaintext) + } + + pub fn has_received_message(&self) -> bool { + self.0.has_received_message() + } + + pub fn is_sender_chain_empty(&self) -> bool { + self.0.is_sender_chain_empty() + } +} + +pub fn session_from_pickle( + session_state: String, + session_key: String, +) -> Result, String> { + let key_bytes = session_key.as_bytes(); + + // 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. + let key: &[u8; 32] = &key_bytes[0..32] + .try_into() + .expect("String must be at least 32 bytes"); + + let session_pickle = + match SessionPickle::from_encrypted(session_state.as_str(), key) { + Ok(pickle) => Some(pickle), + Err(e) => match e { + PickleError::Base64(base64_error) => { + return Err(base64_error.to_string()); + } + PickleError::Decryption(_) => None, + PickleError::Serialization(serialization_error) => { + return Err(serialization_error.to_string()); + } + }, + }; + + let session: VodozemacSession = if let Some(pickle) = session_pickle { + Session::from_pickle(pickle).into() + } else { + Session::from_libolm_pickle(&session_state, session_key.as_bytes()) + .map_err(|e| e.to_string())? + .into() + }; + + Ok(Box::from(session)) +} + +pub struct InboundCreationResult { + session: Option, + plaintext: String, +} + +impl From for InboundCreationResult { + fn from(result: vodozemac::olm::InboundCreationResult) -> Self { + InboundCreationResult { + session: Some(VodozemacSession(result.session)), + plaintext: String::from_utf8(result.plaintext) + .expect("Invalid UTF-8 in plaintext"), + } + } +} + +impl InboundCreationResult { + pub fn plaintext(&self) -> String { + self.plaintext.clone() + } + + pub fn take_session(&mut self) -> Box { + Box::new(self.session.take().expect("Session has already been taken")) + } +} + +pub struct VodozemacAccount(pub(crate) vodozemac::olm::Account); + +impl From for VodozemacAccount { + fn from(account: Account) -> Self { + VodozemacAccount(account) + } +} + +impl VodozemacAccount { + pub fn pickle(&self, pickle_key: &[u8; 32]) -> String { + self.0.pickle().encrypt(pickle_key) + } + + pub fn ed25519_key(&self) -> String { + self.0.ed25519_key().to_base64() + } + + pub fn curve25519_key(&self) -> String { + self.0.curve25519_key().to_base64() + } + + pub fn sign(&self, message: &str) -> String { + self.0.sign(message).to_base64() + } + + pub fn generate_one_time_keys(&mut self, count: usize) { + self.0.generate_one_time_keys(count); + } + + pub fn one_time_keys(&self) -> Vec { + self + .0 + .one_time_keys() + .into_values() + .map(|v| v.to_base64()) + .collect() + } + + pub fn mark_keys_as_published(&mut self) { + self.0.mark_keys_as_published() + } + + pub fn max_number_of_one_time_keys(&self) -> usize { + self.0.max_number_of_one_time_keys() + } + + pub fn mark_prekey_as_published(&mut self) -> bool { + self.0.mark_prekey_as_published() + } + + pub fn generate_prekey(&mut self) { + self.0.generate_prekey() + } + + pub fn forget_old_prekey(&mut self) { + self.0.forget_old_prekey() + } + + pub fn last_prekey_publish_time(&mut self) -> u64 { + self.0.get_last_prekey_publish_time() + } + + pub fn prekey(&self) -> String { + self + .0 + .prekey() + .map(|key| key.to_base64()) + .unwrap_or_default() + } + + pub fn unpublished_prekey(&self) -> String { + self + .0 + .unpublished_prekey() + .map(|key| key.to_base64()) + .unwrap_or_default() + } + + pub fn prekey_signature(&self) -> String { + self.0.get_prekey_signature().unwrap_or_default() + } + + pub fn create_outbound_session( + &self, + identity_key: &str, + signing_key: &str, + one_time_key: &str, + pre_key: &str, + pre_key_signature: &str, + olm_compatibility_mode: bool, + ) -> Result, String> { + let session_config = vodozemac::olm::SessionConfig::version_1(); + let identity_key = + vodozemac::Curve25519PublicKey::from_base64(identity_key) + .map_err(|e| e.to_string())?; + let signing_key = vodozemac::Ed25519PublicKey::from_base64(signing_key) + .map_err(|e| e.to_string())?; + // NOTE: We use an empty string to represent None because cxx doesn't + // support Option<&str> in FFI function signatures. + let one_time_key = if one_time_key.is_empty() { + None + } else { + Some( + vodozemac::Curve25519PublicKey::from_base64(one_time_key) + .map_err(|e| e.to_string())?, + ) + }; + let pre_key = vodozemac::Curve25519PublicKey::from_base64(pre_key) + .map_err(|e| e.to_string())?; + + let session = self + .0 + .create_outbound_session( + session_config, + identity_key, + signing_key, + one_time_key, + pre_key, + pre_key_signature.to_string(), + olm_compatibility_mode, + ) + .map_err(|e| e.to_string())?; + + Ok(Box::new(VodozemacSession(session))) + } + + pub fn create_inbound_session( + &mut self, + identity_key: &str, + message: &EncryptResult, + ) -> Result, String> { + let identity_key = + vodozemac::Curve25519PublicKey::from_base64(identity_key) + .map_err(|e| e.to_string())?; + let olm_message: olm::OlmMessage = message + .try_into() + .map_err(|e: anyhow::Error| e.to_string())?; + + if let olm::OlmMessage::PreKey(message) = olm_message { + Ok(Box::new( + self + .0 + .create_inbound_session(identity_key, &message) + .map_err(|e| e.to_string())? + .into(), + )) + } else { + Err("Invalid message type, a pre-key message is required".to_string()) + } + } +} + +pub fn account_new() -> Box { + let account = Account::new(); + Box::new(VodozemacAccount(account)) +} + +pub fn account_from_pickle( + account_state: String, + account_key: String, +) -> Result, String> { + let key_bytes = account_key.as_bytes(); + + // 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. + let key: &[u8; 32] = &key_bytes[0..32] + .try_into() + .expect("String must be at least 32 bytes"); + + let account_pickle = + match AccountPickle::from_encrypted(account_state.as_str(), key) { + Ok(pickle) => Some(pickle), + Err(e) => match e { + PickleError::Base64(base64_error) => { + return Err(base64_error.to_string()); + } + PickleError::Decryption(_) => None, + PickleError::Serialization(serialization_error) => { + return Err(serialization_error.to_string()); + } + }, + }; + + let account: VodozemacAccount = if let Some(pickle) = account_pickle { + Account::from_pickle(pickle).into() + } else { + Account::from_libolm_pickle(&account_state, account_key.as_bytes()) + .map_err(|e| e.to_string())? + .into() + }; + + Ok(Box::from(account)) +} + +pub fn verify_ed25519_signature( + public_key: &str, + message: &str, + signature: &str, +) -> Result<(), String> { + let public_key = vodozemac::Ed25519PublicKey::from_base64(public_key) + .map_err(|e| e.to_string())?; + + let signature = vodozemac::Ed25519Signature::from_base64(signature) + .map_err(|e| e.to_string())?; + + public_key + .verify(message.as_bytes(), &signature) + .map_err(|e| e.to_string()) +} + +pub fn verify_prekey_signature( + public_key: &str, + prekey_base64: &str, + signature: &str, +) -> Result<(), String> { + let public_key = vodozemac::Ed25519PublicKey::from_base64(public_key) + .map_err(|e| e.to_string())?; + + let signature = vodozemac::Ed25519Signature::from_base64(signature) + .map_err(|e| e.to_string())?; + + // Decode the base64 prekey to raw bytes for verification + let prekey_bytes = + vodozemac::base64_decode(prekey_base64).map_err(|e| e.to_string())?; + + public_key + .verify(&prekey_bytes, &signature) + .map_err(|e| e.to_string()) +} + +pub fn sha256(input: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(&input); + let hash = hasher.finalize(); + + vodozemac::base64_encode(hash) +} diff --git a/native/vodozemac_bindings/Cargo.lock b/native/vodozemac_bindings/Cargo.lock --- a/native/vodozemac_bindings/Cargo.lock +++ b/native/vodozemac_bindings/Cargo.lock @@ -897,7 +897,7 @@ [[package]] name = "vodozemac" version = "0.9.0" -source = "git+https://github.com/CommE2E/vodozemac#3c142a35f114fcc9edb9dc766b051716c5160150" +source = "git+https://github.com/CommE2E/vodozemac#040c9875016241680cb89387a3d6aa1af93b1214" dependencies = [ "aes", "arrayvec", @@ -934,6 +934,7 @@ "regex", "serde", "serde_json", + "sha2", "vodozemac", ] diff --git a/native/vodozemac_bindings/Cargo.toml b/native/vodozemac_bindings/Cargo.toml --- a/native/vodozemac_bindings/Cargo.toml +++ b/native/vodozemac_bindings/Cargo.toml @@ -10,6 +10,7 @@ serde_json = "1.0" derive_more = "0.99" anyhow = "1.0.97" +sha2 = "0.10" [build-dependencies] cxx-build = "=1.0.75" diff --git a/native/vodozemac_bindings/build.rs b/native/vodozemac_bindings/build.rs --- a/native/vodozemac_bindings/build.rs +++ b/native/vodozemac_bindings/build.rs @@ -3,4 +3,5 @@ cxx_build::bridge("src/lib.rs").flag_if_supported("-std=c++17"); println!("cargo:rerun-if-changed=src/lib.rs"); + println!("cargo:rerun-if-changed=src/vodozemac.rs"); } diff --git a/native/vodozemac_bindings/src/lib.rs b/native/vodozemac_bindings/src/lib.rs --- a/native/vodozemac_bindings/src/lib.rs +++ b/native/vodozemac_bindings/src/lib.rs @@ -1,23 +1,31 @@ use std::error::Error as StdError; -mod session; +mod vodozemac; -use session::{session_from_pickle, VodozemacSession}; +use vodozemac::{session_from_pickle, VodozemacSession}; -use crate::session::*; +use crate::vodozemac::*; #[cxx::bridge] pub mod ffi { // Vodozemac crypto functions - // NOTE: Keep in sync with Vodozemac crypto functions block - // in native/native_rust_library/src/lib.rs extern "Rust" { - type VodozemacSession; + // NOTE: Keep in sync with Vodozemac crypto functions block + // in native/native_rust_library/src/lib.rs + + // EncryptResult type type EncryptResult; - fn pickle(self: &VodozemacSession, pickle_key: &[u8; 32]) -> String; + fn encrypt_result_new( + encrypted_message: String, + message_type: u32, + ) -> Box; fn encrypted_message(self: &EncryptResult) -> String; fn message_type(self: &EncryptResult) -> u32; + + // VodozemacSession type + type VodozemacSession; + fn pickle(self: &VodozemacSession, pickle_key: &[u8; 32]) -> String; fn encrypt( self: &mut VodozemacSession, plaintext: &str, @@ -27,11 +35,71 @@ encrypted_message: String, message_type: u32, ) -> Result; + fn has_received_message(self: &VodozemacSession) -> bool; + fn is_sender_chain_empty(self: &VodozemacSession) -> bool; pub fn session_from_pickle( session_state: String, session_key: String, ) -> Result>; + + // VodozemacAccount type + type VodozemacAccount; + fn pickle(self: &VodozemacAccount, pickle_key: &[u8; 32]) -> String; + fn ed25519_key(self: &VodozemacAccount) -> String; + fn curve25519_key(self: &VodozemacAccount) -> String; + fn sign(self: &VodozemacAccount, message: &str) -> String; + fn generate_one_time_keys(self: &mut VodozemacAccount, count: usize); + fn one_time_keys(self: &VodozemacAccount) -> Vec; + fn mark_keys_as_published(self: &mut VodozemacAccount); + fn max_number_of_one_time_keys(self: &VodozemacAccount) -> usize; + fn mark_prekey_as_published(self: &mut VodozemacAccount) -> bool; + fn generate_prekey(self: &mut VodozemacAccount); + fn forget_old_prekey(self: &mut VodozemacAccount); + fn last_prekey_publish_time(self: &mut VodozemacAccount) -> u64; + fn prekey(self: &VodozemacAccount) -> String; + fn unpublished_prekey(self: &VodozemacAccount) -> String; + fn prekey_signature(self: &VodozemacAccount) -> String; + fn create_outbound_session( + self: &VodozemacAccount, + identity_key: &str, + signing_key: &str, + one_time_key: &str, + pre_key: &str, + pre_key_signature: &str, + olm_compatibility_mode: bool, + ) -> Result>; + fn create_inbound_session( + self: &mut VodozemacAccount, + identity_key: &str, + message: &EncryptResult, + ) -> Result>; + + pub fn account_new() -> Box; + + pub fn account_from_pickle( + account_state: String, + session_key: String, + ) -> Result>; + + pub fn verify_ed25519_signature( + public_key: &str, + message: &str, + signature: &str, + ) -> Result<()>; + + pub fn verify_prekey_signature( + public_key: &str, + prekey_base64: &str, + signature: &str, + ) -> Result<()>; + + pub fn sha256(input: &[u8]) -> String; + + // InboundCreationResult type + type InboundCreationResult; + fn plaintext(self: &InboundCreationResult) -> String; + fn take_session(self: &mut InboundCreationResult) -> Box; } } diff --git a/native/vodozemac_bindings/src/session.rs b/native/vodozemac_bindings/src/session.rs deleted file mode 120000 --- a/native/vodozemac_bindings/src/session.rs +++ /dev/null @@ -1 +0,0 @@ -../../native_rust_library/src/session.rs \ No newline at end of file diff --git a/native/vodozemac_bindings/src/vodozemac.rs b/native/vodozemac_bindings/src/vodozemac.rs new file mode 120000 --- /dev/null +++ b/native/vodozemac_bindings/src/vodozemac.rs @@ -0,0 +1 @@ +../../native_rust_library/src/vodozemac.rs \ No newline at end of file diff --git a/web/crypto/olm-api.js b/web/crypto/olm-api.js --- a/web/crypto/olm-api.js +++ b/web/crypto/olm-api.js @@ -3,7 +3,10 @@ import { type OlmAPI } from 'lib/types/crypto-types.js'; import { getCommSharedWorker } from '../shared-worker/shared-worker-provider.js'; -import { getOlmWasmPath } from '../shared-worker/utils/constants.js'; +import { + getOlmWasmPath, + getVodozemacWasmPath, +} from '../shared-worker/utils/constants.js'; import { workerRequestMessageTypes, workerResponseMessageTypes, @@ -41,6 +44,7 @@ await sharedWorker.schedule({ type: workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, olmWasmPath: getOlmWasmPath(), + vodozemacWasmPath: getVodozemacWasmPath(), }); }, getUserPublicKey: proxyToWorker('getUserPublicKey'), diff --git a/web/olm/olm-utils.js b/web/olm/olm-utils.js deleted file mode 100644 --- a/web/olm/olm-utils.js +++ /dev/null @@ -1,16 +0,0 @@ -// @flow - -import olm from '@commapp/olm'; - -declare var olmFilename: string; - -async function initOlm(): Promise { - if (!olmFilename) { - return await olm.init(); - } - const locateFile = (wasmFilename: string, httpAssetsHost: string) => - httpAssetsHost + olmFilename; - return await olm.init({ locateFile }); -} - -export { initOlm }; diff --git a/web/olm/olm.test.js b/web/olm/olm.test.js deleted file mode 100644 --- a/web/olm/olm.test.js +++ /dev/null @@ -1,28 +0,0 @@ -// @flow - -import olm from '@commapp/olm'; - -describe('olm.Account', () => { - it('should construct an empty olm.Account', async () => { - await olm.init(); - const account = new olm.Account(); - expect(account).toBeDefined(); - }); - it('should be able to generate and return prekey', async () => { - await olm.init(); - const account = new olm.Account(); - account.create(); - account.generate_prekey(); - expect(account.prekey()).toBeDefined(); - }); - it('should be able to generate and return one-time keys', async () => { - await olm.init(); - const account = new olm.Account(); - account.create(); - account.generate_one_time_keys(5); - const oneTimeKeysObject = JSON.parse(account.one_time_keys()); - expect(oneTimeKeysObject).toBeDefined(); - const oneTimeKeys = oneTimeKeysObject.curve25519; - expect(Object.keys(oneTimeKeys).length).toBe(5); - }); -}); diff --git a/web/package.json b/web/package.json --- a/web/package.json +++ b/web/package.json @@ -46,6 +46,7 @@ "@babel/runtime": "^7.28.3", "@commapp/olm": "0.2.6", "@commapp/opaque-ke-wasm": "npm:@commapp/opaque-ke-wasm@^0.0.4", + "@commapp/vodozemac": "0.1.0", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", "@fortawesome/fontawesome-svg-core": "1.2.25", 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,7 +1,9 @@ // @flow -import olm from '@commapp/olm'; -import type { EncryptResult } from '@commapp/olm'; +import initVodozemac, { + OlmMessage, + type Account as VodozemacAccount, +} from '@commapp/vodozemac'; import invariant from 'invariant'; import localforage from 'localforage'; import uuid from 'uuid'; @@ -13,6 +15,7 @@ type OlmEncryptedMessageTypes, } from 'lib/types/crypto-types.js'; import { olmEncryptedMessageTypesValidator } from 'lib/types/crypto-types.js'; +import type { EncryptResult } from 'lib/types/encrypted-type.js'; import type { PlainTextWebNotification, EncryptedWebNotification, @@ -22,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, @@ -37,8 +45,8 @@ encryptedAESDataValidator, extendedCryptoKeyValidator, } from '../crypto/aes-gcm-crypto-utils.js'; -import { initOlm } from '../olm/olm-utils.js'; import { + getVodozemacWasmPath, NOTIFICATIONS_OLM_DATA_CONTENT, NOTIFICATIONS_OLM_DATA_ENCRYPTION_KEY, } from '../shared-worker/utils/constants.js'; @@ -52,11 +60,12 @@ export type WebNotifsServiceUtilsData = { +olmWasmPath: string, + +vodozemacWasmPath: ?string, +staffCanSee: boolean, }; export type NotificationAccountWithPicklingKey = { - +notificationAccount: olm.Account, + +notificationAccount: VodozemacAccount, +picklingKey: string, +synchronizationValue: ?string, +accountEncryptionKey?: CryptoKey, @@ -366,10 +375,11 @@ WEB_NOTIFS_SERVICE_UTILS_KEY, ); - if (!utilsData) { + if (!utilsData || !utilsData.vodozemacWasmPath) { return { id, error: 'Necessary data not found in IndexedDB' }; } - const { olmWasmPath, staffCanSee } = (utilsData: WebNotifsServiceUtilsData); + const { vodozemacWasmPath, staffCanSee } = + (utilsData: WebNotifsServiceUtilsData); let notifsAccountWithOlmData; try { @@ -404,7 +414,7 @@ encryptedOlmAccount, accountEncryptionKey, ), - olm.init({ locateFile: () => olmWasmPath }), + initVodozemac(vodozemacWasmPath), ]); let decryptedNotification; @@ -517,7 +527,7 @@ try { [notifsAccountWithOlmData] = await Promise.all([ getNotifsAccountWithOlmData(senderDeviceDescriptor), - initOlm(), + initVodozemac(getVodozemacWasmPath()), ]); } catch (e) { return { @@ -720,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(); @@ -758,15 +767,13 @@ authMetadata, ); - // Memory is freed below after pickling. - const account = new olm.Account(); - const session = new olm.Session(); + // Memory is freed in finally block. + let account; + let session; + let olmMessage; try { - account.unpickle( - notificationAccount.picklingKey, - notificationAccount.pickledAccount, - ); + account = unpickleVodozemacAccount(notificationAccount); if (notifInboundKeys.error) { throw new Error(notifInboundKeys.error); @@ -777,20 +784,25 @@ 'curve25519 must be present in notifs inbound keys', ); - session.create_inbound_from( - account, + 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(); - const decryptedNotification: T = JSON.parse( - session.decrypt(messageType, encryptedPayload), - ); + const decryptedNotification: T = 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 = @@ -831,8 +843,9 @@ // any session state return { decryptedNotification }; } finally { - session.free(); - account.free(); + olmMessage?.free(); + session?.free(); + account?.free(); } } @@ -842,22 +855,32 @@ encryptedPayload: string, 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 newPendingSessionUpdate = session.pickle(picklingKey); - session.free(); + // Memory is freed in finally block. + let session; + let olmMessage; - const newUpdateCreationTimestamp = Date.now(); + try { + session = unpickleVodozemacSession({ picklingKey, pickledSession }); - return { - decryptedNotification, - newUpdateCreationTimestamp, - newPendingSessionUpdate, - }; + olmMessage = new OlmMessage(type, encryptedPayload); + const decryptedString = session.decrypt(olmMessage); + + const decryptedNotification: T = JSON.parse(decryptedString); + const newPendingSessionUpdate = session.pickle( + getVodozemacPickleKey(picklingKey), + ); + + const newUpdateCreationTimestamp = Date.now(); + + return { + decryptedNotification, + newUpdateCreationTimestamp, + newPendingSessionUpdate, + }; + } finally { + olmMessage?.free(); + session?.free(); + } } function decryptWithPendingSession( @@ -967,10 +990,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 = { @@ -1037,10 +1072,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/push-notif/push-notifs-handler.js b/web/push-notif/push-notifs-handler.js --- a/web/push-notif/push-notifs-handler.js +++ b/web/push-notif/push-notifs-handler.js @@ -32,7 +32,10 @@ import PushNotifModal from '../modals/push-notif-modal.react.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; -import { getOlmWasmPath } from '../shared-worker/utils/constants.js'; +import { + getOlmWasmPath, + getVodozemacWasmPath, +} from '../shared-worker/utils/constants.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; function useCreateDesktopPushSubscription() { @@ -162,6 +165,7 @@ workerRegistration.active?.postMessage({ olmWasmPath: getOlmWasmPath(), + vodozemacWasmPath: getVodozemacWasmPath(), staffCanSee, authMetadata, }); diff --git a/web/push-notif/service-worker.js b/web/push-notif/service-worker.js --- a/web/push-notif/service-worker.js +++ b/web/push-notif/service-worker.js @@ -29,6 +29,7 @@ declare class CommAppMessage extends ExtendableEvent { +data: { +olmWasmPath?: string, + +vodozemacWasmPath?: string, +staffCanSee?: boolean, +authMetadata?: AuthMetadata, }; @@ -71,14 +72,21 @@ localforage.config(localforageConfig); event.waitUntil( (async () => { - const { olmWasmPath, staffCanSee, authMetadata } = event.data; - - if (!olmWasmPath || staffCanSee === undefined || !authMetadata) { + const { olmWasmPath, vodozemacWasmPath, staffCanSee, authMetadata } = + event.data; + + if ( + !olmWasmPath || + !vodozemacWasmPath || + staffCanSee === undefined || + !authMetadata + ) { return; } const webNotifsServiceUtils: WebNotifsServiceUtilsData = { olmWasmPath: olmWasmPath, + vodozemacWasmPath: vodozemacWasmPath, staffCanSee: staffCanSee, }; diff --git a/web/redux/persist.js b/web/redux/persist.js --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -86,7 +86,10 @@ import { legacyUnshimClientDB, unshimClientDB } from './unshim-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { getCommSharedWorker } from '../shared-worker/shared-worker-provider.js'; -import { getOlmWasmPath } from '../shared-worker/utils/constants.js'; +import { + getOlmWasmPath, + getVodozemacWasmPath, +} from '../shared-worker/utils/constants.js'; import { isSQLiteSupported } from '../shared-worker/utils/db-utils.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; @@ -341,6 +344,7 @@ await sharedWorker.schedule({ type: workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT, olmWasmPath: getOlmWasmPath(), + vodozemacWasmPath: getVodozemacWasmPath(), initialCryptoStore: cryptoStore, }); return rest; diff --git a/web/shared-worker/utils/constants.js b/web/shared-worker/utils/constants.js --- a/web/shared-worker/utils/constants.js +++ b/web/shared-worker/utils/constants.js @@ -15,6 +15,7 @@ export const DEFAULT_BACKUP_CLIENT_FILENAME = 'backup-client-wasm_bg.wasm'; export const DEFAULT_OLM_FILENAME = 'olm.wasm'; +export const DEFAULT_VODOZEMAC_FILENAME = 'vodozemac_bg.wasm'; export const DEFAULT_WEBWORKERS_OPAQUE_FILENAME = 'comm_opaque2_wasm_bg.wasm'; @@ -69,3 +70,13 @@ : DEFAULT_WEBWORKERS_OPAQUE_FILENAME; return `${opaqueWasmDirPath}/${opaqueWasmFilename}`; } + +declare var vodozemacFilename: string; +export function getVodozemacWasmPath(): string { + const origin = window.location.origin; + const vodozemacWasmDirPath = `${origin}${baseURL}${WORKERS_MODULES_DIR_PATH}`; + const vodozemacWasmFilename = vodozemacFilename + ? vodozemacFilename + : DEFAULT_VODOZEMAC_FILENAME; + return `${vodozemacWasmDirPath}/${vodozemacWasmFilename}`; +} 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,9 +1,12 @@ // @flow -import olm, { - type Account as OlmAccount, - type Utility as OlmUtility, -} from '@commapp/olm'; +import initVodozemac, { + Account, + type Account as VodozemacAccount, + OlmMessage, + Session, + Utility, +} from '@commapp/vodozemac'; import base64 from 'base-64'; import localforage from 'localforage'; import uuid from 'uuid'; @@ -11,16 +14,16 @@ 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'; @@ -28,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, }; @@ -113,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, })); @@ -143,7 +154,9 @@ accountEncryptionKey, } = notifsCryptoAccount; - const pickledAccount = notificationAccount.pickle(picklingKey); + const pickledAccount = notificationAccount.pickle( + getVodozemacPickleKey(picklingKey), + ); const accountWithPicklingKey: PickledOLMAccount = { pickledAccount, picklingKey, @@ -189,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, @@ -222,7 +227,9 @@ picklingKey, }; - const pickledAccount = notificationAccount.pickle(picklingKey); + const pickledAccount = notificationAccount.pickle( + getVodozemacPickleKey(picklingKey), + ); const accountWithPicklingKey: PickledOLMAccount = { pickledAccount, picklingKey, @@ -246,7 +253,7 @@ async function getOrCreateOlmAccount(accountIDInDB: number): Promise<{ +picklingKey: string, - +account: olm.Account, + +account: VodozemacAccount, +synchronizationValue?: ?string, }> { const sqliteQueryExecutor = getSQLiteQueryExecutor(); @@ -288,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()) { @@ -328,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, @@ -341,19 +350,8 @@ return sessionsData; } -function unpickleInitialCryptoStoreAccount( - account: PickledOLMAccount, -): olm.Account { - const { picklingKey, pickledAccount } = account; - // This `olm.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; -} - async function initializeCryptoAccount( - olmWasmPath: string, + vodozemacWasmPath: string, initialCryptoStore: ?LegacyCryptoStore, ) { const sqliteQueryExecutor = getSQLiteQueryExecutor(); @@ -361,20 +359,20 @@ throw new Error('Database not initialized'); } - await olm.init({ locateFile: () => olmWasmPath }); + await initVodozemac(vodozemacWasmPath); if (initialCryptoStore) { clearCryptoStore(); cryptoStore = { contentAccountPickleKey: initialCryptoStore.primaryAccount.picklingKey, - contentAccount: unpickleInitialCryptoStoreAccount( + contentAccount: unpickleVodozemacAccount( initialCryptoStore.primaryAccount, ), contentSessions: {}, }; const notifsCryptoAccount = { picklingKey: initialCryptoStore.notificationAccount.picklingKey, - notificationAccount: unpickleInitialCryptoStoreAccount( + notificationAccount: unpickleVodozemacAccount( initialCryptoStore.notificationAccount, ), synchronizationValue: uuid.v4(), @@ -391,10 +389,9 @@ ): Promise { if (message.type === workerRequestMessageTypes.INITIALIZE_CRYPTO_ACCOUNT) { await initializeCryptoAccount( - message.olmWasmPath, + message.vodozemacWasmPath, message.initialCryptoStore, ); - verifyMemoryUsage('INITIALIZE_CRYPTO_ACCOUNT'); } else if (message.type === workerRequestMessageTypes.CALL_OLM_API_METHOD) { const method: (...$ReadOnlyArray) => mixed = (olmAPI[ message.method @@ -402,7 +399,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, @@ -421,10 +417,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); @@ -444,13 +444,14 @@ 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'); + const isSignatureInvalid = getMessageForException(err)?.includes( + 'The signature was invalid', + ); if (isSignatureInvalid) { return false; } @@ -458,8 +459,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; @@ -586,10 +587,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, }; @@ -605,15 +610,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, @@ -627,8 +634,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(); @@ -690,14 +701,18 @@ 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) { throw new Error(`error decrypt => ${OLM_ERROR_FLAG} ` + e.message); + } finally { + olmMessage.free(); } await persistCryptoStore(); @@ -726,14 +741,18 @@ 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) { throw new Error(`error decrypt => ${OLM_ERROR_FLAG} ` + e.message); + } finally { + olmMessage.free(); } const sqliteQueryExecutor = getSQLiteQueryExecutor(); @@ -784,27 +803,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(); throw new Error(`error decrypt => ${OLM_ERROR_FLAG} ` + e.message); + } finally { + olmMessage.free(); } + // into_session() is consuming object. + // There is no need to call free() on inboundCreationResult + const initialEncryptedMessage = inboundCreationResult.plaintext; + const session = inboundCreationResult.into_session(); + if (existingSession) { existingSession.session.free(); } @@ -826,30 +848,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) { diff --git a/web/types/worker-types.js b/web/types/worker-types.js --- a/web/types/worker-types.js +++ b/web/types/worker-types.js @@ -169,6 +169,7 @@ export type InitializeCryptoAccountRequestMessage = { +type: 12, +olmWasmPath: string, + +vodozemacWasmPath: string, +initialCryptoStore?: LegacyCryptoStore, }; diff --git a/web/webpack.config.cjs b/web/webpack.config.cjs --- a/web/webpack.config.cjs +++ b/web/webpack.config.cjs @@ -134,6 +134,14 @@ }, ], }), + new CopyPlugin({ + patterns: [ + { + from: 'node_modules/@commapp/vodozemac/wasm/vodozemac_bg.wasm', + to: path.join(__dirname, 'dist', 'webworkers'), + }, + ], + }), new CopyPlugin({ patterns: [ { @@ -181,6 +189,19 @@ }, ], }), + new CopyPlugin({ + patterns: [ + { + from: 'node_modules/@commapp/vodozemac/wasm/vodozemac_bg.wasm', + to: path.join( + __dirname, + 'dist', + 'webworkers', + 'vodozemac.[contenthash:12].wasm', + ), + }, + ], + }), new CopyPlugin({ patterns: [ { diff --git a/yarn.lock b/yarn.lock --- a/yarn.lock +++ b/yarn.lock @@ -1495,6 +1495,11 @@ resolved "https://registry.yarnpkg.com/@commapp/sqlcipher-amalgamation/-/sqlcipher-amalgamation-4.5.5-e.tgz#05245e7c1f07ed46cdc652ebeeb7e1a923fa748a" integrity sha512-f+fuT/mbN/cB+Z4+QhTEmuRSbOLvaTdE3R2emcTK0P4gCPtwtgdZ3BGyxDLHkQPjlDaJ9G0Z6/+WqDJJLD0NnA== +"@commapp/vodozemac@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@commapp/vodozemac/-/vodozemac-0.1.0.tgz#8a3016bb4354915600d0915a979bfa04b1db5cd7" + integrity sha512-JEUeFrVMLYXACxKqOze+6aqITGt46QrsSGZ19+Nrpx3Wo85Gml7nUm47HF+Pkc3iNS1uiNYhK3pliq9N/SMomQ== + "@commapp/windowspush@file:desktop/addons/windows-pushnotifications": version "0.0.1" dependencies: