diff --git a/keyserver/addons/rust-node-addon/rust-binding-types.js b/keyserver/addons/rust-node-addon/rust-binding-types.js index 25ad996d1..b321bd0ef 100644 --- a/keyserver/addons/rust-node-addon/rust-binding-types.js +++ b/keyserver/addons/rust-node-addon/rust-binding-types.js @@ -1,61 +1,62 @@ // @flow import type { SignedIdentityKeysBlob } from 'lib/types/crypto-types.js'; import type { InboundKeyInfoResponse } from 'lib/types/identity-service-types.js'; import type { IdentityInfo } from '../../src/user/identity.js'; type RustNativeBindingAPI = { +loginUser: ( username: string, password: string, signedIdentityKeysBlob: SignedIdentityKeysBlob, contentPrekey: string, contentPrekeySignature: string, notifPrekey: string, notifPrekeySignature: string, contentOneTimeKeys: $ReadOnlyArray, notifOneTimeKeys: $ReadOnlyArray, + force: ?boolean, ) => Promise, +registerUser: ( username: string, password: string, signedIdentityKeysBlob: SignedIdentityKeysBlob, contentPrekey: string, contentPrekeySignature: string, notifPrekey: string, notifPrekeySignature: string, contentOneTimeKeys: $ReadOnlyArray, notifOneTimeKeys: $ReadOnlyArray, ) => Promise, +addReservedUsernames: (message: string, signature: string) => Promise, +removeReservedUsername: ( message: string, signature: string, ) => Promise, +publishPrekeys: ( userId: string, deviceId: string, accessToken: string, contentPrekey: string, contentPrekeySignature: string, notifPrekey: string, notifPrekeySignature: string, ) => Promise, +uploadOneTimeKeys: ( userId: string, deviceId: string, accessToken: string, contentOneTimePrekeys: $ReadOnlyArray, notifOneTimePrekeys: $ReadOnlyArray, ) => Promise, +getInboundKeysForUserDevice: ( authUserId: string, authDeviceId: string, authAccessToken: string, userId: string, deviceId: string, ) => Promise, }; export type { RustNativeBindingAPI }; diff --git a/keyserver/addons/rust-node-addon/src/identity_client/login.rs b/keyserver/addons/rust-node-addon/src/identity_client/login.rs index 5dd8b08b9..3f89eed6d 100644 --- a/keyserver/addons/rust-node-addon/src/identity_client/login.rs +++ b/keyserver/addons/rust-node-addon/src/identity_client/login.rs @@ -1,89 +1,90 @@ use super::*; use comm_opaque2::client::Login; use grpc_clients::identity::protos::unauthenticated::{ OpaqueLoginFinishRequest, OpaqueLoginStartRequest, }; use tracing::debug; #[napi] #[instrument(skip_all)] pub async fn login_user( username: String, password: String, signed_identity_keys_blob: SignedIdentityKeysBlob, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, + force: Option, ) -> Result { - debug!("Attempting to login user: {}", username); + debug!("Attempting to log in user: {}", username); // Set up the gRPC client that will be used to talk to the Identity service let mut identity_client = get_identity_client().await?; // Start OPAQUE registration and send initial registration request let mut client_login = Login::new(); let opaque_login_request = client_login .start(&password) .map_err(|_| Error::from_reason("Failed to create opaque login request"))?; let login_start_request = OpaqueLoginStartRequest { opaque_login_request, username, device_key_upload: Some(DeviceKeyUpload { device_key_info: Some(IdentityKeyInfo { payload: signed_identity_keys_blob.payload, payload_signature: signed_identity_keys_blob.signature, social_proof: None, }), content_upload: Some(Prekey { prekey: content_prekey, prekey_signature: content_prekey_signature, }), notif_upload: Some(Prekey { prekey: notif_prekey, prekey_signature: notif_prekey_signature, }), one_time_content_prekeys: content_one_time_keys, one_time_notif_prekeys: notif_one_time_keys, device_type: DeviceType::Keyserver.into(), }), - force: None, + force, }; debug!("Starting login to identity service"); let response = identity_client .log_in_password_user_start(login_start_request) .await .map_err(handle_grpc_error)?; debug!("Received login response from identity service"); let login_start_response = response.into_inner(); let opaque_login_upload = client_login .finish(&login_start_response.opaque_login_response) .map_err(|_| Error::from_reason("Failed to finish opaque login request"))?; let login_finish_request = OpaqueLoginFinishRequest { session_id: login_start_response.session_id, opaque_login_upload, }; debug!("Attempting to finalize opaque login exchange with identity service"); let login_finish_response = identity_client .log_in_password_user_finish(login_finish_request) .await .map_err(handle_grpc_error)? .into_inner(); debug!("Finished login with identity service"); let user_info = UserLoginInfo { user_id: login_finish_response.user_id, access_token: login_finish_response.access_token, }; Ok(user_info) } diff --git a/keyserver/src/user/checks.js b/keyserver/src/user/checks.js index 8fbe260b1..06fb70195 100644 --- a/keyserver/src/user/checks.js +++ b/keyserver/src/user/checks.js @@ -1,35 +1,36 @@ // @flow import { getCommConfig } from 'lib/utils/comm-config.js'; export type UserCredentials = { +username: string, +password: string, - +usingIdentityCredentials: boolean, + +usingIdentityCredentials?: boolean, + +forceLogin?: boolean, }; async function ensureUserCredentials() { const userCredentials = await getCommConfig({ folder: 'secrets', name: 'user_credentials', }); if (!userCredentials) { console.warn( 'User credentials for the keyserver owner must be specified. They can be ' + 'specified either by setting the ' + '`COMM_JSONCONFIG_secrets_user_credentials` environmental variable, or by ' + 'setting a file at keyserver/secrets/user_credentials.json. The contents ' + 'should be a JSON blob that looks like this:\n' + '{\n' + ' "username": ,\n' + ' "password": \n' + '}\n', ); // Since we don't want to apply the migration until there are credentials; // throw the error and force keyserver to be configured next restart throw new Error('missing_keyserver_owner_credentials'); } } export { ensureUserCredentials }; diff --git a/keyserver/src/user/login.js b/keyserver/src/user/login.js index a5301688c..082a6e364 100644 --- a/keyserver/src/user/login.js +++ b/keyserver/src/user/login.js @@ -1,139 +1,139 @@ // @flow import type { Account as OlmAccount } from '@commapp/olm'; import { getRustAPI } from 'rust-node-addon'; import { getCommConfig } from 'lib/utils/comm-config.js'; import { ServerError } from 'lib/utils/errors.js'; import { retrieveAccountKeysSet } from 'lib/utils/olm-utils.js'; +import type { UserCredentials } from './checks.js'; import { saveIdentityInfo, fetchIdentityInfo, type IdentityInfo, } from './identity.js'; import { getMessageForException } from '../responders/utils.js'; import { fetchCallUpdateOlmAccount } from '../updaters/olm-account-updater.js'; -type UserCredentials = { +username: string, +password: string }; - // 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, oneTimeKeys: notificationsOneTimeKeys, } = await fetchCallUpdateOlmAccount('notifications', retrieveAccountKeysSet); const contentAccountCallback = async (account: OlmAccount) => { const { identityKeys: contentIdentityKeys, oneTimeKeys, 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, oneTimeKeys, prekey, prekeySignature, }; }; const [ rustAPI, { signedIdentityKeysBlob, prekey: contentPrekey, prekeySignature: contentPrekeySignature, oneTimeKeys: contentOneTimeKeys, }, ] = await Promise.all([ rustAPIPromise, fetchCallUpdateOlmAccount('content', contentAccountCallback), ]); try { const identity_info = await rustAPI.loginUser( userInfo.username, userInfo.password, signedIdentityKeysBlob, contentPrekey, contentPrekeySignature, notificationsPrekey, notificationsPrekeySignature, contentOneTimeKeys, notificationsOneTimeKeys, + userInfo.forceLogin, ); 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, contentOneTimeKeys, notificationsOneTimeKeys, ); 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/scripts/set-admin-data.js b/scripts/set-admin-data.js index 60252873c..2a8c25841 100644 --- a/scripts/set-admin-data.js +++ b/scripts/set-admin-data.js @@ -1,84 +1,85 @@ /* eslint-disable flowtype/require-valid-file-annotation */ const fs = require('fs'); const readline = require('readline'); // Create an interface for reading input const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const data = {}; rl.question('username: ', value1 => { data.username = value1; rl.question('password: ', value2 => { data.password = value2; rl.question('user id: ', value3 => { data.id = value3; writeFiles(data); // Close the readline interface rl.close(); }); }); }); function writeFiles(credentials) { try { const userCredentials = { username: credentials.username, password: credentials.password, usingIdentityCredentials: true, + forceLogin: true, }; const userCredentialsJSON = JSON.stringify(userCredentials, null, 2); const keyserverSecrets = 'keyserver/secrets'; if (!fs.existsSync(keyserverSecrets)) { fs.mkdirSync(keyserverSecrets); } fs.writeFileSync( 'keyserver/secrets/user_credentials.json', userCredentialsJSON, ); // Create configs containing authoritative keyservers data. // Because we have different mechanisms for fetching configs, // we need this config in two places const authoritativeKeyserver = { authoritativeKeyserverID: credentials.id, }; const authoritativeKeyserverJSON = JSON.stringify( authoritativeKeyserver, null, 2, ); const nativeFacts = 'native/facts'; if (!fs.existsSync(nativeFacts)) { fs.mkdirSync(nativeFacts); } fs.writeFileSync( 'native/facts/authoritative_keyserver.json', authoritativeKeyserverJSON, ); const keyserverFacts = 'keyserver/facts'; if (!fs.existsSync(keyserverFacts)) { fs.mkdirSync(keyserverFacts); } fs.writeFileSync( 'keyserver/facts/authoritative_keyserver.json', authoritativeKeyserverJSON, ); } catch (e) { console.error( 'Failure creating configuration files: ' + 'admin data could not be correctly written', ); throw e; } }