diff --git a/package.json b/package.json index 00fae909e2..83abff2e66 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "@react-aria/utils": "3.33.1", "@react-spring/web": "10.0.3", "@react-types/shared": "3.33.1", - "@signalapp/libsignal-client": "0.89.2", + "@signalapp/libsignal-client": "0.90.0", "@signalapp/minimask": "1.0.1", "@signalapp/mute-state-change": "workspace:1.0.0", "@signalapp/quill-cjs": "2.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa5d247dfa..f8db3c2c09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,8 +126,8 @@ importers: specifier: 3.33.1 version: 3.33.1(react@19.2.4) '@signalapp/libsignal-client': - specifier: 0.89.2 - version: 0.89.2 + specifier: 0.90.0 + version: 0.90.0 '@signalapp/minimask': specifier: 1.0.1 version: 1.0.1 @@ -3425,6 +3425,9 @@ packages: '@signalapp/libsignal-client@0.89.2': resolution: {integrity: sha512-LGvE50XxiCB7vXHtx/TElPXl8sFr6kLO6CkZVh33pc5FME3j/PMtdTZnUE7bFDV15yxW//pCntFrpV0XzV5lSA==} + '@signalapp/libsignal-client@0.90.0': + resolution: {integrity: sha512-jNS5Xy7043QKXlcFYHA5HnxhrVvYHI+zaWgpeRLTKAdJLycYV6OesG6Y1lqxhkOWQcXjiOg/cDWt8ZOGl5pVYw==} + '@signalapp/minimask@1.0.1': resolution: {integrity: sha512-QAwo0joA60urTNbW9RIz6vLKQjy+jdVtH7cvY0wD9PVooD46MAjE40MLssp4xUJrph91n2XvtJ3pbEUDrmT2AA==} @@ -14070,6 +14073,12 @@ snapshots: type-fest: 4.26.1 uuid: 11.0.2 + '@signalapp/libsignal-client@0.90.0': + dependencies: + node-gyp-build: 4.8.4 + type-fest: 4.26.1 + uuid: 11.0.2 + '@signalapp/minimask@1.0.1': {} '@signalapp/mock-server@18.3.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': diff --git a/ts/components/Preferences.dom.stories.tsx b/ts/components/Preferences.dom.stories.tsx index 71349ffc7d..ab3e07995c 100644 --- a/ts/components/Preferences.dom.stories.tsx +++ b/ts/components/Preferences.dom.stories.tsx @@ -11,6 +11,7 @@ import { Preferences } from './Preferences.dom.js'; import { DEFAULT_CONVERSATION_COLOR } from '../types/Colors.std.js'; import { PhoneNumberSharingMode } from '../types/PhoneNumberSharingMode.std.js'; import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability.std.js'; +import { sleep } from '../util/sleep.std.js'; import { EmojiSkinTone } from './fun/data/emojis.std.js'; import { DAY, @@ -659,6 +660,10 @@ export default { setIsGroupVp9Enabled: action('setIsDirectVp9Enabled'), sfuUrl: 'https://sfu.voip.signal.org', setSfuUrl: action('setSfuUrl'), + forceKeyTransparencyCheck: async () => { + await sleep(1000); + }, + keyTransparencySelfHealth: 'ok', } satisfies PropsType, } satisfies Meta; diff --git a/ts/components/Preferences.dom.tsx b/ts/components/Preferences.dom.tsx index d078f5483c..f957d850e8 100644 --- a/ts/components/Preferences.dom.tsx +++ b/ts/components/Preferences.dom.tsx @@ -63,6 +63,7 @@ import type { NotificationSettingType, SentMediaQualitySettingType, ZoomFactorType, + StorageAccessType, } from '../types/StorageKeys.std.js'; import type { ThemeSettingType } from '../util/theme.std.js'; import type { AnyToast } from '../types/Toast.dom.js'; @@ -359,6 +360,8 @@ type PropsFunctionType = { setIsGroupVp9Enabled: (value: boolean | undefined) => void; setGroupMaxBitrate: (value: number | undefined) => void; setSfuUrl: (value: string | undefined) => void; + forceKeyTransparencyCheck: () => Promise; + keyTransparencySelfHealth: StorageAccessType['keyTransparencySelfHealth']; // Localization i18n: LocalizerType; @@ -580,6 +583,8 @@ export function Preferences({ groupMaxBitrate, setSfuUrl, sfuUrl, + forceKeyTransparencyCheck, + keyTransparencySelfHealth, }: PropsType): React.JSX.Element { const storiesId = useId(); const themeSelectId = useId(); @@ -2371,6 +2376,8 @@ export function Preferences({ groupMaxBitrate={groupMaxBitrate} sfuUrl={sfuUrl} setSfuUrl={setSfuUrl} + forceKeyTransparencyCheck={forceKeyTransparencyCheck} + keyTransparencySelfHealth={keyTransparencySelfHealth} /> } contentsRef={settingsPaneRef} diff --git a/ts/components/PreferencesInternal.dom.tsx b/ts/components/PreferencesInternal.dom.tsx index 6d7f3c0b23..d4b2b8b59e 100644 --- a/ts/components/PreferencesInternal.dom.tsx +++ b/ts/components/PreferencesInternal.dom.tsx @@ -15,6 +15,7 @@ import { SettingsRow, FlowingSettingsControl } from './PreferencesUtil.dom.js'; import type { MessageCountBySchemaVersionType } from '../sql/Interface.std.js'; import type { MessageAttributesType } from '../model-types.d.ts'; import type { DonationReceipt } from '../types/Donations.std.js'; +import type { StorageAccessType } from '../types/StorageKeys.std.js'; import { createLogger } from '../logging/log.std.js'; import { isStagingServer } from '../util/isStagingServer.dom.js'; import { getHumanDonationAmount } from '../util/currency.dom.js'; @@ -53,6 +54,8 @@ export function PreferencesInternal({ setGroupMaxBitrate, sfuUrl, setSfuUrl, + forceKeyTransparencyCheck, + keyTransparencySelfHealth, }: { i18n: LocalizerType; validateBackup: () => Promise; @@ -90,6 +93,8 @@ export function PreferencesInternal({ setGroupMaxBitrate: (value: number | undefined) => void; sfuUrl: string | undefined; setSfuUrl: (value: string | undefined) => void; + forceKeyTransparencyCheck: () => Promise; + keyTransparencySelfHealth: StorageAccessType['keyTransparencySelfHealth']; }): React.JSX.Element { const [messageCountBySchemaVersion, setMessageCountBySchemaVersion] = useState(); @@ -276,6 +281,31 @@ export function PreferencesInternal({ [] ); + // Key Transparancy + + const [isKeyTransparencyRunning, setIsKeyTransparencyRunning] = + useState(false); + + const handleKeyTransparencyCheck = useCallback(async () => { + setIsKeyTransparencyRunning(true); + try { + await forceKeyTransparencyCheck(); + } finally { + setIsKeyTransparencyRunning(false); + } + }, [forceKeyTransparencyCheck]); + + let keyTransparencySymbol: undefined | 'check-circle-fill' | 'error-fill'; + if (keyTransparencySelfHealth == null) { + keyTransparencySymbol = undefined; + } else if (keyTransparencySelfHealth === 'ok') { + keyTransparencySymbol = 'check-circle-fill'; + } else if (keyTransparencySelfHealth === 'fail') { + keyTransparencySymbol = 'error-fill'; + } else if (keyTransparencySelfHealth === 'intermittent') { + keyTransparencySymbol = 'error-fill'; + } + const prevAbortControlerRef = useRef(null); const handleReadOnlySqlInputSubmit = useCallback(async () => { @@ -723,6 +753,26 @@ export function PreferencesInternal({ + + +
Force Self Check
+
+ + Check + +
+
+
); } diff --git a/ts/services/keyTransparency.preload.ts b/ts/services/keyTransparency.preload.ts index 2b4ed9dfd1..d893cae411 100644 --- a/ts/services/keyTransparency.preload.ts +++ b/ts/services/keyTransparency.preload.ts @@ -11,16 +11,11 @@ import type { Request, E164Info, } from '@signalapp/libsignal-client/dist/net/KeyTransparency.js'; -import { MonitorMode } from '@signalapp/libsignal-client/dist/net/KeyTransparency.js'; import pTimeout from 'p-timeout'; -import { - keyTransparencySearch, - keyTransparencyMonitor, -} from '../textsecure/WebAPI.preload.js'; +import { keyTransparencyCheck } from '../textsecure/WebAPI.preload.js'; import { signalProtocolStore } from '../SignalProtocolStore.preload.js'; import { itemStorage } from '../textsecure/Storage.preload.js'; -import { fromAciObject } from '../types/ServiceId.std.js'; import { toLogFormat } from '../types/errors.std.js'; import { toAciObject } from '../util/ServiceId.node.js'; import { TaskDeduplicator } from '../util/TaskDeduplicator.std.js'; @@ -142,8 +137,9 @@ export class KeyTransparency { }; } - await this.#verify( + await this.#check( { + mode: 'contact', aciInfo: { aci: toAciObject(aci), identityKey: PublicKey.deserialize(identityKey), @@ -195,23 +191,16 @@ export class KeyTransparency { const me = window.ConversationController.getOurConversationOrThrow(); - let e164Info: E164Info | undefined; - if ( + const isE164Discoverable = itemStorage.get('phoneNumberDiscoverability') === - PhoneNumberDiscoverability.Discoverable - ) { - const ourE164 = itemStorage.user.getNumber(); - strictAssert(ourE164 != null, 'missing our e164'); + PhoneNumberDiscoverability.Discoverable; - me.deriveAccessKeyIfNeeded(); - const ourAccessKey = me.get('accessKey'); - strictAssert(ourAccessKey != null, 'missing our access key'); + const ourE164 = itemStorage.user.getNumber(); + strictAssert(ourE164 != null, 'missing our e164'); - e164Info = { - e164: ourE164, - unidentifiedAccessKey: Bytes.fromBase64(ourAccessKey), - }; - } + me.deriveAccessKeyIfNeeded(); + const ourAccessKey = me.get('accessKey'); + strictAssert(ourAccessKey != null, 'missing our access key'); let usernameHash: Uint8Array | undefined; @@ -235,13 +224,18 @@ export class KeyTransparency { } try { - await this.#verify( + await this.#check( { + mode: 'self', + isE164Discoverable, aciInfo: { aci: toAciObject(ourAci), identityKey: keyPair.publicKey, }, - e164Info, + e164Info: { + e164: ourE164, + unidentifiedAccessKey: Bytes.fromBase64(ourAccessKey), + }, usernameHash, }, abortSignal @@ -312,29 +306,16 @@ export class KeyTransparency { } } - async #verify( + async #check( request: Request, abortSignal?: AbortSignal, backOff = new BackOff(KEY_TRANSPARENCY_TIMEOUTS) ): Promise { try { - const existing = await signalProtocolStore.getKTAccountData( - request.aciInfo.aci - ); if (abortSignal?.aborted) { throw new Error('Aborted'); } - const aciString = fromAciObject(request.aciInfo.aci); - if (existing == null) { - log.info('search', aciString); - await keyTransparencySearch(request, abortSignal); - } else { - const mode = itemStorage.user.isOurServiceId(aciString) - ? MonitorMode.Self - : MonitorMode.Other; - log.info('monitor', aciString); - await keyTransparencyMonitor(request, mode, abortSignal); - } + await keyTransparencyCheck(request, abortSignal); } catch (error) { if (abortSignal?.aborted) { throw new Error('Aborted'); @@ -368,7 +349,7 @@ export class KeyTransparency { throw new Error('Aborted'); } - return this.#verify(request, abortSignal, backOff); + return this.#check(request, abortSignal, backOff); } } } diff --git a/ts/state/smart/Preferences.preload.tsx b/ts/state/smart/Preferences.preload.tsx index 3449891434..ad91b306ce 100644 --- a/ts/state/smart/Preferences.preload.tsx +++ b/ts/state/smart/Preferences.preload.tsx @@ -204,6 +204,10 @@ function getSystemTraySettingValues( }; } +async function forceKeyTransparencyCheck(): Promise { + await keyTransparency.selfCheck(); +} + export function SmartPreferences(): React.JSX.Element | null { const { addCustomColor, @@ -1025,6 +1029,8 @@ export function SmartPreferences(): React.JSX.Element | null { groupMaxBitrate={items.groupMaxBitrate} sfuUrl={items.sfuUrl} setSfuUrl={setSfuUrl} + forceKeyTransparencyCheck={forceKeyTransparencyCheck} + keyTransparencySelfHealth={items.keyTransparencySelfHealth} /> diff --git a/ts/textsecure/WebAPI.preload.ts b/ts/textsecure/WebAPI.preload.ts index 6a2eb8e89b..f03cd6b35b 100644 --- a/ts/textsecure/WebAPI.preload.ts +++ b/ts/textsecure/WebAPI.preload.ts @@ -30,10 +30,7 @@ import type { ProvisioningConnectionListener, } from '@signalapp/libsignal-client/dist/net.js'; import { GroupSendFullToken } from '@signalapp/libsignal-client/zkgroup.js'; -import type { - Request as KTRequest, - MonitorMode as KTMonitorMode, -} from '@signalapp/libsignal-client/dist/net/KeyTransparency.js'; +import type { Request as KTRequest } from '@signalapp/libsignal-client/dist/net/KeyTransparency.js'; import { assertDev, strictAssert } from '../util/assert.std.js'; import * as durations from '../util/durations/index.std.js'; @@ -2492,7 +2489,7 @@ export async function getAccountForUsername({ return aci ? fromAciObject(aci) : null; } -export async function keyTransparencySearch( +export async function keyTransparencyCheck( request: KTRequest, abortSignal?: AbortSignal ): Promise { @@ -2503,30 +2500,7 @@ export async function keyTransparencySearch( } const kt = chat.keyTransparencyClient(); const store = new KeyTransparencyStore(signalProtocolStore); - return kt.search(request, store, { abortSignal }); - }); -} - -export async function keyTransparencyMonitor( - request: KTRequest, - mode: KTMonitorMode, - abortSignal?: AbortSignal -): Promise { - return _retry(async () => { - const chat = await socketManager.getUnauthenticatedApi(); - if (abortSignal?.aborted) { - throw new Error('Aborted'); - } - const kt = chat.keyTransparencyClient(); - const store = new KeyTransparencyStore(signalProtocolStore); - return kt.monitor( - { - ...request, - mode, - }, - store, - { abortSignal } - ); + return kt.check(request, store, { abortSignal }); }); }