Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3510072
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
18 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 11:15 AM (19 h, 55 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690815
Default Alt Text
(18 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment