From 89579ebd351e5f917973a7978b3c6d7a9f47df79 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 30 Jul 2020 10:58:17 +1000 Subject: [PATCH 1/4] refactor SessionPasswordModal to handle errors on length --- app/password_util.js | 4 + js/views/create_group_dialog_view.js | 2 +- .../session/SessionPasswordModal.tsx | 137 +++++++++--------- ts/receiver/contentMessage.ts | 4 +- 4 files changed, 76 insertions(+), 71 deletions(-) diff --git a/app/password_util.js b/app/password_util.js index 915f017fe..75fb0fb25 100644 --- a/app/password_util.js +++ b/app/password_util.js @@ -22,6 +22,10 @@ const validatePassword = (phrase, i18n) => { } const trimmed = phrase.trim(); + if (trimmed.length === 0) { + return i18n ? i18n('noGivenPassword') : ERRORS.LENGTH; + } + if (trimmed.length < 6 || trimmed.length > 50) { return i18n ? i18n('passwordLengthError') : ERRORS.LENGTH; } diff --git a/js/views/create_group_dialog_view.js b/js/views/create_group_dialog_view.js index df542b021..c9d00af96 100644 --- a/js/views/create_group_dialog_view.js +++ b/js/views/create_group_dialog_view.js @@ -62,7 +62,7 @@ return this; }, onSubmit(groupName, avatar) { - if(groupName !== this.groupName || avatar !== this.avatarPath) { + if (groupName !== this.groupName || avatar !== this.avatarPath) { window.MediumGroups.initiateGroupUpdate( this.groupId, groupName, diff --git a/ts/components/session/SessionPasswordModal.tsx b/ts/components/session/SessionPasswordModal.tsx index ef1a4c38d..2cf3c6c5a 100644 --- a/ts/components/session/SessionPasswordModal.tsx +++ b/ts/components/session/SessionPasswordModal.tsx @@ -17,17 +17,20 @@ interface Props { interface State { error: string | null; + currentPasswordEntered: string | null; + currentPasswordConfirmEntered: string | null; } export class SessionPasswordModal extends React.Component { - private readonly passwordInput: React.RefObject; - private readonly passwordInputConfirm: React.RefObject; + private passportInput: HTMLInputElement | null = null; constructor(props: any) { super(props); this.state = { error: null, + currentPasswordEntered: null, + currentPasswordConfirmEntered: null, }; this.showError = this.showError.bind(this); @@ -35,32 +38,28 @@ export class SessionPasswordModal extends React.Component { this.setPassword = this.setPassword.bind(this); this.closeDialog = this.closeDialog.bind(this); - this.onKeyUp = this.onKeyUp.bind(this); - this.onPaste = this.onPaste.bind(this); + this.onPasswordInput = this.onPasswordInput.bind(this); + this.onPasswordConfirmInput = this.onPasswordConfirmInput.bind(this); - this.passwordInput = React.createRef(); - this.passwordInputConfirm = React.createRef(); + this.onPaste = this.onPaste.bind(this); } public componentDidMount() { setTimeout(() => { - if (!this.passwordInput.current) { - return; - } - - this.passwordInput.current.focus(); - }, 100); + // tslint:disable-next-line: no-unused-expression + this.passportInput && this.passportInput.focus(); + }, 1); } public render() { const { action, onOk } = this.props; const placeholders = - this.props.action === PasswordAction.Change + action === PasswordAction.Change ? [window.i18n('typeInOldPassword'), window.i18n('enterPassword')] : [window.i18n('enterPassword'), window.i18n('confirmPassword')]; const confirmButtonColor = - this.props.action === PasswordAction.Remove + action === PasswordAction.Remove ? SessionButtonColor.Danger : SessionButtonColor.Primary; @@ -76,9 +75,11 @@ export class SessionPasswordModal extends React.Component { { + this.passportInput = input; + }} placeholder={placeholders[0]} - onKeyUp={this.onKeyUp} + onKeyUp={this.onPasswordInput} maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH} onPaste={this.onPaste} /> @@ -86,9 +87,8 @@ export class SessionPasswordModal extends React.Component { @@ -139,61 +139,60 @@ export class SessionPasswordModal extends React.Component { ); } + // tslint:disable-next-line: cyclomatic-complexity private async setPassword(onSuccess?: any) { - // Only initial input required for PasswordAction.Remove - if ( - !this.passwordInput.current || - (!this.passwordInputConfirm.current && - this.props.action !== PasswordAction.Remove) - ) { - return; - } + const { action } = this.props; + const { + currentPasswordEntered, + currentPasswordConfirmEntered, + } = this.state; + const { Set, Remove, Change } = PasswordAction; // Trim leading / trailing whitespace for UX - const enteredPassword = String(this.passwordInput.current.value).trim(); - const enteredPasswordConfirm = - (this.passwordInputConfirm.current && - String(this.passwordInputConfirm.current.value).trim()) || - ''; - - if ( - enteredPassword.length === 0 || - (enteredPasswordConfirm.length === 0 && - this.props.action !== PasswordAction.Remove) - ) { - return; - } + const enteredPassword = (currentPasswordEntered || '').trim(); + const enteredPasswordConfirm = (currentPasswordConfirmEntered || '').trim(); - // Check passwords entered - if ( - enteredPassword.length === 0 || - (this.props.action === PasswordAction.Change && - enteredPasswordConfirm.length === 0) - ) { + // if user did not fill the first password field, we can't do anything + const errorFirstInput = window.passwordUtil.validatePassword( + enteredPassword, + window.i18n + ); + if (errorFirstInput !== null) { this.setState({ - error: window.i18n('noGivenPassword'), + error: errorFirstInput, }); - return; } + // if action is Set or Change, we need a valid ConfirmPassword + if (action === Set || action === Change) { + const errorSecondInput = window.passwordUtil.validatePassword( + enteredPasswordConfirm, + window.i18n + ); + if (errorSecondInput !== null) { + this.setState({ + error: errorSecondInput, + }); + return; + } + } + // Passwords match or remove password successful - const newPassword = - this.props.action === PasswordAction.Remove - ? null - : enteredPasswordConfirm; - const oldPassword = - this.props.action === PasswordAction.Set ? null : enteredPassword; + const newPassword = action === Remove ? null : enteredPasswordConfirm; + const oldPassword = action === Set ? null : enteredPassword; // Check if password match, when setting, changing or removing - const valid = - this.props.action !== PasswordAction.Set - ? Boolean(await this.validatePasswordHash(oldPassword)) - : enteredPassword === enteredPasswordConfirm; + let valid; + if (action === Set) { + valid = enteredPassword === enteredPasswordConfirm; + } else { + valid = Boolean(await this.validatePasswordHash(oldPassword)); + } if (!valid) { this.setState({ - error: window.i18n(`${this.props.action}PasswordInvalid`), + error: window.i18n(`${action}PasswordInvalid`), }); return; @@ -202,10 +201,10 @@ export class SessionPasswordModal extends React.Component { await window.setPassword(newPassword, oldPassword); const toastParams = { - title: window.i18n(`${this.props.action}PasswordTitle`), - description: window.i18n(`${this.props.action}PasswordToastDescription`), - type: this.props.action !== PasswordAction.Remove ? 'success' : 'warning', - icon: this.props.action !== PasswordAction.Remove ? 'lock' : undefined, + title: window.i18n(`${action}PasswordTitle`), + description: window.i18n(`${action}PasswordToastDescription`), + type: action !== Remove ? 'success' : 'warning', + icon: action !== Remove ? 'lock' : undefined, }; window.pushToast({ @@ -244,13 +243,17 @@ export class SessionPasswordModal extends React.Component { return false; } - private async onKeyUp(event: any) { - const { onOk } = this.props; - + private async onPasswordInput(event: any) { if (event.key === 'Enter') { - await this.setPassword(onOk); + return this.setPassword(this.props.onOk); } + this.setState({ currentPasswordEntered: event.target.value }); + } - event.preventDefault(); + private async onPasswordConfirmInput(event: any) { + if (event.key === 'Enter') { + return this.setPassword(this.props.onOk); + } + this.setState({ currentPasswordConfirmEntered: event.target.value }); } } diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index eb3fed81b..87439cb82 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -8,9 +8,7 @@ import * as Lodash from 'lodash'; import * as libsession from '../session'; import { handleSessionRequestMessage } from './sessionHandling'; import { handlePairingAuthorisationMessage } from './multidevice'; -import { - MediumGroupRequestKeysMessage, -} from '../session/messages/outgoing'; +import { MediumGroupRequestKeysMessage } from '../session/messages/outgoing'; import { MultiDeviceProtocol, SessionProtocol } from '../session/protocols'; import { PubKey } from '../session/types'; From e806e912a3c30111cc686353910ad8c0d276c6f0 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 30 Jul 2020 11:07:36 +1000 Subject: [PATCH 2/4] move password_utils.js to typescript --- main.js | 2 +- password_preload.js | 2 +- preload.js | 2 +- test/app/password_util_test.js | 2 +- ts/components/session/SessionPasswordModal.tsx | 8 ++++---- ts/util/index.ts | 2 ++ .../util/passwordUtils.ts | 17 ++++++----------- 7 files changed, 16 insertions(+), 19 deletions(-) rename app/password_util.js => ts/util/passwordUtils.ts (73%) diff --git a/main.js b/main.js index 0b4f7f688..68a04b572 100644 --- a/main.js +++ b/main.js @@ -54,7 +54,7 @@ const config = require('./app/config'); // Very important to put before the single instance check, since it is based on the // userData directory. const userConfig = require('./app/user_config'); -const passwordUtil = require('./app/password_util'); +const passwordUtil = require('./ts/util/passwordUtils'); const importMode = process.argv.some(arg => arg === '--import') || config.get('import'); diff --git a/password_preload.js b/password_preload.js index bd18b92db..d5c29af89 100644 --- a/password_preload.js +++ b/password_preload.js @@ -40,7 +40,7 @@ window.CONSTANTS = { MAX_USERNAME_LENGTH: 20, }; -window.passwordUtil = require('./app/password_util'); +window.passwordUtil = require('./ts/util/passwordUtils'); window.Signal.Logs = require('./js/modules/logs'); window.resetDatabase = () => { diff --git a/preload.js b/preload.js index 457de7c11..bf4471ec0 100644 --- a/preload.js +++ b/preload.js @@ -164,7 +164,7 @@ window.setPassword = (passPhrase, oldPhrase) => ipc.send('set-password', passPhrase, oldPhrase); }); -window.passwordUtil = require('./app/password_util'); +window.passwordUtil = require('./ts/util/passwordUtils'); window.libsession = require('./ts/session'); // We never do these in our code, so we'll prevent it everywhere diff --git a/test/app/password_util_test.js b/test/app/password_util_test.js index 700b99c74..7037ec32e 100644 --- a/test/app/password_util_test.js +++ b/test/app/password_util_test.js @@ -1,6 +1,6 @@ const { assert } = require('chai'); -const passwordUtil = require('../../app/password_util'); +const passwordUtil = require('../../ts/util/passwordUtils'); describe('Password Util', () => { describe('hash generation', () => { diff --git a/ts/components/session/SessionPasswordModal.tsx b/ts/components/session/SessionPasswordModal.tsx index 2cf3c6c5a..a00f56ab8 100644 --- a/ts/components/session/SessionPasswordModal.tsx +++ b/ts/components/session/SessionPasswordModal.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { SessionModal } from './SessionModal'; import { SessionButton, SessionButtonColor } from './SessionButton'; - +import { PasswordUtil } from '../../util/'; export enum PasswordAction { Set = 'set', Change = 'change', @@ -117,7 +117,7 @@ export class SessionPasswordModal extends React.Component { public async validatePasswordHash(password: string | null) { // Check if the password matches the hash we have stored const hash = await window.Signal.Data.getPasswordHash(); - if (hash && !window.passwordUtil.matchesHash(password, hash)) { + if (hash && !PasswordUtil.matchesHash(password, hash)) { return false; } @@ -153,7 +153,7 @@ export class SessionPasswordModal extends React.Component { const enteredPasswordConfirm = (currentPasswordConfirmEntered || '').trim(); // if user did not fill the first password field, we can't do anything - const errorFirstInput = window.passwordUtil.validatePassword( + const errorFirstInput = PasswordUtil.validatePassword( enteredPassword, window.i18n ); @@ -166,7 +166,7 @@ export class SessionPasswordModal extends React.Component { // if action is Set or Change, we need a valid ConfirmPassword if (action === Set || action === Change) { - const errorSecondInput = window.passwordUtil.validatePassword( + const errorSecondInput = PasswordUtil.validatePassword( enteredPasswordConfirm, window.i18n ); diff --git a/ts/util/index.ts b/ts/util/index.ts index c5428412b..699b5fcb8 100644 --- a/ts/util/index.ts +++ b/ts/util/index.ts @@ -5,6 +5,7 @@ import { missingCaseError } from './missingCaseError'; import { migrateColor } from './migrateColor'; import { makeLookup } from './makeLookup'; import * as UserUtil from './user'; +import * as PasswordUtil from './passwordUtils'; export * from './blockedNumberController'; @@ -16,4 +17,5 @@ export { migrateColor, missingCaseError, UserUtil, + PasswordUtil, }; diff --git a/app/password_util.js b/ts/util/passwordUtils.ts similarity index 73% rename from app/password_util.js rename to ts/util/passwordUtils.ts index 75fb0fb25..ea2ca0d24 100644 --- a/app/password_util.js +++ b/ts/util/passwordUtils.ts @@ -1,4 +1,5 @@ -const crypto = require('crypto'); +import * as crypto from 'crypto'; +import { LocalizerType } from '../types/Util'; const ERRORS = { TYPE: 'Password must be a string', @@ -6,17 +7,17 @@ const ERRORS = { CHARACTER: 'Password must only contain letters, numbers and symbols', }; -const sha512 = text => { +const sha512 = (text: string) => { const hash = crypto.createHash('sha512'); hash.update(text.trim()); return hash.digest('hex'); }; -const generateHash = phrase => phrase && sha512(phrase.trim()); -const matchesHash = (phrase, hash) => +export const generateHash = (phrase: string) => phrase && sha512(phrase.trim()); +export const matchesHash = (phrase: string | null, hash: string) => phrase && sha512(phrase.trim()) === hash.trim(); -const validatePassword = (phrase, i18n) => { +export const validatePassword = (phrase: string, i18n: LocalizerType) => { if (typeof phrase !== 'string') { return i18n ? i18n('passwordTypeError') : ERRORS.TYPE; } @@ -38,9 +39,3 @@ const validatePassword = (phrase, i18n) => { return null; }; - -module.exports = { - generateHash, - matchesHash, - validatePassword, -}; From 12ec5beb9501556f943f137660a308fce413f470 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 30 Jul 2020 11:19:52 +1000 Subject: [PATCH 3/4] fix bug preventing loading of out of view cells in conversations list --- ts/components/session/LeftPaneContactSection.tsx | 2 +- ts/components/session/LeftPaneMessageSection.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/components/session/LeftPaneContactSection.tsx b/ts/components/session/LeftPaneContactSection.tsx index 5df607f93..68de97471 100644 --- a/ts/components/session/LeftPaneContactSection.tsx +++ b/ts/components/session/LeftPaneContactSection.tsx @@ -269,7 +269,7 @@ export class LeftPaneContactSection extends React.Component { rowHeight={64} rowRenderer={this.renderRow} width={width} - autoHeight={true} + autoHeight={false} /> )} diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 9b33e50cf..93c5c1f04 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -192,7 +192,7 @@ export class LeftPaneMessageSection extends React.Component { rowHeight={64} rowRenderer={this.renderRow} width={width} - autoHeight={true} + autoHeight={false} /> )} From b237d20e620caa250a926e855147de67eb3d2286 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 30 Jul 2020 12:44:47 +1000 Subject: [PATCH 4/4] treat mentions to our primary as us from secondary --- ts/components/conversation/AddMentions.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ts/components/conversation/AddMentions.tsx b/ts/components/conversation/AddMentions.tsx index ae7a50dde..26c079ee1 100644 --- a/ts/components/conversation/AddMentions.tsx +++ b/ts/components/conversation/AddMentions.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { RenderTextCallbackType } from '../../types/Util'; import classNames from 'classnames'; +import { MultiDeviceProtocol } from '../../session/protocols'; declare global { interface Window { @@ -19,6 +20,7 @@ interface MentionProps { interface MentionState { found: any; + us: boolean; } class Mention extends React.Component { @@ -45,8 +47,7 @@ class Mention extends React.Component { public render() { if (this.state.found) { // TODO: We don't have to search the database of message just to know that the message is for us! - const us = - this.state.found.authorPhoneNumber === window.lokiPublicChatAPI.ourKey; + const us = this.state.us; const className = classNames( 'mention-profile-name', us && 'mention-profile-name-us' @@ -73,7 +74,9 @@ class Mention extends React.Component { private async tryRenameMention() { const found = await this.findMember(this.props.text.slice(1)); if (found) { - this.setState({ found }); + const us = await MultiDeviceProtocol.isOurDevice(found.authorPhoneNumber); + + this.setState({ found, us }); this.clearOurInterval(); } }