diff --git a/keyserver/src/updaters/olm-account-updater.js b/keyserver/src/updaters/olm-account-updater.js index 5a918e925..e07745f33 100644 --- a/keyserver/src/updaters/olm-account-updater.js +++ b/keyserver/src/updaters/olm-account-updater.js @@ -1,112 +1,117 @@ // @flow import type { Account as OlmAccount } from '@commapp/olm'; -import { ServerError } from 'lib/utils/errors.js'; +import { ServerError, getMessageForException } from 'lib/utils/errors.js'; import sleep from 'lib/utils/sleep.js'; import { SQL, dbQuery } from '../database/database.js'; import { unpickleOlmAccount } from '../utils/olm-utils.js'; const maxOlmAccountUpdateRetriesCount = 5; const olmAccountUpdateRetryDelay = 200; async function fetchCallUpdateOlmAccount( olmAccountType: 'content' | 'notifications', callback: (account: OlmAccount, picklingKey: string) => Promise | T, ): Promise { const isContent = olmAccountType === 'content'; let retriesLeft = maxOlmAccountUpdateRetriesCount; while (retriesLeft > 0) { const [olmAccountResult] = await dbQuery( SQL` SELECT version, pickling_key, pickled_olm_account FROM olm_accounts WHERE is_content = ${isContent} `, ); if (olmAccountResult.length === 0) { throw new ServerError('missing_olm_account'); } const [ { version, pickling_key: picklingKey, pickled_olm_account: pickledAccount, }, ] = olmAccountResult; const account = await unpickleOlmAccount({ picklingKey, pickledAccount, }); - const result = await callback(account, picklingKey); + let result; + try { + result = await callback(account, picklingKey); + } catch (e) { + throw new ServerError(getMessageForException(e) ?? 'unknown_error'); + } const updatedAccount = account.pickle(picklingKey); const [transactionResult] = await dbQuery( SQL` START TRANSACTION; SELECT version INTO @currentVersion FROM olm_accounts WHERE is_content = ${isContent} FOR UPDATE; UPDATE olm_accounts SET pickled_olm_account = ${updatedAccount}, version = ${version} + 1 WHERE version = ${version} AND is_content = ${isContent}; COMMIT; SELECT @currentVersion AS versionOnUpdateAttempt; `, { multipleStatements: true }, ); const selectResult = transactionResult.pop(); const [{ versionOnUpdateAttempt }] = selectResult; if (version === versionOnUpdateAttempt) { return result; } retriesLeft = retriesLeft - 1; await sleep(olmAccountUpdateRetryDelay); } throw new ServerError('max_olm_account_update_retry_exceeded'); } async function fetchOlmAccount( olmAccountType: 'content' | 'notifications', ): Promise<{ account: OlmAccount, picklingKey: string, }> { const isContent = olmAccountType === 'content'; const [olmAccountResult] = await dbQuery( SQL` SELECT pickling_key, pickled_olm_account FROM olm_accounts WHERE is_content = ${isContent} `, ); if (olmAccountResult.length === 0) { throw new ServerError('missing_olm_account'); } const picklingKey = olmAccountResult[0].pickling_key; const pickledAccount = olmAccountResult[0].pickled_olm_account; const account = await unpickleOlmAccount({ picklingKey, pickledAccount, }); return { account, picklingKey }; } export { fetchCallUpdateOlmAccount, fetchOlmAccount }; diff --git a/keyserver/src/user/login.js b/keyserver/src/user/login.js index d66dbd60b..a5301688c 100644 --- a/keyserver/src/user/login.js +++ b/keyserver/src/user/login.js @@ -1,171 +1,139 @@ // @flow import type { Account as OlmAccount } from '@commapp/olm'; import { getRustAPI } from 'rust-node-addon'; -import { getOneTimeKeyValuesFromBlob } from 'lib/shared/crypto-utils.js'; -import { ONE_TIME_KEYS_NUMBER } from 'lib/types/identity-service-types.js'; import { getCommConfig } from 'lib/utils/comm-config.js'; import { ServerError } from 'lib/utils/errors.js'; +import { retrieveAccountKeysSet } from 'lib/utils/olm-utils.js'; import { saveIdentityInfo, fetchIdentityInfo, type IdentityInfo, } from './identity.js'; import { getMessageForException } from '../responders/utils.js'; import { fetchCallUpdateOlmAccount } from '../updaters/olm-account-updater.js'; -import { - getAccountPrekeysSet, - validateAccountPrekey, -} from '../utils/olm-utils.js'; type UserCredentials = { +username: string, +password: string }; -export type AccountKeysSet = { - +identityKeys: string, - +prekey: string, - +prekeySignature: string, - +oneTimeKeys: $ReadOnlyArray, -}; - -function retrieveAccountKeysSet(account: OlmAccount): AccountKeysSet { - const identityKeys = account.identity_keys(); - - validateAccountPrekey(account); - const { prekey, prekeySignature } = getAccountPrekeysSet(account); - - if (!prekeySignature || !prekey) { - throw new ServerError('invalid_prekey'); - } - - let oneTimeKeys = getOneTimeKeyValuesFromBlob(account.one_time_keys()); - - if (oneTimeKeys.length < ONE_TIME_KEYS_NUMBER) { - account.generate_one_time_keys(ONE_TIME_KEYS_NUMBER); - oneTimeKeys = getOneTimeKeyValuesFromBlob(account.one_time_keys()); - } - - return { identityKeys, oneTimeKeys, prekey, prekeySignature }; -} - // After register or login is successful function markKeysAsPublished(account: OlmAccount) { account.mark_prekey_as_published(); account.mark_keys_as_published(); } async function verifyUserLoggedIn(): Promise { const result = await fetchIdentityInfo(); if (result) { return result; } const identityInfo = await registerOrLogIn(); await saveIdentityInfo(identityInfo); return identityInfo; } async function registerOrLogIn(): Promise { const rustAPIPromise = getRustAPI(); const userInfo = await getCommConfig({ folder: 'secrets', name: 'user_credentials', }); if (!userInfo) { throw new ServerError('missing_user_credentials'); } const { identityKeys: notificationsIdentityKeys, prekey: notificationsPrekey, prekeySignature: notificationsPrekeySignature, oneTimeKeys: notificationsOneTimeKeys, } = await fetchCallUpdateOlmAccount('notifications', retrieveAccountKeysSet); const contentAccountCallback = async (account: OlmAccount) => { const { identityKeys: contentIdentityKeys, oneTimeKeys, prekey, prekeySignature, } = await retrieveAccountKeysSet(account); const identityKeysBlob = { primaryIdentityPublicKeys: JSON.parse(contentIdentityKeys), notificationIdentityPublicKeys: JSON.parse(notificationsIdentityKeys), }; const identityKeysBlobPayload = JSON.stringify(identityKeysBlob); const signedIdentityKeysBlob = { payload: identityKeysBlobPayload, signature: account.sign(identityKeysBlobPayload), }; return { signedIdentityKeysBlob, oneTimeKeys, prekey, prekeySignature, }; }; const [ rustAPI, { signedIdentityKeysBlob, prekey: contentPrekey, prekeySignature: contentPrekeySignature, oneTimeKeys: contentOneTimeKeys, }, ] = await Promise.all([ rustAPIPromise, fetchCallUpdateOlmAccount('content', contentAccountCallback), ]); try { const identity_info = await rustAPI.loginUser( userInfo.username, userInfo.password, signedIdentityKeysBlob, contentPrekey, contentPrekeySignature, notificationsPrekey, notificationsPrekeySignature, contentOneTimeKeys, notificationsOneTimeKeys, ); await Promise.all([ fetchCallUpdateOlmAccount('content', markKeysAsPublished), fetchCallUpdateOlmAccount('notifications', markKeysAsPublished), ]); return identity_info; } catch (e) { console.warn('Failed to login user: ' + getMessageForException(e)); try { const identity_info = await rustAPI.registerUser( userInfo.username, userInfo.password, signedIdentityKeysBlob, contentPrekey, contentPrekeySignature, notificationsPrekey, notificationsPrekeySignature, contentOneTimeKeys, notificationsOneTimeKeys, ); await Promise.all([ fetchCallUpdateOlmAccount('content', markKeysAsPublished), fetchCallUpdateOlmAccount('notifications', markKeysAsPublished), ]); return identity_info; } catch (err) { console.warn('Failed to register user: ' + getMessageForException(err)); throw new ServerError('identity_auth_failed'); } } } export { verifyUserLoggedIn }; diff --git a/keyserver/src/utils/olm-utils.js b/keyserver/src/utils/olm-utils.js index 32b455fef..13cecf2a6 100644 --- a/keyserver/src/utils/olm-utils.js +++ b/keyserver/src/utils/olm-utils.js @@ -1,276 +1,216 @@ // @flow import olm from '@commapp/olm'; import type { Account as OlmAccount, Utility as OlmUtility, Session as OlmSession, } from '@commapp/olm'; import { getRustAPI } from 'rust-node-addon'; import uuid from 'uuid'; import { getOneTimeKeyValuesFromBlob } from 'lib/shared/crypto-utils.js'; import { olmEncryptedMessageTypes } from 'lib/types/crypto-types.js'; import { ServerError } from 'lib/utils/errors.js'; -import { values } from 'lib/utils/objects.js'; +import { + getAccountPrekeysSet, + shouldForgetPrekey, + shouldRotatePrekey, +} from 'lib/utils/olm-utils.js'; import { fetchCallUpdateOlmAccount, fetchOlmAccount, } from '../updaters/olm-account-updater.js'; import { verifyUserLoggedIn } from '../user/login.js'; type PickledOlmAccount = { +picklingKey: string, +pickledAccount: string, }; -const maxPublishedPrekeyAge = 30 * 24 * 60 * 60 * 1000; -const maxOldPrekeyAge = 24 * 60 * 60 * 1000; - -function getLastPrekeyPublishTime(account: OlmAccount): Date { - const olmLastPrekeyPublishTime = account.last_prekey_publish_time(); - - // Olm uses seconds, while in Node we need milliseconds. - return new Date(olmLastPrekeyPublishTime * 1000); -} - -function shouldRotatePrekey(account: OlmAccount): 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 - // result in the old active prekey being discarded. - if (account.unpublished_prekey()) { - return false; - } - - const currentDate = new Date(); - const lastPrekeyPublishDate = getLastPrekeyPublishTime(account); - - return ( - currentDate.getTime() - lastPrekeyPublishDate.getTime() >= - maxPublishedPrekeyAge - ); -} - -function shouldForgetPrekey(account: OlmAccount): 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()) { - return false; - } - - const currentDate = new Date(); - const lastPrekeyPublishDate = getLastPrekeyPublishTime(account); - - return ( - currentDate.getTime() - lastPrekeyPublishDate.getTime() >= maxOldPrekeyAge - ); -} - async function createPickledOlmAccount(): Promise { await olm.init(); const account = new olm.Account(); account.create(); const picklingKey = uuid.v4(); const pickledAccount = account.pickle(picklingKey); return { picklingKey: picklingKey, pickledAccount: pickledAccount, }; } async function unpickleOlmAccount( pickledOlmAccount: PickledOlmAccount, ): Promise { await olm.init(); const account = new olm.Account(); account.unpickle( pickledOlmAccount.picklingKey, pickledOlmAccount.pickledAccount, ); return account; } async function createPickledOlmSession( account: OlmAccount, accountPicklingKey: string, initialEncryptedMessage: string, theirCurve25519Key?: string, ): Promise { await olm.init(); const session = new olm.Session(); if (theirCurve25519Key) { session.create_inbound_from( account, theirCurve25519Key, initialEncryptedMessage, ); } else { session.create_inbound(account, initialEncryptedMessage); } account.remove_one_time_keys(session); session.decrypt(olmEncryptedMessageTypes.PREKEY, initialEncryptedMessage); return session.pickle(accountPicklingKey); } async function unpickleOlmSession( pickledSession: string, picklingKey: string, ): Promise { await olm.init(); const session = new olm.Session(); session.unpickle(picklingKey, pickledSession); return session; } let cachedOLMUtility: OlmUtility; function getOlmUtility(): OlmUtility { if (cachedOLMUtility) { return cachedOLMUtility; } cachedOLMUtility = new olm.Utility(); return cachedOLMUtility; } -function validateAccountPrekey(account: OlmAccount) { - if (shouldRotatePrekey(account)) { - account.generate_prekey(); - } - if (shouldForgetPrekey(account)) { - account.forget_old_prekey(); - } -} - async function uploadNewOneTimeKeys(numberOfKeys: number) { const [rustAPI, identityInfo, deviceID] = await Promise.all([ getRustAPI(), verifyUserLoggedIn(), getContentSigningKey(), ]); if (!identityInfo) { throw new ServerError('missing_identity_info'); } await fetchCallUpdateOlmAccount('content', (contentAccount: OlmAccount) => { contentAccount.generate_one_time_keys(numberOfKeys); const contentOneTimeKeys = getOneTimeKeyValuesFromBlob( contentAccount.one_time_keys(), ); return fetchCallUpdateOlmAccount( 'notifications', async (notifAccount: OlmAccount) => { notifAccount.generate_one_time_keys(numberOfKeys); const notifOneTimeKeys = getOneTimeKeyValuesFromBlob( notifAccount.one_time_keys(), ); await rustAPI.uploadOneTimeKeys( identityInfo.userId, deviceID, identityInfo.accessToken, contentOneTimeKeys, notifOneTimeKeys, ); notifAccount.mark_keys_as_published(); contentAccount.mark_keys_as_published(); }, ); }); } async function getContentSigningKey(): Promise { const accountInfo = await fetchOlmAccount('content'); return JSON.parse(accountInfo.account.identity_keys()).ed25519; } -function getAccountPrekeysSet(account: OlmAccount): { - +prekey: string, - +prekeySignature: ?string, -} { - const prekeyMap = JSON.parse(account.prekey()).curve25519; - const [prekey] = values(prekeyMap); - const prekeySignature = account.prekey_signature(); - return { prekey, prekeySignature }; -} - async function validateAndUploadAccountPrekeys( contentAccount: OlmAccount, notifAccount: OlmAccount, ): Promise { // Since keys are rotated synchronously, only check validity of one if (shouldRotatePrekey(contentAccount)) { contentAccount.generate_prekey(); notifAccount.generate_prekey(); await publishPrekeysToIdentity(contentAccount, notifAccount); contentAccount.mark_prekey_as_published(); notifAccount.mark_prekey_as_published(); } if (shouldForgetPrekey(contentAccount)) { contentAccount.forget_old_prekey(); notifAccount.forget_old_prekey(); } } async function publishPrekeysToIdentity( contentAccount: OlmAccount, notifAccount: OlmAccount, ): Promise { const rustAPIPromise = getRustAPI(); const verifyUserLoggedInPromise = verifyUserLoggedIn(); const deviceID = JSON.parse(contentAccount.identity_keys()).ed25519; const { prekey: contentPrekey, prekeySignature: contentPrekeySignature } = getAccountPrekeysSet(contentAccount); const { prekey: notifPrekey, prekeySignature: notifPrekeySignature } = getAccountPrekeysSet(notifAccount); if (!contentPrekeySignature || !notifPrekeySignature) { console.warn('Unable to create valid signature for a prekey'); return; } const [rustAPI, identityInfo] = await Promise.all([ rustAPIPromise, verifyUserLoggedInPromise, ]); if (!identityInfo) { console.warn( 'Attempted to refresh prekeys before registering with Identity service', ); return; } await rustAPI.publishPrekeys( identityInfo.userId, deviceID, identityInfo.accessToken, contentPrekey, contentPrekeySignature, notifPrekey, notifPrekeySignature, ); } export { createPickledOlmAccount, createPickledOlmSession, getOlmUtility, unpickleOlmAccount, unpickleOlmSession, - validateAccountPrekey, uploadNewOneTimeKeys, getContentSigningKey, - getAccountPrekeysSet, validateAndUploadAccountPrekeys, publishPrekeysToIdentity, }; diff --git a/lib/flow-typed/npm/@commapp/olm_vx.x.x.js b/lib/flow-typed/npm/@commapp/olm_vx.x.x.js new file mode 100644 index 000000000..f365eecbc --- /dev/null +++ b/lib/flow-typed/npm/@commapp/olm_vx.x.x.js @@ -0,0 +1,182 @@ +// flow-typed signature: 085f002da86534cfd8cee47ffa99dd67 +// flow-typed version: <>/@commapp/olm_v3.2.4/flow_v0.182.0 + +declare module '@commapp/olm' { + +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + + declare export class Account { + constructor(): void; + free(): void; + create(): void; + identity_keys(): string; + sign(message: string | Uint8Array): string; + one_time_keys(): string; + mark_keys_as_published(): void; + max_number_of_one_time_keys(): number; + generate_one_time_keys(number_of_keys: number): void; + remove_one_time_keys(session: Session): void; + generate_prekey(): void; + prekey(): string; + unpublished_prekey(): ?string; + prekey_signature(): ?string; + forget_old_prekey(): void; + mark_prekey_as_published(): void; + last_prekey_publish_time(): number; + generate_fallback_key(): void; + fallback_key(): string; + unpublished_fallback_key(): string; + forget_old_fallback_key(): void; + pickle(key: string | Uint8Array): string; + unpickle(key: string | Uint8Array, pickle: string): void; + } + + declare export type EncryptResult = { + +type: 0 | 1, // 0: PreKey, 1: Message + +body: string, + }; + declare export class Session { + constructor(): void; + free(): void; + pickle(key: string | Uint8Array): string; + unpickle(key: string | Uint8Array, pickle: string): void; + create_outbound( + account: Account, + their_identity_key: string, + their_signing_key: string, + their_prekey: string, + their_prekey_signature: string, + their_one_time_key: string, + ): void; + create_inbound(account: Account, one_time_key_message: string): void; + create_inbound_from( + account: Account, + identity_key: string, + one_time_key_message: string, + ): void; + session_id(): string; + has_received_message(): boolean; + matches_inbound(one_time_key_message: string): boolean; + matches_inbound_from( + identity_key: string, + one_time_key_message: string, + ): boolean; + encrypt(plaintext: string): EncryptResult; + decrypt(message_type: number, message: string): string; + describe(): string; + } + + declare export class Utility { + constructor(): void; + free(): void; + sha256(input: string | Uint8Array): string; + ed25519_verify( + key: string, + message: string | Uint8Array, + signature: string, + ): void; + } + + declare export type DecryptResult = { + +message_index: string, + +plaintext: string, + }; + + declare export class InboundGroupSession { + constructor(): void; + free(): void; + pickle(key: string | Uint8Array): string; + unpickle(key: string | Uint8Array, pickle: string): void; + create(session_key: string): string; + import_session(session_key: string): string; + decrypt(message: string): DecryptResult; + session_id(): string; + first_known_index(): number; + export_session(message_index: number): string; + } + + declare export class OutboundGroupSession { + constructor(): void; + free(): void; + pickle(key: string | Uint8Array): string; + unpickle(key: string | Uint8Array, pickle: string): void; + create(): void; + encrypt(plaintext: string): string; + session_id(): string; + session_key(): string; + message_index(): number; + } + + declare export type PkEncryptionEncryptResult = { + +ciphertext: string, + +mac: string, + +ephemeral: string, + }; + + declare export class PkEncryption { + constructor(): void; + free(): void; + set_recipient_key(key: string): void; + encrypt(plaintext: string): PkEncryptionEncryptResult; + } + + declare export class PkDecryption { + constructor(): void; + free(): void; + init_with_private_key(key: Uint8Array): string; + generate_key(): string; + get_private_key(): Uint8Array; + pickle(key: string | Uint8Array): string; + unpickle(key: string | Uint8Array, pickle: string): string; + decrypt(ephemeral_key: string, mac: string, ciphertext: string): string; + } + + declare export class PkSigning { + constructor(): void; + free(): void; + init_with_seed(seed: Uint8Array): string; + generate_seed(): Uint8Array; + sign(message: string): string; + } + + declare export class SAS { + constructor(): void; + free(): void; + get_pubkey(): string; + set_their_key(their_key: string): void; + generate_bytes(info: string, length: number): Uint8Array; + calculate_mac(input: string, info: string): string; + calculate_mac_fixed_base64(input: string, info: string): string; + calculate_mac_long_kdf(input: string, info: string): string; + } + + declare export function init(opts?: Object): Promise; + + declare export function get_library_version(): [number, number, number]; + + declare export var PRIVATE_KEY_LENGTH: number; + + declare export default { + init: typeof init, + get_library_version: typeof get_library_version, + PRIVATE_KEY_LENGTH: typeof PRIVATE_KEY_LENGTH, + Account: typeof Account, + Utility: typeof Utility, + Session: typeof Session, + }; + +} diff --git a/lib/package.json b/lib/package.json index 6dd44c377..3e2ee4eb0 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,81 +1,82 @@ { "name": "lib", "version": "0.0.1", "type": "module", "private": true, "license": "BSD-3-Clause", "scripts": { "clean": "rm -rf node_modules/", "test": "jest" }, "devDependencies": { "@babel/core": "^7.23.7", "@babel/plugin-transform-class-properties": "^7.23.3", "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", "@babel/plugin-transform-object-rest-spread": "^7.23.4", "@babel/plugin-transform-optional-chaining": "^7.23.4", "@babel/plugin-transform-runtime": "^7.23.7", "@babel/preset-env": "^7.23.7", "@babel/preset-flow": "^7.23.3", "@babel/preset-react": "^7.23.3", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "babel-jest": "^29.7.0", "babel-loader": "^9.1.3", "buffer": "^6.0.3", "clean-webpack-plugin": "^4.0.0", "css-loader": "^6.7.3", "css-minimizer-webpack-plugin": "^4.2.2", "flow-bin": "^0.202.1", "flow-typed": "^3.2.1", "jest": "^29.7.0", "mini-css-extract-plugin": "^2.7.2", "react-refresh": "^0.14.0", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.6", "webpack": "^5.76.0" }, "dependencies": { + "@commapp/olm": "0.1.0", "@rainbow-me/rainbowkit": "^1.1.1", "dateformat": "^3.0.3", "emoji-regex": "^10.2.1", "eth-ens-namehash": "^2.0.8", "ethers": "^5.7.2", "fast-json-stable-stringify": "^2.0.0", "file-type": "^12.3.0", "focus-trap-react": "^10.1.4", "idna-uts46-hx": "^2.3.1", "invariant": "^2.2.4", "just-clone": "^3.2.1", "lodash": "^4.17.21", "react": "18.1.0", "react-icomoon": "^2.5.7", "react-redux": "^7.1.1", "redux-persist": "^6.0.0", "reselect": "^4.0.0", "reselect-map": "^1.0.5", "simple-markdown": "^0.7.2", "siwe": "^1.1.6", "string-hash": "^1.1.3", "tcomb": "^3.2.29", "tinycolor2": "^1.4.1", "tokenize-text": "^1.1.3", "util-inspect": "^0.1.8", "utils-copy-error": "^1.0.1", "uuid": "^3.4.0", "viem": "^1.15.4", "wagmi": "^1.4.3" }, "jest": { "transform": { "\\.js$": [ "babel-jest", { "rootMode": "upward" } ] }, "transformIgnorePatterns": [ "/node_modules/(?!@babel/runtime)" ] } } diff --git a/lib/utils/olm-utils.js b/lib/utils/olm-utils.js new file mode 100644 index 000000000..4c080ec85 --- /dev/null +++ b/lib/utils/olm-utils.js @@ -0,0 +1,103 @@ +// @flow + +import type { Account as OlmAccount } from '@commapp/olm'; + +import { values } from './objects.js'; +import { getOneTimeKeyValuesFromBlob } from '../shared/crypto-utils.js'; +import { ONE_TIME_KEYS_NUMBER } from '../types/identity-service-types.js'; + +const maxPublishedPrekeyAge = 30 * 24 * 60 * 60 * 1000; +const maxOldPrekeyAge = 24 * 60 * 60 * 1000; + +type AccountKeysSet = { + +identityKeys: string, + +prekey: string, + +prekeySignature: string, + +oneTimeKeys: $ReadOnlyArray, +}; + +function validateAccountPrekey(account: OlmAccount) { + if (shouldRotatePrekey(account)) { + account.generate_prekey(); + } + if (shouldForgetPrekey(account)) { + account.forget_old_prekey(); + } +} + +function shouldRotatePrekey(account: OlmAccount): 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 + // result in the old active prekey being discarded. + if (account.unpublished_prekey()) { + return false; + } + + const currentDate = new Date(); + const lastPrekeyPublishDate = getLastPrekeyPublishTime(account); + + return ( + currentDate.getTime() - lastPrekeyPublishDate.getTime() >= + maxPublishedPrekeyAge + ); +} + +function shouldForgetPrekey(account: OlmAccount): 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()) { + return false; + } + + const currentDate = new Date(); + const lastPrekeyPublishDate = getLastPrekeyPublishTime(account); + + return ( + currentDate.getTime() - lastPrekeyPublishDate.getTime() >= maxOldPrekeyAge + ); +} + +function getLastPrekeyPublishTime(account: OlmAccount): Date { + const olmLastPrekeyPublishTime = account.last_prekey_publish_time(); + + // Olm uses seconds, while the Date() constructor expects milliseconds. + return new Date(olmLastPrekeyPublishTime * 1000); +} + +function getAccountPrekeysSet(account: OlmAccount): { + +prekey: string, + +prekeySignature: ?string, +} { + const prekeyMap = JSON.parse(account.prekey()).curve25519; + const [prekey] = values(prekeyMap); + const prekeySignature = account.prekey_signature(); + return { prekey, prekeySignature }; +} + +function retrieveAccountKeysSet(account: OlmAccount): AccountKeysSet { + const identityKeys = account.identity_keys(); + + validateAccountPrekey(account); + const { prekey, prekeySignature } = getAccountPrekeysSet(account); + + if (!prekeySignature || !prekey) { + throw new Error('invalid_prekey'); + } + + let oneTimeKeys = getOneTimeKeyValuesFromBlob(account.one_time_keys()); + + if (oneTimeKeys.length < ONE_TIME_KEYS_NUMBER) { + account.generate_one_time_keys(ONE_TIME_KEYS_NUMBER); + oneTimeKeys = getOneTimeKeyValuesFromBlob(account.one_time_keys()); + } + + return { identityKeys, oneTimeKeys, prekey, prekeySignature }; +} + +export { + retrieveAccountKeysSet, + getAccountPrekeysSet, + shouldForgetPrekey, + shouldRotatePrekey, +};