update our profile on incoming configMessage sent after our last update

This commit is contained in:
Audric Ackermann 2021-03-01 14:21:29 +11:00
parent e6cf28cb2a
commit 305ece1c7c
8 changed files with 250 additions and 103 deletions

View File

@ -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() {

View File

@ -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<any, State> {
constructor() {
super({});

View File

@ -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}

View File

@ -1131,7 +1131,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
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();

View File

@ -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<void> {
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<void> {
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);
}

View File

@ -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
) {

View File

@ -71,12 +71,12 @@ export async function getUserED25519KeyPair(): Promise<HexKeyPair | undefined> {
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 {

View File

@ -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(),
]);
}