Update safety number change warning dialog

This commit is contained in:
Josh Perez 2020-06-25 20:08:58 -04:00 committed by Scott Nonnenberg
parent e87a0103cc
commit 5b83485c89
38 changed files with 1221 additions and 425 deletions

View file

@ -1,4 +1,3 @@
// tslint:disable: no-backbone-get-set-outside-model
import { get, throttle } from 'lodash';
import { WebAPIType } from './textsecure/WebAPI';

View file

@ -70,33 +70,35 @@ export const ConfirmationDialog = React.memo(
<div className="module-confirmation-dialog__container__content">
{children}
</div>
<div className="module-confirmation-dialog__container__buttons">
<button
onClick={handleCancel}
ref={focusRef}
className="module-confirmation-dialog__container__buttons__button"
>
{i18n('confirmation-dialog--Cancel')}
</button>
{actions.map((action, i) => (
{actions.length > 0 && (
<div className="module-confirmation-dialog__container__buttons">
<button
key={i}
onClick={handleAction}
data-action={i}
className={classNames(
'module-confirmation-dialog__container__buttons__button',
action.style === 'affirmative'
? 'module-confirmation-dialog__container__buttons__button--affirmative'
: null,
action.style === 'negative'
? 'module-confirmation-dialog__container__buttons__button--negative'
: null
)}
onClick={handleCancel}
ref={focusRef}
className="module-confirmation-dialog__container__buttons__button"
>
{action.text}
{i18n('confirmation-dialog--Cancel')}
</button>
))}
</div>
{actions.map((action, i) => (
<button
key={i}
onClick={handleAction}
data-action={i}
className={classNames(
'module-confirmation-dialog__container__buttons__button',
action.style === 'affirmative'
? 'module-confirmation-dialog__container__buttons__button--affirmative'
: null,
action.style === 'negative'
? 'module-confirmation-dialog__container__buttons__button--negative'
: null
)}
>
{action.text}
</button>
))}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,76 @@
import * as React from 'react';
import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog';
import { ConversationType } from '../state/ducks/conversations';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
const i18n = setupI18n('en', enMessages);
const contact = {
avatarPath: undefined,
color: 'signal-blue',
profileName: undefined,
name: 'Rick Sanchez',
phoneNumber: '3051234567',
} as ConversationType;
storiesOf('Components/SafetyNumberChangeDialog', module)
.add('Single Contact Dialog', () => {
return (
<SafetyNumberChangeDialog
contacts={[contact]}
i18n={i18n}
onCancel={action('cancel')}
onConfirm={action('confirm')}
renderSafetyNumber={() => {
action('renderSafetyNumber');
return <div>This is a mock Safety Number View</div>;
}}
/>
);
})
.add('Multi Contact Dialog', () => {
return (
<SafetyNumberChangeDialog
contacts={[contact, contact, contact, contact]}
i18n={i18n}
onCancel={action('cancel')}
onConfirm={action('confirm')}
renderSafetyNumber={() => {
action('renderSafetyNumber');
return <div>This is a mock Safety Number View</div>;
}}
/>
);
})
.add('Scroll Dialog', () => {
return (
<SafetyNumberChangeDialog
contacts={[
contact,
contact,
contact,
contact,
contact,
contact,
contact,
contact,
contact,
contact,
]}
i18n={i18n}
onCancel={action('cancel')}
onConfirm={action('confirm')}
renderSafetyNumber={() => {
action('renderSafetyNumber');
return <div>This is a mock Safety Number View</div>;
}}
/>
);
});

View file

@ -0,0 +1,135 @@
import * as React from 'react';
import { Avatar } from './Avatar';
import { ConfirmationModal } from './ConfirmationModal';
import { ConversationType } from '../state/ducks/conversations';
import { LocalizerType } from '../types/Util';
type SafetyNumberProps = {
contactID: string;
onClose?: () => void;
};
export type Props = {
readonly contacts: Array<ConversationType>;
readonly i18n: LocalizerType;
readonly onCancel: () => void;
readonly onConfirm: () => void;
readonly renderSafetyNumber: (props: SafetyNumberProps) => JSX.Element;
};
type SafetyDialogContentProps = Props & {
readonly onView: (contact: ConversationType) => void;
};
const SafetyDialogContents = ({
contacts,
i18n,
onCancel,
onConfirm,
onView,
}: SafetyDialogContentProps): JSX.Element => {
const cancelButtonRef = React.createRef<HTMLButtonElement>();
React.useEffect(() => {
if (cancelButtonRef && cancelButtonRef.current) {
cancelButtonRef.current.focus();
}
}, [contacts]);
return (
<>
<h1 className="module-sfn-dialog__title">
{i18n('safetyNumberChanges')}
</h1>
<div className="module-sfn-dialog__message">
{i18n('changedVerificationWarning')}
</div>
<ul className="module-sfn-dialog__contacts">
{contacts.map((contact: ConversationType) => (
<li className="module-sfn-dialog__contact" key={contact.phoneNumber}>
<Avatar
avatarPath={contact.avatarPath}
color={contact.color}
conversationType="direct"
i18n={i18n}
name={contact.name}
phoneNumber={contact.phoneNumber}
profileName={contact.profileName}
size={52}
/>
<div className="module-sfn-dialog__contact--wrapper">
{contact.name && (
<>
<div className="module-sfn-dialog__contact--name">
{contact.name}
</div>
<div className="module-sfn-dialog__contact--number">
{contact.phoneNumber}
</div>
</>
)}
{!contact.name && (
<div className="module-sfn-dialog__contact--name">
{contact.phoneNumber}
</div>
)}
</div>
<button
className="module-sfn-dialog__contact--view"
onClick={() => {
onView(contact);
}}
tabIndex={0}
>
{i18n('view')}
</button>
</li>
))}
</ul>
<div className="module-sfn-dialog__actions">
<button
className="module-sfn-dialog__actions--cancel"
onClick={onCancel}
ref={cancelButtonRef}
tabIndex={0}
>
{i18n('cancel')}
</button>
<button
className="module-sfn-dialog__actions--confirm"
onClick={onConfirm}
tabIndex={0}
>
{i18n('sendMessageToContact')}
</button>
</div>
</>
);
};
export const SafetyNumberChangeDialog = (props: Props): JSX.Element => {
const { i18n, onCancel, renderSafetyNumber } = props;
const [contact, setViewSafetyNumber] = React.useState<
ConversationType | undefined
>(undefined);
const onClose = contact
? () => {
setViewSafetyNumber(undefined);
}
: onCancel;
return (
<ConfirmationModal actions={[]} i18n={i18n} onClose={onClose}>
{contact && renderSafetyNumber({ contactID: contact.id, onClose })}
{!contact && (
<SafetyDialogContents
{...props}
onView={selectedContact => {
setViewSafetyNumber(selectedContact);
}}
/>
)}
</ConfirmationModal>
);
};

View file

@ -0,0 +1,85 @@
import * as React from 'react';
import { SafetyNumberViewer } from './SafetyNumberViewer';
import { ConversationType } from '../state/ducks/conversations';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { action } from '@storybook/addon-actions';
import { boolean, text } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
contact: {
title: 'Summer Smith',
isVerified: true,
} as ConversationType,
generateSafetyNumber: action('generate-safety-number'),
i18n,
safetyNumber: 'XXX',
safetyNumberChanged: false,
toggleVerified: action('toggle-verified'),
verificationDisabled: false,
};
const permutations = [
{
title: 'Safety Number',
props: {},
},
{
title: 'Safety Number (not verified)',
props: {
contact: {
title: 'Morty Smith',
isVerified: false,
} as ConversationType,
},
},
{
title: 'Verification Disabled',
props: {
verificationDisabled: true,
},
},
{
title: 'Safety Number Changed',
props: {
safetyNumberChanged: true,
},
},
{
title: 'Safety Number (dialog close)',
props: {
onClose: action('close'),
},
},
];
storiesOf('Components/SafetyNumberViewer', module)
.add('Knobs Playground', () => {
const safetyNumber = text('safetyNumber', 'XXX');
const safetyNumberChanged = boolean('safetyNumberChanged', false);
const verificationDisabled = boolean('verificationDisabled', false);
return (
<SafetyNumberViewer
{...defaultProps}
safetyNumber={safetyNumber}
safetyNumberChanged={safetyNumberChanged}
verificationDisabled={verificationDisabled}
/>
);
})
.add('Iterations', () => {
return permutations.map(({ props, title }) => (
<>
<h3>{title}</h3>
<SafetyNumberViewer {...defaultProps} {...props} />
</>
));
});

View file

@ -0,0 +1,82 @@
import React from 'react';
import { ConversationType } from '../state/ducks/conversations';
import { LocalizerType } from '../types/Util';
import { getPlaceholder } from '../util/safetyNumber';
type SafetyNumberViewerProps = {
contact?: ConversationType;
generateSafetyNumber: (contact: ConversationType) => void;
i18n: LocalizerType;
onClose?: () => void;
safetyNumber: string;
safetyNumberChanged?: boolean;
toggleVerified: (contact: ConversationType) => void;
verificationDisabled: boolean;
};
export const SafetyNumberViewer = ({
contact,
generateSafetyNumber,
i18n,
onClose,
safetyNumber,
safetyNumberChanged,
toggleVerified,
verificationDisabled,
}: SafetyNumberViewerProps): JSX.Element | null => {
if (!contact) {
return null;
}
React.useEffect(() => {
generateSafetyNumber(contact);
}, [safetyNumber]);
const name = contact.title;
const isVerified = contact.isVerified;
const verifiedStatus = isVerified
? i18n('isVerified', [name])
: i18n('isNotVerified', [name]);
const verifyButtonText = isVerified ? i18n('unverify') : i18n('verify');
return (
<div className="module-safety-number">
{onClose && (
<div className="module-safety-number__close-button">
<button onClick={onClose} tabIndex={0}>
<span />
</button>
</div>
)}
<div className="module-safety-number__verification-label">
{safetyNumberChanged
? i18n('changedRightAfterVerify', [name, name])
: i18n('yourSafetyNumberWith', [name])}
</div>
<div className="module-safety-number__number">
{safetyNumber || getPlaceholder()}
</div>
{i18n('verifyHelp', [name])}
<div className="module-safety-number__verification-status">
{isVerified ? (
<span className="module-safety-number__icon--verified" />
) : (
<span className="module-safety-number__icon--shield" />
)}
{verifiedStatus}
</div>
<div className="module-safety-number__verify-container">
<button
className="module-safety-number__button--verify"
disabled={verificationDisabled}
onClick={() => {
toggleVerified(contact);
}}
tabIndex={0}
>
{verifyButtonText}
</button>
</div>
</div>
);
};

View file

@ -37,7 +37,6 @@ const SENT = 'sent' as 'sent';
const START_NEW_CONVERSATION = 'start-new-conversation' as 'start-new-conversation';
const SMS_MMS_NOT_SUPPORTED = 'sms-mms-not-supported-text' as 'sms-mms-not-supported-text';
// tslint:disable-next-line no-backbone-get-set-outside-model
messageLookup.set('1-guid-guid-guid-guid-guid', {
id: '1-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0015',
@ -56,7 +55,6 @@ messageLookup.set('1-guid-guid-guid-guid-guid', {
},
});
// tslint:disable-next-line no-backbone-get-set-outside-model
messageLookup.set('2-guid-guid-guid-guid-guid', {
id: '2-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0016',
@ -73,7 +71,6 @@ messageLookup.set('2-guid-guid-guid-guid-guid', {
},
});
// tslint:disable-next-line no-backbone-get-set-outside-model
messageLookup.set('3-guid-guid-guid-guid-guid', {
id: '3-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',
@ -91,7 +88,6 @@ messageLookup.set('3-guid-guid-guid-guid-guid', {
},
});
// tslint:disable-next-line no-backbone-get-set-outside-model
messageLookup.set('4-guid-guid-guid-guid-guid', {
id: '4-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',

View file

@ -0,0 +1,13 @@
export async function toggleVerification(id: string): Promise<void> {
const contact = window.getConversations().get(id);
if (contact) {
await contact.toggleVerified();
}
}
export async function reloadProfiles(id: string): Promise<void> {
const contact = window.getConversations().get(id);
if (contact) {
await contact.getProfiles();
}
}

View file

@ -1,4 +1,4 @@
// tslint:disable no-backbone-get-set-outside-model no-console no-default-export no-unnecessary-local-variable
// tslint:disable no-console no-default-export no-unnecessary-local-variable
import { join } from 'path';
import mkdirp from 'mkdirp';

View file

@ -4,6 +4,7 @@ import { actions as emojis } from './ducks/emojis';
import { actions as expiration } from './ducks/expiration';
import { actions as items } from './ducks/items';
import { actions as network } from './ducks/network';
import { actions as safetyNumber } from './ducks/safetyNumber';
import { actions as search } from './ducks/search';
import { actions as stickers } from './ducks/stickers';
import { actions as updates } from './ducks/updates';
@ -16,6 +17,7 @@ export const mapDispatchToProps = {
...expiration,
...items,
...network,
...safetyNumber,
...search,
...stickers,
...updates,

View file

@ -24,12 +24,15 @@ export type DBConversationType = {
};
export type ConversationType = {
id: string;
uuid?: string;
e164: string;
name?: string;
profileName?: string;
avatarPath?: string;
color?: ColorType;
isArchived?: boolean;
isBlocked?: boolean;
isVerified?: boolean;
activeAt?: number;
timestamp: number;
inboxPosition: number;
@ -42,6 +45,7 @@ export type ConversationType = {
type: 'direct' | 'group';
isMe: boolean;
lastUpdated: number;
title: string;
unreadCount: number;
isSelected: boolean;
typingContact?: {

View file

@ -0,0 +1,214 @@
import { generateSecurityNumberBlock } from '../../util/safetyNumber';
import { ConversationType } from './conversations';
import {
reloadProfiles,
toggleVerification,
} from '../../shims/contactVerification';
export type SafetyNumberContactType = {
safetyNumber: string;
safetyNumberChanged?: boolean;
verificationDisabled: boolean;
};
export type SafetyNumberStateType = {
contacts: {
[key: string]: SafetyNumberContactType;
};
};
const GENERATE = 'safetyNumber/GENERATE';
const GENERATE_FULFILLED = 'safetyNumber/GENERATE_FULFILLED';
const TOGGLE_VERIFIED = 'safetyNumber/TOGGLE_VERIFIED';
const TOGGLE_VERIFIED_FULFILLED = 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
const TOGGLE_VERIFIED_PENDING = 'safetyNumber/TOGGLE_VERIFIED_PENDING';
type GenerateAsyncActionType = {
contact: ConversationType;
safetyNumber: string;
};
type GenerateActionType = {
type: 'safetyNumber/GENERATE';
payload: Promise<GenerateAsyncActionType>;
};
type GenerateFulfilledActionType = {
type: 'safetyNumber/GENERATE_FULFILLED';
payload: GenerateAsyncActionType;
};
type ToggleVerifiedAsyncActionType = {
contact: ConversationType;
safetyNumber?: string;
safetyNumberChanged?: boolean;
};
type ToggleVerifiedActionType = {
type: 'safetyNumber/TOGGLE_VERIFIED';
payload: {
data: { contact: ConversationType };
promise: Promise<ToggleVerifiedAsyncActionType>;
};
};
type ToggleVerifiedPendingActionType = {
type: 'safetyNumber/TOGGLE_VERIFIED_PENDING';
payload: ToggleVerifiedAsyncActionType;
};
type ToggleVerifiedFulfilledActionType = {
type: 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
payload: ToggleVerifiedAsyncActionType;
};
export type SafetyNumberActionTypes =
| GenerateActionType
| GenerateFulfilledActionType
| ToggleVerifiedActionType
| ToggleVerifiedPendingActionType
| ToggleVerifiedFulfilledActionType;
function generate(contact: ConversationType): GenerateActionType {
return {
type: GENERATE,
payload: doGenerate(contact),
};
}
async function doGenerate(
contact: ConversationType
): Promise<GenerateAsyncActionType> {
const securityNumberBlock = await generateSecurityNumberBlock(contact);
return {
contact,
safetyNumber: securityNumberBlock.join(' '),
};
}
function toggleVerified(contact: ConversationType): ToggleVerifiedActionType {
return {
type: TOGGLE_VERIFIED,
payload: {
data: { contact },
promise: doToggleVerified(contact),
},
};
}
async function alterVerification(contact: ConversationType): Promise<void> {
try {
await toggleVerification(contact.id);
} catch (result) {
if (result instanceof Error) {
if (result.name === 'OutgoingIdentityKeyError') {
throw result;
} else {
window.log.error(
'failed to toggle verified:',
result && result.stack ? result.stack : result
);
}
} else {
const keyError = result.errors.find(
(error: Error) => error.name === 'OutgoingIdentityKeyError'
);
if (keyError) {
throw keyError;
} else {
result.errors.forEach((error: Error) => {
window.log.error(
'failed to toggle verified:',
error && error.stack ? error.stack : error
);
});
}
}
}
}
async function doToggleVerified(
contact: ConversationType
): Promise<ToggleVerifiedAsyncActionType> {
try {
await alterVerification(contact);
} catch (err) {
if (err.name === 'OutgoingIdentityKeyError') {
await reloadProfiles(contact.id);
const securityNumberBlock = await generateSecurityNumberBlock(contact);
return {
contact,
safetyNumber: securityNumberBlock.join(' '),
safetyNumberChanged: true,
};
}
}
return { contact };
}
export const actions = {
generateSafetyNumber: generate,
toggleVerified,
};
function getEmptyState(): SafetyNumberStateType {
return {
contacts: {},
};
}
export function reducer(
state: SafetyNumberStateType = getEmptyState(),
action: SafetyNumberActionTypes
): SafetyNumberStateType {
if (action.type === TOGGLE_VERIFIED_PENDING) {
const { contact } = action.payload;
const { id } = contact;
const record = state.contacts[id];
return {
contacts: {
...state.contacts,
[id]: {
...record,
safetyNumberChanged: false,
verificationDisabled: true,
},
},
};
}
if (action.type === TOGGLE_VERIFIED_FULFILLED) {
const { contact, ...restProps } = action.payload;
const { id } = contact;
const record = state.contacts[id];
return {
contacts: {
...state.contacts,
[id]: {
...record,
...restProps,
verificationDisabled: false,
},
},
};
}
if (action.type === GENERATE_FULFILLED) {
const { contact, safetyNumber } = action.payload;
const { id } = contact;
const record = state.contacts[id];
return {
contacts: {
...state.contacts,
[id]: {
...record,
safetyNumber,
},
},
};
}
return state;
}

View file

@ -30,6 +30,11 @@ import {
NetworkStateType,
reducer as network,
} from './ducks/network';
import {
reducer as safetyNumber,
SafetyNumberActionTypes,
SafetyNumberStateType,
} from './ducks/safetyNumber';
import {
reducer as search,
SEARCH_TYPES as SearchActionType,
@ -54,6 +59,7 @@ export type StateType = {
expiration: ExpirationStateType;
items: ItemsStateType;
network: NetworkStateType;
safetyNumber: SafetyNumberStateType;
search: SearchStateType;
stickers: StickersStateType;
updates: UpdatesStateType;
@ -67,6 +73,7 @@ export type ActionsType =
| ConversationActionType
| ItemsActionType
| NetworkActionType
| SafetyNumberActionTypes
| StickersActionType
| SearchActionType
| UpdatesActionType;
@ -78,6 +85,7 @@ export const reducers = {
expiration,
items,
network,
safetyNumber,
search,
stickers,
updates,

View file

@ -0,0 +1,21 @@
import React from 'react';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import { SmartSafetyNumberViewer } from '../smart/SafetyNumberViewer';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
const FilteredSafetyNumberViewer = SmartSafetyNumberViewer as any;
type Props = {
contactID: string;
onClose?: () => void;
};
export const createSafetyNumberViewer = (store: Store, props: Props) => (
<Provider store={store}>
<FilteredSafetyNumberViewer {...props} />
</Provider>
);

View file

@ -0,0 +1,24 @@
import { createSelector } from 'reselect';
import { StateType } from '../reducer';
import {
SafetyNumberContactType,
SafetyNumberStateType,
} from '../ducks/safetyNumber';
const getSafetyNumber = (state: StateType): SafetyNumberStateType =>
state.safetyNumber;
type Props = {
contactID: string;
};
const getContactID = (_: StateType, props: Props): string => props.contactID;
export const getContactSafetyNumber = createSelector(
[getSafetyNumber, getContactID],
(
{ contacts }: SafetyNumberStateType,
contactID: string
): SafetyNumberContactType => contacts[contactID]
);

View file

@ -0,0 +1,25 @@
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { SafetyNumberViewer } from '../../components/SafetyNumberViewer';
import { StateType } from '../reducer';
import { getContactSafetyNumber } from '../selectors/safetyNumber';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
type Props = {
contactID: string;
onClose?: () => void;
};
const mapStateToProps = (state: StateType, props: Props) => {
return {
...props,
...getContactSafetyNumber(state, props),
contact: getConversationSelector(state)(props.contactID),
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartSafetyNumberViewer = smart(SafetyNumberViewer);

View file

@ -14,6 +14,7 @@ describe('state/selectors/conversations', () => {
const data: ConversationLookupType = {
id1: {
id: 'id1',
e164: '+18005551111',
activeAt: Date.now(),
name: 'No timestamp',
timestamp: 0,
@ -24,6 +25,7 @@ describe('state/selectors/conversations', () => {
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'No timestamp',
unreadCount: 1,
isSelected: false,
typingContact: {
@ -36,6 +38,7 @@ describe('state/selectors/conversations', () => {
},
id2: {
id: 'id2',
e164: '+18005551111',
activeAt: Date.now(),
name: 'B',
timestamp: 20,
@ -46,6 +49,7 @@ describe('state/selectors/conversations', () => {
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'B',
unreadCount: 1,
isSelected: false,
typingContact: {
@ -58,6 +62,7 @@ describe('state/selectors/conversations', () => {
},
id3: {
id: 'id3',
e164: '+18005551111',
activeAt: Date.now(),
name: 'C',
timestamp: 20,
@ -68,6 +73,7 @@ describe('state/selectors/conversations', () => {
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'C',
unreadCount: 1,
isSelected: false,
typingContact: {
@ -80,6 +86,7 @@ describe('state/selectors/conversations', () => {
},
id4: {
id: 'id4',
e164: '+18005551111',
activeAt: Date.now(),
name: 'Á',
timestamp: 20,
@ -90,6 +97,7 @@ describe('state/selectors/conversations', () => {
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'A',
unreadCount: 1,
isSelected: false,
typingContact: {
@ -102,6 +110,7 @@ describe('state/selectors/conversations', () => {
},
id5: {
id: 'id5',
e164: '+18005551111',
activeAt: Date.now(),
name: 'First!',
timestamp: 30,
@ -112,6 +121,7 @@ describe('state/selectors/conversations', () => {
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'First!',
unreadCount: 1,
isSelected: false,
typingContact: {

17
ts/textsecure.d.ts vendored
View file

@ -96,6 +96,14 @@ type StoredSignedPreKeyType = SignedPreKeyType & {
created_at: number;
};
type IdentityKeyRecord = {
publicKey: ArrayBuffer;
firstUse: boolean;
timestamp: number;
verified: number;
nonblockingApproval: boolean;
};
export type StorageProtocolType = StorageType & {
VerifiedStatus: {
DEFAULT: number;
@ -105,6 +113,7 @@ export type StorageProtocolType = StorageType & {
archiveSiblingSessions: (identifier: string) => Promise<void>;
removeSession: (identifier: string) => Promise<void>;
getDeviceIds: (identifier: string) => Promise<Array<number>>;
getIdentityRecord: (identifier: string) => IdentityKeyRecord | undefined;
hydrateCaches: () => Promise<void>;
clearPreKeyStore: () => Promise<void>;
clearSignedPreKeysStore: () => Promise<void>;
@ -119,13 +128,7 @@ export type StorageProtocolType = StorageType & {
loadSignedPreKeys: () => Promise<Array<StoredSignedPreKeyType>>;
saveIdentityWithAttributes: (
number: string,
options: {
publicKey: ArrayBuffer;
firstUse: boolean;
timestamp: number;
verified: number;
nonblockingApproval: boolean;
}
options: IdentityKeyRecord
) => Promise<void>;
removeSignedPreKey: (keyId: number) => Promise<void>;
removeAllData: () => Promise<void>;

View file

@ -1,4 +1,4 @@
// tslint:disable no-backbone-get-set-outside-model no-default-export no-unnecessary-local-variable
// tslint:disable no-default-export no-unnecessary-local-variable
import EventTarget from './EventTarget';
import { WebAPIType } from './WebAPI';

View file

@ -1,4 +1,4 @@
// tslint:disable no-backbone-get-set-outside-model no-bitwise no-default-export
// tslint:disable no-bitwise no-default-export
import { without } from 'lodash';
import PQueue from 'p-queue';

View file

@ -189,7 +189,6 @@ const agents: AgentCacheType = {};
function getContentType(response: Response) {
if (response.headers && response.headers.get) {
// tslint:disable-next-line no-backbone-get-set-outside-model
return response.headers.get('content-type');
}
@ -310,7 +309,6 @@ async function _promiseAjax(
let resultPromise;
if (
options.responseType === 'json' &&
// tslint:disable-next-line no-backbone-get-set-outside-model
response.headers.get('Content-Type') === 'application/json'
) {
resultPromise = response.json();
@ -1464,7 +1462,6 @@ export function initialize({
throw new Error('makeProxiedRequest: Problem retrieving header value');
}
// tslint:disable-next-line no-backbone-get-set-outside-model
const range = response.headers.get('content-range');
const match = PARSE_RANGE_HEADER.exec(range);

View file

@ -6,6 +6,10 @@ import { createBatcher } from './batcher';
import { createWaitBatcher } from './waitBatcher';
import { deleteForEveryone } from './deleteForEveryone';
import { downloadAttachment } from './downloadAttachment';
import {
generateSecurityNumber,
getPlaceholder as getSafetyNumberPlaceholder,
} from './safetyNumber';
import { hasExpired } from './hasExpired';
import { isFileDangerous } from './isFileDangerous';
import { makeLookup } from './makeLookup';
@ -20,6 +24,8 @@ export {
createWaitBatcher,
deleteForEveryone,
downloadAttachment,
generateSecurityNumber,
getSafetyNumberPlaceholder,
GoogleChrome,
hasExpired,
isFileDangerous,

View file

@ -223,7 +223,7 @@
"rule": "jQuery-wrap(",
"path": "js/models/conversations.js",
"line": " await wrap(",
"lineNumber": 641,
"lineNumber": 644,
"reasonCategory": "falseMatch",
"updated": "2020-06-09T20:26:46.515Z"
},
@ -710,46 +710,18 @@
{
"rule": "jQuery-$(",
"path": "js/views/key_verification_view.js",
"line": " new QRCode(this.$('.qr')[0]).makeCode(",
"lineNumber": 43,
"line": " this.$('.key-verification-wrapper').append(view.el);",
"lineNumber": 23,
"reasonCategory": "usageTrusted",
"updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Hardcoded selector"
"updated": "2020-06-23T06:48:06.829Z"
},
{
"rule": "jQuery-wrap(",
"rule": "jQuery-append(",
"path": "js/views/key_verification_view.js",
"line": " dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64')",
"lineNumber": 44,
"reasonCategory": "falseMatch",
"updated": "2020-02-14T20:02:37.507Z"
},
{
"rule": "jQuery-insertBefore(",
"path": "js/views/key_verification_view.js",
"line": " dialog.$el.insertBefore(this.el);",
"lineNumber": 86,
"line": " this.$('.key-verification-wrapper').append(view.el);",
"lineNumber": 23,
"reasonCategory": "usageTrusted",
"updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Known DOM elements"
},
{
"rule": "jQuery-$(",
"path": "js/views/key_verification_view.js",
"line": " this.$('button.verify').attr('disabled', true);",
"lineNumber": 90,
"reasonCategory": "usageTrusted",
"updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Hardcoded selector"
},
{
"rule": "jQuery-$(",
"path": "js/views/key_verification_view.js",
"line": " this.$('button.verify').removeAttr('disabled');",
"lineNumber": 121,
"reasonCategory": "usageTrusted",
"updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Hardcoded selector"
"updated": "2020-06-23T06:48:06.829Z"
},
{
"rule": "jQuery-append(",
@ -841,6 +813,22 @@
"updated": "2018-10-11T19:22:47.331Z",
"reasonDetail": "Operating on already-existing DOM elements"
},
{
"rule": "jQuery-$(",
"path": "js/views/safety_number_change_dialog_view.js",
"line": " this.$('.safety-number-change-dialog-wrapper').append(dialog.el);",
"lineNumber": 36,
"reasonCategory": "usageTrusted",
"updated": "2020-06-23T06:48:06.829Z"
},
{
"rule": "jQuery-append(",
"path": "js/views/safety_number_change_dialog_view.js",
"line": " this.$('.safety-number-change-dialog-wrapper').append(dialog.el);",
"lineNumber": 36,
"reasonCategory": "usageTrusted",
"updated": "2020-06-23T06:48:06.829Z"
},
{
"rule": "jQuery-$(",
"path": "js/views/settings_view.js",
@ -11579,6 +11567,15 @@
"updated": "2020-02-14T20:02:37.507Z",
"reasonDetail": "Used only to set focus"
},
{
"rule": "React-createRef",
"path": "ts/components/SafetyNumberChangeDialog.js",
"line": " const cancelButtonRef = React.createRef();",
"lineNumber": 14,
"reasonCategory": "usageTrusted",
"updated": "2020-06-23T06:48:06.829Z",
"reasonDetail": "Used to focus cancel button when dialog opens"
},
{
"rule": "React-createRef",
"path": "ts/components/SearchResults.js",

View file

@ -13,11 +13,9 @@ export function remove() {
}
export function isDone() {
// tslint:disable-next-line no-backbone-get-set-outside-model
return window.storage.get('chromiumRegistrationDone') === '';
}
export function everDone() {
// tslint:disable-next-line no-backbone-get-set-outside-model
return window.storage.get('chromiumRegistrationDoneEver') === '' || isDone();
}

58
ts/util/safetyNumber.ts Normal file
View file

@ -0,0 +1,58 @@
import { ConversationType } from '../state/ducks/conversations';
export async function generateSecurityNumber(
ourNumber: string,
ourKey: ArrayBuffer,
theirNumber: string,
theirKey: ArrayBuffer
): Promise<string> {
return new window.libsignal.FingerprintGenerator(5200).createFor(
ourNumber,
ourKey,
theirNumber,
theirKey
);
}
export function getPlaceholder(): string {
return Array.from(Array(12))
.map(() => 'XXXXX')
.join(' ');
}
export async function generateSecurityNumberBlock(
contact: ConversationType
): Promise<Array<string>> {
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const us = window.textsecure.storage.protocol.getIdentityRecord(
ourUuid || ourNumber
);
const ourKey = us ? us.publicKey : null;
const them = window.textsecure.storage.protocol.getIdentityRecord(contact.id);
const theirKey = them ? them.publicKey : null;
if (!ourKey) {
throw new Error('Could not load our key');
}
if (!theirKey) {
throw new Error('Could not load their key');
}
const securityNumber = await generateSecurityNumber(
ourNumber,
ourKey,
contact.e164,
theirKey
);
const chunks = [];
for (let i = 0; i < securityNumber.length; i += 5) {
chunks.push(securityNumber.substring(i, i + 5));
}
return chunks;
}

7
ts/window.d.ts vendored
View file

@ -13,9 +13,12 @@ import * as Crypto from './Crypto';
import { ColorType, LocalizerType } from './types/Util';
import { SendOptionsType } from './textsecure/SendMessage';
type TaskResultType = any;
declare global {
interface Window {
dcodeIO: DCodeIOType;
getConversations: () => ConversationControllerType;
getExpiration: () => string;
getEnvironment: () => string;
getSocketStatus: () => number;
@ -83,12 +86,16 @@ export type ConversationType = {
getColor(): ColorType | undefined;
getName(): string | undefined;
getNumber(): string;
getProfiles(): Promise<Array<Promise<void>>>;
getProfileName(): string | undefined;
getRecipients: () => Array<string>;
getSendOptions(): SendOptionsType;
getTitle(): string;
isVerified(): boolean;
safeGetVerified(): Promise<number>;
getIsAddedByContact(): boolean;
addCallHistory(details: CallHistoryDetailsType): void;
toggleVerified(): Promise<TaskResultType>;
};
export type ConversationControllerType = {