Add support for ACI safety numbers behind a feature flag
This commit is contained in:

committed by
Fedor Indutnyy

parent
42cd8ce792
commit
c1580a5eb3
@@ -7,6 +7,7 @@ import type { WebAPIType } from './textsecure/WebAPI';
|
||||
import * as log from './logging/log';
|
||||
import type { UUIDStringType } from './types/UUID';
|
||||
import { parseIntOrThrow } from './util/parseIntOrThrow';
|
||||
import { SECOND, HOUR } from './util/durations';
|
||||
import * as Bytes from './Bytes';
|
||||
import { hash, uuidToBytes } from './Crypto';
|
||||
import { HashType } from './types/Crypto';
|
||||
@@ -29,8 +30,7 @@ export type ConfigKeyType =
|
||||
| 'desktop.messageRequests'
|
||||
| 'desktop.pnp'
|
||||
| 'desktop.retryRespondMaxAge'
|
||||
| 'desktop.safetyNumberUUID.timestamp'
|
||||
| 'desktop.safetyNumberUUID'
|
||||
| 'desktop.safetyNumberAci'
|
||||
| 'desktop.senderKey.retry'
|
||||
| 'desktop.senderKey.send'
|
||||
| 'desktop.senderKeyMaxAge'
|
||||
@@ -47,7 +47,8 @@ export type ConfigKeyType =
|
||||
| 'global.groupsv2.groupSizeHardLimit'
|
||||
| 'global.groupsv2.maxGroupSize'
|
||||
| 'global.nicknames.max'
|
||||
| 'global.nicknames.min';
|
||||
| 'global.nicknames.min'
|
||||
| 'global.safetyNumberAci';
|
||||
|
||||
type ConfigValueType = {
|
||||
name: ConfigKeyType;
|
||||
@@ -88,7 +89,15 @@ export const refreshRemoteConfig = async (
|
||||
server: WebAPIType
|
||||
): Promise<void> => {
|
||||
const now = Date.now();
|
||||
const newConfig = await server.getConfig();
|
||||
const { config: newConfig, serverEpochTime } = await server.getConfig();
|
||||
const serverTimeSkew = serverEpochTime * SECOND - now;
|
||||
|
||||
if (Math.abs(serverTimeSkew) > HOUR) {
|
||||
log.warn(
|
||||
'Remote Config: sever clock skew detected. ' +
|
||||
`Server time ${serverEpochTime * SECOND}, local time ${now}`
|
||||
);
|
||||
}
|
||||
|
||||
// Process new configuration in light of the old configuration
|
||||
// The old configuration is not set as the initial value in reduce because
|
||||
@@ -129,6 +138,7 @@ export const refreshRemoteConfig = async (
|
||||
}, {});
|
||||
|
||||
await window.storage.put('remoteConfig', config);
|
||||
await window.storage.put('serverTimeSkew', serverTimeSkew);
|
||||
};
|
||||
|
||||
export const maybeRefreshRemoteConfig = throttle(
|
||||
|
@@ -12,7 +12,7 @@ const ERROR_CORRECTION_LEVEL = 'L';
|
||||
type PropsType = Readonly<{
|
||||
alt: string;
|
||||
className?: string;
|
||||
data: string;
|
||||
data: string | Uint8Array;
|
||||
}>;
|
||||
|
||||
export function QrCode(props: PropsType): ReactElement {
|
||||
@@ -37,6 +37,9 @@ export function QrCode(props: PropsType): ReactElement {
|
||||
if (getEnvironment() === Environment.Production) {
|
||||
return;
|
||||
}
|
||||
if (data instanceof Uint8Array) {
|
||||
return;
|
||||
}
|
||||
|
||||
void navigator.clipboard.writeText(data);
|
||||
|
||||
|
@@ -1,34 +1,81 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
|
||||
import { SafetyNumberMode } from '../types/safetyNumber';
|
||||
import { isSafetyNumberNotAvailable } from '../util/isSafetyNumberNotAvailable';
|
||||
import { Modal } from './Modal';
|
||||
import type { PropsType as SafetyNumberViewerPropsType } from './SafetyNumberViewer';
|
||||
import { SafetyNumberViewer } from './SafetyNumberViewer';
|
||||
import { SafetyNumberOnboarding } from './SafetyNumberOnboarding';
|
||||
import { SafetyNumberNotReady } from './SafetyNumberNotReady';
|
||||
|
||||
type PropsType = {
|
||||
toggleSafetyNumberModal: () => unknown;
|
||||
hasCompletedSafetyNumberOnboarding: boolean;
|
||||
markHasCompletedSafetyNumberOnboarding: () => unknown;
|
||||
} & Omit<SafetyNumberViewerPropsType, 'onClose'>;
|
||||
|
||||
export function SafetyNumberModal({
|
||||
i18n,
|
||||
toggleSafetyNumberModal,
|
||||
hasCompletedSafetyNumberOnboarding,
|
||||
markHasCompletedSafetyNumberOnboarding,
|
||||
...safetyNumberViewerProps
|
||||
}: PropsType): JSX.Element | null {
|
||||
return (
|
||||
<Modal
|
||||
modalName="SafetyNumberModal"
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
moduleClassName="module-SafetyNumberViewer__modal"
|
||||
onClose={toggleSafetyNumberModal}
|
||||
title={i18n('icu:SafetyNumberModal__title')}
|
||||
>
|
||||
const { contact, safetyNumberMode } = safetyNumberViewerProps;
|
||||
|
||||
const [isOnboarding, setIsOnboarding] = useState(
|
||||
safetyNumberMode === SafetyNumberMode.ACIAndE164 &&
|
||||
!hasCompletedSafetyNumberOnboarding
|
||||
);
|
||||
|
||||
const showOnboarding = useCallback(() => {
|
||||
setIsOnboarding(true);
|
||||
}, [setIsOnboarding]);
|
||||
|
||||
const hideOnboarding = useCallback(() => {
|
||||
setIsOnboarding(false);
|
||||
markHasCompletedSafetyNumberOnboarding();
|
||||
}, [setIsOnboarding, markHasCompletedSafetyNumberOnboarding]);
|
||||
|
||||
let title: string | undefined;
|
||||
let content: JSX.Element;
|
||||
let hasXButton = true;
|
||||
if (isSafetyNumberNotAvailable(contact)) {
|
||||
content = (
|
||||
<SafetyNumberNotReady
|
||||
i18n={i18n}
|
||||
onClose={() => toggleSafetyNumberModal()}
|
||||
/>
|
||||
);
|
||||
hasXButton = false;
|
||||
} else if (isOnboarding) {
|
||||
content = <SafetyNumberOnboarding i18n={i18n} onClose={hideOnboarding} />;
|
||||
} else {
|
||||
title = i18n('icu:SafetyNumberModal__title');
|
||||
|
||||
content = (
|
||||
<SafetyNumberViewer
|
||||
i18n={i18n}
|
||||
onClose={toggleSafetyNumberModal}
|
||||
showOnboarding={showOnboarding}
|
||||
{...safetyNumberViewerProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
modalName="SafetyNumberModal"
|
||||
hasXButton={hasXButton}
|
||||
i18n={i18n}
|
||||
moduleClassName="module-SafetyNumberViewer__modal"
|
||||
onClose={toggleSafetyNumberModal}
|
||||
title={title}
|
||||
>
|
||||
{content}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
23
ts/components/SafetyNumberNotReady.stories.tsx
Normal file
23
ts/components/SafetyNumberNotReady.stories.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { SafetyNumberNotReady } from './SafetyNumberNotReady';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/SafetyNumberNotReady',
|
||||
};
|
||||
|
||||
export function Default(): JSX.Element {
|
||||
return <SafetyNumberNotReady i18n={i18n} onClose={action('close')} />;
|
||||
}
|
||||
|
||||
Default.story = {
|
||||
name: 'Safety Number Not Ready',
|
||||
};
|
42
ts/components/SafetyNumberNotReady.tsx
Normal file
42
ts/components/SafetyNumberNotReady.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Modal } from './Modal';
|
||||
import { Intl } from './Intl';
|
||||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { SAFETY_NUMBER_MIGRATION_URL } from '../types/support';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function onLearnMore() {
|
||||
openLinkInWebBrowser(SAFETY_NUMBER_MIGRATION_URL);
|
||||
}
|
||||
|
||||
export function SafetyNumberNotReady({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element | null {
|
||||
return (
|
||||
<div className="module-SafetyNumberNotReady">
|
||||
<div>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberNotReady__body" />
|
||||
</div>
|
||||
|
||||
<Modal.ButtonFooter>
|
||||
<Button onClick={onLearnMore} variant={ButtonVariant.Secondary}>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberNotReady__learn-more" />
|
||||
</Button>
|
||||
<Button onClick={onClose} variant={ButtonVariant.Secondary}>
|
||||
<Intl i18n={i18n} id="icu:ok" />
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
</div>
|
||||
);
|
||||
}
|
23
ts/components/SafetyNumberOnboarding.stories.tsx
Normal file
23
ts/components/SafetyNumberOnboarding.stories.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { SafetyNumberOnboarding } from './SafetyNumberOnboarding';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/SafetyNumberOnboarding',
|
||||
};
|
||||
|
||||
export function Default(): JSX.Element {
|
||||
return <SafetyNumberOnboarding i18n={i18n} onClose={action('close')} />;
|
||||
}
|
||||
|
||||
Default.story = {
|
||||
name: 'Safety Number Onboarding',
|
||||
};
|
78
ts/components/SafetyNumberOnboarding.tsx
Normal file
78
ts/components/SafetyNumberOnboarding.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import Lottie from 'lottie-react';
|
||||
import type { LottieRefCurrentProps } from 'lottie-react';
|
||||
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Intl } from './Intl';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { SAFETY_NUMBER_MIGRATION_URL } from '../types/support';
|
||||
import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||
import animationData from '../../images/safety-number-onboarding.json';
|
||||
import reducedAnimationData from '../../images/safety-number-onboarding-reduced-motion.json';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function SafetyNumberOnboarding({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const isMotionReduced = useReducedMotion();
|
||||
const lottieRef = useRef<LottieRefCurrentProps | null>(null);
|
||||
|
||||
const onDOMLoaded = useCallback(() => {
|
||||
if (isMotionReduced) {
|
||||
lottieRef.current?.goToAndPlay(0);
|
||||
return;
|
||||
}
|
||||
|
||||
lottieRef.current?.playSegments(
|
||||
[
|
||||
[0, 360],
|
||||
[60, 360],
|
||||
],
|
||||
true
|
||||
);
|
||||
}, [isMotionReduced]);
|
||||
|
||||
return (
|
||||
<div className="module-SafetyNumberOnboarding">
|
||||
<h2>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__title" />
|
||||
</h2>
|
||||
<p>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__p1" />
|
||||
</p>
|
||||
<p>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__p2" />
|
||||
</p>
|
||||
<Lottie
|
||||
lottieRef={lottieRef}
|
||||
animationData={isMotionReduced ? reducedAnimationData : animationData}
|
||||
onDOMLoaded={onDOMLoaded}
|
||||
/>
|
||||
<div className="module-SafetyNumberOnboarding__help">
|
||||
<a
|
||||
key="signal-support"
|
||||
href={SAFETY_NUMBER_MIGRATION_URL}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__help" />
|
||||
</a>
|
||||
</div>
|
||||
<Button
|
||||
className="module-SafetyNumberOnboarding__close"
|
||||
onClick={onClose}
|
||||
variant={ButtonVariant.Primary}
|
||||
>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__close" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -8,9 +8,33 @@ import { boolean, text } from '@storybook/addon-knobs';
|
||||
import type { PropsType } from './SafetyNumberViewer';
|
||||
import { SafetyNumberViewer } from './SafetyNumberViewer';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import {
|
||||
SafetyNumberIdentifierType,
|
||||
SafetyNumberMode,
|
||||
} from '../types/safetyNumber';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
|
||||
function generateQRData() {
|
||||
const data = new Uint8Array(128);
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
data[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function generateNumberBlocks() {
|
||||
const result = new Array<string>();
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
let digits = '';
|
||||
for (let j = 0; j < 5; j += 1) {
|
||||
digits += Math.floor(Math.random() * 10);
|
||||
}
|
||||
result.push(digits);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const contactWithAllData = getDefaultConversation({
|
||||
@@ -49,7 +73,17 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
contact: overrideProps.contact || contactWithAllData,
|
||||
generateSafetyNumber: action('generate-safety-number'),
|
||||
i18n,
|
||||
safetyNumber: text('safetyNumber', overrideProps.safetyNumber || 'XXX'),
|
||||
safetyNumberMode: overrideProps.safetyNumberMode ?? SafetyNumberMode.ACI,
|
||||
safetyNumbers: overrideProps.safetyNumbers ?? [
|
||||
{
|
||||
identifierType: SafetyNumberIdentifierType.ACIIdentifier,
|
||||
numberBlocks: text(
|
||||
'safetyNumber',
|
||||
generateNumberBlocks().join(' ')
|
||||
).split(' '),
|
||||
qrData: generateQRData(),
|
||||
},
|
||||
],
|
||||
toggleVerified: action('toggle-verified'),
|
||||
verificationDisabled: boolean(
|
||||
'verificationDisabled',
|
||||
@@ -68,6 +102,56 @@ export function SafetyNumber(): JSX.Element {
|
||||
return <SafetyNumberViewer {...createProps({})} />;
|
||||
}
|
||||
|
||||
export function SafetyNumberBeforeE164Transition(): JSX.Element {
|
||||
return (
|
||||
<SafetyNumberViewer
|
||||
{...createProps({
|
||||
safetyNumberMode: SafetyNumberMode.E164,
|
||||
safetyNumbers: [
|
||||
{
|
||||
identifierType: SafetyNumberIdentifierType.E164Identifier,
|
||||
numberBlocks: text(
|
||||
'safetyNumber',
|
||||
generateNumberBlocks().join(' ')
|
||||
).split(' '),
|
||||
qrData: generateQRData(),
|
||||
},
|
||||
],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
SafetyNumberBeforeE164Transition.story = {
|
||||
name: 'Safety Number (before e164 transition)',
|
||||
};
|
||||
|
||||
export function SafetyNumberE164Transition(): JSX.Element {
|
||||
return (
|
||||
<SafetyNumberViewer
|
||||
{...createProps({
|
||||
safetyNumberMode: SafetyNumberMode.ACIAndE164,
|
||||
safetyNumbers: [
|
||||
{
|
||||
identifierType: SafetyNumberIdentifierType.E164Identifier,
|
||||
numberBlocks: generateNumberBlocks(),
|
||||
qrData: generateQRData(),
|
||||
},
|
||||
{
|
||||
identifierType: SafetyNumberIdentifierType.ACIIdentifier,
|
||||
numberBlocks: generateNumberBlocks(),
|
||||
qrData: generateQRData(),
|
||||
},
|
||||
],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
SafetyNumberE164Transition.story = {
|
||||
name: 'Safety Number (e164 transition)',
|
||||
};
|
||||
|
||||
export function SafetyNumberNotVerified(): JSX.Element {
|
||||
return (
|
||||
<SafetyNumberViewer
|
||||
|
@@ -1,19 +1,31 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { QrCode } from './QrCode';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { Intl } from './Intl';
|
||||
import { Emojify } from './conversation/Emojify';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { SafetyNumberType } from '../types/safetyNumber';
|
||||
import { SAFETY_NUMBER_MIGRATION_URL } from '../types/support';
|
||||
import {
|
||||
SafetyNumberIdentifierType,
|
||||
SafetyNumberMode,
|
||||
} from '../types/safetyNumber';
|
||||
|
||||
export type PropsType = {
|
||||
contact: ConversationType;
|
||||
generateSafetyNumber: (contact: ConversationType) => void;
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
safetyNumber: string;
|
||||
safetyNumberMode: SafetyNumberMode;
|
||||
safetyNumbers?: ReadonlyArray<SafetyNumberType>;
|
||||
toggleVerified: (contact: ConversationType) => void;
|
||||
showOnboarding?: () => void;
|
||||
verificationDisabled: boolean;
|
||||
};
|
||||
|
||||
@@ -22,23 +34,28 @@ export function SafetyNumberViewer({
|
||||
generateSafetyNumber,
|
||||
i18n,
|
||||
onClose,
|
||||
safetyNumber,
|
||||
safetyNumberMode,
|
||||
safetyNumbers,
|
||||
toggleVerified,
|
||||
showOnboarding,
|
||||
verificationDisabled,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const hasSafetyNumbers = safetyNumbers != null;
|
||||
React.useEffect(() => {
|
||||
if (!contact) {
|
||||
return;
|
||||
}
|
||||
|
||||
generateSafetyNumber(contact);
|
||||
}, [contact, generateSafetyNumber, safetyNumber]);
|
||||
}, [contact, generateSafetyNumber]);
|
||||
|
||||
if (!contact) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
if (!contact || !hasSafetyNumbers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!contact.phoneNumber) {
|
||||
if (!safetyNumbers.length) {
|
||||
return (
|
||||
<div className="module-SafetyNumberViewer">
|
||||
<div>{i18n('icu:cannotGenerateSafetyNumber')}</div>
|
||||
@@ -55,12 +72,10 @@ export function SafetyNumberViewer({
|
||||
);
|
||||
}
|
||||
|
||||
const showNumber = Boolean(contact.name || contact.profileName);
|
||||
const numberFragment =
|
||||
showNumber && contact.phoneNumber ? ` · ${contact.phoneNumber}` : '';
|
||||
const name = `${contact.title}${numberFragment}`;
|
||||
const boldName = (
|
||||
<span className="module-SafetyNumberViewer__bold-name">{name}</span>
|
||||
<span className="module-SafetyNumberViewer__bold-name">
|
||||
<Emojify text={contact.title} />
|
||||
</span>
|
||||
);
|
||||
|
||||
const { isVerified } = contact;
|
||||
@@ -68,32 +83,136 @@ export function SafetyNumberViewer({
|
||||
? i18n('icu:SafetyNumberViewer__clearVerification')
|
||||
: i18n('icu:SafetyNumberViewer__markAsVerified');
|
||||
|
||||
const isMigrationVisible = safetyNumberMode === SafetyNumberMode.ACIAndE164;
|
||||
|
||||
const visibleSafetyNumber = safetyNumbers.at(selectedIndex);
|
||||
if (!visibleSafetyNumber) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cardClassName = classNames('module-SafetyNumberViewer__card', {
|
||||
'module-SafetyNumberViewer__card--aci':
|
||||
visibleSafetyNumber.identifierType ===
|
||||
SafetyNumberIdentifierType.ACIIdentifier,
|
||||
'module-SafetyNumberViewer__card--e164':
|
||||
visibleSafetyNumber.identifierType ===
|
||||
SafetyNumberIdentifierType.E164Identifier,
|
||||
});
|
||||
|
||||
const numberBlocks = visibleSafetyNumber.numberBlocks.join(' ');
|
||||
|
||||
const safetyNumberCard = (
|
||||
<div className="module-SafetyNumberViewer__card-container">
|
||||
<div className={cardClassName}>
|
||||
<QrCode
|
||||
className="module-SafetyNumberViewer__card__qr"
|
||||
data={visibleSafetyNumber.qrData}
|
||||
alt={i18n('icu:Install__scan-this-code')}
|
||||
/>
|
||||
<div className="module-SafetyNumberViewer__card__number">
|
||||
{numberBlocks}
|
||||
</div>
|
||||
|
||||
{selectedIndex > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('icu:SafetyNumberViewer__card__prev')}
|
||||
className="module-SafetyNumberViewer__card__prev"
|
||||
onClick={() => setSelectedIndex(x => x - 1)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedIndex < safetyNumbers.length - 1 && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('icu:SafetyNumberViewer__card__next')}
|
||||
className="module-SafetyNumberViewer__card__next"
|
||||
onClick={() => setSelectedIndex(x => x + 1)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const carousel = (
|
||||
<div className="module-SafetyNumberViewer__carousel">
|
||||
{safetyNumbers.map(({ identifierType }, index) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('icu:SafetyNumberViewer__carousel__dot', {
|
||||
index: index + 1,
|
||||
total: safetyNumbers.length,
|
||||
})}
|
||||
aria-pressed={index === selectedIndex}
|
||||
key={identifierType}
|
||||
className="module-SafetyNumberViewer__carousel__dot"
|
||||
onClick={() => setSelectedIndex(index)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="module-SafetyNumberViewer">
|
||||
<div className="module-SafetyNumberViewer__number">
|
||||
{safetyNumber || getPlaceholder()}
|
||||
</div>
|
||||
<Intl i18n={i18n} id="icu:verifyHelp" components={{ name: boldName }} />
|
||||
<div className="module-SafetyNumberViewer__verification-status">
|
||||
{isVerified ? (
|
||||
<span className="module-SafetyNumberViewer__icon--verified" />
|
||||
) : (
|
||||
<span className="module-SafetyNumberViewer__icon--shield" />
|
||||
)}
|
||||
{isVerified ? (
|
||||
{isMigrationVisible && (
|
||||
<div className="module-SafetyNumberViewer__migration">
|
||||
<div className="module-SafetyNumberViewer__migration__icon" />
|
||||
|
||||
<div className="module-SafetyNumberViewer__migration__text">
|
||||
<p>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberViewer__migration__text" />
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href={SAFETY_NUMBER_MIGRATION_URL}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
onClick={e => {
|
||||
if (showOnboarding) {
|
||||
e.preventDefault();
|
||||
showOnboarding();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:SafetyNumberViewer__migration__learn_more"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{safetyNumberCard}
|
||||
|
||||
{safetyNumbers.length > 1 && carousel}
|
||||
|
||||
<div className="module-SafetyNumberViewer__help">
|
||||
{isMigrationVisible ? (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:isVerified"
|
||||
id="icu:SafetyNumberViewer__hint--migration"
|
||||
components={{ name: boldName }}
|
||||
/>
|
||||
) : (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:isNotVerified"
|
||||
id="icu:SafetyNumberViewer__hint--normal"
|
||||
components={{ name: boldName }}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
<a href={SAFETY_NUMBER_MIGRATION_URL} rel="noreferrer" target="_blank">
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:SafetyNumberViewer__migration__learn_more"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="module-SafetyNumberViewer__button">
|
||||
<Button
|
||||
disabled={verificationDisabled}
|
||||
@@ -108,9 +227,3 @@ export function SafetyNumberViewer({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getPlaceholder(): string {
|
||||
return Array.from(Array(12))
|
||||
.map(() => 'XXXXX')
|
||||
.join(' ');
|
||||
}
|
||||
|
@@ -2915,6 +2915,10 @@ export class ConversationModel extends window.Backbone
|
||||
window.reduxActions.calling.keyChanged({ uuid });
|
||||
}
|
||||
|
||||
if (isDirectConversation(this.attributes)) {
|
||||
window.reduxActions?.safetyNumber.clearSafetyNumber(this.id);
|
||||
}
|
||||
|
||||
if (isDirectConversation(this.attributes) && uuid) {
|
||||
const parsedUuid = UUID.checkedLookup(uuid);
|
||||
const groups =
|
||||
|
@@ -30,6 +30,7 @@ export type ItemsStateType = ReadonlyDeep<{
|
||||
[key: string]: unknown;
|
||||
|
||||
remoteConfig?: RemoteConfigType;
|
||||
serverTimeSkew?: number;
|
||||
|
||||
// This property should always be set and this is ensured in background.ts
|
||||
defaultConversationColor?: DefaultConversationColorType;
|
||||
@@ -85,6 +86,7 @@ export type ItemsActionType = ReadonlyDeep<
|
||||
export const actions = {
|
||||
addCustomColor,
|
||||
editCustomColor,
|
||||
markHasCompletedSafetyNumberOnboarding,
|
||||
removeCustomColor,
|
||||
resetDefaultChatColor,
|
||||
savePreferredLeftPaneWidth,
|
||||
@@ -280,6 +282,17 @@ function savePreferredLeftPaneWidth(
|
||||
};
|
||||
}
|
||||
|
||||
function markHasCompletedSafetyNumberOnboarding(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
ItemPutAction
|
||||
> {
|
||||
return dispatch => {
|
||||
dispatch(putItem('hasCompletedSafetyNumberOnboarding', true));
|
||||
};
|
||||
}
|
||||
|
||||
// Reducer
|
||||
|
||||
export function getEmptyState(): ItemsStateType {
|
||||
|
@@ -3,8 +3,10 @@
|
||||
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { ThunkAction } from 'redux-thunk';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import { generateSecurityNumberBlock } from '../../util/safetyNumber';
|
||||
import { generateSafetyNumbers } from '../../util/safetyNumber';
|
||||
import type { SafetyNumberType } from '../../types/safetyNumber';
|
||||
import type { ConversationType } from './conversations';
|
||||
import {
|
||||
reloadProfiles,
|
||||
@@ -13,10 +15,10 @@ import {
|
||||
import * as log from '../../logging/log';
|
||||
import * as Errors from '../../types/errors';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import { getSecurityNumberIdentifierType } from '../selectors/items';
|
||||
import { getSafetyNumberMode } from '../selectors/items';
|
||||
|
||||
export type SafetyNumberContactType = ReadonlyDeep<{
|
||||
safetyNumber: string;
|
||||
safetyNumbers: ReadonlyArray<SafetyNumberType>;
|
||||
safetyNumberChanged?: boolean;
|
||||
verificationDisabled: boolean;
|
||||
}>;
|
||||
@@ -27,15 +29,23 @@ export type SafetyNumberStateType = ReadonlyDeep<{
|
||||
};
|
||||
}>;
|
||||
|
||||
const CLEAR_SAFETY_NUMBER = 'safetyNumber/CLEAR_SAFETY_NUMBER';
|
||||
const GENERATE_FULFILLED = 'safetyNumber/GENERATE_FULFILLED';
|
||||
const TOGGLE_VERIFIED_FULFILLED = 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
|
||||
const TOGGLE_VERIFIED_PENDING = 'safetyNumber/TOGGLE_VERIFIED_PENDING';
|
||||
|
||||
type ClearSafetyNumberActionType = ReadonlyDeep<{
|
||||
type: 'safetyNumber/CLEAR_SAFETY_NUMBER';
|
||||
payload: {
|
||||
contactId: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
type GenerateFulfilledActionType = ReadonlyDeep<{
|
||||
type: 'safetyNumber/GENERATE_FULFILLED';
|
||||
payload: {
|
||||
contact: ConversationType;
|
||||
safetyNumber: string;
|
||||
safetyNumbers: ReadonlyArray<SafetyNumberType>;
|
||||
};
|
||||
}>;
|
||||
|
||||
@@ -50,31 +60,39 @@ type ToggleVerifiedFulfilledActionType = ReadonlyDeep<{
|
||||
type: 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
|
||||
payload: {
|
||||
contact: ConversationType;
|
||||
safetyNumber?: string;
|
||||
safetyNumbers?: ReadonlyArray<SafetyNumberType>;
|
||||
safetyNumberChanged?: boolean;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type SafetyNumberActionType = ReadonlyDeep<
|
||||
| ClearSafetyNumberActionType
|
||||
| GenerateFulfilledActionType
|
||||
| ToggleVerifiedPendingActionType
|
||||
| ToggleVerifiedFulfilledActionType
|
||||
>;
|
||||
|
||||
function clearSafetyNumber(contactId: string): ClearSafetyNumberActionType {
|
||||
return {
|
||||
type: CLEAR_SAFETY_NUMBER,
|
||||
payload: { contactId },
|
||||
};
|
||||
}
|
||||
|
||||
function generate(
|
||||
contact: ConversationType
|
||||
): ThunkAction<void, RootStateType, unknown, GenerateFulfilledActionType> {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const securityNumberBlock = await generateSecurityNumberBlock(
|
||||
const safetyNumbers = await generateSafetyNumbers(
|
||||
contact,
|
||||
getSecurityNumberIdentifierType(getState(), { now: Date.now() })
|
||||
getSafetyNumberMode(getState(), { now: Date.now() })
|
||||
);
|
||||
dispatch({
|
||||
type: GENERATE_FULFILLED,
|
||||
payload: {
|
||||
contact,
|
||||
safetyNumber: securityNumberBlock.join(' '),
|
||||
safetyNumbers,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -114,16 +132,16 @@ function toggleVerified(
|
||||
} catch (err) {
|
||||
if (err.name === 'OutgoingIdentityKeyError') {
|
||||
await reloadProfiles(contact.id);
|
||||
const securityNumberBlock = await generateSecurityNumberBlock(
|
||||
const safetyNumbers = await generateSafetyNumbers(
|
||||
contact,
|
||||
getSecurityNumberIdentifierType(getState(), { now: Date.now() })
|
||||
getSafetyNumberMode(getState(), { now: Date.now() })
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: TOGGLE_VERIFIED_FULFILLED,
|
||||
payload: {
|
||||
contact,
|
||||
safetyNumber: securityNumberBlock.join(' '),
|
||||
safetyNumbers,
|
||||
safetyNumberChanged: true,
|
||||
},
|
||||
});
|
||||
@@ -158,6 +176,7 @@ async function alterVerification(contact: ConversationType): Promise<void> {
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
clearSafetyNumber,
|
||||
generateSafetyNumber: generate,
|
||||
toggleVerified,
|
||||
};
|
||||
@@ -172,6 +191,13 @@ export function reducer(
|
||||
state: Readonly<SafetyNumberStateType> = getEmptyState(),
|
||||
action: Readonly<SafetyNumberActionType>
|
||||
): SafetyNumberStateType {
|
||||
if (action.type === CLEAR_SAFETY_NUMBER) {
|
||||
const { contactId } = action.payload;
|
||||
return {
|
||||
contacts: omit(state.contacts, contactId),
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_VERIFIED_PENDING) {
|
||||
const { contact } = action.payload;
|
||||
const { id } = contact;
|
||||
@@ -205,7 +231,7 @@ export function reducer(
|
||||
}
|
||||
|
||||
if (action.type === GENERATE_FULFILLED) {
|
||||
const { contact, safetyNumber } = action.payload;
|
||||
const { contact, safetyNumbers } = action.payload;
|
||||
const { id } = contact;
|
||||
const record = state.contacts[id];
|
||||
return {
|
||||
@@ -213,7 +239,7 @@ export function reducer(
|
||||
...state.contacts,
|
||||
[id]: {
|
||||
...record,
|
||||
safetyNumber,
|
||||
safetyNumbers,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
|
||||
import { isInteger } from 'lodash';
|
||||
|
||||
import { ITEM_NAME as UNIVERSAL_EXPIRE_TIMER_ITEM } from '../../util/universalExpireTimer';
|
||||
import { SecurityNumberIdentifierType } from '../../util/safetyNumber';
|
||||
import { SafetyNumberMode } from '../../types/safetyNumber';
|
||||
import { innerIsBucketValueEnabled } from '../../RemoteConfig';
|
||||
import type { ConfigKeyType, ConfigMapType } from '../../RemoteConfig';
|
||||
import type { StateType } from '../reducer';
|
||||
@@ -69,6 +69,11 @@ export const getRemoteConfig = createSelector(
|
||||
(state: ItemsStateType): ConfigMapType => state.remoteConfig || {}
|
||||
);
|
||||
|
||||
export const getServerTimeSkew = createSelector(
|
||||
getItems,
|
||||
(state: ItemsStateType): number => state.serverTimeSkew || 0
|
||||
);
|
||||
|
||||
export const getUsernamesEnabled = createSelector(
|
||||
getRemoteConfig,
|
||||
(remoteConfig: ConfigMapType): boolean =>
|
||||
@@ -81,6 +86,12 @@ export const getHasCompletedUsernameOnboarding = createSelector(
|
||||
Boolean(state.hasCompletedUsernameOnboarding)
|
||||
);
|
||||
|
||||
export const getHasCompletedSafetyNumberOnboarding = createSelector(
|
||||
getItems,
|
||||
(state: ItemsStateType): boolean =>
|
||||
Boolean(state.hasCompletedSafetyNumberOnboarding)
|
||||
);
|
||||
|
||||
export const isInternalUser = createSelector(
|
||||
getRemoteConfig,
|
||||
(remoteConfig: ConfigMapType): boolean => {
|
||||
@@ -146,22 +157,29 @@ export const getContactManagementEnabled = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
export const getSecurityNumberIdentifierType = createSelector(
|
||||
export const getSafetyNumberMode = createSelector(
|
||||
getRemoteConfig,
|
||||
getServerTimeSkew,
|
||||
(_state: StateType, { now }: { now: number }) => now,
|
||||
(remoteConfig: ConfigMapType, now: number): SecurityNumberIdentifierType => {
|
||||
if (isRemoteConfigFlagEnabled(remoteConfig, 'desktop.safetyNumberUUID')) {
|
||||
return SecurityNumberIdentifierType.UUIDIdentifier;
|
||||
(
|
||||
remoteConfig: ConfigMapType,
|
||||
serverTimeSkew: number,
|
||||
now: number
|
||||
): SafetyNumberMode => {
|
||||
if (!isRemoteConfigFlagEnabled(remoteConfig, 'desktop.safetyNumberAci')) {
|
||||
return SafetyNumberMode.E164;
|
||||
}
|
||||
|
||||
const timestamp = remoteConfig['desktop.safetyNumberUUID.timestamp']?.value;
|
||||
const timestamp = remoteConfig['global.safetyNumberAci']?.value;
|
||||
if (typeof timestamp !== 'number') {
|
||||
return SecurityNumberIdentifierType.E164Identifier;
|
||||
return SafetyNumberMode.ACIAndE164;
|
||||
}
|
||||
|
||||
return now >= timestamp
|
||||
? SecurityNumberIdentifierType.UUIDIdentifier
|
||||
: SecurityNumberIdentifierType.E164Identifier;
|
||||
// Note: serverTimeSkew is a difference between server time and local time,
|
||||
// so we have to add local time to it to correct it for a skew.
|
||||
return now + serverTimeSkew >= timestamp
|
||||
? SafetyNumberMode.ACI
|
||||
: SafetyNumberMode.ACIAndE164;
|
||||
}
|
||||
);
|
||||
|
||||
|
@@ -7,6 +7,10 @@ import { SafetyNumberModal } from '../../components/SafetyNumberModal';
|
||||
import type { StateType } from '../reducer';
|
||||
import { getContactSafetyNumber } from '../selectors/safetyNumber';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import {
|
||||
getSafetyNumberMode,
|
||||
getHasCompletedSafetyNumberOnboarding,
|
||||
} from '../selectors/items';
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
export type Props = {
|
||||
@@ -18,6 +22,9 @@ const mapStateToProps = (state: StateType, props: Props) => {
|
||||
...props,
|
||||
...getContactSafetyNumber(state, props),
|
||||
contact: getConversationSelector(state)(props.contactID),
|
||||
safetyNumberMode: getSafetyNumberMode(state, { now: Date.now() }),
|
||||
hasCompletedSafetyNumberOnboarding:
|
||||
getHasCompletedSafetyNumberOnboarding(state),
|
||||
i18n: getIntl(state),
|
||||
};
|
||||
};
|
||||
|
@@ -8,6 +8,7 @@ import type { StateType } from '../reducer';
|
||||
import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog';
|
||||
import { getContactSafetyNumber } from '../selectors/safetyNumber';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import { getSafetyNumberMode } from '../selectors/items';
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
const mapStateToProps = (state: StateType, props: SafetyNumberProps) => {
|
||||
@@ -15,6 +16,7 @@ const mapStateToProps = (state: StateType, props: SafetyNumberProps) => {
|
||||
...props,
|
||||
...getContactSafetyNumber(state, props),
|
||||
contact: getConversationSelector(state)(props.contactID),
|
||||
safetyNumberMode: getSafetyNumberMode(state, { now: Date.now() }),
|
||||
i18n: getIntl(state),
|
||||
};
|
||||
};
|
||||
|
@@ -2,15 +2,18 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { refreshRemoteConfig } from '../../RemoteConfig';
|
||||
import type { WebAPIType } from '../../textsecure/WebAPI';
|
||||
import type { UnwrapPromise } from '../../types/Util';
|
||||
import type {
|
||||
WebAPIType,
|
||||
RemoteConfigResponseType,
|
||||
} from '../../textsecure/WebAPI';
|
||||
import { SECOND } from '../../util/durations';
|
||||
|
||||
export async function updateRemoteConfig(
|
||||
newConfig: UnwrapPromise<ReturnType<WebAPIType['getConfig']>>
|
||||
newConfig: RemoteConfigResponseType['config']
|
||||
): Promise<void> {
|
||||
const fakeServer = {
|
||||
async getConfig() {
|
||||
return newConfig;
|
||||
return { config: newConfig, serverEpochTime: Date.now() / SECOND };
|
||||
},
|
||||
} as Partial<WebAPIType> as unknown as WebAPIType;
|
||||
|
||||
|
@@ -686,6 +686,18 @@ const uploadAvatarHeadersZod = z.object({
|
||||
});
|
||||
export type UploadAvatarHeadersType = z.infer<typeof uploadAvatarHeadersZod>;
|
||||
|
||||
const remoteConfigResponseZod = z.object({
|
||||
config: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
enabled: z.boolean(),
|
||||
value: z.string().or(z.null()).optional(),
|
||||
})
|
||||
.array(),
|
||||
serverEpochTime: z.number(),
|
||||
});
|
||||
export type RemoteConfigResponseType = z.infer<typeof remoteConfigResponseZod>;
|
||||
|
||||
export type ProfileType = Readonly<{
|
||||
identityKey?: string;
|
||||
name?: string;
|
||||
@@ -1035,9 +1047,7 @@ export type WebAPIType = {
|
||||
) => Promise<string>;
|
||||
whoami: () => Promise<WhoamiResultType>;
|
||||
sendChallengeResponse: (challengeResponse: ChallengeType) => Promise<void>;
|
||||
getConfig: () => Promise<
|
||||
Array<{ name: string; enabled: boolean; value: string | null }>
|
||||
>;
|
||||
getConfig: () => Promise<RemoteConfigResponseType>;
|
||||
authenticate: (credentials: WebAPICredentials) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
getSocketStatus: () => SocketStatus;
|
||||
@@ -1488,19 +1498,20 @@ export function initialize({
|
||||
}
|
||||
|
||||
async function getConfig() {
|
||||
type ResType = {
|
||||
config: Array<{ name: string; enabled: boolean; value: string | null }>;
|
||||
};
|
||||
const res = (await _ajax({
|
||||
const rawRes = await _ajax({
|
||||
call: 'config',
|
||||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
})) as ResType;
|
||||
});
|
||||
const res = remoteConfigResponseZod.parse(rawRes);
|
||||
|
||||
return res.config.filter(
|
||||
({ name }: { name: string }) =>
|
||||
name.startsWith('desktop.') || name.startsWith('global.')
|
||||
);
|
||||
return {
|
||||
...res,
|
||||
config: res.config.filter(
|
||||
({ name }: { name: string }) =>
|
||||
name.startsWith('desktop.') || name.startsWith('global.')
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function getSenderCertificate(omitE164?: boolean) {
|
||||
|
2
ts/types/Storage.d.ts
vendored
2
ts/types/Storage.d.ts
vendored
@@ -74,6 +74,7 @@ export type StorageAccessType = {
|
||||
hasRegisterSupportForUnauthenticatedDelivery: boolean;
|
||||
hasSetMyStoriesPrivacy: boolean;
|
||||
hasCompletedUsernameOnboarding: boolean;
|
||||
hasCompletedSafetyNumberOnboarding: boolean;
|
||||
hasViewedOnboardingStory: boolean;
|
||||
hasStoriesDisabled: boolean;
|
||||
storyViewReceiptsEnabled: boolean;
|
||||
@@ -128,6 +129,7 @@ export type StorageAccessType = {
|
||||
'preferred-audio-output-device': AudioDevice;
|
||||
previousAudioDeviceModule: AudioDeviceModule;
|
||||
remoteConfig: RemoteConfigType;
|
||||
serverTimeSkew: number;
|
||||
unidentifiedDeliveryIndicators: boolean;
|
||||
groupCredentials: ReadonlyArray<GroupCredentialType>;
|
||||
lastReceivedAtCounter: number;
|
||||
|
19
ts/types/safetyNumber.ts
Normal file
19
ts/types/safetyNumber.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export enum SafetyNumberMode {
|
||||
E164 = 'E164',
|
||||
ACIAndE164 = 'ACIAndE164',
|
||||
ACI = 'ACI',
|
||||
}
|
||||
|
||||
export enum SafetyNumberIdentifierType {
|
||||
ACIIdentifier = 'ACIIdentifier',
|
||||
E164Identifier = 'E164Identifier',
|
||||
}
|
||||
|
||||
export type SafetyNumberType = Readonly<{
|
||||
identifierType: SafetyNumberIdentifierType;
|
||||
numberBlocks: ReadonlyArray<string>;
|
||||
qrData: Uint8Array;
|
||||
}>;
|
@@ -7,3 +7,5 @@ export const UNSUPPORTED_OS_URL =
|
||||
'https://support.signal.org/hc/articles/5109141421850';
|
||||
export const LINK_SIGNAL_DESKTOP =
|
||||
'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device';
|
||||
export const SAFETY_NUMBER_MIGRATION_URL =
|
||||
'https://support.signal.org/hc/en-us/articles/360007060632';
|
||||
|
19
ts/util/isSafetyNumberNotAvailable.ts
Normal file
19
ts/util/isSafetyNumberNotAvailable.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
export const isSafetyNumberNotAvailable = (
|
||||
contact?: ConversationType
|
||||
): boolean => {
|
||||
// We have a contact
|
||||
if (!contact) {
|
||||
return true;
|
||||
}
|
||||
// They have a uuid
|
||||
if (!contact.uuid) {
|
||||
return true;
|
||||
}
|
||||
// The uuid is not PNI
|
||||
return contact.pni === contact.uuid;
|
||||
};
|
@@ -905,6 +905,234 @@
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-04-05T20:48:36.065Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "node_modules/lottie-react/build/index.es.js",
|
||||
"line": " var animationInstanceRef = useRef();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "node_modules/lottie-react/build/index.es.js",
|
||||
"line": " var animationContainer = useRef(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "node_modules/lottie-react/build/index.js",
|
||||
"line": " var animationInstanceRef = React.useRef();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "node_modules/lottie-react/build/index.js",
|
||||
"line": " var animationContainer = React.useRef(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "node_modules/lottie-react/build/index.min.js",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "node_modules/lottie-react/build/index.umd.js",
|
||||
"line": " var animationInstanceRef = React.useRef();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "node_modules/lottie-react/build/index.umd.js",
|
||||
"line": " var animationContainer = React.useRef(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "node_modules/lottie-react/build/index.umd.min.js",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/cjs/lottie.min.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/cjs/lottie_canvas.min.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/cjs/lottie_html.min.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/cjs/lottie_svg.min.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/esm/lottie.min.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/esm/lottie_canvas.min.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/esm/lottie_html.min.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/esm/lottie_svg.min.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/lottie.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/lottie.min.js",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_canvas.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_canvas.min.js",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "DOM-innerHTML",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_canvas_worker.js",
|
||||
"line": " animation.container.innerHTML = '';",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_canvas_worker.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "DOM-innerHTML",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_canvas_worker.min.js",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_canvas_worker.min.js",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_html.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_html.min.js",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_svg.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_svg.min.js",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "DOM-innerHTML",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_worker.js",
|
||||
"line": " animation.container.innerHTML = '';",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_worker.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "DOM-innerHTML",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_worker.min.js",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/build/player/lottie_worker.min.js",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "eval",
|
||||
"path": "node_modules/lottie-web/player/js/utils/expressions/ExpressionManager.js",
|
||||
"line": " var expression_function = eval('[function _expression_function(){' + val + ';scoped_bm_rt=$bm_rt}]')[0]; // eslint-disable-line no-eval",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "DOM-innerHTML",
|
||||
"path": "node_modules/lottie-web/player/js/worker_wrapper.js",
|
||||
"line": " animation.container.innerHTML = '';",
|
||||
"reasonCategory": "notExercisedByOurApp",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "DOM-innerHTML",
|
||||
"path": "node_modules/min-document/serialize.js",
|
||||
@@ -2214,6 +2442,13 @@
|
||||
"updated": "2022-01-04T21:43:17.517Z",
|
||||
"reasonDetail": "Used to change the style in non-production builds."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/SafetyNumberOnboarding.tsx",
|
||||
"line": " const lottieRef = useRef<LottieRefCurrentProps | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-06-29T17:01:25.145Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/Slider.tsx",
|
||||
|
@@ -3,100 +3,117 @@
|
||||
|
||||
import { PublicKey, Fingerprint } from '@signalapp/libsignal-client';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { UUID } from '../types/UUID';
|
||||
import { UUID, UUIDKind } from '../types/UUID';
|
||||
|
||||
import { assertDev } from './assert';
|
||||
import { isNotNil } from './isNotNil';
|
||||
import { missingCaseError } from './missingCaseError';
|
||||
import { uuidToBytes } from './uuidToBytes';
|
||||
import * as log from '../logging/log';
|
||||
import * as Bytes from '../Bytes';
|
||||
import type { SafetyNumberType } from '../types/safetyNumber';
|
||||
import {
|
||||
SafetyNumberIdentifierType,
|
||||
SafetyNumberMode,
|
||||
} from '../types/safetyNumber';
|
||||
|
||||
function generateSecurityNumber(
|
||||
ourId: string,
|
||||
ourKey: Uint8Array,
|
||||
theirId: string,
|
||||
theirKey: Uint8Array
|
||||
): string {
|
||||
const ourNumberBuf = Buffer.from(ourId);
|
||||
const ourKeyObj = PublicKey.deserialize(Buffer.from(ourKey));
|
||||
const theirNumberBuf = Buffer.from(theirId);
|
||||
const theirKeyObj = PublicKey.deserialize(Buffer.from(theirKey));
|
||||
const ITERATION_COUNT = 5200;
|
||||
const E164_VERSION = 1;
|
||||
const UUID_VERSION = 2;
|
||||
|
||||
const fingerprint = Fingerprint.new(
|
||||
5200,
|
||||
2,
|
||||
ourNumberBuf,
|
||||
ourKeyObj,
|
||||
theirNumberBuf,
|
||||
theirKeyObj
|
||||
);
|
||||
// Number of digits in a safety number block
|
||||
const BLOCK_SIZE = 5;
|
||||
|
||||
return fingerprint.displayableFingerprint().toString();
|
||||
}
|
||||
|
||||
export enum SecurityNumberIdentifierType {
|
||||
UUIDIdentifier = 'UUIDIdentifier',
|
||||
E164Identifier = 'E164Identifier',
|
||||
}
|
||||
|
||||
export async function generateSecurityNumberBlock(
|
||||
export async function generateSafetyNumbers(
|
||||
contact: ConversationType,
|
||||
identifierType: SecurityNumberIdentifierType
|
||||
): Promise<Array<string>> {
|
||||
const logId = `generateSecurityNumberBlock(${contact.id}, ${identifierType})`;
|
||||
mode: SafetyNumberMode
|
||||
): Promise<ReadonlyArray<SafetyNumberType>> {
|
||||
const logId = `generateSafetyNumbers(${contact.id}, ${mode})`;
|
||||
log.info(`${logId}: starting`);
|
||||
|
||||
const { storage } = window.textsecure;
|
||||
const ourNumber = storage.user.getNumber();
|
||||
const ourUuid = storage.user.getCheckedUuid();
|
||||
const ourAci = storage.user.getCheckedUuid(UUIDKind.ACI);
|
||||
|
||||
const us = storage.protocol.getIdentityRecord(ourUuid);
|
||||
const ourKey = us ? us.publicKey : null;
|
||||
const us = storage.protocol.getIdentityRecord(ourAci);
|
||||
const ourKeyBuffer = us ? us.publicKey : null;
|
||||
|
||||
const theirUuid = UUID.lookup(contact.id);
|
||||
const them = theirUuid
|
||||
? await storage.protocol.getOrMigrateIdentityRecord(theirUuid)
|
||||
const theirAci = contact.pni !== contact.uuid ? contact.uuid : undefined;
|
||||
const them = theirAci
|
||||
? await storage.protocol.getOrMigrateIdentityRecord(new UUID(theirAci))
|
||||
: undefined;
|
||||
const theirKey = them?.publicKey;
|
||||
const theirKeyBuffer = them?.publicKey;
|
||||
|
||||
if (!ourKey) {
|
||||
if (!ourKeyBuffer) {
|
||||
throw new Error('Could not load our key');
|
||||
}
|
||||
|
||||
if (!theirKey) {
|
||||
if (!theirKeyBuffer) {
|
||||
throw new Error('Could not load their key');
|
||||
}
|
||||
|
||||
let securityNumber: string;
|
||||
if (identifierType === SecurityNumberIdentifierType.E164Identifier) {
|
||||
if (!contact.e164) {
|
||||
log.error(
|
||||
`${logId}: Attempted to generate security number for contact with no e164`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
const ourKey = PublicKey.deserialize(Buffer.from(ourKeyBuffer));
|
||||
const theirKey = PublicKey.deserialize(Buffer.from(theirKeyBuffer));
|
||||
|
||||
assertDev(ourNumber, 'Should have our number');
|
||||
securityNumber = generateSecurityNumber(
|
||||
ourNumber,
|
||||
ourKey,
|
||||
contact.e164,
|
||||
theirKey
|
||||
);
|
||||
} else if (identifierType === SecurityNumberIdentifierType.UUIDIdentifier) {
|
||||
assertDev(theirUuid, 'Should have their uuid');
|
||||
securityNumber = generateSecurityNumber(
|
||||
ourUuid.toString(),
|
||||
ourKey,
|
||||
theirUuid.toString(),
|
||||
theirKey
|
||||
);
|
||||
let identifierTypes: ReadonlyArray<SafetyNumberIdentifierType>;
|
||||
if (mode === SafetyNumberMode.ACIAndE164) {
|
||||
// Important: order matters, legacy safety number should be displayed first.
|
||||
identifierTypes = [
|
||||
SafetyNumberIdentifierType.E164Identifier,
|
||||
SafetyNumberIdentifierType.ACIIdentifier,
|
||||
];
|
||||
// Controlled by 'desktop.safetyNumberAci'
|
||||
} else if (mode === SafetyNumberMode.E164) {
|
||||
identifierTypes = [SafetyNumberIdentifierType.E164Identifier];
|
||||
} else {
|
||||
throw missingCaseError(identifierType);
|
||||
assertDev(mode === SafetyNumberMode.ACI, 'Invalid security number mode');
|
||||
identifierTypes = [SafetyNumberIdentifierType.ACIIdentifier];
|
||||
}
|
||||
|
||||
const chunks = [];
|
||||
for (let i = 0; i < securityNumber.length; i += 5) {
|
||||
chunks.push(securityNumber.substring(i, i + 5));
|
||||
}
|
||||
return identifierTypes
|
||||
.map(identifierType => {
|
||||
let fingerprint: Fingerprint;
|
||||
if (identifierType === SafetyNumberIdentifierType.E164Identifier) {
|
||||
if (!contact.e164) {
|
||||
log.error(
|
||||
`${logId}: Attempted to generate security number for contact with no e164`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return chunks;
|
||||
assertDev(ourNumber, 'Should have our number');
|
||||
fingerprint = Fingerprint.new(
|
||||
ITERATION_COUNT,
|
||||
E164_VERSION,
|
||||
Buffer.from(Bytes.fromString(ourNumber)),
|
||||
ourKey,
|
||||
Buffer.from(Bytes.fromString(contact.e164)),
|
||||
theirKey
|
||||
);
|
||||
} else if (identifierType === SafetyNumberIdentifierType.ACIIdentifier) {
|
||||
assertDev(theirAci, 'Should have their uuid');
|
||||
fingerprint = Fingerprint.new(
|
||||
ITERATION_COUNT,
|
||||
UUID_VERSION,
|
||||
Buffer.from(uuidToBytes(ourAci.toString())),
|
||||
ourKey,
|
||||
Buffer.from(uuidToBytes(theirAci)),
|
||||
theirKey
|
||||
);
|
||||
} else {
|
||||
throw missingCaseError(identifierType);
|
||||
}
|
||||
|
||||
const securityNumber = fingerprint.displayableFingerprint().toString();
|
||||
|
||||
const numberBlocks = [];
|
||||
for (let i = 0; i < securityNumber.length; i += BLOCK_SIZE) {
|
||||
numberBlocks.push(securityNumber.substring(i, i + BLOCK_SIZE));
|
||||
}
|
||||
|
||||
const qrData = fingerprint.scannableFingerprint().toBuffer();
|
||||
|
||||
return { identifierType, numberBlocks, qrData };
|
||||
})
|
||||
.filter(isNotNil);
|
||||
}
|
||||
|
Reference in New Issue
Block a user