From 305ece1c7c0d6bf305449fc5908430b2222696b9 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 1 Mar 2021 14:21:29 +1100 Subject: [PATCH] update our profile on incoming configMessage sent after our last update --- libtextsecure/storage/user.js | 12 +- .../session/registration/RegistrationTabs.tsx | 124 +++++++++++------- .../session/registration/SignInTab.tsx | 8 +- ts/models/conversation.ts | 5 +- ts/receiver/contentMessage.ts | 102 ++++++++++---- ts/receiver/dataMessage.ts | 3 +- ts/session/utils/User.ts | 8 +- ts/util/accountManager.ts | 89 ++++++++++--- 8 files changed, 249 insertions(+), 102 deletions(-) diff --git a/libtextsecure/storage/user.js b/libtextsecure/storage/user.js index 1d48ae721..63975535f 100644 --- a/libtextsecure/storage/user.js +++ b/libtextsecure/storage/user.js @@ -24,16 +24,16 @@ return textsecure.utils.unencodeNumber(numberId)[0]; }, - isRestoringFromSeed() { - const isRestoring = textsecure.storage.get('is_restoring_from_seed'); - if (isRestoring === undefined) { + isSignInByLinking() { + const isSignInByLinking = textsecure.storage.get('is_sign_in_by_linking'); + if (isSignInByLinking === undefined) { return false; } - return isRestoring; + return isSignInByLinking; }, - setRestoringFromSeed(isRestoringFromSeed) { - textsecure.storage.put('is_restoring_from_seed', isRestoringFromSeed); + setSignInByLinking(isLinking) { + textsecure.storage.put('is_sign_in_by_linking', isLinking); }, getLastProfileUpdateTimestamp() { diff --git a/ts/components/session/registration/RegistrationTabs.tsx b/ts/components/session/registration/RegistrationTabs.tsx index 28235bbfb..e4c321572 100644 --- a/ts/components/session/registration/RegistrationTabs.tsx +++ b/ts/components/session/registration/RegistrationTabs.tsx @@ -63,6 +63,47 @@ export async function resetRegistration() { await ConversationController.getInstance().load(); } +const passwordsAreValid = (password: string, verifyPassword: string) => { + const passwordErrors = validatePassword(password, verifyPassword); + if (passwordErrors.passwordErrorString) { + window.log.warn('invalid password for registration'); + ToastUtils.pushToastError( + 'invalidPassword', + window.i18n('invalidPassword') + ); + return false; + } + if (!!password && !passwordErrors.passwordFieldsMatch) { + window.log.warn('passwords does not match for registration'); + ToastUtils.pushToastError( + 'invalidPassword', + window.i18n('passwordsDoNotMatch') + ); + return false; + } + + return true; +}; + +/** + * Returns undefined if an error happened, or the trim userName. + * + * Be sure to use the trimmed userName for creating the account. + */ +const displayNameIsValid = (displayName: string): undefined | string => { + const trimName = displayName.trim(); + + if (!trimName) { + window.log.warn('invalid trimmed name for registration'); + ToastUtils.pushToastError( + 'invalidDisplayName', + window.i18n('displayNameEmpty') + ); + return undefined; + } + return trimName; +}; + export async function signUp(signUpDetails: { displayName: string; generatedRecoveryPhrase: string; @@ -76,38 +117,21 @@ export async function signUp(signUpDetails: { generatedRecoveryPhrase, } = signUpDetails; window.log.info('SIGNING UP'); - const trimName = displayName.trim(); + const trimName = displayNameIsValid(displayName); + // shows toast to user about the error if (!trimName) { - window.log.warn('invalid trimmed name for registration'); - ToastUtils.pushToastError( - 'invalidDisplayName', - window.i18n('displayNameEmpty') - ); return; } - const passwordErrors = validatePassword(password, verifyPassword); - if (passwordErrors.passwordErrorString) { - window.log.warn('invalid password for registration'); - ToastUtils.pushToastError( - 'invalidPassword', - window.i18n('invalidPassword') - ); - return; - } - if (!!password && !passwordErrors.passwordFieldsMatch) { - window.log.warn('passwords does not match for registration'); - ToastUtils.pushToastError( - 'invalidPassword', - window.i18n('passwordsDoNotMatch') - ); + + // This will show a toast with the error + if (!passwordsAreValid(password, verifyPassword)) { return; } try { await resetRegistration(); await window.setPassword(password); - UserUtils.setRestoringFromSeed(false); await AccountManager.registerSingleDevice( generatedRecoveryPhrase, 'english', @@ -142,44 +166,26 @@ export async function signInWithRecovery(signInDetails: { userRecoveryPhrase, } = signInDetails; window.log.info('RESTORING FROM SEED'); - const trimName = displayName.trim(); - + const trimName = displayNameIsValid(displayName); + // shows toast to user about the error if (!trimName) { - window.log.warn('invalid trimmed name for registration'); - ToastUtils.pushToastError( - 'invalidDisplayName', - window.i18n('displayNameEmpty') - ); return; } - const passwordErrors = validatePassword(password, verifyPassword); - if (passwordErrors.passwordErrorString) { - window.log.warn('invalid password for registration'); - ToastUtils.pushToastError( - 'invalidPassword', - window.i18n('invalidPassword') - ); - return; - } - if (!!password && !passwordErrors.passwordFieldsMatch) { - window.log.warn('passwords does not match for registration'); - ToastUtils.pushToastError( - 'invalidPassword', - window.i18n('passwordsDoNotMatch') - ); + // This will show a toast with the error + if (!passwordsAreValid(password, verifyPassword)) { return; } try { await resetRegistration(); await window.setPassword(password); - UserUtils.setRestoringFromSeed(false); + await UserUtils.setLastProfileUpdateTimestamp(Date.now()); + await AccountManager.registerSingleDevice( userRecoveryPhrase, 'english', trimName ); - await UserUtils.setLastProfileUpdateTimestamp(Date.now()); trigger('openInbox'); } catch (e) { ToastUtils.pushToastError( @@ -190,6 +196,32 @@ export async function signInWithRecovery(signInDetails: { } } +export async function signInWithLinking(signInDetails: { + userRecoveryPhrase: string; + password: string; + verifyPassword: string; +}) { + const { password, verifyPassword, userRecoveryPhrase } = signInDetails; + window.log.info('LINKING DEVICE'); + // This will show a toast with the error + if (!passwordsAreValid(password, verifyPassword)) { + return; + } + try { + await resetRegistration(); + await window.setPassword(password); + await AccountManager.signInByLinkingDevice(userRecoveryPhrase, 'english'); + // Do not set the lastProfileUpdateTimestamp. + // We expect to get a display name from a configuration message while we are loading messages of this user + trigger('openInbox'); + } catch (e) { + ToastUtils.pushToastError( + 'registrationError', + `Error: ${e.message || 'Something went wrong'}` + ); + window.log.warn('exception during registration:', e); + } +} export class RegistrationTabs extends React.Component { constructor() { super({}); diff --git a/ts/components/session/registration/SignInTab.tsx b/ts/components/session/registration/SignInTab.tsx index 36a69c71a..7018b4346 100644 --- a/ts/components/session/registration/SignInTab.tsx +++ b/ts/components/session/registration/SignInTab.tsx @@ -195,8 +195,12 @@ export const SignInTab = (props: Props) => { password, verifyPassword: passwordVerify, }); - } else { - throw new Error('TODO'); + } else if (isLinking) { + await signInWithLinking({ + userRecoveryPhrase: recoveryPhrase, + password, + verifyPassword: passwordVerify, + }); } }} disabled={!activateContinueButton} diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 64d48d2cc..efe5a9440 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -1131,7 +1131,10 @@ export class ConversationModel extends Backbone.Model { await this.updateProfileName(); } - public async setLokiProfile(newProfile: any) { + public async setLokiProfile(newProfile: { + displayName?: string | null; + avatar?: string; + }) { if (!_.isEqual(this.get('profile'), newProfile)) { this.set({ profile: newProfile }); await this.commit(); diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 6a2d2377c..52fda424b 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -1,5 +1,5 @@ import { EnvelopePlus } from './types'; -import { handleDataMessage } from './dataMessage'; +import { handleDataMessage, updateProfile } from './dataMessage'; import { removeFromCache, updateCache } from './cache'; import { SignalService } from '../protobuf'; @@ -20,6 +20,7 @@ import { ECKeyPair } from './keypairs'; import { handleNewClosedGroup } from './closedGroups'; import { KeyPairRequestManager } from './keyPairRequestManager'; import { requestEncryptionKeyPair } from '../session/group'; +import { ConfigurationMessage } from '../session/messages/outgoing/content/ConfigurationMessage'; export async function handleContentMessage(envelope: EnvelopePlus) { try { @@ -390,7 +391,7 @@ export async function innerHandleContentMessage( 'private' ); - if (content.dataMessage && !UserUtils.isRestoringFromSeed()) { + if (content.dataMessage && !UserUtils.isSignInByLinking()) { if ( content.dataMessage.profileKey && content.dataMessage.profileKey.length === 0 @@ -401,16 +402,16 @@ export async function innerHandleContentMessage( return; } - if (content.receiptMessage && !UserUtils.isRestoringFromSeed()) { + if (content.receiptMessage && !UserUtils.isSignInByLinking()) { await handleReceiptMessage(envelope, content.receiptMessage); return; } - if (content.typingMessage && !UserUtils.isRestoringFromSeed()) { + if (content.typingMessage && !UserUtils.isSignInByLinking()) { await handleTypingMessage(envelope, content.typingMessage); return; } - // Be sure to check for the UserUtils.isRestoringFromSeed() if you add another if here + // Be sure to check for the UserUtils.isSignInByLinking() if you add another if here if (content.configurationMessage) { await handleConfigurationMessage( envelope, @@ -418,7 +419,7 @@ export async function innerHandleContentMessage( ); return; } - // Be sure to check for the UserUtils.isRestoringFromSeed() if you add another if here + // Be sure to check for the UserUtils.isSignInByLinking() if you add another if here } catch (e) { window.log.warn(e); } @@ -529,22 +530,41 @@ async function handleTypingMessage( } } -export async function handleConfigurationMessage( - envelope: EnvelopePlus, - configurationMessage: SignalService.ConfigurationMessage -): Promise { - const ourPubkey = UserUtils.getOurPubKeyStrFromCache(); - if (!ourPubkey) { - return; - } - - if (envelope.source !== ourPubkey) { +async function handleOurProfileUpdate( + sentAt: number | Long, + configMessage: SignalService.ConfigurationMessage, + ourPubkey: string +) { + const latestProfileUpdateTimestamp = UserUtils.getLastProfileUpdateTimestamp(); + if (latestProfileUpdateTimestamp && sentAt > latestProfileUpdateTimestamp) { window?.log?.info( - 'Dropping configuration change from someone else than us.' + `Handling our profileUdpate ourLastUpdate:${latestProfileUpdateTimestamp}, envelope sent at: ${sentAt}` ); - return removeFromCache(envelope); + const { profileKey, profilePicture, displayName } = configMessage; + + const ourConversation = ConversationController.getInstance().get(ourPubkey); + if (!ourConversation) { + window.log.error('We need a convo with ourself at all times'); + return; + } + + if (profileKey?.length) { + window.log.info('Saving our profileKey from configuration message'); + // TODO not sure why we keep our profileKey in storage AND in our conversaio + window.textsecure.storage.put('profileKey', profileKey); + } + const lokiProfile = { + displayName, + profilePicture, + }; + await updateProfile(ourConversation, lokiProfile, profileKey); } +} +async function handleGroupsAndContactsFromConfigMessage( + envelope: EnvelopePlus, + configMessage: SignalService.ConfigurationMessage +) { const ITEM_ID_PROCESSED_CONFIGURATION_MESSAGE = 'ITEM_ID_PROCESSED_CONFIGURATION_MESSAGE'; const didWeHandleAConfigurationMessageAlready = @@ -554,22 +574,23 @@ export async function handleConfigurationMessage( window?.log?.warn( 'Dropping configuration change as we already handled one... ' ); - await removeFromCache(envelope); return; } - await createOrUpdateItem({ - id: ITEM_ID_PROCESSED_CONFIGURATION_MESSAGE, - value: true, - }); + if (didWeHandleAConfigurationMessageAlready) { + window?.log?.warn( + 'Dropping configuration change as we already handled one... ' + ); + return; + } - const numberClosedGroup = configurationMessage.closedGroups?.length || 0; + const numberClosedGroup = configMessage.closedGroups?.length || 0; window?.log?.warn( `Received ${numberClosedGroup} closed group on configuration. Creating them... ` ); await Promise.all( - configurationMessage.closedGroups.map(async c => { + configMessage.closedGroups.map(async c => { const groupUpdate = new SignalService.DataMessage.ClosedGroupControlMessage( { type: SignalService.DataMessage.ClosedGroupControlMessage.Type.NEW, @@ -591,12 +612,12 @@ export async function handleConfigurationMessage( ); const allOpenGroups = OpenGroup.getAllAlreadyJoinedOpenGroupsUrl(); - const numberOpenGroup = configurationMessage.openGroups?.length || 0; + const numberOpenGroup = configMessage.openGroups?.length || 0; // Trigger a join for all open groups we are not already in. // Currently, if you left an open group but kept the conversation, you won't rejoin it here. for (let i = 0; i < numberOpenGroup; i++) { - const current = configurationMessage.openGroups[i]; + const current = configMessage.openGroups[i]; if (!allOpenGroups.includes(current)) { window?.log?.info( `triggering join of public chat '${current}' from ConfigurationMessage` @@ -604,6 +625,33 @@ export async function handleConfigurationMessage( void OpenGroup.join(current); } } +} + +export async function handleConfigurationMessage( + envelope: EnvelopePlus, + configurationMessage: SignalService.ConfigurationMessage +): Promise { + const ourPubkey = UserUtils.getOurPubKeyStrFromCache(); + if (!ourPubkey) { + return; + } + + if (envelope.source !== ourPubkey) { + window?.log?.info( + 'Dropping configuration change from someone else than us.' + ); + return removeFromCache(envelope); + } + + await handleOurProfileUpdate( + envelope.timestamp, + configurationMessage, + ourPubkey + ); + await handleGroupsAndContactsFromConfigMessage( + envelope, + configurationMessage + ); await removeFromCache(envelope); } diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 5a8c46ba1..66ff87b3b 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -15,9 +15,10 @@ import { handleClosedGroupControlMessage } from './closedGroups'; import { MessageModel } from '../models/message'; import { MessageModelType } from '../models/messageType'; import { getMessageBySender } from '../../ts/data/data'; +import { ConversationModel } from '../models/conversation'; export async function updateProfile( - conversation: any, + conversation: ConversationModel, profile: SignalService.DataMessage.ILokiProfile, profileKey: any ) { diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts index c3bc4420d..fdef31020 100644 --- a/ts/session/utils/User.ts +++ b/ts/session/utils/User.ts @@ -71,12 +71,12 @@ export async function getUserED25519KeyPair(): Promise { return undefined; } -export function isRestoringFromSeed(): boolean { - return window.textsecure.storage.user.isRestoringFromSeed(); +export function isSignInByLinking(): boolean { + return window.textsecure.storage.user.isSignInByLinking(); } -export function setRestoringFromSeed(isRestoring: boolean) { - window.textsecure.storage.user.setRestoringFromSeed(isRestoring); +export function setSignInByLinking(isLinking: boolean) { + window.textsecure.storage.user.setSignInByLinking(isLinking); } export interface OurLokiProfile { diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index 134f69347..1f493bd10 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -8,6 +8,13 @@ import { } from '../session/utils/String'; import { getOurPubKeyStrFromCache } from '../session/utils/User'; import { trigger } from '../shims/events'; +import { + removeAllContactPreKeys, + removeAllContactSignedPreKeys, + removeAllPreKeys, + removeAllSessions, + removeAllSignedPreKeys, +} from '../data/data'; /** * Might throw @@ -56,19 +63,68 @@ const generateKeypair = async (mnemonic: string, mnemonicLanguage: string) => { // TODO not sure why AccountManager was a singleton before. Can we get rid of it as a singleton? // tslint:disable-next-line: no-unnecessary-class export class AccountManager { - public static async registerSingleDevice( + /** + * Sign in with a recovery phrase. We won't try to recover an existing profile name + * @param mnemonic the mnemonic the user duly saved in a safe place. We will restore his sessionID based on this. + * @param mnemonicLanguage 'english' only is supported + * @param profileName the displayName to use for this user + */ + public static async signInWithRecovery( mnemonic: string, mnemonicLanguage: string, profileName: string ) { - const createAccount = this.createAccount.bind(this); - const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); - const registrationDone = this.registrationDone.bind(this); + return AccountManager.registerSingleDevice( + mnemonic, + mnemonicLanguage, + profileName + ); + } + + /** + * Sign in with a recovery phrase but trying to recover display name and avatar from the first encountered configuration message. + * @param mnemonic the mnemonic the user duly saved in a safe place. We will restore his sessionID based on this. + * @param mnemonicLanguage 'english' only is supported + */ + public static async signInByLinkingDevice( + mnemonic: string, + mnemonicLanguage: string + ) { if (!mnemonic) { throw new Error( 'Session always needs a mnemonic. Either generated or given by the user' ); } + if (!mnemonicLanguage) { + throw new Error('We always needs a mnemonicLanguage'); + } + + const identityKeyPair = await generateKeypair(mnemonic, mnemonicLanguage); + UserUtils.setSignInByLinking(true); + await AccountManager.createAccount(identityKeyPair); + UserUtils.saveRecoveryPhrase(mnemonic); + await AccountManager.clearSessionsAndPreKeys(); + const pubKeyString = toHex(identityKeyPair.pubKey); + + // await for the first configuration message to come in. + await AccountManager.registrationDone(pubKeyString, profileName); + } + /** + * This is a signup. User has no recovery and does not try to link a device + * @param mnemonic The mnemonic generated on first app loading and to use for this brand new user + * @param mnemonicLanguage only 'english' is supported + * @param profileName the display name to register toi + */ + public static async registerSingleDevice( + generatedMnemonic: string, + mnemonicLanguage: string, + profileName: string + ) { + if (!generatedMnemonic) { + throw new Error( + 'Session always needs a mnemonic. Either generated or given by the user' + ); + } if (!profileName) { throw new Error('We always needs a profileName'); } @@ -76,19 +132,22 @@ export class AccountManager { throw new Error('We always needs a mnemonicLanguage'); } - const identityKeyPair = await generateKeypair(mnemonic, mnemonicLanguage); - await createAccount(identityKeyPair); - UserUtils.saveRecoveryPhrase(mnemonic); - await clearSessionsAndPreKeys(); + const identityKeyPair = await generateKeypair( + generatedMnemonic, + mnemonicLanguage + ); + await AccountManager.createAccount(identityKeyPair); + UserUtils.saveRecoveryPhrase(generatedMnemonic); + await AccountManager.clearSessionsAndPreKeys(); const pubKeyString = toHex(identityKeyPair.pubKey); - await registrationDone(pubKeyString, profileName); + await AccountManager.registrationDone(pubKeyString, profileName); } public static async generateMnemonic(language = 'english') { // Note: 4 bytes are converted into 3 seed words, so length 12 seed words // (13 - 1 checksum) are generated using 12 * 4 / 3 = 16 bytes. const seedSize = 16; - const seed = window.Signal.Crypto.getRandomBytes(seedSize); + const seed = (await getSodium()).randombytes_buf(seedSize); const hex = toHex(seed); return window.mnemonic.mn_encode(hex, language); } @@ -98,11 +157,11 @@ export class AccountManager { // During secondary device registration we need to keep our prekeys sent // to other pubkeys await Promise.all([ - window.Signal.Data.removeAllPreKeys(), - window.Signal.Data.removeAllSignedPreKeys(), - window.Signal.Data.removeAllContactPreKeys(), - window.Signal.Data.removeAllContactSignedPreKeys(), - window.Signal.Data.removeAllSessions(), + removeAllPreKeys(), + removeAllSignedPreKeys(), + removeAllContactPreKeys(), + removeAllContactSignedPreKeys(), + removeAllSessions(), ]); }