Use libsignal-client validation for nicknames

This commit is contained in:
Fedor Indutny 2023-03-08 16:58:54 -08:00 committed by GitHub
parent f46b93d806
commit 5d07167222
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 56 additions and 145 deletions

View file

@ -1,7 +1,11 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { usernames } from '@signalapp/libsignal-client'; import {
usernames,
LibSignalErrorBase,
ErrorCode,
} from '@signalapp/libsignal-client';
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue'; import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
@ -102,6 +106,35 @@ export async function reserveUsername(
return reserveUsername(options); return reserveUsername(options);
} }
} }
if (error instanceof LibSignalErrorBase) {
if (
error.code === ErrorCode.CannotBeEmpty ||
error.code === ErrorCode.NicknameTooShort
) {
return {
ok: false,
error: ReserveUsernameError.NotEnoughCharacters,
};
}
if (error.code === ErrorCode.NicknameTooLong) {
return {
ok: false,
error: ReserveUsernameError.TooManyCharacters,
};
}
if (error.code === ErrorCode.CannotStartWithDigit) {
return {
ok: false,
error: ReserveUsernameError.CheckStartingCharacter,
};
}
if (error.code === ErrorCode.BadNicknameCharacter) {
return {
ok: false,
error: ReserveUsernameError.CheckCharacters,
};
}
}
throw error; throw error;
} }
} }

View file

@ -11,11 +11,6 @@ import {
} from '../../types/Username'; } from '../../types/Username';
import * as usernameServices from '../../services/username'; import * as usernameServices from '../../services/username';
import type { ReserveUsernameResultType } from '../../services/username'; import type { ReserveUsernameResultType } from '../../services/username';
import {
isValidNickname,
getMinNickname,
getMaxNickname,
} from '../../util/Username';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
import { sleep } from '../../util/sleep'; import { sleep } from '../../util/sleep';
import { assertDev } from '../../util/assert'; import { assertDev } from '../../util/assert';
@ -166,17 +161,6 @@ export function reserveUsername(
return; return;
} }
if (!isValidNickname(nickname)) {
const error = getNicknameInvalidError(nickname);
if (error) {
dispatch(setUsernameReservationError(error));
} else {
assertDev(false, 'This should not happen');
dispatch(setUsernameReservationError(UsernameReservationError.General));
}
return;
}
const { username } = getMe(getState()); const { username } = getMe(getState());
const abortController = new AbortController(); const abortController = new AbortController();
@ -364,6 +348,14 @@ export function reducer(
stateError = UsernameReservationError.CheckCharacters; stateError = UsernameReservationError.CheckCharacters;
} else if (error === ReserveUsernameError.Conflict) { } else if (error === ReserveUsernameError.Conflict) {
stateError = UsernameReservationError.UsernameNotAvailable; stateError = UsernameReservationError.UsernameNotAvailable;
} else if (error === ReserveUsernameError.NotEnoughCharacters) {
stateError = UsernameReservationError.NotEnoughCharacters;
} else if (error === ReserveUsernameError.TooManyCharacters) {
stateError = UsernameReservationError.TooManyCharacters;
} else if (error === ReserveUsernameError.CheckStartingCharacter) {
stateError = UsernameReservationError.CheckStartingCharacter;
} else if (error === ReserveUsernameError.CheckCharacters) {
stateError = UsernameReservationError.CheckCharacters;
} else { } else {
throw missingCaseError(error); throw missingCaseError(error);
} }
@ -485,30 +477,3 @@ export function reducer(
return state; return state;
} }
// Helpers
function getNicknameInvalidError(
nickname: string | undefined
): UsernameReservationError | undefined {
if (!nickname) {
return undefined;
}
if (nickname.length < getMinNickname()) {
return UsernameReservationError.NotEnoughCharacters;
}
if (!/^[0-9a-z_]+$/.test(nickname)) {
return UsernameReservationError.CheckCharacters;
}
if (!/^[a-z_]/.test(nickname)) {
return UsernameReservationError.CheckStartingCharacter;
}
if (nickname.length > getMaxNickname()) {
return UsernameReservationError.TooManyCharacters;
}
return undefined;
}

View file

