From 7c8cb0c5fce1d01805199de992bf4323f4765f1f Mon Sep 17 00:00:00 2001 From: moiseev-signal <122060238+moiseev-signal@users.noreply.github.com> Date: Sat, 16 May 2026 12:07:54 -0700 Subject: [PATCH] keytrans: Add reset account data field function --- RELEASE_NOTES.md | 2 + .../signal/libsignal/net/KeyTransparency.kt | 46 +++++++++++++++- .../libsignal/net/KeyTransparencyTest.java | 26 ++++++++++ .../org/signal/libsignal/internal/Native.kt | 2 + .../libsignal/internal/NativeTesting.kt | 2 + node/ts/Native.ts | 6 +++ node/ts/net/KeyTransparency.ts | 40 ++++++++++++++ node/ts/test/KeyTransparencyTest.ts | 27 ++++++++++ rust/bridge/shared/src/net/keytrans.rs | 18 ++++++- .../bridge/shared/testing/src/net/keytrans.rs | 22 ++++++++ rust/net/chat/src/api/keytrans.rs | 52 ++++++++++++++++++- .../chat/src/api/keytrans/maybe_partial.rs | 10 ++-- .../LibSignalClient/KeyTransparency.swift | 39 ++++++++++++++ swift/Sources/SignalFfi/signal_ffi.h | 2 + swift/Sources/SignalFfi/signal_ffi_testing.h | 2 + .../KeyTransparencyTests.swift | 43 +++++++++++++++ 16 files changed, 331 insertions(+), 8 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index fda34e252..10e78fe35 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,5 @@ v0.94.1 - Add `grpc.BackupsAnonymousGetUploadForm` remote config, for both backup and backup media uploads. This is separate from the `grpc.AttachmentsGetUploadForm` config added previously, which applies to regular attachment uploads. + +- keytrans: Add reset account data field functionality for all platforms. diff --git a/java/client/src/main/java/org/signal/libsignal/net/KeyTransparency.kt b/java/client/src/main/java/org/signal/libsignal/net/KeyTransparency.kt index 776defa32..ac962fe10 100644 --- a/java/client/src/main/java/org/signal/libsignal/net/KeyTransparency.kt +++ b/java/client/src/main/java/org/signal/libsignal/net/KeyTransparency.kt @@ -4,7 +4,11 @@ // package org.signal.libsignal.net -public abstract class KeyTransparency { +import org.signal.libsignal.internal.Native +import org.signal.libsignal.keytrans.Store +import org.signal.libsignal.protocol.ServiceId + +public object KeyTransparency { /** * Mode of the key transparency operation. * @@ -32,4 +36,44 @@ public abstract class KeyTransparency { public fun isSelf(): Boolean = this is Self } + + /** + * A tag identifying an optional field of the account data. + * + * (Must be in sync with the Rust counterpart) + */ + public enum class AccountDataField( + public val value: Int, + ) { + E164(0), + USERNAME_HASH(1), + } + + /** + * Resets a particular field in the data associated with given ACI. + * + * Must only be called for the "self" account when either E.164 or username change is performed. + * + * Upon successful completion the data associated with the account will be updated in the store, if it + * was present to begin with, noop if it was not. + * + * @param aci An ACI of "self" account. + * @param field Account data field to be reset (E.164 or username hash) + * @param store local persistent storage for key transparency-related data. + * @throws IllegalArgumentException if the stored data cannot be decoded correctly, which means data corruption. + */ + @JvmStatic + public fun resetField( + aci: ServiceId.Aci, + field: AccountDataField, + store: Store, + ) { + store.getAccountData(aci).map { + val updated = Native.KeyTransparency_ResetDataField(it, field.value) + if (updated.isEmpty()) { + throw IllegalArgumentException("failed to decode account data") + } + store.setAccountData(aci, updated) + } + } } diff --git a/java/client/src/test/java/org/signal/libsignal/net/KeyTransparencyTest.java b/java/client/src/test/java/org/signal/libsignal/net/KeyTransparencyTest.java index d184b6ddc..8c4339388 100644 --- a/java/client/src/test/java/org/signal/libsignal/net/KeyTransparencyTest.java +++ b/java/client/src/test/java/org/signal/libsignal/net/KeyTransparencyTest.java @@ -11,6 +11,7 @@ import java.util.UUID; import org.junit.Test; import org.signal.libsignal.internal.NativeTesting; import org.signal.libsignal.keytrans.KeyTransparencyException; +import org.signal.libsignal.keytrans.TestStore; import org.signal.libsignal.keytrans.VerificationFailedException; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; @@ -60,4 +61,29 @@ public class KeyTransparencyTest { public void canBridgeChatSendError() { assertThrows(TimeoutException.class, NativeTesting::TESTING_KeyTransChatSendError); } + + @Test + public void resetFieldThrowsOnCorruptData() { + var store = new TestStore(); + store.setAccountData(TEST_ACI, new byte[] {1, 2, 3}); + assertThrows( + IllegalArgumentException.class, + () -> KeyTransparency.resetField(TEST_ACI, KeyTransparency.AccountDataField.E164, store)); + } + + @Test + public void resetFieldIsNoopWhenDataIsMissing() { + var store = new TestStore(); + KeyTransparency.resetField(TEST_ACI, KeyTransparency.AccountDataField.E164, store); + assert (store.storage.get(TEST_ACI).isEmpty()); + } + + @Test + public void resetFieldUpdatesStoreOnSuccess() { + var store = new TestStore(); + store.setAccountData(TEST_ACI, NativeTesting.TESTING_KeyTransStoredAccountData()); + assertEquals(1, store.storage.get(TEST_ACI).size()); + KeyTransparency.resetField(TEST_ACI, KeyTransparency.AccountDataField.E164, store); + assertEquals(2, store.storage.get(TEST_ACI).size()); + } } diff --git a/java/shared/java/org/signal/libsignal/internal/Native.kt b/java/shared/java/org/signal/libsignal/internal/Native.kt index d13fec79c..e8d701e01 100644 --- a/java/shared/java/org/signal/libsignal/internal/Native.kt +++ b/java/shared/java/org/signal/libsignal/internal/Native.kt @@ -629,6 +629,8 @@ internal object Native { @JvmStatic public external fun KeyTransparency_E164SearchKey(e164: String): ByteArray @JvmStatic + public external fun KeyTransparency_ResetDataField(accountData: ByteArray, field: Int): ByteArray + @JvmStatic public external fun KeyTransparency_UsernameHashSearchKey(hash: ByteArray): ByteArray @JvmStatic diff --git a/java/shared/java/org/signal/libsignal/internal/NativeTesting.kt b/java/shared/java/org/signal/libsignal/internal/NativeTesting.kt index c1f31caaa..72e8b57be 100644 --- a/java/shared/java/org/signal/libsignal/internal/NativeTesting.kt +++ b/java/shared/java/org/signal/libsignal/internal/NativeTesting.kt @@ -186,6 +186,8 @@ public object NativeTesting { @JvmStatic @Throws(Exception::class) public external fun TESTING_KeyTransNonFatalVerificationFailure(): Unit @JvmStatic + public external fun TESTING_KeyTransStoredAccountData(): ByteArray + @JvmStatic public external fun TESTING_NonSuspendingBackgroundThreadRuntime_Destroy(handle: ObjectHandle): Unit @JvmStatic public external fun TESTING_NonSuspendingBackgroundThreadRuntime_New(): ObjectHandle diff --git a/node/ts/Native.ts b/node/ts/Native.ts index 8a0cff021..3edec85ae 100644 --- a/node/ts/Native.ts +++ b/node/ts/Native.ts @@ -318,6 +318,7 @@ type NativeFunctions = { KeyTransparency_AciSearchKey: (aci: Uint8Array,) => Uint8Array; KeyTransparency_Check: (asyncRuntime: Wrapper,environment: number,chatConnection: Wrapper,aci: Uint8Array,aciIdentityKey: Wrapper,e164: (string | null),unidentifiedAccessKey: (Uint8Array | null),usernameHash: (Uint8Array | null),accountData: (Uint8Array | null),lastDistinguishedTreeHead: (Uint8Array | null),isSelfCheck: boolean,isE164Discoverable: boolean,) => CancellablePromise<[Uint8Array, Uint8Array]>; KeyTransparency_E164SearchKey: (e164: string,) => Uint8Array; + KeyTransparency_ResetDataField: (accountData: Uint8Array,field: number,) => Uint8Array; KeyTransparency_UsernameHashSearchKey: (hash: Uint8Array,) => Uint8Array; KyberKeyPair_Generate: () => KyberKeyPair; KyberKeyPair_GetPublicKey: (keyPair: Wrapper,) => KyberPublicKey; @@ -634,6 +635,7 @@ type NativeFunctions = { TESTING_KeyTransChatSendError: () => void; TESTING_KeyTransFatalVerificationFailure: () => void; TESTING_KeyTransNonFatalVerificationFailure: () => void; + TESTING_KeyTransStoredAccountData: () => Uint8Array; TESTING_NonSuspendingBackgroundThreadRuntime_New: () => NonSuspendingBackgroundThreadRuntime; TESTING_OtherTestingHandleType_getValue: (handle: Wrapper,) => string; TESTING_PanicInBodyAsync: (_input: null,) => Promise; @@ -898,6 +900,7 @@ const { registerErrors, KeyTransparency_AciSearchKey, KeyTransparency_Check, KeyTransparency_E164SearchKey, + KeyTransparency_ResetDataField, KeyTransparency_UsernameHashSearchKey, KyberKeyPair_Generate, KyberKeyPair_GetPublicKey, @@ -1214,6 +1217,7 @@ const { registerErrors, TESTING_KeyTransChatSendError, TESTING_KeyTransFatalVerificationFailure, TESTING_KeyTransNonFatalVerificationFailure, + TESTING_KeyTransStoredAccountData, TESTING_NonSuspendingBackgroundThreadRuntime_New, TESTING_OtherTestingHandleType_getValue, TESTING_PanicInBodyAsync, @@ -1479,6 +1483,7 @@ export { registerErrors, KeyTransparency_AciSearchKey, KeyTransparency_Check, KeyTransparency_E164SearchKey, + KeyTransparency_ResetDataField, KeyTransparency_UsernameHashSearchKey, KyberKeyPair_Generate, KyberKeyPair_GetPublicKey, @@ -1795,6 +1800,7 @@ export { registerErrors, TESTING_KeyTransChatSendError, TESTING_KeyTransFatalVerificationFailure, TESTING_KeyTransNonFatalVerificationFailure, + TESTING_KeyTransStoredAccountData, TESTING_NonSuspendingBackgroundThreadRuntime_New, TESTING_OtherTestingHandleType_getValue, TESTING_PanicInBodyAsync, diff --git a/node/ts/net/KeyTransparency.ts b/node/ts/net/KeyTransparency.ts index d8aa9e54a..c62836054 100644 --- a/node/ts/net/KeyTransparency.ts +++ b/node/ts/net/KeyTransparency.ts @@ -168,6 +168,46 @@ export interface Client { ) => Promise; } +/** + * A tag identifying an optional field of the account data. + * + * (Must be in sync with the Rust counterpart) + */ +export enum AccountDataField { + E164 = 0, + UsernameHash = 1, +} + +/** + * Resets a particular field in the data associated with given ACI. + * + * Must only be called for the "self" account when either E.164 or username + * change is performed. + * + * Upon successful completion the data associated with the account will be + * updated in the store, if it was present to begin with, noop if it was not. + * + * @param aci - An ACI of "self" account. + * @param field - Account data field to be reset (E.164 or username hash). + * @param store - local persistent storage for key transparency-related data. + * @throws {TypeError} if the stored data cannot be decoded correctly, which means data corruption. + */ +export async function resetField( + aci: Aci, + field: AccountDataField, + store: Store +): Promise { + const accountData = await store.getAccountData(aci); + if (accountData === null) { + return; + } + const updated = Native.KeyTransparency_ResetDataField(accountData, field); + if (updated.length === 0) { + throw new TypeError('failed to decode account data'); + } + await store.setAccountData(aci, updated); +} + export class ClientImpl implements Client { constructor( private readonly asyncContext: TokioAsyncContext, diff --git a/node/ts/test/KeyTransparencyTest.ts b/node/ts/test/KeyTransparencyTest.ts index e5e8092ec..3c961e367 100644 --- a/node/ts/test/KeyTransparencyTest.ts +++ b/node/ts/test/KeyTransparencyTest.ts @@ -90,6 +90,33 @@ describe('KeyTransparency bridging', () => { }); }); +describe('KeyTransparency.resetField', () => { + it('throws on corrupt data', async () => { + const store = new InMemoryKtStore(); + await store.setAccountData(testAci, new Uint8Array([1, 2, 3])); + await expect( + KT.resetField(testAci, KT.AccountDataField.E164, store) + ).to.be.rejectedWith(TypeError); + }); + + it('is a noop when data is missing', async () => { + const store = new InMemoryKtStore(); + await KT.resetField(testAci, KT.AccountDataField.E164, store); + expect(store.storage.get(testAci)).to.equal(undefined); + }); + + it('updates store on success', async () => { + const store = new InMemoryKtStore(); + await store.setAccountData( + testAci, + Native.TESTING_KeyTransStoredAccountData() + ); + expect(store.storage.get(testAci)).to.have.lengthOf(1); + await KT.resetField(testAci, KT.AccountDataField.E164, store); + expect(store.storage.get(testAci)).to.have.lengthOf(2); + }); +}); + describe('KeyTransparency network errors', () => { it('can bridge network errors', async () => { async function run(statusCode: number, headers: string[] = []) { diff --git a/rust/bridge/shared/src/net/keytrans.rs b/rust/bridge/shared/src/net/keytrans.rs index 82908a781..392829718 100644 --- a/rust/bridge/shared/src/net/keytrans.rs +++ b/rust/bridge/shared/src/net/keytrans.rs @@ -14,8 +14,8 @@ use libsignal_core::{Aci, E164}; use libsignal_keytrans::{AccountData, StoredAccountData}; use libsignal_net_chat::api::RequestError; use libsignal_net_chat::api::keytrans::{ - CheckMode, Error, KeyTransparencyClient, MaybePartial, SearchKey, TreeHeadWithTimestamp, - UsernameHash, check, + AccountDataField, AccountDataFieldReset as _, CheckMode, Error, KeyTransparencyClient, + MaybePartial, SearchKey, TreeHeadWithTimestamp, UsernameHash, check, }; use libsignal_protocol::PublicKey; use prost::{DecodeError, Message}; @@ -38,6 +38,20 @@ fn KeyTransparency_UsernameHashSearchKey(hash: &[u8]) -> Vec { UsernameHash::from_slice(hash).as_search_key() } +#[bridge_fn] +fn KeyTransparency_ResetDataField( + account_data: Box<[u8]>, + field: AsType, +) -> Vec { + // The only failure is decoding error, we'll use empty vec for that. + let decoded: Result = try_decode(account_data); + let Ok(account_data) = decoded else { + log::warn!("Failed to decode stored account data"); + return vec![]; + }; + account_data.reset(field.into_inner()).encode_to_vec() +} + #[bridge_io(TokioAsyncContext)] #[expect(clippy::too_many_arguments)] async fn KeyTransparency_Check( diff --git a/rust/bridge/shared/testing/src/net/keytrans.rs b/rust/bridge/shared/testing/src/net/keytrans.rs index bc3f3a828..979058c55 100644 --- a/rust/bridge/shared/testing/src/net/keytrans.rs +++ b/rust/bridge/shared/testing/src/net/keytrans.rs @@ -3,8 +3,10 @@ // SPDX-License-Identifier: AGPL-3.0-only // +use libsignal_keytrans::{StoredAccountData, StoredMonitoringData}; use libsignal_net_chat::api::RequestError; use libsignal_net_chat::api::keytrans::Error as KeyTransError; +use prost::Message; use crate::*; @@ -30,3 +32,23 @@ fn TESTING_KeyTransNonFatalVerificationFailure() -> Result<(), RequestError Result<(), RequestError> { Err(RequestError::Timeout) } + +#[bridge_fn] +fn TESTING_KeyTransStoredAccountData() -> Vec { + StoredAccountData { + aci: Some(StoredMonitoringData { + pos: 1, + ..Default::default() + }), + e164: Some(StoredMonitoringData { + pos: 2, + ..Default::default() + }), + username_hash: Some(StoredMonitoringData { + pos: 3, + ..Default::default() + }), + last_tree_head: None, + } + .encode_to_vec() +} diff --git a/rust/net/chat/src/api/keytrans.rs b/rust/net/chat/src/api/keytrans.rs index 37349f806..0f04fdde5 100644 --- a/rust/net/chat/src/api/keytrans.rs +++ b/rust/net/chat/src/api/keytrans.rs @@ -19,7 +19,7 @@ use libsignal_keytrans::{ AccountData, ChatDistinguishedResponse, ChatMonitorResponse, ChatSearchResponse, CondensedTreeSearchResponse, FullSearchResponse, FullTreeHead, KeyTransparency, LastTreeHead, LocalStateUpdate, MonitorContext, MonitorKey, MonitorProof, MonitorRequest, MonitorResponse, - SearchContext, SearchStateUpdate, SlimSearchRequest, + SearchContext, SearchStateUpdate, SlimSearchRequest, StoredAccountData, }; use libsignal_net::env::KeyTransConfig; use libsignal_protocol::PublicKey; @@ -538,6 +538,20 @@ impl UnauthenticatedChatApi for KeyTransparencyClient<'_> { } } +pub trait AccountDataFieldReset { + fn reset(self, field: AccountDataField) -> Self; +} + +impl AccountDataFieldReset for StoredAccountData { + fn reset(mut self, field: AccountDataField) -> Self { + match field { + AccountDataField::E164 => self.e164 = None, + AccountDataField::UsernameHash => self.username_hash = None, + } + self + } +} + #[cfg(test)] pub(crate) mod test_support { use std::cell::Cell; @@ -758,8 +772,9 @@ pub(crate) mod test_support { #[cfg(test)] mod test { use assert_matches::assert_matches; + use libsignal_keytrans::StoredMonitoringData; use prost::Message as _; - use test_case::test_case; + use test_case::{test_case, test_matrix}; use super::test_support::{ CHAT_SEARCH_RESPONSE, CHAT_SEARCH_RESPONSE_VALID_AT, KEYTRANS_CONFIG_STAGING, test_account, @@ -859,4 +874,37 @@ mod test { assert_eq!(skip.to_vec(), missing_fields.into_iter().collect::>()) ); } + + #[test_matrix([AccountDataField::E164, AccountDataField::UsernameHash])] + fn reset_account_data_field(field: AccountDataField) { + let field_data = StoredMonitoringData::default(); + let data = StoredAccountData { + aci: None, + e164: Some(StoredMonitoringData { + pos: 1, + ..field_data.clone() + }), + username_hash: Some(StoredMonitoringData { + pos: 2, + ..field_data + }), + last_tree_head: None, + }; + + let updated = data.clone().reset(field); + + match field { + AccountDataField::E164 => { + assert!(updated.e164.is_none()); + assert_matches!( + updated.username_hash, + Some(StoredMonitoringData { pos: 2, .. }) + ); + } + AccountDataField::UsernameHash => { + assert_matches!(updated.e164, Some(StoredMonitoringData { pos: 1, .. })); + assert!(updated.username_hash.is_none()); + } + } + } } diff --git a/rust/net/chat/src/api/keytrans/maybe_partial.rs b/rust/net/chat/src/api/keytrans/maybe_partial.rs index 9e4b284c1..ce782fd6c 100644 --- a/rust/net/chat/src/api/keytrans/maybe_partial.rs +++ b/rust/net/chat/src/api/keytrans/maybe_partial.rs @@ -6,12 +6,16 @@ use std::collections::BTreeSet; /// A tag identifying an optional field in [`libsignal_keytrans::AccountData`] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, displaydoc::Display)] +#[repr(u8)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, displaydoc::Display, derive_more::TryFrom, +)] +#[try_from(repr)] pub enum AccountDataField { /// E.164 - E164, + E164 = 0, /// Username hash - UsernameHash, + UsernameHash = 1, } /// This struct adds to its type parameter a (potentially empty) list of diff --git a/swift/Sources/LibSignalClient/KeyTransparency.swift b/swift/Sources/LibSignalClient/KeyTransparency.swift index 1b7115f93..ecf8c2ffc 100644 --- a/swift/Sources/LibSignalClient/KeyTransparency.swift +++ b/swift/Sources/LibSignalClient/KeyTransparency.swift @@ -63,6 +63,45 @@ public enum KeyTransparency { } } + /// A tag identifying an optional field of the account data. + /// + /// (Must be in sync with the Rust counterpart) + public enum AccountDataField: UInt8 { + case e164 = 0 + case usernameHash = 1 + } + + /// Resets a particular field in the data associated with given ACI. + /// + /// Must only be called for the "self" account when either E.164 or username + /// change is performed. + /// + /// Upon successful completion the data associated with the account will be + /// updated in the store, if it was present to begin with, noop if it was not. + /// + /// - Parameters: + /// - field: Account data field to be reset (E.164 or username hash). + /// - aci: An ACI of "self" account. + /// - store: local persistent storage for key transparency-related data. + /// - Throws: ``SignalError/invalidArgument(_:)`` if the stored data cannot + /// be decoded correctly, which means data corruption. + public static func resetField( + _ field: AccountDataField, + for aci: Aci, + store: some Store + ) async throws { + guard let accountData = await store.getAccountData(for: aci) else { return } + let updated = try accountData.withUnsafeBorrowedBuffer { accountDataBuffer in + try invokeFnReturningData { + signal_key_transparency_reset_data_field($0, accountDataBuffer, field.rawValue) + } + } + if updated.isEmpty { + throw SignalError.invalidArgument("failed to decode account data") + } + await store.setAccountData(updated, for: aci) + } + /// Typed API to access the key transparency subsystem using an existing /// unauthenticated chat connection. /// diff --git a/swift/Sources/SignalFfi/signal_ffi.h b/swift/Sources/SignalFfi/signal_ffi.h index 24490f211..7612181b5 100644 --- a/swift/Sources/SignalFfi/signal_ffi.h +++ b/swift/Sources/SignalFfi/signal_ffi.h @@ -2150,6 +2150,8 @@ SignalFfiError *signal_key_transparency_check(SignalCPromisePairOfOwnedBufferOfc SignalFfiError *signal_key_transparency_e164_search_key(SignalOwnedBuffer *out, const char *e164); +SignalFfiError *signal_key_transparency_reset_data_field(SignalOwnedBuffer *out, SignalBorrowedBuffer account_data, uint8_t field); + SignalFfiError *signal_key_transparency_username_hash_search_key(SignalOwnedBuffer *out, SignalBorrowedBuffer hash); SignalFfiError *signal_kyber_key_pair_clone(SignalMutPointerKyberKeyPair *new_obj, SignalConstPointerKyberKeyPair obj); diff --git a/swift/Sources/SignalFfi/signal_ffi_testing.h b/swift/Sources/SignalFfi/signal_ffi_testing.h index 6506e48b6..b1f9426c9 100644 --- a/swift/Sources/SignalFfi/signal_ffi_testing.h +++ b/swift/Sources/SignalFfi/signal_ffi_testing.h @@ -389,6 +389,8 @@ SignalFfiError *signal_testing_key_trans_fatal_verification_failure(void); SignalFfiError *signal_testing_key_trans_non_fatal_verification_failure(void); +SignalFfiError *signal_testing_key_trans_stored_account_data(SignalOwnedBuffer *out); + SignalFfiError *signal_testing_other_testing_handle_type_get_value(const char **out, SignalConstPointerOtherTestingHandleType handle); SignalFfiError *signal_testing_panic_in_body_async(const void *_input); diff --git a/swift/Tests/LibSignalClientTests/KeyTransparencyTests.swift b/swift/Tests/LibSignalClientTests/KeyTransparencyTests.swift index 5e1b7579c..b83c4680e 100644 --- a/swift/Tests/LibSignalClientTests/KeyTransparencyTests.swift +++ b/swift/Tests/LibSignalClientTests/KeyTransparencyTests.swift @@ -113,6 +113,32 @@ final class KeyTransparencyTests: TestCaseBase { XCTAssertEqual(1, store.distinguishedTreeHeads.count) } + func testResetFieldThrowsOnCorruptData() async throws { + let store = TestStore() + await store.setAccountData(Data([1, 2, 3]), for: self.testAccount.aci) + do { + try await KeyTransparency.resetField( + .e164, + for: self.testAccount.aci, + store: store + ) + XCTFail("should have failed") + } catch SignalError.invalidArgument(_) { + } catch { + XCTFail("unexpected exception thrown: \(error)") + } + } + + func testResetFieldIsNoopWhenDataIsMissing() async throws { + let store = TestStore() + try await KeyTransparency.resetField( + .e164, + for: self.testAccount.aci, + store: store + ) + XCTAssertNil(store.accountData[self.testAccount.aci]) + } + // These testing endpoints aren't generated in device builds, to save on code size. #if !os(iOS) || targetEnvironment(simulator) func testNonFatalErrorBridging() throws { @@ -145,6 +171,23 @@ final class KeyTransparencyTests: TestCaseBase { } } + func testResetFieldUpdatesStoreOnSuccess() async throws { + let store = TestStore() + let storedAccountData = failOnError { + try invokeFnReturningData { + signal_testing_key_trans_stored_account_data($0) + } + } + await store.setAccountData(storedAccountData, for: self.testAccount.aci) + XCTAssertEqual(1, store.accountData[self.testAccount.aci]!.count) + try await KeyTransparency.resetField( + .e164, + for: self.testAccount.aci, + store: store + ) + XCTAssertEqual(2, store.accountData[self.testAccount.aci]!.count) + } + func customNetworkErrorTestImpl(status: UInt16, headers: [String: String] = [:]) async throws { let tokio = TokioAsyncContext() let (chat, remote) = UnauthenticatedChatConnection.fakeConnect(