import React from 'react'; import { PromiseUtils, StringUtils, ToastUtils, UserUtils } from '../../../session/utils'; import { getConversationController } from '../../../session/conversations'; import { createOrUpdateItem, removeAll } from '../../../data/data'; import { SignUpTab } from './SignUpTab'; import { SignInTab } from './SignInTab'; import { TabLabel, TabType } from './TabLabel'; import { PasswordUtil } from '../../../util'; import { trigger } from '../../../shims/events'; import { generateMnemonic, registerSingleDevice, sessionGenerateKeyPair, signInByLinkingDevice, } from '../../../util/accountManager'; import { fromHex } from '../../../session/utils/String'; import { TaskTimedOutError } from '../../../session/utils/Promise'; import { mn_decode } from '../../../session/crypto/mnemonic'; import { getSwarmPollingInstance } from '../../../session/snode_api/swarmPolling'; export const MAX_USERNAME_LENGTH = 20; // tslint:disable: use-simple-attributes interface State { selectedTab: TabType; generatedRecoveryPhrase: string; hexGeneratedPubKey: string; } export function validatePassword(password: string, verifyPassword: string) { const trimmedPassword = password.trim(); const trimmedVerifyPassword = verifyPassword.trim(); // If user hasn't set a value then skip if (!trimmedPassword && !trimmedVerifyPassword) { return { passwordErrorString: '', passwordFieldsMatch: true, }; } const error = PasswordUtil.validatePassword(trimmedPassword); if (error) { return { passwordErrorString: error, passwordFieldsMatch: true, }; } if (trimmedPassword !== trimmedVerifyPassword) { return { passwordErrorString: '', passwordFieldsMatch: false, }; } return { passwordErrorString: '', passwordFieldsMatch: true, }; } export async function resetRegistration() { await removeAll(); await window.storage.reset(); await window.storage.fetch(); getConversationController().reset(); await getConversationController().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; password: string; verifyPassword: string; }) { const { displayName, password, verifyPassword, generatedRecoveryPhrase } = signUpDetails; window?.log?.info('SIGNING UP'); const trimName = displayNameIsValid(displayName); // shows toast to user about the error if (!trimName) { return; } // This will show a toast with the error if (!passwordsAreValid(password, verifyPassword)) { return; } try { await resetRegistration(); await window.setPassword(password); await registerSingleDevice(generatedRecoveryPhrase, 'english', trimName); await createOrUpdateItem({ id: 'hasSyncedInitialConfigurationItem', value: true, }); trigger('openInbox'); } catch (e) { await resetRegistration(); ToastUtils.pushToastError('registrationError', `Error: ${e.message || 'Something went wrong'}`); window?.log?.warn('exception during registration:', e); } } /** * Sign in/restore from seed. * Ask for a display name, as we will drop incoming ConfigurationMessages if any are saved on the swarm. * We will handle a ConfigurationMessage */ export async function signInWithRecovery(signInDetails: { displayName: string; userRecoveryPhrase: string; password: string; verifyPassword: string; }) { const { displayName, password, verifyPassword, userRecoveryPhrase } = signInDetails; window?.log?.info('RESTORING FROM SEED'); const trimName = displayNameIsValid(displayName); // shows toast to user about the error if (!trimName) { return; } // This will show a toast with the error if (!passwordsAreValid(password, verifyPassword)) { return; } try { await resetRegistration(); await window.setPassword(password); await registerSingleDevice(userRecoveryPhrase, 'english', trimName); trigger('openInbox'); } catch (e) { await resetRegistration(); ToastUtils.pushToastError('registrationError', `Error: ${e.message || 'Something went wrong'}`); window?.log?.warn('exception during registration:', e); } } /** * This is will try to sign in with the user recovery phrase. * If no ConfigurationMessage is received in 60seconds, the loading will be canceled. */ 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 signInByLinkingDevice(userRecoveryPhrase, 'english'); let displayNameFromNetwork = ''; await getSwarmPollingInstance().start(); await PromiseUtils.waitForTask(done => { window.Whisper.events.on('configurationMessageReceived', (displayName: string) => { window.Whisper.events.off('configurationMessageReceived'); UserUtils.setSignInByLinking(false); done(displayName); displayNameFromNetwork = displayName; }); }, 60000); if (displayNameFromNetwork.length) { // display name, avatars, groups and contacts should already be handled when this event was triggered. window?.log?.info('We got a displayName from network: '); } else { window?.log?.info('Got a config message from network but without a displayName...'); throw new Error('Got a config message from network but without a displayName...'); } // 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) { await resetRegistration(); if (e instanceof TaskTimedOutError) { ToastUtils.pushToastError( 'registrationError', 'Could not find your display name. Please Sign In by Restoring Your Account instead.' ); } else { ToastUtils.pushToastError( 'registrationError', `Error: ${e.message || 'Something went wrong'}` ); } window?.log?.warn('exception during registration:', e); } } export class RegistrationTabs extends React.Component { constructor() { super({}); this.state = { selectedTab: TabType.SignUp, generatedRecoveryPhrase: '', hexGeneratedPubKey: '', }; } public componentDidMount() { void this.generateMnemonicAndKeyPair(); void resetRegistration(); } public render() { const { selectedTab } = this.state; return (
{this.renderSections()}
); } private async generateMnemonicAndKeyPair() { if (this.state.generatedRecoveryPhrase === '') { const mnemonic = await generateMnemonic(); let seedHex = mn_decode(mnemonic); // handle shorter than 32 bytes seeds const privKeyHexLength = 32 * 2; if (seedHex.length !== privKeyHexLength) { seedHex = seedHex.concat('0'.repeat(32)); seedHex = seedHex.substring(0, privKeyHexLength); } const seed = fromHex(seedHex); const keyPair = await sessionGenerateKeyPair(seed); const hexGeneratedPubKey = StringUtils.decode(keyPair.pubKey, 'hex'); this.setState({ generatedRecoveryPhrase: mnemonic, hexGeneratedPubKey, // our 'frontend' sessionID }); } } private readonly handleTabSelect = (tabType: TabType): void => { this.setState({ selectedTab: tabType, }); }; private renderSections() { const { selectedTab, generatedRecoveryPhrase, hexGeneratedPubKey } = this.state; if (selectedTab === TabType.SignUp) { return ( ); } return ; } }