diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js --- a/keyserver/src/keyserver.js +++ b/keyserver/src/keyserver.js @@ -30,6 +30,7 @@ multimediaUploadResponder, uploadDownloadResponder, } from './uploads/uploads.js'; +import { verifyUserLoggedIn } from './user/login.js'; import { initENSCache } from './utils/ens-cache.js'; import { prefetchAllURLFacts, @@ -58,6 +59,14 @@ // in https://github.com/remy/nodemon/issues/751 process.exit(2); } + + // Allow login to be optional until staging environment is available + try { + await verifyUserLoggedIn(); + } catch (e) { + console.warn('failed_identity_login'); + } + const cpuCount = os.cpus().length; for (let i = 0; i < cpuCount; i++) { cluster.fork(); 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 @@ -13,7 +13,7 @@ async function fetchCallUpdateOlmAccount( olmAccountType: 'content' | 'notifications', - callback: (account: OlmAccount, picklingKey: string) => Promise, + callback: (account: OlmAccount, picklingKey: string) => Promise | T, ): Promise { const isContent = olmAccountType === 'content'; let retriesLeft = maxOlmAccountUpdateRetriesCount; diff --git a/keyserver/src/user/login.js b/keyserver/src/user/login.js new file mode 100644 --- /dev/null +++ b/keyserver/src/user/login.js @@ -0,0 +1,199 @@ +// @flow + +import type { Account as OlmAccount } from '@commapp/olm'; +import type { QueryResults } from 'mysql'; +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 { SQL, dbQuery } from '../database/database.js'; +import { getMessageForException } from '../responders/utils.js'; +import { fetchCallUpdateOlmAccount } from '../updaters/olm-account-updater.js'; +import { validateAccountPrekey } from '../utils/olm-utils.js'; + +type UserCredentials = { +username: string, +password: string }; +type IdentityInfo = { +userId: string, +accessToken: string }; + +const userIDMetadataKey = 'user_id'; +const accessTokenMetadataKey = 'access_token'; + +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 fetchIdentityInfo(): Promise { + const versionQuery = SQL` + SELECT data + FROM metadata + WHERE name IN (${userIDMetadataKey}, ${accessTokenMetadataKey}) + `; + + const [[userId, accessToken]] = await dbQuery(versionQuery); + if (!userId || !accessToken) { + return null; + } + return { userId, accessToken }; +} + +async function saveIdentityInfo(userInfo: IdentityInfo): Promise { + const updateQuery = SQL` + REPLACE INTO metadata (name, data) + VALUES (${userIDMetadataKey}, ${userInfo.userId}), + (${accessTokenMetadataKey}, ${userInfo.accessToken}) + `; + + return dbQuery(updateQuery); +} + +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) { + 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 --- a/keyserver/src/utils/olm-utils.js +++ b/keyserver/src/utils/olm-utils.js @@ -77,7 +77,7 @@ return cachedOLMUtility; } -async function validateAccountPrekey(account: OlmAccount): Promise { +function validateAccountPrekey(account: OlmAccount) { const currentDate = new Date(); const lastPrekeyPublishDate = new Date(account.last_prekey_publish_time());