Fix the password length limit when not setting a new password

Relates #1446
This commit is contained in:
Audric Ackermann 2021-01-22 16:29:02 +11:00
parent eadfbc9f6d
commit 43ec14e741
14 changed files with 156 additions and 185 deletions

View File

@ -1915,7 +1915,7 @@
"message": "Failed to set password"
},
"passwordLengthError": {
"message": "Password must be between 6 and 50 characters long",
"message": "Password must be between 6 and 64 characters long",
"description": "Error string shown to the user when password doesn't meet length criteria"
},
"passwordTypeError": {

View File

@ -1698,7 +1698,7 @@
"message": "Échec de la définition du mot de passe"
},
"passwordLengthError": {
"message": "Le mot de passe doit avoir une longueur comprise entre 6 et 50 caractères",
"message": "Le mot de passe doit avoir une longueur comprise entre 6 et 64 caractères",
"description": "Error string shown to the user when password doesn't meet length criteria"
},
"passwordTypeError": {

View File

@ -1698,7 +1698,7 @@
"message": "Failed to set password"
},
"passwordLengthError": {
"message": "Password must be between 6 and 50 characters long",
"message": "Password must be between 6 and 64 characters long",
"description": "Error string shown to the user when password doesn't meet length criteria"
},
"passwordTypeError": {

View File

@ -40,7 +40,6 @@ window.CONSTANTS = {
MAX_USERNAME_LENGTH: 20,
};
window.passwordUtil = require('./ts/util/passwordUtils');
window.Signal.Logs = require('./js/modules/logs');
window.resetDatabase = () => {

View File

@ -173,7 +173,6 @@ window.setPassword = (passPhrase, oldPhrase) =>
ipc.send('set-password', passPhrase, oldPhrase);
});
window.passwordUtil = require('./ts/util/passwordUtils');
window.libsession = require('./ts/session');
window.getMessageController =

View File

