From d87e0e8ddd26d620288a986f601f95b9c3ca5ea2 Mon Sep 17 00:00:00 2001 From: xsanm Date: Thu, 27 Nov 2025 12:29:55 +0100 Subject: [PATCH 1/2] [lib][web][keyserver] replace Olm with Vodozemac's Olm Summary: [ENG-11531](https://linear.app/comm/issue/ENG-11531/migrate-the-full-crypto-api-to-vodozemac). There is one thing with naming which is not obvious, this is using Olm name - Olm is still part of Vodozemac crate, so I think this is still correct, but I am okay with updating this too. I tried to make API as similar as it is possible, so there are files when changing import is everything we have to do. I redefined `EncryptResult` in `lib/`. Importing it from Olm was bad idea because it was used on native too, which causes Olm/Vodozemac wasm to be bundled into native, which worked with Olm but not with Vodozemac. Depends on D15547 Test Plan: Test encrypt/decrypt and creating session with Olm and Vodozemac. Reviewers: ashoat Subscribers: tomek Differential Revision: https://phab.comm.dev/D15548 --- keyserver/package.json | 2 +- keyserver/src/creators/olm-session-creator.js | 2 +- keyserver/src/cron/cron.js | 30 +- keyserver/src/database/migration-config.js | 2 +- .../src/push/encrypted-notif-utils-api.js | 3 +- keyserver/src/responders/keys-responders.js | 29 +- keyserver/src/responders/user-responders.js | 2 +- keyserver/src/socket/tunnelbroker.js | 7 - keyserver/src/updaters/olm-account-updater.js | 2 +- keyserver/src/updaters/olm-session-updater.js | 11 +- keyserver/src/user/login.js | 2 +- keyserver/src/utils/olm-objects.js | 62 ++-- keyserver/src/utils/olm-utils.js | 15 +- keyserver/src/utils/olm-utils.test.js | 337 ------------------ lib/shared/crypto-utils.js | 4 + lib/types/encrypted-type.js | 6 + lib/types/notif-types.js | 2 +- lib/utils/olm-memory-utils.js | 75 ---- lib/utils/olm-utility.js | 11 +- lib/utils/olm-utils.js | 43 +-- lib/utils/vodozemac-utils.js | 61 ++++ web/olm/olm-utils.js | 16 - web/olm/olm.test.js | 28 -- web/push-notif/notif-crypto-utils.js | 127 ++++--- web/shared-worker/worker/worker-crypto.js | 280 ++++++++------- 25 files changed, 403 insertions(+), 756 deletions(-) delete mode 100644 keyserver/src/utils/olm-utils.test.js create mode 100644 lib/types/encrypted-type.js delete mode 100644 lib/utils/olm-memory-utils.js create mode 100644 lib/utils/vodozemac-utils.js delete mode 100644 web/olm/olm-utils.js delete mode 100644 web/olm/olm.test.js diff --git a/keyserver/package.json b/keyserver/package.json index c1664241bf..9d784e66d8 100644 --- a/keyserver/package.json +++ b/keyserver/package.json @@ -118,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 index 0f5a813850..eb4c006e22 100644 --- 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 index 47dffc7336..790b6de55e 100644 --- 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 @@ if (cluster.isMaster) { 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 @@ if (cluster.isMaster) { ); } 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 @@ if (cluster.isMaster) { '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 index 25ed2ec247..8582c29fc5 100644 --- 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/push/encrypted-notif-utils-api.js b/keyserver/src/push/encrypted-notif-utils-api.js index 4de985df1e..9006cb0c73 100644 --- 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 index 0f88ef1165..570d36b2cc 100644 --- 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 @@ type SessionInitializationKeysSet = { 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 index be9d63aec3..6263681ee6 100644 --- 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/socket/tunnelbroker.js b/keyserver/src/socket/tunnelbroker.js index e2a884ed87..fd1f8341db 100644 --- a/keyserver/src/socket/tunnelbroker.js +++ b/keyserver/src/socket/tunnelbroker.js @@ -44,10 +44,6 @@ import { 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 @@ class TunnelbrokerSocket { 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 index f134551874..a78ace78b5 100644 --- 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 index ec06f6289e..c3ff709ac0 100644 --- 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 @@ async function encryptAndUpdateOlmSession( }, (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 index aab1f40de7..42bec81386 100644 --- 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 index 4b54bc3e3b..1be0fdc34d 100644 --- 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 @@ async function unpickleAccountAndUseCallback( 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 unpickleAccountAndUseCallback( } 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 @@ async function unpickleSessionAndUseCallback( 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 @@ async function createPickledOlmSession( 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 index 74fd367be0..8595a2a9d2 100644 --- 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 @@ async function uploadNewOneTimeKeys(numberOfKeys: number) { 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 @@ async function getContentSigningKey(): Promise { 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 @@ async function publishPrekeysToIdentity( 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 index 86c243d505..0000000000 --- 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/shared/crypto-utils.js b/lib/shared/crypto-utils.js index 48af67a9c0..7232bebd31 100644 --- a/lib/shared/crypto-utils.js +++ b/lib/shared/crypto-utils.js @@ -84,6 +84,10 @@ function useInitialNotificationsEncryptedMessage( ); } +// 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 index 0000000000..042c3d912a --- /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 index dd21b33f31..f3461adf42 100644 --- 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 index ea2b4838f0..0000000000 --- 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 index cacaad9b67..ec6a0d334e 100644 --- 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 index e6f5e8e269..e1bb9ea1b7 100644 --- 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 @@ type AccountKeysSet = { +oneTimeKeys: $ReadOnlyArray, }; -function validateAccountPrekey(account: OlmAccount) { +function validateAccountPrekey(account: VodozemacAccount) { if (shouldRotatePrekey(account)) { account.generate_prekey(); } @@ -27,7 +23,7 @@ function validateAccountPrekey(account: OlmAccount) { } } -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 shouldRotatePrekey(account: OlmAccount): boolean { ); } -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 shouldForgetPrekey(account: OlmAccount): boolean { ); } -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 @@ const olmSessionErrors = Object.freeze({ // 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 index 0000000000..190dd255ca --- /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/web/olm/olm-utils.js b/web/olm/olm-utils.js deleted file mode 100644 index 7345736ad1..0000000000 --- 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 index 481d036075..0000000000 --- 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/push-notif/notif-crypto-utils.js b/web/push-notif/notif-crypto-utils.js index 41da120f44..cadb7586bc 100644 --- a/web/push-notif/notif-crypto-utils.js +++ b/web/push-notif/notif-crypto-utils.js @@ -1,8 +1,9 @@ // @flow -import olm from '@commapp/olm'; -import type { EncryptResult } from '@commapp/olm'; -import initVodozemac from '@commapp/vodozemac'; +import initVodozemac, { + OlmMessage, + type Account as VodozemacAccount, +} from '@commapp/vodozemac'; import invariant from 'invariant'; import localforage from 'localforage'; import uuid from 'uuid'; @@ -14,6 +15,7 @@ import { 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, @@ -23,6 +25,11 @@ import { getCookieIDFromCookie } from 'lib/utils/cookie-utils.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { promiseAll } from 'lib/utils/promises.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; +import { + getVodozemacPickleKey, + unpickleVodozemacAccount, + unpickleVodozemacSession, +} from 'lib/utils/vodozemac-utils.js'; import { fetchAuthMetadata, @@ -58,7 +65,7 @@ export type WebNotifsServiceUtilsData = { }; export type NotificationAccountWithPicklingKey = { - +notificationAccount: olm.Account, + +notificationAccount: VodozemacAccount, +picklingKey: string, +synchronizationValue: ?string, +accountEncryptionKey?: CryptoKey, @@ -723,11 +730,10 @@ async function commonPeerDecrypt( 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(); @@ -761,15 +767,13 @@ async function commonPeerDecrypt( 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); @@ -780,20 +784,25 @@ async function commonPeerDecrypt( '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 = @@ -834,8 +843,9 @@ async function commonPeerDecrypt( // any session state return { decryptedNotification }; } finally { - session.free(); - account.free(); + olmMessage?.free(); + session?.free(); + account?.free(); } } @@ -845,22 +855,32 @@ function decryptWithSession( 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( @@ -970,10 +990,22 @@ async function encryptNotificationWithOlmSession( ); // Memory is freed below after pickling. - const session = new olm.Session(); - session.unpickle(picklingKey, pendingSessionUpdate); - const encryptedNotification = session.encrypt(payload); - const newPendingSessionUpdate = session.pickle(picklingKey); + + const session = unpickleVodozemacSession({ + picklingKey, + pickledSession: pendingSessionUpdate, + }); + + const olmMessage = session.encrypt(payload); + const encryptedNotification = { + body: olmMessage.ciphertext, + type: olmMessage.message_type, + }; + olmMessage.free(); + + const newPendingSessionUpdate = session.pickle( + getVodozemacPickleKey(picklingKey), + ); session.free(); const updatedOlmData: NotificationsOlmDataType = { @@ -1040,10 +1072,9 @@ async function getNotifsCryptoAccount_WITH_MANUAL_MEMORY_MANAGEMENT(): Promise ({ targetDeviceID, - sessionData: sessionData.session.pickle(contentAccountPickleKey), + sessionData: sessionData.session.pickle( + getVodozemacPickleKey(contentAccountPickleKey), + ), version: sessionData.version, })); @@ -144,7 +154,9 @@ async function persistCryptoStore( accountEncryptionKey, } = notifsCryptoAccount; - const pickledAccount = notificationAccount.pickle(picklingKey); + const pickledAccount = notificationAccount.pickle( + getVodozemacPickleKey(picklingKey), + ); const accountWithPicklingKey: PickledOLMAccount = { pickledAccount, picklingKey, @@ -190,31 +202,23 @@ async function createAndPersistNotificationsOutboundSession( const notificationsPrekey = notificationsInitializationInfo.prekey; // Memory is freed below after persisting. - const session = new olm.Session(); - if (notificationsInitializationInfo.oneTimeKey) { - session.create_outbound( - notificationAccount, - notificationsIdentityKeys.curve25519, - notificationsIdentityKeys.ed25519, - notificationsPrekey, - notificationsInitializationInfo.prekeySignature, - notificationsInitializationInfo.oneTimeKey, - ); - } else { - session.create_outbound_without_otk( - notificationAccount, - notificationsIdentityKeys.curve25519, - notificationsIdentityKeys.ed25519, - notificationsPrekey, - notificationsInitializationInfo.prekeySignature, - ); - } - const { body: message, type: messageType } = session.encrypt( + const session = notificationAccount.create_outbound_session( + notificationsIdentityKeys.curve25519, + notificationsIdentityKeys.ed25519, + notificationsInitializationInfo.oneTimeKey || '', + notificationsPrekey, + notificationsInitializationInfo.prekeySignature, + true, // olmCompatibilityMode + ); + + const encryptedMessage = session.encrypt( JSON.stringify(initialEncryptedMessageContent), ); + const message = encryptedMessage.ciphertext; + const messageType = encryptedMessage.message_type; const mainSession = session.pickle( - notificationAccountWithPicklingKey.picklingKey, + getVodozemacPickleKey(notificationAccountWithPicklingKey.picklingKey), ); const notificationsOlmData: NotificationsOlmDataType = { mainSession, @@ -223,7 +227,9 @@ async function createAndPersistNotificationsOutboundSession( picklingKey, }; - const pickledAccount = notificationAccount.pickle(picklingKey); + const pickledAccount = notificationAccount.pickle( + getVodozemacPickleKey(picklingKey), + ); const accountWithPicklingKey: PickledOLMAccount = { pickledAccount, picklingKey, @@ -247,7 +253,7 @@ async function createAndPersistNotificationsOutboundSession( async function getOrCreateOlmAccount(accountIDInDB: number): Promise<{ +picklingKey: string, - +account: olm.Account, + +account: VodozemacAccount, +synchronizationValue?: ?string, }> { const sqliteQueryExecutor = getSQLiteQueryExecutor(); @@ -289,18 +295,18 @@ async function getOrCreateOlmAccount(accountIDInDB: number): Promise<{ }; } - // This `olm.Account` is created once and is cached for the entire + // This `Account` is created once and is cached for the entire // program lifetime. Freeing is done as part of `clearCryptoStore`. - const account = new olm.Account(); + let account; let picklingKey; if (!accountDBString) { picklingKey = uuid.v4(); - account.create(); + account = new Account(); } else { const dbAccount: PickledOLMAccount = JSON.parse(accountDBString); picklingKey = dbAccount.picklingKey; - account.unpickle(picklingKey, dbAccount.pickledAccount); + account = unpickleVodozemacAccount(dbAccount); } if (accountIDInDB === sqliteQueryExecutor.getNotifsAccountID()) { @@ -329,10 +335,12 @@ function getOlmSessions(picklingKey: string): OlmSessions { 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, @@ -342,17 +350,6 @@ function getOlmSessions(picklingKey: string): OlmSessions { 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( vodozemacWasmPath: string, initialCryptoStore: ?LegacyCryptoStore, @@ -368,14 +365,14 @@ async function initializeCryptoAccount( 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(), @@ -395,7 +392,6 @@ async function processAppOlmApiRequest( message.vodozemacWasmPath, message.initialCryptoStore, ); - verifyMemoryUsage('INITIALIZE_CRYPTO_ACCOUNT'); } else if (message.type === workerRequestMessageTypes.CALL_OLM_API_METHOD) { const method: (...$ReadOnlyArray) => mixed = (olmAPI[ message.method @@ -403,7 +399,6 @@ async function processAppOlmApiRequest( // 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, @@ -422,10 +417,14 @@ async function getSignedIdentityKeysBlob(): Promise { 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); @@ -445,13 +444,14 @@ function olmVerifyMessage( 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; } @@ -459,8 +459,8 @@ function olmVerifyMessage( } } -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; @@ -587,10 +587,14 @@ const olmAPI: OlmAPI = { ); 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, }; @@ -606,15 +610,17 @@ const olmAPI: OlmAPI = { 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, @@ -628,8 +634,12 @@ const olmAPI: OlmAPI = { 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(); @@ -691,14 +701,18 @@ const olmAPI: OlmAPI = { 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(); @@ -727,14 +741,18 @@ const olmAPI: OlmAPI = { 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(); @@ -785,27 +803,30 @@ const olmAPI: OlmAPI = { } } - // 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(); } @@ -827,30 +848,25 @@ const olmAPI: OlmAPI = { 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) { From 6e81fde88dd88b7b7d52cea4271f645dedcd0748 Mon Sep 17 00:00:00 2001 From: Kamil Kurowski Date: Wed, 3 Dec 2025 18:09:15 +0100 Subject: [PATCH 2/2] [native] implement CXX bindings to Vodozemac (#326) Summary: [ENG-11531](https://linear.app/comm/issue/ENG-11531/migrate-the-full-crypto-api-to-vodozemac). Making vodozemac available on native. Branch needs to be updated after PRs are merged Test Plan: Tested in next diff Reviewers: ashoat Subscribers: tomek Differential Revision: https://phab.comm.dev/D15549 --- native/native_rust_library/Cargo.lock | 4 +- native/native_rust_library/Cargo.toml | 2 + native/native_rust_library/build.rs | 2 +- native/native_rust_library/src/lib.rs | 92 ++++- native/native_rust_library/src/session.rs | 114 ------ native/native_rust_library/src/vodozemac.rs | 409 ++++++++++++++++++++ native/vodozemac_bindings/Cargo.lock | 3 +- native/vodozemac_bindings/Cargo.toml | 1 + native/vodozemac_bindings/build.rs | 1 + native/vodozemac_bindings/src/lib.rs | 82 +++- native/vodozemac_bindings/src/session.rs | 1 - native/vodozemac_bindings/src/vodozemac.rs | 1 + 12 files changed, 575 insertions(+), 137 deletions(-) delete mode 100644 native/native_rust_library/src/session.rs create mode 100644 native/native_rust_library/src/vodozemac.rs delete mode 120000 native/vodozemac_bindings/src/session.rs create mode 120000 native/vodozemac_bindings/src/vodozemac.rs diff --git a/native/native_rust_library/Cargo.lock b/native/native_rust_library/Cargo.lock index f3ac5aa721..6f370f8be0 100644 --- a/native/native_rust_library/Cargo.lock +++ b/native/native_rust_library/Cargo.lock @@ -1418,6 +1418,7 @@ dependencies = [ name = "native_rust_library" version = "0.1.0" dependencies = [ + "anyhow", "argon2", "backup_client", "base64 0.21.7", @@ -1431,6 +1432,7 @@ dependencies = [ "regex", "serde", "serde_json", + "sha2", "siwe", "tokio", "tokio-util", @@ -2870,7 +2872,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[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 index e9e2fd806c..35ccc1ac44 100644 --- a/native/native_rust_library/Cargo.toml +++ b/native/native_rust_library/Cargo.toml @@ -22,6 +22,8 @@ grpc_clients = { path = "../../shared/grpc_clients" } 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 index 1a8ad5a605..0c7daac860 100644 --- a/native/native_rust_library/build.rs +++ b/native/native_rust_library/build.rs @@ -197,7 +197,7 @@ fn main() { .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 index 2fed9fb461..7d2448b44b 100644 --- a/native/native_rust_library/src/lib.rs +++ b/native/native_rust_library/src/lib.rs @@ -10,12 +10,16 @@ mod argon2_tools; 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 @@ mod ffi { } // 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 index 9883698884..0000000000 --- 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 index 0000000000..3f570f152c --- /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 index 37682ecbe3..0eeec3b817 100644 --- a/native/vodozemac_bindings/Cargo.lock +++ b/native/vodozemac_bindings/Cargo.lock @@ -897,7 +897,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[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 @@ dependencies = [ "regex", "serde", "serde_json", + "sha2", "vodozemac", ] diff --git a/native/vodozemac_bindings/Cargo.toml b/native/vodozemac_bindings/Cargo.toml index 58c9d4d1a1..bdbaa1e906 100644 --- a/native/vodozemac_bindings/Cargo.toml +++ b/native/vodozemac_bindings/Cargo.toml @@ -10,6 +10,7 @@ serde = { version = "1.0", features = ["derive"] } 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 index 1d9bb19a4a..d2a5dfdfd6 100644 --- a/native/vodozemac_bindings/build.rs +++ b/native/vodozemac_bindings/build.rs @@ -3,4 +3,5 @@ fn main() { 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 index a16d48788c..3129bdf875 100644 --- 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 @@ pub mod ffi { 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 index 9a82488ee9..0000000000 --- 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 index 0000000000..4525d8a15d --- /dev/null +++ b/native/vodozemac_bindings/src/vodozemac.rs @@ -0,0 +1 @@ +../../native_rust_library/src/vodozemac.rs \ No newline at end of file