diff --git a/keyserver/src/user/login.js b/keyserver/src/user/login.js index 8ef4393fd..16d47cedc 100644 --- a/keyserver/src/user/login.js +++ b/keyserver/src/user/login.js @@ -1,175 +1,171 @@ // @flow import type { Account as OlmAccount } from '@commapp/olm'; import { getRustAPI } from 'rust-node-addon'; -import type { OLMOneTimeKeys } from 'lib/types/crypto-types'; import { getCommConfig } from 'lib/utils/comm-config.js'; import { ServerError } from 'lib/utils/errors.js'; import { values } from 'lib/utils/objects.js'; import { saveIdentityInfo, fetchIdentityInfo, type IdentityInfo, } from './identity.js'; import { getMessageForException } from '../responders/utils.js'; import { fetchCallUpdateOlmAccount } from '../updaters/olm-account-updater.js'; -import { validateAccountPrekey } from '../utils/olm-utils.js'; +import { + getOneTimeKeyValues, + validateAccountPrekey, +} from '../utils/olm-utils.js'; type UserCredentials = { +username: string, +password: string }; export type AccountKeysSet = { +identityKeys: string, +prekey: string, +prekeySignature: string, +oneTimeKey: $ReadOnlyArray, }; -function getOneTimeKeyValues(keyBlob: string): $ReadOnlyArray { - const content: OLMOneTimeKeys = JSON.parse(keyBlob); - const keys: $ReadOnlyArray = values(content.curve25519); - return keys; -} - function retrieveAccountKeysSet(account: OlmAccount): AccountKeysSet { const identityKeys = account.identity_keys(); validateAccountPrekey(account); const prekeyMap = JSON.parse(account.prekey()).curve25519; const [prekey] = values(prekeyMap); const prekeySignature = account.prekey_signature(); if (!prekeySignature || !prekey) { throw new ServerError('invalid_prekey'); } if (getOneTimeKeyValues(account.one_time_keys()).length < 10) { account.generate_one_time_keys(10); } const oneTimeKey = getOneTimeKeyValues(account.one_time_keys()); return { identityKeys, oneTimeKey, 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, oneTimeKey: notificationsOneTimeKey, } = await fetchCallUpdateOlmAccount('notifications', retrieveAccountKeysSet); const contentAccountCallback = async (account: OlmAccount) => { const { identityKeys: contentIdentityKeys, oneTimeKey, 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, oneTimeKey, prekey, prekeySignature, }; }; const [ rustAPI, { signedIdentityKeysBlob, prekey: contentPrekey, prekeySignature: contentPrekeySignature, oneTimeKey: contentOneTimeKey, }, ] = await Promise.all([ rustAPIPromise, fetchCallUpdateOlmAccount('content', contentAccountCallback), ]); try { const identity_info = await rustAPI.loginUser( userInfo.username, userInfo.password, signedIdentityKeysBlob, contentPrekey, contentPrekeySignature, notificationsPrekey, notificationsPrekeySignature, contentOneTimeKey, notificationsOneTimeKey, ); 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, contentOneTimeKey, notificationsOneTimeKey, ); 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 e525089b3..5727849d3 100644 --- a/keyserver/src/utils/olm-utils.js +++ b/keyserver/src/utils/olm-utils.js @@ -1,109 +1,120 @@ // @flow import olm from '@commapp/olm'; import type { Account as OlmAccount, Utility as OlmUtility, Session as OlmSession, } from '@commapp/olm'; import uuid from 'uuid'; -import { olmEncryptedMessageTypes } from 'lib/types/crypto-types.js'; +import { + olmEncryptedMessageTypes, + type OLMOneTimeKeys, +} from 'lib/types/crypto-types.js'; +import { values } from 'lib/utils/objects.js'; type PickledOlmAccount = { +picklingKey: string, +pickledAccount: string, }; const maxPublishedPrekeyAge = 30 * 24 * 60 * 60 * 1000; const maxOldPrekeyAge = 24 * 60 * 60 * 1000; 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, ): Promise { await olm.init(); const session = new olm.Session(); 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) { const currentDate = new Date(); const lastPrekeyPublishDate = new Date(account.last_prekey_publish_time()); const prekeyPublished = !account.unpublished_prekey(); if ( prekeyPublished && currentDate - lastPrekeyPublishDate > maxPublishedPrekeyAge ) { // If there is no prekey or the current prekey is older than month // we need to generate new one. account.generate_prekey(); } if ( prekeyPublished && currentDate - lastPrekeyPublishDate >= maxOldPrekeyAge ) { account.forget_old_prekey(); } } +function getOneTimeKeyValues(keyBlob: string): $ReadOnlyArray { + const content: OLMOneTimeKeys = JSON.parse(keyBlob); + const keys: $ReadOnlyArray = values(content.curve25519); + return keys; +} + export { createPickledOlmAccount, createPickledOlmSession, getOlmUtility, unpickleOlmAccount, unpickleOlmSession, validateAccountPrekey, + getOneTimeKeyValues, };