@ -237,8 +237,7 @@ export class Lightbox extends React.Component<Props> {
}
if (current.paused) {
// tslint:disable-next-line no-floating-promises
current.play();
void current.play();
} else {
current.pause();
}
@ -272,7 +271,7 @@ export class Lightbox extends React.Component<Props> {
</div>
</div>
<div style={styles.controls}>
<Flex flexGrow={1}>
<Flex flex="1 1 auto">
<IconButton
type="close"
onClick={this.onClose}

View File

@ -14,6 +14,7 @@ import { SessionSpinner } from './SessionSpinner';
import { StringUtils, ToastUtils } from '../../session/utils';
import { lightTheme } from '../../state/ducks/SessionTheme';
import { ConversationController } from '../../session/conversations';
import { PasswordUtil } from '../../util';
enum SignInMode {
Default,
@ -454,7 +455,6 @@ export class RegistrationTabs extends React.Component<any, State> {
error={this.state.passwordErrorString}
type="password"
placeholder={window.i18n('enterOptionalPassword')}
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
onValueChanged={(val: string) => {
this.onPasswordChanged(val);
}}
@ -470,7 +470,6 @@ export class RegistrationTabs extends React.Component<any, State> {
error={passwordsDoNotMatch}
type="password"
placeholder={window.i18n('confirmPassword')}
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
onValueChanged={(val: string) => {
this.onPasswordVerifyChanged(val);
}}
@ -592,7 +591,7 @@ export class RegistrationTabs extends React.Component<any, State> {
return;
}
const error = window.passwordUtil.validatePassword(input, window.i18n);
const error = PasswordUtil.validatePassword(input, window.i18n);
if (error) {
this.setState({
passwordErrorString: error,

View File

@ -2,7 +2,7 @@ import React from 'react';
import { SessionModal } from './SessionModal';
import { SessionButton, SessionButtonColor } from './SessionButton';
import { PasswordUtil } from '../../util/';
import { missingCaseError, PasswordUtil } from '../../util/';
import { ToastUtils } from '../../session/utils';
import { toast } from 'react-toastify';
import { SessionToast, SessionToastType } from './SessionToast';
@ -46,8 +46,6 @@ class SessionPasswordModalInner extends React.Component<Props, State> {
this.onPasswordInput = this.onPasswordInput.bind(this);
this.onPasswordConfirmInput = this.onPasswordConfirmInput.bind(this);
this.onPaste = this.onPaste.bind(this);
}
public componentDidMount() {
@ -86,8 +84,6 @@ class SessionPasswordModalInner extends React.Component<Props, State> {
}}
placeholder={placeholders[0]}
onKeyUp={this.onPasswordInput}
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
onPaste={this.onPaste}
/>
{action !== PasswordAction.Remove && (
<input
@ -95,8 +91,6 @@ class SessionPasswordModalInner extends React.Component<Props, State> {
id="password-modal-input-confirm"
placeholder={placeholders[1]}
onKeyUp={this.onPasswordConfirmInput}
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
onPaste={this.onPaste}
/>
)}
</div>
@ -108,7 +102,7 @@ class SessionPasswordModalInner extends React.Component<Props, State> {
<SessionButton
text={window.i18n('ok')}
buttonColor={confirmButtonColor}
onClick={async () => this.setPassword(onOk)}
onClick={this.setPassword}
/>
<SessionButton
@ -145,8 +139,106 @@ class SessionPasswordModalInner extends React.Component<Props, State> {
);
}
/**
* Returns false and set the state error field in the input is not a valid password
* or returns true
*/
private validatePassword(firstPassword: string) {
// if user did not fill the first password field, we can't do anything
const errorFirstInput = PasswordUtil.validatePassword(
firstPassword,
window.i18n
);
if (errorFirstInput !== null) {
this.setState({
error: errorFirstInput,
});
return false;
}
return true;
}
private async handleActionSet(
enteredPassword: string,
enteredPasswordConfirm: string
) {
// be sure both password are valid
if (!this.validatePassword(enteredPassword)) {
return;
}
// no need to validate second password. we just need to check that enteredPassword is valid, and that both password matches
if (enteredPassword !== enteredPasswordConfirm) {
this.setState({
error: window.i18n('setPasswordInvalid'),
});
return;
}
await window.setPassword(enteredPassword, null);
ToastUtils.pushToastSuccess(
'setPasswordSuccessToast',
window.i18n('setPasswordTitle'),
window.i18n('setPasswordToastDescription'),
SessionIconType.Lock
);
this.props.onOk(this.props.action);
this.closeDialog();
}
private async handleActionChange(oldPassword: string, newPassword: string) {
// We don't validate oldPassword on change: this is validate on the validatePasswordHash below
// we only validate the newPassword here
if (!this.validatePassword(newPassword)) {
return;
}
const isValidWithStoredInDB = Boolean(
await this.validatePasswordHash(oldPassword)
);
if (!isValidWithStoredInDB) {
this.setState({
error: window.i18n('changePasswordInvalid'),
});
return;
}
await window.setPassword(newPassword, oldPassword);
ToastUtils.pushToastSuccess(
'setPasswordSuccessToast',
window.i18n('changePasswordTitle'),
window.i18n('changePasswordToastDescription'),
SessionIconType.Lock
);
this.props.onOk(this.props.action);
this.closeDialog();
}
private async handleActionRemove(oldPassword: string) {
// We don't validate oldPassword on change: this is validate on the validatePasswordHash below
const isValidWithStoredInDB = Boolean(
await this.validatePasswordHash(oldPassword)
);
if (!isValidWithStoredInDB) {
this.setState({
error: window.i18n('removePasswordInvalid'),
});
return;
}
await window.setPassword(null, oldPassword);
ToastUtils.pushToastWarning(
'setPasswordSuccessToast',
window.i18n('removePasswordTitle'),
window.i18n('removePasswordToastDescription')
);
this.props.onOk(this.props.action);
this.closeDialog();
}
// tslint:disable-next-line: cyclomatic-complexity
private async setPassword(onSuccess?: any) {
private async setPassword() {
const { action } = this.props;
const {
currentPasswordEntered,
@ -155,106 +247,28 @@ class SessionPasswordModalInner extends React.Component<Props, State> {
const { Set, Remove, Change } = PasswordAction;
// Trim leading / trailing whitespace for UX
const enteredPassword = (currentPasswordEntered || '').trim();
const enteredPasswordConfirm = (currentPasswordConfirmEntered || '').trim();
const firstPasswordEntered = (currentPasswordEntered || '').trim();
const secondPasswordEntered = (currentPasswordConfirmEntered || '').trim();
// if user did not fill the first password field, we can't do anything
const errorFirstInput = PasswordUtil.validatePassword(
enteredPassword,
window.i18n
);
if (errorFirstInput !== null) {
this.setState({
error: errorFirstInput,
});
return;
}
// if action is Set or Change, we need a valid ConfirmPassword
if (action === Set || action === Change) {
const errorSecondInput = PasswordUtil.validatePassword(
enteredPasswordConfirm,
window.i18n
);
if (errorSecondInput !== null) {
this.setState({
error: errorSecondInput,
});
switch (action) {
case Set: {
await this.handleActionSet(firstPasswordEntered, secondPasswordEntered);
return;
}
}
// Passwords match or remove password successful
const newPassword = action === Remove ? null : enteredPasswordConfirm;
const oldPassword = action === Set ? null : enteredPassword;
// Check if password match, when setting, changing or removing
let valid;
if (action === Set) {
valid = enteredPassword === enteredPasswordConfirm;
} else {
valid = Boolean(await this.validatePasswordHash(oldPassword));
}
if (!valid) {
let str;
switch (action) {
case Set:
str = window.i18n('setPasswordInvalid');
break;
case Change:
str = window.i18n('changePasswordInvalid');
break;
case Remove:
str = window.i18n('removePasswordInvalid');
break;
default:
throw new Error(`Invalid action ${action}`);
case Change: {
await this.handleActionChange(
firstPasswordEntered,
secondPasswordEntered
);
return;
}
case Remove: {
await this.handleActionRemove(firstPasswordEntered);
return;
}
this.setState({
error: str,
});
return;
}
await window.setPassword(newPassword, oldPassword);
let title;
let description;
switch (action) {
case Set:
title = window.i18n('setPasswordTitle');
description = window.i18n('setPasswordToastDescription');
break;
case Change:
title = window.i18n('changePasswordTitle');
description = window.i18n('changePasswordToastDescription');
break;
case Remove:
title = window.i18n('removePasswordTitle');
description = window.i18n('removePasswordToastDescription');
break;
default:
throw new Error(`Invalid action ${action}`);
throw missingCaseError(action);
}
if (action !== Remove) {
ToastUtils.pushToastSuccess(
'setPasswordSuccessToast',
title,
description,
SessionIconType.Lock
);
} else {
ToastUtils.pushToastWarning(
'setPasswordSuccessToast',
title,
description
);
}
onSuccess(this.props.action);
this.closeDialog();
}
private closeDialog() {
@ -263,26 +277,9 @@ class SessionPasswordModalInner extends React.Component<Props, State> {
}
}
private onPaste(event: any) {
const clipboard = event.clipboardData.getData('text');
if (clipboard.length > window.CONSTANTS.MAX_PASSWORD_LENGTH) {
const title = String(
window.i18n(
'pasteLongPasswordToastTitle',
window.CONSTANTS.MAX_PASSWORD_LENGTH
)
);
ToastUtils.pushToastWarning('passwordModal', title);
}
// Prevent pating into input
return false;
}
private async onPasswordInput(event: any) {
if (event.key === 'Enter') {
return this.setPassword(this.props.onOk);
return this.setPassword();
}
const currentPasswordEntered = event.target.value;
@ -291,7 +288,7 @@ class SessionPasswordModalInner extends React.Component<Props, State> {
private async onPasswordConfirmInput(event: any) {
if (event.key === 'Enter') {
return this.setPassword(this.props.onOk);
return this.setPassword();
}
const currentPasswordConfirmEntered = event.target.value;

View File

@ -32,7 +32,6 @@ class SessionPasswordPromptInner extends React.PureComponent<
};
this.onKeyUp = this.onKeyUp.bind(this);
this.onPaste = this.onPaste.bind(this);
this.initLogin = this.initLogin.bind(this);
this.initClearDataView = this.initClearDataView.bind(this);
@ -72,8 +71,6 @@ class SessionPasswordPromptInner extends React.PureComponent<
defaultValue=""
placeholder={' '}
onKeyUp={this.onKeyUp}
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
onPaste={this.onPaste}
ref={this.inputRef}
/>
);
@ -135,24 +132,6 @@ class SessionPasswordPromptInner extends React.PureComponent<
event.preventDefault();
}
public onPaste(event: any) {
const clipboard = event.clipboardData.getData('text');
if (clipboard.length > window.CONSTANTS.MAX_PASSWORD_LENGTH) {
this.setState({
error: String(
window.i18n(
'pasteLongPasswordToastTitle',
window.CONSTANTS.MAX_PASSWORD_LENGTH
)
),
});
}
// Prevent pasting into input
return false;
}
public async onLogin(passPhrase: string) {
const passPhraseTrimmed = passPhrase.trim();

View File

@ -4,6 +4,7 @@ import { SessionModal } from './SessionModal';
import { SessionButton } from './SessionButton';
import { ToastUtils } from '../../session/utils';
import { DefaultTheme, withTheme } from 'styled-components';
import { PasswordUtil } from '../../util';
interface Props {
onClose: any;
@ -77,7 +78,6 @@ class SessionSeedModalInner extends React.Component<Props, State> {
}
private renderPasswordView() {
const maxPasswordLen = 64;
const error = this.state.error;
const i18n = window.i18n;
const { onClose } = this.props;
@ -90,7 +90,6 @@ class SessionSeedModalInner extends React.Component<Props, State> {
id="seed-input-password"
placeholder={i18n('password')}
onKeyUp={this.onEnter}
maxLength={maxPasswordLen}
/>
{error && (
@ -143,8 +142,8 @@ class SessionSeedModalInner extends React.Component<Props, State> {
private confirmPassword() {
const passwordHash = this.state.passwordHash;
const passwordValue = jQuery('#seed-input-password').val();
const isPasswordValid = window.passwordUtil.matchesHash(
passwordValue,
const isPasswordValid = PasswordUtil.matchesHash(
passwordValue as string,
passwordHash
);

View File

@ -7,7 +7,7 @@ import {
SessionButtonColor,
SessionButtonType,
} from '../SessionButton';
import { BlockedNumberController } from '../../../util';
import { BlockedNumberController, PasswordUtil } from '../../../util';
import { ToastUtils } from '../../../session/utils';
import { ConversationLookupType } from '../../../state/ducks/conversations';
import { StateType } from '../../../state/reducer';
@ -172,8 +172,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
type="password"
id="password-lock-input"
defaultValue=""
placeholder={' '}
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
placeholder="Password"
/>
<div className="spacer-xs" />
@ -211,7 +210,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
// Check if the password matches the hash we have stored
const hash = await window.Signal.Data.getPasswordHash();
if (hash && !window.passwordUtil.matchesHash(enteredPassword, hash)) {
if (hash && !PasswordUtil.matchesHash(enteredPassword, hash)) {
this.setState({
pwdLockError: window.i18n('invalidPassword'),
});

View File

@ -1,17 +1,16 @@
const { assert } = require('chai');
const passwordUtil = require('../../ts/util/passwordUtils');
import { assert } from 'chai';
import { PasswordUtil } from '../../../../util';
describe('Password Util', () => {
describe('hash generation', () => {
it('generates the same hash for the same phrase', () => {
const first = passwordUtil.generateHash('phrase');
const second = passwordUtil.generateHash('phrase');
const first = PasswordUtil.generateHash('phrase');
const second = PasswordUtil.generateHash('phrase');
assert.strictEqual(first, second);
});
it('generates different hashes for different phrases', () => {
const first = passwordUtil.generateHash('0');
const second = passwordUtil.generateHash('1');
const first = PasswordUtil.generateHash('0');
const second = PasswordUtil.generateHash('1');
assert.notStrictEqual(first, second);
});
});
@ -19,12 +18,12 @@ describe('Password Util', () => {
describe('hash matching', () => {
it('returns true for the same hash', () => {
const phrase = 'phrase';
const hash = passwordUtil.generateHash(phrase);
assert.isTrue(passwordUtil.matchesHash(phrase, hash));
const hash = PasswordUtil.generateHash(phrase);
assert.isTrue(PasswordUtil.matchesHash(phrase, hash));
});
it('returns false for different hashes', () => {
const hash = passwordUtil.generateHash('phrase');
assert.isFalse(passwordUtil.matchesHash('phrase2', hash));
const hash = PasswordUtil.generateHash('phrase');
assert.isFalse(PasswordUtil.matchesHash('phrase2', hash));
});
});
@ -44,26 +43,26 @@ describe('Password Util', () => {
'#'.repeat(50),
];
valid.forEach(pass => {
assert.isNull(passwordUtil.validatePassword(pass));
assert.isNull(PasswordUtil.validatePassword(pass));
});
});
it('should return an error if password is not a string', () => {
const invalid = [0, 123456, [], {}, null, undefined];
invalid.forEach(pass => {
const invalid = [0, 123456, [], {}, null, undefined] as any;
invalid.forEach((pass: any) => {
assert.strictEqual(
passwordUtil.validatePassword(pass),
PasswordUtil.validatePassword(pass),
'Password must be a string'
);
});
});
it('should return an error if password is not between 6 and 50 characters', () => {
it('should return an error if password is not between 6 and 64 characters', () => {
const invalid = ['a', 'abcde', '#'.repeat(51), '#'.repeat(100)];
invalid.forEach(pass => {
assert.strictEqual(
passwordUtil.validatePassword(pass),
'Password must be between 6 and 50 characters long'
PasswordUtil.validatePassword(pass),
'Password must be between 6 and 64 characters long'
);
});
});
@ -82,7 +81,7 @@ describe('Password Util', () => {
];
invalid.forEach(pass => {
assert.strictEqual(
passwordUtil.validatePassword(pass),
PasswordUtil.validatePassword(pass),
'Password must only contain letters, numbers and symbols'
);
});

View File

@ -3,7 +3,7 @@ import { LocalizerType } from '../types/Util';
const ERRORS = {
TYPE: 'Password must be a string',
LENGTH: 'Password must be between 6 and 50 characters long',
LENGTH: 'Password must be between 6 and 64 characters long',
CHARACTER: 'Password must only contain letters, numbers and symbols',
};
@ -17,7 +17,7 @@ export const generateHash = (phrase: string) => phrase && sha512(phrase.trim());
export const matchesHash = (phrase: string | null, hash: string) =>
phrase && sha512(phrase.trim()) === hash.trim();
export const validatePassword = (phrase: string, i18n: LocalizerType) => {
export const validatePassword = (phrase: string, i18n?: LocalizerType) => {
if (typeof phrase !== 'string') {
return i18n ? i18n('passwordTypeError') : ERRORS.TYPE;
}
@ -27,7 +27,10 @@ export const validatePassword = (phrase: string, i18n: LocalizerType) => {
return i18n ? i18n('noGivenPassword') : ERRORS.LENGTH;
}
if (trimmed.length < 6 || trimmed.length > 50) {
if (
trimmed.length < 6 ||
trimmed.length > window.CONSTANTS.MAX_PASSWORD_LENGTH
) {
return i18n ? i18n('passwordLengthError') : ERRORS.LENGTH;
}

1
ts/window.d.ts vendored
View File

@ -72,7 +72,6 @@ declare global {
lokiSnodeAPI: LokiSnodeAPI;
mnemonic: RecoveryPhraseUtil;
onLogin: any;
passwordUtil: any;
resetDatabase: any;
restart: any;
seedNodeList: any;