@ -131,36 +131,6 @@ describe('electron/state/ducks/username', () => {
); );
}); });
const NICKNAME_ERROR_COMBOS = [
['x', UsernameReservationError.NotEnoughCharacters],
['x'.repeat(128), UsernameReservationError.TooManyCharacters],
['#$&^$)(', UsernameReservationError.CheckCharacters],
['1abcdefg', UsernameReservationError.CheckStartingCharacter],
];
for (const [nickname, error] of NICKNAME_ERROR_COMBOS) {
// eslint-disable-next-line no-loop-func
it(`should dispatch ${error} error for "${nickname}"`, async () => {
const clock = sandbox.useFakeTimers();
const doReserveUsername = sinon.stub().resolves(DEFAULT_RESERVATION);
const dispatch = sinon.spy();
actions.reserveUsername(nickname, {
doReserveUsername,
})(dispatch, () => emptyState, null);
await clock.runToLastAsync();
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'username/SET_RESERVATION_ERROR',
payload: {
error,
},
});
});
}
it('should update reservation on success', () => { it('should update reservation on success', () => {
let state = emptyState; let state = emptyState;

View file

@ -1,36 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import * as Username from '../../util/Username';
describe('Username', () => {
describe('isValidUsername', () => {
const { isValidUsername } = Username;
it('does not match invalid username searches', () => {
assert.isFalse(isValidUsername('username!'));
assert.isFalse(isValidUsername('1username'));
assert.isFalse(isValidUsername('u'));
assert.isFalse(isValidUsername('username9012345678901234567890123'));
assert.isFalse(isValidUsername('username.abc'));
});
it('matches valid usernames', () => {
assert.isTrue(isValidUsername('username_34'));
assert.isTrue(isValidUsername('u5ername'));
assert.isTrue(isValidUsername('_username'));
assert.isTrue(isValidUsername('use'));
assert.isTrue(isValidUsername('username901234567890123456789012'));
assert.isTrue(isValidUsername('username.0123'));
});
it('does not match valid and invalid usernames with @ prefix or suffix', () => {
assert.isFalse(isValidUsername('@username_34'));
assert.isFalse(isValidUsername('@1username'));
assert.isFalse(isValidUsername('username_34@'));
assert.isFalse(isValidUsername('1username@'));
});
});
});

View file

@ -10,6 +10,12 @@ export type UsernameReservationType = Readonly<{
export enum ReserveUsernameError { export enum ReserveUsernameError {
Unprocessable = 'Unprocessable', Unprocessable = 'Unprocessable',
Conflict = 'Conflict', Conflict = 'Conflict',
// Maps to UsernameReservationError in state/ducks/usernameEnums.ts
NotEnoughCharacters = 'NotEnoughCharacters',
TooManyCharacters = 'TooManyCharacters',
CheckStartingCharacter = 'CheckStartingCharacter',
CheckCharacters = 'CheckCharacters',
} }
export enum ConfirmUsernameResult { export enum ConfirmUsernameResult {

View file

@ -13,29 +13,3 @@ export function getMaxNickname(): number {
export function getMinNickname(): number { export function getMinNickname(): number {
return parseIntWithFallback(RemoteConfig.getValue('global.nicknames.min'), 3); return parseIntWithFallback(RemoteConfig.getValue('global.nicknames.min'), 3);
} }
export function isValidNickname(nickname: string): boolean {
if (!/^[a-z_][0-9a-z_]*$/i.test(nickname)) {
return false;
}
if (nickname.length < getMinNickname()) {
return false;
}
if (nickname.length > getMaxNickname()) {
return false;
}
return true;
}
export function isValidUsername(username: string): boolean {
const match = username.match(/^([a-z_][0-9a-z_]*)(\.\d+)?$/i);
if (!match) {
return false;
}
const [, nickname] = match;
return isValidNickname(nickname);
}

View file

@ -1,7 +1,7 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { usernames } from '@signalapp/libsignal-client'; import { usernames, LibSignalErrorBase } from '@signalapp/libsignal-client';
import { ToastFailedToFetchUsername } from '../components/ToastFailedToFetchUsername'; import { ToastFailedToFetchUsername } from '../components/ToastFailedToFetchUsername';
import { ToastFailedToFetchPhoneNumber } from '../components/ToastFailedToFetchPhoneNumber'; import { ToastFailedToFetchPhoneNumber } from '../components/ToastFailedToFetchPhoneNumber';
@ -15,7 +15,6 @@ import { showToast } from './showToast';
import { strictAssert } from './assert'; import { strictAssert } from './assert';
import type { UUIDFetchStateKeyType } from './uuidFetchState'; import type { UUIDFetchStateKeyType } from './uuidFetchState';
import { getUuidsForE164s } from './getUuidsForE164s'; import { getUuidsForE164s } from './getUuidsForE164s';
import { isValidUsername } from './Username';
export type LookupConversationWithoutUuidActionsType = Readonly<{ export type LookupConversationWithoutUuidActionsType = Readonly<{
lookupConversationWithoutUuid: typeof lookupConversationWithoutUuid; lookupConversationWithoutUuid: typeof lookupConversationWithoutUuid;
@ -137,10 +136,6 @@ export async function lookupConversationWithoutUuid(
async function checkForUsername( async function checkForUsername(
username: string username: string
): Promise<FoundUsernameType | undefined> { ): Promise<FoundUsernameType | undefined> {
if (!isValidUsername(username)) {
return undefined;
}
const { server } = window.textsecure; const { server } = window.textsecure;
if (!server) { if (!server) {
throw new Error('server is not available!'); throw new Error('server is not available!');
@ -161,11 +156,15 @@ async function checkForUsername(
username, username,
}; };
} catch (error) { } catch (error) {
if (!(error instanceof HTTPError)) { if (error instanceof HTTPError) {
throw error; if (error.code === 404) {
return undefined;
}
} }
if (error.code === 404) { // Invalid username
if (error instanceof LibSignalErrorBase) {
log.error('checkForUsername: invalid username');
return undefined; return undefined;
} }