diff --git a/keyserver/src/updaters/olm-account-updater.js b/keyserver/src/updaters/olm-account-updater.js --- a/keyserver/src/updaters/olm-account-updater.js +++ b/keyserver/src/updaters/olm-account-updater.js @@ -2,7 +2,7 @@ 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'; @@ -43,7 +43,12 @@ 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( diff --git a/keyserver/src/user/login.js b/keyserver/src/user/login.js --- a/keyserver/src/user/login.js +++ b/keyserver/src/user/login.js @@ -3,10 +3,9 @@ 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, @@ -15,40 +14,9 @@ } 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(); diff --git a/keyserver/src/utils/olm-utils.js b/keyserver/src/utils/olm-utils.js --- a/keyserver/src/utils/olm-utils.js +++ b/keyserver/src/utils/olm-utils.js @@ -12,7 +12,11 @@ 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, @@ -25,49 +29,6 @@ +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(); @@ -139,15 +100,6 @@ 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(), @@ -192,16 +144,6 @@ 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, @@ -267,10 +209,8 @@ 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 --- /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 --- a/lib/package.json +++ b/lib/package.json @@ -35,6 +35,7 @@ "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", diff --git a/lib/utils/olm-utils.js b/lib/utils/olm-utils.js new file mode 100644 --- /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, +};