{i18n('icu:cannotGenerateSafetyNumber')}
@@ -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 = (
-
{name}
+
+
+
);
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 = (
+
+
+
+
+ {numberBlocks}
+
+
+ {selectedIndex > 0 && (
+
+
+ );
+
+ const carousel = (
+
+ {safetyNumbers.map(({ identifierType }, index) => {
+ return (
+ setSelectedIndex(index)}
+ />
+ );
+ })}
+
+ );
+
return (
-
- {safetyNumber || getPlaceholder()}
-
-
-
- {isVerified ? (
-
- ) : (
-
- )}
- {isVerified ? (
+ {isMigrationVisible && (
+
+ )}
+
+ {safetyNumberCard}
+
+ {safetyNumbers.length > 1 && carousel}
+
+
+ {isMigrationVisible ? (
) : (
)}
+
+
+
+
+
);
}
-
-function getPlaceholder(): string {
- return Array.from(Array(12))
- .map(() => 'XXXXX')
- .join(' ');
-}
diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts
index b2babb23c..4dc54099d 100644
--- a/ts/models/conversations.ts
+++ b/ts/models/conversations.ts
@@ -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 =
diff --git a/ts/state/ducks/items.ts b/ts/state/ducks/items.ts
index 58a2eb3df..52089b57b 100644
--- a/ts/state/ducks/items.ts
+++ b/ts/state/ducks/items.ts
@@ -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 {
diff --git a/ts/state/ducks/safetyNumber.ts b/ts/state/ducks/safetyNumber.ts
index 9eb236b4a..6a655557f 100644
--- a/ts/state/ducks/safetyNumber.ts
+++ b/ts/state/ducks/safetyNumber.ts
@@ -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;
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;
};
}>;
@@ -50,31 +60,39 @@ type ToggleVerifiedFulfilledActionType = ReadonlyDeep<{
type: 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
payload: {
contact: ConversationType;
- safetyNumber?: string;
+ safetyNumbers?: ReadonlyArray;
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 {
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 {
}
export const actions = {
+ clearSafetyNumber,
generateSafetyNumber: generate,
toggleVerified,
};
@@ -172,6 +191,13 @@ export function reducer(
state: Readonly = getEmptyState(),
action: Readonly
): 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,
},
},
};
diff --git a/ts/state/selectors/items.ts b/ts/state/selectors/items.ts
index 3ff3e5537..883672301 100644
--- a/ts/state/selectors/items.ts
+++ b/ts/state/selectors/items.ts
@@ -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;
}
);
diff --git a/ts/state/smart/SafetyNumberModal.tsx b/ts/state/smart/SafetyNumberModal.tsx
index 728adcb62..b35f25bb8 100644
--- a/ts/state/smart/SafetyNumberModal.tsx
+++ b/ts/state/smart/SafetyNumberModal.tsx
@@ -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),
};
};
diff --git a/ts/state/smart/SafetyNumberViewer.tsx b/ts/state/smart/SafetyNumberViewer.tsx
index b40226ef3..a64009863 100644
--- a/ts/state/smart/SafetyNumberViewer.tsx
+++ b/ts/state/smart/SafetyNumberViewer.tsx
@@ -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),
};
};
diff --git a/ts/test-both/helpers/RemoteConfigStub.ts b/ts/test-both/helpers/RemoteConfigStub.ts
index e8d88c1b6..48c863da6 100644
--- a/ts/test-both/helpers/RemoteConfigStub.ts
+++ b/ts/test-both/helpers/RemoteConfigStub.ts
@@ -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>
+ newConfig: RemoteConfigResponseType['config']
): Promise {
const fakeServer = {
async getConfig() {
- return newConfig;
+ return { config: newConfig, serverEpochTime: Date.now() / SECOND };
},
} as Partial as unknown as WebAPIType;
diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts
index 4b398b239..1fc79bd8d 100644
--- a/ts/textsecure/WebAPI.ts
+++ b/ts/textsecure/WebAPI.ts
@@ -686,6 +686,18 @@ const uploadAvatarHeadersZod = z.object({
});
export type UploadAvatarHeadersType = z.infer;
+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;
+
export type ProfileType = Readonly<{
identityKey?: string;
name?: string;
@@ -1035,9 +1047,7 @@ export type WebAPIType = {
) => Promise;
whoami: () => Promise;
sendChallengeResponse: (challengeResponse: ChallengeType) => Promise;
- getConfig: () => Promise<
- Array<{ name: string; enabled: boolean; value: string | null }>
- >;
+ getConfig: () => Promise;
authenticate: (credentials: WebAPICredentials) => Promise;
logout: () => Promise;
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) {
diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts
index ab5e839d9..51d1c34fc 100644
--- a/ts/types/Storage.d.ts
+++ b/ts/types/Storage.d.ts
@@ -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;
lastReceivedAtCounter: number;
diff --git a/ts/types/safetyNumber.ts b/ts/types/safetyNumber.ts
new file mode 100644
index 000000000..af35602a5
--- /dev/null
+++ b/ts/types/safetyNumber.ts
@@ -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;
+ qrData: Uint8Array;
+}>;
diff --git a/ts/types/support.ts b/ts/types/support.ts
index 251fb37f7..3400ae723 100644
--- a/ts/types/support.ts
+++ b/ts/types/support.ts
@@ -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';
diff --git a/ts/util/isSafetyNumberNotAvailable.ts b/ts/util/isSafetyNumberNotAvailable.ts
new file mode 100644
index 000000000..60798a968
--- /dev/null
+++ b/ts/util/isSafetyNumberNotAvailable.ts
@@ -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;
+};
diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json
index b5ee93503..b970210b3 100644
--- a/ts/util/lint/exceptions.json
+++ b/ts/util/lint/exceptions.json
@@ -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(null);",
+ "reasonCategory": "usageTrusted",
+ "updated": "2023-06-29T17:01:25.145Z"
+ },
{
"rule": "React-useRef",
"path": "ts/components/Slider.tsx",
diff --git a/ts/util/safetyNumber.ts b/ts/util/safetyNumber.ts
index 6291f86e3..94712ca30 100644
--- a/ts/util/safetyNumber.ts
+++ b/ts/util/safetyNumber.ts
@@ -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> {
- const logId = `generateSecurityNumberBlock(${contact.id}, ${identifierType})`;
+ mode: SafetyNumberMode
+): Promise> {
+ 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;
+ 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);
}
diff --git a/yarn.lock b/yarn.lock
index 8a49b6578..798795ef0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12457,6 +12457,18 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
+lottie-react@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/lottie-react/-/lottie-react-2.4.0.tgz#f7249eee2b1deee70457a2d142194fdf2456e4bd"
+ integrity sha512-pDJGj+AQlnlyHvOHFK7vLdsDcvbuqvwPZdMlJ360wrzGFurXeKPr8SiRCjLf3LrNYKANQtSsh5dz9UYQHuqx4w==
+ dependencies:
+ lottie-web "^5.10.2"
+
+lottie-web@^5.10.2:
+ version "5.12.2"
+ resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.12.2.tgz#579ca9fe6d3fd9e352571edd3c0be162492f68e5"
+ integrity sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==
+
loud-rejection@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"