import React from 'react'; import classNames from 'classnames'; import { SessionInput } from './SessionInput'; import { SessionButton, SessionButtonColor, SessionButtonType, } from './SessionButton'; import { trigger } from '../../shims/events'; import { SessionHtmlRenderer } from './SessionHTMLRenderer'; import { SessionIdEditable } from './SessionIdEditable'; import { SessionSpinner } from './SessionSpinner'; enum SignInMode { Default, UsingSeed, LinkingDevice, } enum SignUpMode { Default, SessionIDShown, EnterDetails, } enum TabType { Create, SignIn, } interface State { selectedTab: TabType; signInMode: SignInMode; signUpMode: SignUpMode; displayName: string; password: string; validatePassword: string; passwordErrorString: string; passwordFieldsMatch: boolean; mnemonicSeed: string; hexGeneratedPubKey: string; primaryDevicePubKey: string; mnemonicError: string | undefined; displayNameError: string | undefined; loading: boolean; } const Tab = ({ isSelected, label, onSelect, type, }: { isSelected: boolean; label: string; onSelect?: (event: TabType) => void; type: TabType; }) => { const handleClick = onSelect ? () => { onSelect(type); } : undefined; return (
{label}
); }; export class RegistrationTabs extends React.Component<{}, State> { private readonly accountManager: any; constructor(props: any) { super(props); this.onSeedChanged = this.onSeedChanged.bind(this); this.onDisplayNameChanged = this.onDisplayNameChanged.bind(this); this.onPasswordChanged = this.onPasswordChanged.bind(this); this.onPasswordVerifyChanged = this.onPasswordVerifyChanged.bind(this); this.onSignUpGenerateSessionIDClick = this.onSignUpGenerateSessionIDClick.bind( this ); this.onSignUpGetStartedClick = this.onSignUpGetStartedClick.bind(this); this.onSecondDeviceSessionIDChanged = this.onSecondDeviceSessionIDChanged.bind( this ); this.onSecondaryDeviceRegistered = this.onSecondaryDeviceRegistered.bind( this ); this.onCompleteSignUpClick = this.onCompleteSignUpClick.bind(this); this.handlePressEnter = this.handlePressEnter.bind(this); this.handleContinueYourSessionClick = this.handleContinueYourSessionClick.bind( this ); this.state = { selectedTab: TabType.Create, signInMode: SignInMode.Default, signUpMode: SignUpMode.Default, displayName: '', password: '', validatePassword: '', passwordErrorString: '', passwordFieldsMatch: false, mnemonicSeed: '', hexGeneratedPubKey: '', primaryDevicePubKey: '', mnemonicError: undefined, displayNameError: undefined, loading: false, }; this.accountManager = window.getAccountManager(); // Clean status in case the app closed unexpectedly window.textsecure.storage.remove('secondaryDeviceStatus'); } public render() { this.generateMnemonicAndKeyPair().ignore(); return this.renderTabs(); } private async generateMnemonicAndKeyPair() { if (this.state.mnemonicSeed === '') { const language = 'english'; const mnemonic = await this.accountManager.generateMnemonic(language); let seedHex = window.mnemonic.mn_decode(mnemonic, language); // handle shorter than 32 bytes seeds const privKeyHexLength = 32 * 2; if (seedHex.length !== privKeyHexLength) { seedHex = seedHex.concat(seedHex); seedHex = seedHex.substring(0, privKeyHexLength); } const seed = window.dcodeIO.ByteBuffer.wrap( seedHex, 'hex' ).toArrayBuffer(); const keyPair = await window.libsignal.Curve.async.createKeyPair(seed); const hexGeneratedPubKey = Buffer.from(keyPair.pubKey).toString('hex'); this.setState({ mnemonicSeed: mnemonic, hexGeneratedPubKey, // our 'frontend' sessionID }); } } private renderTabs() { const { selectedTab } = this.state; const createAccount = window.i18n('createAccount'); const signIn = window.i18n('signIn'); const isCreateSelected = selectedTab === TabType.Create; const isSignInSelected = selectedTab === TabType.SignIn; return (
{this.renderSections()}
); } private readonly handleTabSelect = (tabType: TabType): void => { if (tabType !== TabType.SignIn) { this.cancelSecondaryDevice().ignore(); } this.setState({ selectedTab: tabType, signInMode: SignInMode.Default, signUpMode: SignUpMode.Default, displayName: '', password: '', validatePassword: '', passwordErrorString: '', passwordFieldsMatch: false, mnemonicSeed: '', hexGeneratedPubKey: '', primaryDevicePubKey: '', mnemonicError: undefined, displayNameError: undefined, }); }; private onSeedChanged(val: string) { this.setState({ mnemonicSeed: val, mnemonicError: !val ? window.i18n('mnemonicEmpty') : undefined, }); } private onDisplayNameChanged(val: string) { const sanitizedName = this.sanitiseNameInput(val); const trimName = sanitizedName.trim(); this.setState({ displayName: sanitizedName, displayNameError: !trimName ? window.i18n('displayNameEmpty') : undefined, }); } private onPasswordChanged(val: string) { this.setState({ password: val }, () => { this.validatePassword(); }); } private onPasswordVerifyChanged(val: string) { this.setState({ validatePassword: val }); this.setState({ validatePassword: val }, () => { this.validatePassword(); }); } private renderSections() { const { selectedTab } = this.state; if (selectedTab === TabType.Create) { return this.renderSignUp(); } return this.renderSignIn(); } private renderSignUp() { const { signUpMode } = this.state; switch (signUpMode) { case SignUpMode.Default: return (
{this.renderSignUpHeader()} {this.renderSignUpButton()}
); case SignUpMode.SessionIDShown: return (
{this.renderSignUpHeader()}
{window.i18n('yourUniqueSessionID')}
{this.renderEnterSessionID(false)} {this.renderSignUpButton()} {this.getRenderTermsConditionAgreement()}
); default: const { passwordErrorString, passwordFieldsMatch, displayNameError, displayName, password, } = this.state; let enableCompleteSignUp = true; const displayNameOK = !displayNameError && !!displayName; //display name required const passwordsOK = !password || (!passwordErrorString && passwordFieldsMatch); // password is valid if empty, or if no error and fields are matching enableCompleteSignUp = displayNameOK && passwordsOK; return (
{window.i18n('welcomeToYourSession')}
{this.renderRegistrationContent()} { this.onCompleteSignUpClick(); }} buttonType={SessionButtonType.Brand} buttonColor={SessionButtonColor.Green} text={window.i18n('getStarted')} disabled={!enableCompleteSignUp} />
); } } private getRenderTermsConditionAgreement() { const { selectedTab, signInMode, signUpMode } = this.state; if (selectedTab === TabType.Create) { return signUpMode !== SignUpMode.Default ? this.renderTermsConditionAgreement() : null; } else { return signInMode !== SignInMode.Default ? this.renderTermsConditionAgreement() : null; } } private renderSignUpHeader() { const allUsersAreRandomly = window.i18n('allUsersAreRandomly...'); return (
{allUsersAreRandomly}
); } private renderSignUpButton() { const { signUpMode } = this.state; let buttonType: SessionButtonType; let buttonColor: SessionButtonColor; let buttonText: string; if (signUpMode !== SignUpMode.Default) { buttonType = SessionButtonType.Brand; buttonColor = SessionButtonColor.Green; buttonText = window.i18n('continue'); } else { buttonType = SessionButtonType.BrandOutline; buttonColor = SessionButtonColor.Green; buttonText = window.i18n('generateSessionID'); } return ( { if (signUpMode === SignUpMode.Default) { this.onSignUpGenerateSessionIDClick().ignore(); } else { this.onSignUpGetStartedClick(); } }} buttonType={buttonType} buttonColor={buttonColor} text={buttonText} /> ); } private async onSignUpGenerateSessionIDClick() { this.setState( { signUpMode: SignUpMode.SessionIDShown, }, () => { window.Session.setNewSessionID(this.state.hexGeneratedPubKey); } ); } private onSignUpGetStartedClick() { this.setState({ signUpMode: SignUpMode.EnterDetails, }); } private onCompleteSignUpClick() { this.register('english').ignore(); } private renderSignIn() { return (
{this.renderRegistrationContent()} {this.renderSignInButtons()} {this.getRenderTermsConditionAgreement()}
); } private renderRegistrationContent() { const { signInMode, signUpMode } = this.state; if (signInMode === SignInMode.UsingSeed) { return (
{ this.onSeedChanged(val); }} onEnterPressed={() => { this.handlePressEnter(); }} /> {this.renderNamePasswordAndVerifyPasswordFields()}
); } if (signInMode === SignInMode.LinkingDevice) { return (
{window.i18n('devicePairingHeader')}
{this.renderEnterSessionID(true)}
); } if (signUpMode === SignUpMode.EnterDetails) { return (
{this.renderNamePasswordAndVerifyPasswordFields()}
); } return null; } private renderNamePasswordAndVerifyPasswordFields() { const { password, passwordFieldsMatch } = this.state; const passwordsDoNotMatch = !passwordFieldsMatch && this.state.password ? window.i18n('passwordsDoNotMatch') : undefined; return (
{ this.onDisplayNameChanged(val); }} onEnterPressed={() => { this.handlePressEnter(); }} /> { this.onPasswordChanged(val); }} onEnterPressed={() => { this.handlePressEnter(); }} /> {!!password && ( { this.onPasswordVerifyChanged(val); }} onEnterPressed={() => { this.handlePressEnter(); }} /> )}
); } private renderEnterSessionID(contentEditable: boolean) { const enterSessionIDHere = window.i18n('enterSessionIDHere'); return ( { this.onSecondDeviceSessionIDChanged(value); }} /> ); } private onSecondDeviceSessionIDChanged(value: string) { this.setState({ primaryDevicePubKey: value, }); } private renderSignInButtons() { const { signInMode } = this.state; const or = window.i18n('or'); if (signInMode === SignInMode.Default) { return (
{this.renderRestoreUsingSeedButton( SessionButtonType.BrandOutline, SessionButtonColor.Green )}

{or}

{this.renderLinkDeviceToExistingAccountButton()}
); } if (signInMode === SignInMode.LinkingDevice) { return (
{this.renderContinueYourSessionButton()}

{or}

{this.renderRestoreUsingSeedButton( SessionButtonType.BrandOutline, SessionButtonColor.White )}
); } return (
{this.renderContinueYourSessionButton()}

{or}

{this.renderLinkDeviceToExistingAccountButton()}
); } private renderTermsConditionAgreement() { // FIXME add link to our Terms and Conditions and privacy statement return (
); } private handleContinueYourSessionClick() { if (this.state.signInMode === SignInMode.UsingSeed) { this.register('english').ignore(); } else { this.registerSecondaryDevice().ignore(); } } private renderContinueYourSessionButton() { const { signUpMode, signInMode, passwordErrorString, passwordFieldsMatch, displayNameError, mnemonicError, primaryDevicePubKey, displayName, mnemonicSeed, password, } = this.state; let enableContinue = true; let text = window.i18n('continueYourSession'); const displayNameOK = !displayNameError && !!displayName; //display name required const mnemonicOK = !mnemonicError && !!mnemonicSeed; //Mnemonic required const passwordsOK = !password || (!passwordErrorString && passwordFieldsMatch); // password is valid if empty, or if no error and fields are matching if (signInMode === SignInMode.UsingSeed) { enableContinue = displayNameOK && mnemonicOK && passwordsOK; } else if (signInMode === SignInMode.LinkingDevice) { enableContinue = !!primaryDevicePubKey; text = window.i18n('linkDevice'); } else if (signUpMode === SignUpMode.EnterDetails) { enableContinue = displayNameOK && passwordsOK; } return ( { this.handleContinueYourSessionClick(); }} buttonType={SessionButtonType.Brand} buttonColor={SessionButtonColor.Green} text={text} disabled={!enableContinue} /> ); } private renderRestoreUsingSeedButton( buttonType: SessionButtonType, buttonColor: SessionButtonColor ) { return ( { this.cancelSecondaryDevice().ignore(); this.setState({ signInMode: SignInMode.UsingSeed, primaryDevicePubKey: '', mnemonicSeed: '', displayName: '', signUpMode: SignUpMode.Default, }); }} buttonType={buttonType} buttonColor={buttonColor} text={window.i18n('restoreUsingSeed')} /> ); } private renderLinkDeviceToExistingAccountButton() { return ( { this.setState({ signInMode: SignInMode.LinkingDevice, mnemonicSeed: '', displayName: '', signUpMode: SignUpMode.Default, }); }} buttonType={SessionButtonType.BrandOutline} buttonColor={SessionButtonColor.White} text={window.i18n('linkDeviceToExistingAccount')} /> ); } private handlePressEnter() { const { signInMode, signUpMode } = this.state; if (signUpMode === SignUpMode.EnterDetails) { this.onCompleteSignUpClick(); return; } if (signInMode === SignInMode.UsingSeed) { this.handleContinueYourSessionClick(); return; } } private trim(value: string) { return value ? value.trim() : value; } private validatePassword() { const input = this.trim(this.state.password); const confirmationInput = this.trim(this.state.validatePassword); // If user hasn't set a value then skip if (!input && !confirmationInput) { this.setState({ passwordErrorString: '', passwordFieldsMatch: true, }); return; } const error = window.passwordUtil.validatePassword(input, window.i18n); if (error) { this.setState({ passwordErrorString: error, passwordFieldsMatch: true, }); return; } if (input !== confirmationInput) { this.setState({ passwordErrorString: '', passwordFieldsMatch: false, }); return; } this.setState({ passwordErrorString: '', passwordFieldsMatch: true, }); } private sanitiseNameInput(val: string) { return val.replace(window.displayNameRegex, ''); } private async resetRegistration() { await window.Signal.Data.removeAllIdentityKeys(); await window.Signal.Data.removeAllPrivateConversations(); window.Whisper.Registration.remove(); // Do not remove all items since they are only set // at startup. window.textsecure.storage.remove('identityKey'); window.textsecure.storage.remove('secondaryDeviceStatus'); window.ConversationController.reset(); await window.ConversationController.load(); window.Whisper.RotateSignedPreKeyListener.stop(window.Whisper.events); } private async register(language: string) { const { password, mnemonicSeed, displayName, passwordErrorString, passwordFieldsMatch, } = this.state; // Make sure the password is valid const trimName = displayName.trim(); if (!trimName) { window.pushToast({ title: window.i18n('displayNameEmpty'), type: 'error', id: 'invalidDisplayName', }); return; } if (passwordErrorString) { window.pushToast({ title: window.i18n('invalidPassword'), type: 'error', id: 'invalidPassword', }); return; } if (!!password && !passwordFieldsMatch) { window.pushToast({ title: window.i18n('passwordsDoNotMatch'), type: 'error', id: 'invalidPassword', }); return; } if (!mnemonicSeed) { return; } // Ensure we clear the secondary device registration status window.textsecure.storage.remove('secondaryDeviceStatus'); try { await this.resetRegistration(); await window.setPassword(password); await this.accountManager.registerSingleDevice( mnemonicSeed, language, trimName ); trigger('openInbox'); } catch (e) { if (typeof e === 'string') { //this.showToast(e); } //this.log(e); } } private async cancelSecondaryDevice() { window.Whisper.events.off( 'secondaryDeviceRegistration', this.onSecondaryDeviceRegistered ); await this.resetRegistration(); } private async registerSecondaryDevice() { // tslint:disable-next-line: no-backbone-get-set-outside-model if (window.textsecure.storage.get('secondaryDeviceStatus') === 'ongoing') { return; } this.setState({ loading: true, }); await this.resetRegistration(); window.textsecure.storage.put('secondaryDeviceStatus', 'ongoing'); const primaryPubKey = this.state.primaryDevicePubKey; // Ensure only one listener window.Whisper.events.off( 'secondaryDeviceRegistration', this.onSecondaryDeviceRegistered ); window.Whisper.events.once( 'secondaryDeviceRegistration', this.onSecondaryDeviceRegistered ); const onError = async (error: any) => { window.log.error(error); // clear the ... to make sure the user realize we're not doing anything this.setState({ loading: false, }); await this.resetRegistration(); }; const c = new window.Whisper.Conversation({ id: primaryPubKey, type: 'private', }); const validationError = c.validateNumber(); if (validationError) { onError('Invalid public key').ignore(); window.pushToast({ title: window.i18n('invalidNumberError'), type: 'error', id: 'invalidNumberError', }); return; } try { const fakeMnemonic = this.state.mnemonicSeed; await this.accountManager.registerSingleDevice( fakeMnemonic, 'english', null ); await this.accountManager.requestPairing(primaryPubKey); const pubkey = window.textsecure.storage.user.getNumber(); const words = window.mnemonic.pubkey_to_secret_words(pubkey); window.console.log(`Here is your secret:\n${words}`); window.pushToast({ title: `${window.i18n('secretPrompt')}`, description: words, id: 'yourSecret', shouldFade: false, }); } catch (e) { window.console.log(e); this.setState({ loading: false, }); } } private async onSecondaryDeviceRegistered() { // Ensure the left menu is updated this.setState({ loading: false, }); trigger('userChanged', { isSecondaryDevice: true }); // will re-run the background initialisation trigger('registration_done'); trigger('openInbox'); } }