Page MenuHomePhabricator

No OneTemporary

diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js
index 57a1baeea..02dac6daf 100644
--- a/keyserver/src/keyserver.js
+++ b/keyserver/src/keyserver.js
@@ -1,184 +1,193 @@
// @flow
import olm from '@commapp/olm';
import cluster from 'cluster';
import cookieParser from 'cookie-parser';
import express from 'express';
import expressWs from 'express-ws';
import os from 'os';
import './cron/cron.js';
import { migrate } from './database/migrations.js';
import { jsonEndpoints } from './endpoints.js';
import { emailSubscriptionResponder } from './responders/comm-landing-responders.js';
import {
jsonHandler,
downloadHandler,
htmlHandler,
uploadHandler,
} from './responders/handlers.js';
import landingHandler from './responders/landing-handler.js';
import { errorReportDownloadResponder } from './responders/report-responders.js';
import {
inviteResponder,
websiteResponder,
} from './responders/website-responders.js';
import { webWorkerResponder } from './responders/webworker-responders.js';
import { onConnection } from './socket/socket.js';
import {
multerProcessor,
multimediaUploadResponder,
uploadDownloadResponder,
} from './uploads/uploads.js';
+import { verifyUserLoggedIn } from './user/login.js';
import { initENSCache } from './utils/ens-cache.js';
import {
prefetchAllURLFacts,
getSquadCalURLFacts,
getLandingURLFacts,
getCommAppURLFacts,
} from './utils/urls.js';
(async () => {
await Promise.all([olm.init(), prefetchAllURLFacts(), initENSCache()]);
const squadCalBaseRoutePath = getSquadCalURLFacts()?.baseRoutePath;
const landingBaseRoutePath = getLandingURLFacts()?.baseRoutePath;
const commAppBaseRoutePath = getCommAppURLFacts()?.baseRoutePath;
const compiledFolderOptions =
process.env.NODE_ENV === 'development'
? undefined
: { maxAge: '1y', immutable: true };
if (cluster.isMaster) {
const didMigrationsSucceed: boolean = await migrate();
if (!didMigrationsSucceed) {
// The following line uses exit code 2 to ensure nodemon exits
// in a dev environment, instead of restarting. Context provided
// 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();
}
cluster.on('exit', () => cluster.fork());
} else {
const server = express();
expressWs(server);
server.use(express.json({ limit: '250mb' }));
server.use(cookieParser());
const setupAppRouter = router => {
router.use('/images', express.static('images'));
router.use('/fonts', express.static('fonts'));
router.use('/misc', express.static('misc'));
router.use(
'/.well-known',
express.static(
'.well-known',
// Necessary for apple-app-site-association file
{
setHeaders: res =>
res.setHeader('Content-Type', 'application/json'),
},
),
);
router.use(
'/compiled',
express.static('app_compiled', compiledFolderOptions),
);
router.use('/', express.static('icons'));
for (const endpoint in jsonEndpoints) {
// $FlowFixMe Flow thinks endpoint is string
const responder = jsonEndpoints[endpoint];
const expectCookieInvalidation = endpoint === 'log_out';
router.post(
`/${endpoint}`,
jsonHandler(responder, expectCookieInvalidation),
);
}
router.get(
'/download_error_report/:reportID',
downloadHandler(errorReportDownloadResponder),
);
router.get(
'/upload/:uploadID/:secret',
downloadHandler(uploadDownloadResponder),
);
router.get('/invite/:secret', inviteResponder);
// $FlowFixMe express-ws has side effects that can't be typed
router.ws('/ws', onConnection);
router.get('/worker/:worker', webWorkerResponder);
router.get('*', htmlHandler(websiteResponder));
router.post(
'/upload_multimedia',
multerProcessor,
uploadHandler(multimediaUploadResponder),
);
};
// Note - the order of router declarations matters. On prod we have
// squadCalBaseRoutePath configured to '/', which means it's a catch-all. If
// we call server.use on squadCalRouter first, it will catch all requests
// and prevent commAppRouter and landingRouter from working correctly. So we
// make sure that squadCalRouter goes last
server.get('/invite/:secret', inviteResponder);
if (landingBaseRoutePath) {
const landingRouter = express.Router();
landingRouter.get('/invite/:secret', inviteResponder);
landingRouter.use(
'/.well-known',
express.static(
'.well-known',
// Necessary for apple-app-site-association file
{
setHeaders: res =>
res.setHeader('Content-Type', 'application/json'),
},
),
);
landingRouter.use('/images', express.static('images'));
landingRouter.use('/fonts', express.static('fonts'));
landingRouter.use(
'/compiled',
express.static('landing_compiled', compiledFolderOptions),
);
landingRouter.use('/', express.static('landing_icons'));
landingRouter.post('/subscribe_email', emailSubscriptionResponder);
landingRouter.get('*', landingHandler);
server.use(landingBaseRoutePath, landingRouter);
}
if (commAppBaseRoutePath) {
const commAppRouter = express.Router();
setupAppRouter(commAppRouter);
server.use(commAppBaseRoutePath, commAppRouter);
}
if (squadCalBaseRoutePath) {
const squadCalRouter = express.Router();
setupAppRouter(squadCalRouter);
server.use(squadCalBaseRoutePath, squadCalRouter);
}
const listenAddress = (() => {
if (process.env.COMM_LISTEN_ADDR) {
return process.env.COMM_LISTEN_ADDR;
} else if (process.env.NODE_ENV === 'development') {
return undefined;
} else {
return 'localhost';
}
})();
server.listen(parseInt(process.env.PORT, 10) || 3000, listenAddress);
}
})();
diff --git a/keyserver/src/updaters/olm-account-updater.js b/keyserver/src/updaters/olm-account-updater.js
index 6fe744c55..5a918e925 100644
--- a/keyserver/src/updaters/olm-account-updater.js
+++ b/keyserver/src/updaters/olm-account-updater.js
@@ -1,112 +1,112 @@
// @flow
import type { Account as OlmAccount } from '@commapp/olm';
import { ServerError } 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<T>(
olmAccountType: 'content' | 'notifications',
- callback: (account: OlmAccount, picklingKey: string) => Promise<T>,
+ callback: (account: OlmAccount, picklingKey: string) => Promise<T> | T,
): Promise<T> {
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);
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
new file mode 100644
index 000000000..74640efb7
--- /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<string>,
+};
+
+function getOneTimeKeyValues(keyBlob: string): $ReadOnlyArray<string> {
+ const content: OLMOneTimeKeys = JSON.parse(keyBlob);
+ const keys: $ReadOnlyArray<string> = 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<?IdentityInfo> {
+ 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 };
+}
+
+function saveIdentityInfo(userInfo: IdentityInfo): Promise<QueryResults> {
+ const updateQuery = SQL`
+ REPLACE INTO metadata (name, data)
+ VALUES (${userIDMetadataKey}, ${userInfo.userId}),
+ (${accessTokenMetadataKey}, ${userInfo.accessToken})
+ `;
+
+ return dbQuery(updateQuery);
+}
+
+async function verifyUserLoggedIn(): Promise<IdentityInfo> {
+ const result = await fetchIdentityInfo();
+
+ if (result) {
+ return result;
+ }
+
+ const identityInfo = await registerOrLogin();
+ await saveIdentityInfo(identityInfo);
+ return identityInfo;
+}
+
+async function registerOrLogin(): Promise<IdentityInfo> {
+ const rustAPIPromise = getRustAPI();
+
+ const userInfo = await getCommConfig<UserCredentials>({
+ 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
index 14f6a573d..e525089b3 100644
--- a/keyserver/src/utils/olm-utils.js
+++ b/keyserver/src/utils/olm-utils.js
@@ -1,109 +1,109 @@
// @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';
type PickledOlmAccount = {
+picklingKey: string,
+pickledAccount: string,
};
const maxPublishedPrekeyAge = 30 * 24 * 60 * 60 * 1000;
const maxOldPrekeyAge = 24 * 60 * 60 * 1000;
async function createPickledOlmAccount(): Promise<PickledOlmAccount> {
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<OlmAccount> {
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<string> {
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<OlmSession> {
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;
}
-async function validateAccountPrekey(account: OlmAccount): Promise<void> {
+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();
}
}
export {
createPickledOlmAccount,
createPickledOlmSession,
getOlmUtility,
unpickleOlmAccount,
unpickleOlmSession,
validateAccountPrekey,
};

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 11:15 AM (15 h, 33 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690815
Default Alt Text
(18 KB)

Event Timeline