diff --git a/lib/types/crypto-types.js b/lib/types/crypto-types.js index 02aef80d9..783b29958 100644 --- a/lib/types/crypto-types.js +++ b/lib/types/crypto-types.js @@ -1,133 +1,134 @@ // @flow import t, { type TInterface } from 'tcomb'; import { tShape } from '../utils/validation-utils.js'; export type OLMIdentityKeys = { +ed25519: string, +curve25519: string, }; const olmIdentityKeysValidator: TInterface = tShape({ ed25519: t.String, curve25519: t.String, }); export type OLMPrekey = { +curve25519: { +id: string, +key: string, }, }; export type SignedPrekeys = { +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, }; export const signedPrekeysValidator: TInterface = tShape({ contentPrekey: t.String, contentPrekeySignature: t.String, notifPrekey: t.String, notifPrekeySignature: t.String, }); export type OLMOneTimeKeys = { +curve25519: { +[string]: string }, }; export type OneTimeKeysResult = { +contentOneTimeKeys: OLMOneTimeKeys, +notificationsOneTimeKeys: OLMOneTimeKeys, }; export type OneTimeKeysResultValues = { +contentOneTimeKeys: $ReadOnlyArray, +notificationsOneTimeKeys: $ReadOnlyArray, }; export type PickledOLMAccount = { +picklingKey: string, +pickledAccount: string, }; export type CryptoStore = { +primaryAccount: PickledOLMAccount, +primaryIdentityKeys: OLMIdentityKeys, +notificationAccount: PickledOLMAccount, +notificationIdentityKeys: OLMIdentityKeys, }; export type CryptoStoreContextType = { +getInitializedCryptoStore: () => Promise, }; export type NotificationsOlmDataType = { +mainSession: string, +picklingKey: string, +pendingSessionUpdate: string, +updateCreationTimestamp: number, }; export type IdentityKeysBlob = { +primaryIdentityPublicKeys: OLMIdentityKeys, +notificationIdentityPublicKeys: OLMIdentityKeys, }; export const identityKeysBlobValidator: TInterface = tShape({ primaryIdentityPublicKeys: olmIdentityKeysValidator, notificationIdentityPublicKeys: olmIdentityKeysValidator, }); export type SignedIdentityKeysBlob = { +payload: string, +signature: string, }; export const signedIdentityKeysBlobValidator: TInterface = tShape({ payload: t.String, signature: t.String, }); export type UserDetail = { +username: string, +userID: string, }; // This type should not be changed without making equivalent changes to // `Message` in Identity service's `reserved_users` module export type ReservedUsernameMessage = | { +statement: 'Add the following usernames to reserved list', +payload: $ReadOnlyArray, +issuedAt: string, } | { +statement: 'Remove the following username from reserved list', +payload: string, +issuedAt: string, } | { +statement: 'This user is the owner of the following username and user ID', +payload: UserDetail, +issuedAt: string, }; export const olmEncryptedMessageTypes = Object.freeze({ PREKEY: 0, TEXT: 1, }); export type OlmAPI = { +initializeCryptoAccount: () => Promise, +encrypt: (content: string, deviceID: string) => Promise, +decrypt: (encryptedContent: string, deviceID: string) => Promise, +contentInboundSessionCreator: ( contentIdentityKeys: OLMIdentityKeys, initialEncryptedContent: string, ) => Promise, + +getOneTimeKeys: (numberOfKeys: number) => Promise, }; diff --git a/lib/utils/olm-utils.js b/lib/utils/olm-utils.js index 4c080ec85..80c01642b 100644 --- a/lib/utils/olm-utils.js +++ b/lib/utils/olm-utils.js @@ -1,103 +1,111 @@ // @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 getAccountOneTimeKeys( + account: OlmAccount, + numberOfKeys: number, +): $ReadOnlyArray { + let oneTimeKeys = getOneTimeKeyValuesFromBlob(account.one_time_keys()); + if (oneTimeKeys.length < numberOfKeys) { + account.generate_one_time_keys(numberOfKeys - oneTimeKeys.length); + oneTimeKeys = getOneTimeKeyValuesFromBlob(account.one_time_keys()); + } + return oneTimeKeys; +} + 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()); - } + const oneTimeKeys = getAccountOneTimeKeys(account, ONE_TIME_KEYS_NUMBER); return { identityKeys, oneTimeKeys, prekey, prekeySignature }; } export { retrieveAccountKeysSet, getAccountPrekeysSet, shouldForgetPrekey, shouldRotatePrekey, + getAccountOneTimeKeys, }; diff --git a/native/crypto/olm-api.js b/native/crypto/olm-api.js index adc6afd31..4ea57349f 100644 --- a/native/crypto/olm-api.js +++ b/native/crypto/olm-api.js @@ -1,29 +1,42 @@ // @flow -import type { OlmAPI, OLMIdentityKeys } from 'lib/types/crypto-types'; +import { getOneTimeKeyValues } from 'lib/shared/crypto-utils.js'; +import type { + OneTimeKeysResultValues, + OlmAPI, + OLMIdentityKeys, +} from 'lib/types/crypto-types'; import { commCoreModule } from '../native-modules.js'; const olmAPI: OlmAPI = { async initializeCryptoAccount(): Promise { await commCoreModule.initializeCryptoAccount(); }, encrypt: commCoreModule.encrypt, decrypt: commCoreModule.decrypt, async contentInboundSessionCreator( contentIdentityKeys: OLMIdentityKeys, initialEncryptedContent: string, ): Promise { const identityKeys = JSON.stringify({ curve25519: contentIdentityKeys.curve25519, ed25519: contentIdentityKeys.ed25519, }); return commCoreModule.initializeContentInboundSession( identityKeys, initialEncryptedContent, contentIdentityKeys.ed25519, ); }, + async getOneTimeKeys(numberOfKeys: number): Promise { + const { contentOneTimeKeys, notificationsOneTimeKeys } = + await commCoreModule.getOneTimeKeys(numberOfKeys); + return { + contentOneTimeKeys: getOneTimeKeyValues(contentOneTimeKeys), + notificationsOneTimeKeys: getOneTimeKeyValues(notificationsOneTimeKeys), + }; + }, }; export { olmAPI }; diff --git a/web/crypto/olm-api.js b/web/crypto/olm-api.js index 03bd043a5..9b73a73c3 100644 --- a/web/crypto/olm-api.js +++ b/web/crypto/olm-api.js @@ -1,69 +1,92 @@ // @flow import olm from '@commapp/olm'; import type { Account, Session } from '@commapp/olm'; import { type OlmAPI, olmEncryptedMessageTypes, type OLMIdentityKeys, + type OneTimeKeysResultValues, } from 'lib/types/crypto-types.js'; +import { getAccountOneTimeKeys } from 'lib/utils/olm-utils.js'; // methods below are just mocks to SQLite API // implement proper methods tracked in ENG-6462 function getOlmAccount(): Account { - return new olm.Account(); + const account = new olm.Account(); + account.create(); + return account; } // eslint-disable-next-line no-unused-vars function getOlmSession(deviceID: string): Session { return new olm.Session(); } // eslint-disable-next-line no-unused-vars function storeOlmAccount(account: Account): void {} // eslint-disable-next-line no-unused-vars function storeOlmSession(session: Session): void {} const olmAPI: OlmAPI = { async initializeCryptoAccount(): Promise { await olm.init(); }, async encrypt(content: string, deviceID: string): Promise { const session = getOlmSession(deviceID); const { body } = session.encrypt(content); storeOlmSession(session); return body; }, async decrypt(encryptedContent: string, deviceID: string): Promise { const session = getOlmSession(deviceID); const result = session.decrypt( olmEncryptedMessageTypes.TEXT, encryptedContent, ); storeOlmSession(session); return result; }, async contentInboundSessionCreator( contentIdentityKeys: OLMIdentityKeys, initialEncryptedContent: string, ): Promise { const account = getOlmAccount(); const session = new olm.Session(); session.create_inbound_from( account, contentIdentityKeys.curve25519, initialEncryptedContent, ); account.remove_one_time_keys(session); const initialEncryptedMessage = session.decrypt( olmEncryptedMessageTypes.PREKEY, initialEncryptedContent, ); storeOlmAccount(account); storeOlmSession(session); return initialEncryptedMessage; }, + async getOneTimeKeys(numberOfKeys: number): Promise { + const contentAccount = getOlmAccount(); + const notifAccount = getOlmAccount(); + const contentOneTimeKeys = getAccountOneTimeKeys( + contentAccount, + numberOfKeys, + ); + contentAccount.mark_keys_as_published(); + storeOlmAccount(contentAccount); + + const notificationsOneTimeKeys = getAccountOneTimeKeys( + notifAccount, + numberOfKeys, + ); + notifAccount.mark_keys_as_published(); + storeOlmAccount(notifAccount); + + return { contentOneTimeKeys, notificationsOneTimeKeys }; + }, }; export { olmAPI };