keytrans: Persist latest distinguished tree head in local store

This commit is contained in:
moiseev-signal
2026-04-10 14:32:20 -07:00
committed by GitHub
parent 00709fe70b
commit c683a45242
21 changed files with 183 additions and 262 deletions

View File

@@ -1,2 +1,4 @@
v0.92.2
- keytrans: Periodically update distinguished tree head inside libsignal

View File

@@ -49,65 +49,6 @@ public class KeyTransparencyClient internal constructor(
private val tokioAsyncContext: TokioAsyncContext,
private val environment: Network.Environment,
) {
/**
* Request the latest distinguished tree head from the server and update it in the local store.
*
* Possible non-success results include:
* - [RequestResult.RetryableNetworkError] for errors related to communication with the server,
* including [RetryLaterException] when the client is being throttled,
* [ServerSideErrorException], [NetworkException], [NetworkProtocolException], and
* [TimeoutException].
* - [RequestResult.NonSuccess] with [KeyTransparencyException] for errors related to key
* transparency logic. Retrying without changing any of the arguments (including the state of
* the store) is unlikely to yield a different result.
* - [RequestResult.NonSuccess] with [VerificationFailedException] (a subclass of
* [KeyTransparencyException]) indicating a failure to verify the data in key transparency
* server response, such as an incorrect proof or a wrong signature.
* - [RequestResult.ApplicationError] for invalid arguments or other caller errors that could have
* been avoided.
*
* @param store local persistent storage for key transparency related data, such as the latest
* tree heads and account monitoring data. It will be queried for the latest distinguished tree
* head before performing the server request and updated with data from the server response if
* it succeeds. Distinguished tree does not have to be present in the store prior to the call.
* @return an instance of [CompletableFuture] that completes with a [RequestResult] indicating
* success or containing the error details.
*/
public fun updateDistinguished(store: Store): CompletableFuture<RequestResult<Unit, KeyTransparencyException>> {
val lastDistinguished =
try {
store.lastDistinguishedTreeHead.orElse(null)
} catch (t: Throwable) {
return CompletableFuture.completedFuture(RequestResult.ApplicationError(t))
}
return try {
NativeHandleGuard(tokioAsyncContext).use { tokioContextGuard ->
NativeHandleGuard(chatConnection).use { chatConnectionGuard ->
Native
.KeyTransparency_Distinguished(
tokioContextGuard.nativeHandle(),
environment.value,
chatConnectionGuard.nativeHandle(),
lastDistinguished,
).mapWithCancellation(
onSuccess = { distinguished ->
try {
store.setLastDistinguishedTreeHead(distinguished)
RequestResult.Success(Unit)
} catch (t: Throwable) {
RequestResult.ApplicationError(t)
}
},
onError = { err -> err.toRequestResult<KeyTransparencyException>() },
)
}
}
} catch (t: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(t))
}
}
/**
* A unified key transparency operation that performs a search, a monitor, or both.
*
@@ -124,9 +65,6 @@ public class KeyTransparencyClient internal constructor(
* - [CheckMode.Contact] - Another search request will be performed automatically and, if it succeeds,
* the updated account data will be stored.
*
* If the latest distinguished tree head is not present in the store, it will be requested from
* the server prior to performing the monitor via [updateDistinguished].
*
* Possible non-success results include:
* - [RequestResult.RetryableNetworkError] for errors related to communication with the server,
* including [RetryLaterException] when the client is being throttled,
@@ -172,16 +110,6 @@ public class KeyTransparencyClient internal constructor(
return CompletableFuture.completedFuture(RequestResult.ApplicationError(t))
}
if (lastDistinguishedTreeHead.isEmpty) {
return updateDistinguished(store).thenCompose { result ->
when (result) {
is RequestResult.Success ->
check(mode, aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, store)
else -> CompletableFuture.completedFuture(result)
}
}
}
return try {
NativeHandleGuard(tokioAsyncContext).use { tokioContextGuard ->
NativeHandleGuard(aciIdentityKey.publicKey).use { identityKeyGuard ->
@@ -199,13 +127,16 @@ public class KeyTransparencyClient internal constructor(
// Technically this is a required parameter, but passing null
// to generate the error on the Rust side.
store.getAccountData(aci).orElse(null),
lastDistinguishedTreeHead.get(),
lastDistinguishedTreeHead.orElse(null),
mode.isSelf(),
mode.isE164Discoverable() ?: true,
).mapWithCancellation(
onSuccess = { updatedAccountData ->
onSuccess = { (updatedAccountData, distinguished) ->
try {
store.setAccountData(aci, updatedAccountData)
if (distinguished.isNotEmpty()) {
store.setLastDistinguishedTreeHead(distinguished)
}
RequestResult.Success(Unit)
} catch (t: Throwable) {
RequestResult.ApplicationError(t)

View File

@@ -33,7 +33,7 @@ public class UnauthenticatedChatConnection extends ChatConnection {
this.keyTransparencyClient = new KeyTransparencyClient(this, tokioAsyncContext, ktEnvironment);
}
private KeyTransparencyClient keyTransparencyClient;
private final KeyTransparencyClient keyTransparencyClient;
static CompletableFuture<UnauthenticatedChatConnection> connect(
final TokioAsyncContext tokioAsyncContext,

View File

@@ -11,16 +11,16 @@ import org.signal.libsignal.protocol.ServiceId;
public class TestStore implements Store {
public HashMap<ServiceId.Aci, Deque<byte[]>> storage = new HashMap<>();
public byte[] lastDistinguishedTreeHead;
public Deque<byte[]> distinguishedTreeHeads = new ArrayDeque<>();
@Override
public Optional<byte[]> getLastDistinguishedTreeHead() {
return Optional.ofNullable(lastDistinguishedTreeHead);
return Optional.ofNullable(this.distinguishedTreeHeads.peekLast());
}
@Override
public void setLastDistinguishedTreeHead(byte[] lastDistinguishedTreeHead) {
this.lastDistinguishedTreeHead = lastDistinguishedTreeHead;
this.distinguishedTreeHeads.push(lastDistinguishedTreeHead);
}
@Override

View File

@@ -70,26 +70,10 @@ class KeyTransparencyClientTest {
assertIs<RequestResult.Success<*>>(it)
}
Assert.assertTrue(store.getLastDistinguishedTreeHead().isPresent)
Assert.assertTrue(store.lastDistinguishedTreeHead.isPresent)
Assert.assertTrue(store.getAccountData(KeyTransparencyTest.TEST_ACI).isPresent)
}
@Test
@Throws(Exception::class)
fun updateDistinguishedStagingIntegration() {
Assume.assumeTrue(INTEGRATION_TESTS_ENABLED)
val net = Network(Network.Environment.STAGING, USER_AGENT, mapOf(), Network.BuildVariant.BETA)
val ktClient = connectAndGetClient(net).get()
val store = TestStore()
ktClient.updateDistinguished(store).get().also {
assertIs<RequestResult.Success<*>>(it)
}
Assert.assertTrue(store.getLastDistinguishedTreeHead().isPresent)
}
@Test
@Throws(Exception::class)
fun monitorInStagingIntegration() {
@@ -118,6 +102,8 @@ class KeyTransparencyClientTest {
// Following search there should be a single entry in the account history
Assert.assertEquals(1, accountDataHistory.size.toLong())
// Should have requested and stored the latest distinguished tree head
Assert.assertEquals(1, store.distinguishedTreeHeads.size)
ktClient
.check(
@@ -134,6 +120,8 @@ class KeyTransparencyClientTest {
}
// Another entry in the account history after a successful monitor request
Assert.assertEquals(2, accountDataHistory.size.toLong())
// Should not have updated the distinguished tree head, as the last one was reused
Assert.assertEquals(1, store.distinguishedTreeHeads.size)
}
inline fun <reified E : Throwable> retryableNetworkExceptionsTestImpl(
@@ -150,9 +138,20 @@ class KeyTransparencyClientTest {
)
val store = TestStore()
val responseFuture = chat.keyTransparencyClient().updateDistinguished(store)
val responseFuture =
chat
.keyTransparencyClient()
.check(
CheckMode.Contact,
KeyTransparencyTest.TEST_ACI,
KeyTransparencyTest.TEST_ACI_IDENTITY_KEY,
null,
null,
null,
store,
)
val (_, requestId) = remote.getNextIncomingRequest().get()
val (_, requestId) = remote.nextIncomingRequest.get()
remote.sendResponse(requestId, statusCode, message, headers, byteArrayOf())
val result = responseFuture.get()
@@ -174,9 +173,20 @@ class KeyTransparencyClientTest {
)
val store = TestStore()
val responseFuture = chat.keyTransparencyClient().updateDistinguished(store)
val responseFuture =
chat
.keyTransparencyClient()
.check(
CheckMode.Contact,
KeyTransparencyTest.TEST_ACI,
KeyTransparencyTest.TEST_ACI_IDENTITY_KEY,
null,
null,
null,
store,
)
val (_, requestId) = remote.getNextIncomingRequest().get()
val (_, requestId) = remote.nextIncomingRequest.get()
remote.sendResponse(requestId, statusCode, message, headers, byteArrayOf())
val result = responseFuture.get()
@@ -194,9 +204,20 @@ class KeyTransparencyClientTest {
)
val store = TestStore()
val responseFuture = chat.keyTransparencyClient().updateDistinguished(store)
val responseFuture =
chat
.keyTransparencyClient()
.check(
CheckMode.Contact,
KeyTransparencyTest.TEST_ACI,
KeyTransparencyTest.TEST_ACI_IDENTITY_KEY,
null,
null,
null,
store,
)
remote.getNextIncomingRequest().get()
remote.nextIncomingRequest.get()
remote.guardedRun(NativeTesting::TESTING_FakeChatRemoteEnd_InjectConnectionInterrupted)
val result = responseFuture.get()

View File

@@ -18,14 +18,14 @@ import org.signal.libsignal.protocol.ServiceId;
import org.signal.libsignal.protocol.util.Hex;
public class KeyTransparencyTest {
static final ServiceId.Aci TEST_ACI =
public static final ServiceId.Aci TEST_ACI =
new ServiceId.Aci(UUID.fromString("90c979fd-eab4-4a08-b6da-69dedeab9b29"));
static final IdentityKey TEST_ACI_IDENTITY_KEY;
static final String TEST_E164 = "+18005550100";
static final byte[] TEST_USERNAME_HASH =
public static final IdentityKey TEST_ACI_IDENTITY_KEY;
public static final String TEST_E164 = "+18005550100";
public static final byte[] TEST_USERNAME_HASH =
Hex.fromStringCondensedAssert(
"dc711808c2cf66d5e6a33ce41f27d69d942d2e1ff4db22d39b42d2eff8d09746");
static final byte[] TEST_UNIDENTIFIED_ACCESS_KEY =
public static final byte[] TEST_UNIDENTIFIED_ACCESS_KEY =
Hex.fromStringCondensedAssert("108d84b71be307bdf101e380a1d7f2a2");
static {

View File

@@ -619,9 +619,7 @@ internal object Native {
@JvmStatic
public external fun KeyTransparency_AciSearchKey(aci: ByteArray): ByteArray
@JvmStatic
public external fun KeyTransparency_Check(asyncRuntime: ObjectHandle, environment: Int, chatConnection: ObjectHandle, aci: ByteArray, aciIdentityKey: ObjectHandle, e164: String?, unidentifiedAccessKey: ByteArray?, usernameHash: ByteArray?, accountData: ByteArray?, lastDistinguishedTreeHead: ByteArray, isSelfCheck: Boolean, isE164Discoverable: Boolean): CompletableFuture<ByteArray>
@JvmStatic
public external fun KeyTransparency_Distinguished(asyncRuntime: ObjectHandle, environment: Int, chatConnection: ObjectHandle, lastDistinguishedTreeHead: ByteArray?): CompletableFuture<ByteArray>
public external fun KeyTransparency_Check(asyncRuntime: ObjectHandle, environment: Int, chatConnection: ObjectHandle, aci: ByteArray, aciIdentityKey: ObjectHandle, e164: String?, unidentifiedAccessKey: ByteArray?, usernameHash: ByteArray?, accountData: ByteArray?, lastDistinguishedTreeHead: ByteArray?, isSelfCheck: Boolean, isE164Discoverable: Boolean): CompletableFuture<Pair<ByteArray, ByteArray>>
@JvmStatic
public external fun KeyTransparency_E164SearchKey(e164: String): ByteArray
@JvmStatic

View File

@@ -511,8 +511,7 @@ type NativeFunctions = {
KeyTransparency_AciSearchKey: (aci: Uint8Array<ArrayBuffer>) => Uint8Array<ArrayBuffer>;
KeyTransparency_E164SearchKey: (e164: string) => Uint8Array<ArrayBuffer>;
KeyTransparency_UsernameHashSearchKey: (hash: Uint8Array<ArrayBuffer>) => Uint8Array<ArrayBuffer>;
KeyTransparency_Check: (asyncRuntime: Wrapper<TokioAsyncContext>, environment: number, chatConnection: Wrapper<UnauthenticatedChatConnection>, aci: Uint8Array<ArrayBuffer>, aciIdentityKey: Wrapper<PublicKey>, e164: string | null, unidentifiedAccessKey: Uint8Array<ArrayBuffer> | null, usernameHash: Uint8Array<ArrayBuffer> | null, accountData: Uint8Array<ArrayBuffer> | null, lastDistinguishedTreeHead: Uint8Array<ArrayBuffer>, isSelfCheck: boolean, isE164Discoverable: boolean) => CancellablePromise<Uint8Array<ArrayBuffer>>;
KeyTransparency_Distinguished: (asyncRuntime: Wrapper<TokioAsyncContext>, environment: number, chatConnection: Wrapper<UnauthenticatedChatConnection>, lastDistinguishedTreeHead: Uint8Array<ArrayBuffer> | null) => CancellablePromise<Uint8Array<ArrayBuffer>>;
KeyTransparency_Check: (asyncRuntime: Wrapper<TokioAsyncContext>, environment: number, chatConnection: Wrapper<UnauthenticatedChatConnection>, aci: Uint8Array<ArrayBuffer>, aciIdentityKey: Wrapper<PublicKey>, e164: string | null, unidentifiedAccessKey: Uint8Array<ArrayBuffer> | null, usernameHash: Uint8Array<ArrayBuffer> | null, accountData: Uint8Array<ArrayBuffer> | null, lastDistinguishedTreeHead: Uint8Array<ArrayBuffer> | null, isSelfCheck: boolean, isE164Discoverable: boolean) => CancellablePromise<[Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>]>;
RegistrationService_CreateSession: (asyncRuntime: Wrapper<TokioAsyncContext>, createSession: RegistrationCreateSessionRequest, connectChat: ConnectChatBridge) => CancellablePromise<RegistrationService>;
RegistrationService_ResumeSession: (asyncRuntime: Wrapper<TokioAsyncContext>, sessionId: string, number: string, connectChat: ConnectChatBridge) => CancellablePromise<RegistrationService>;
RegistrationService_RequestVerificationCode: (asyncRuntime: Wrapper<TokioAsyncContext>, service: Wrapper<RegistrationService>, transport: string, client: string, languages: string[]) => CancellablePromise<void>;
@@ -1073,7 +1072,6 @@ const { registerErrors,
KeyTransparency_E164SearchKey,
KeyTransparency_UsernameHashSearchKey,
KeyTransparency_Check,
KeyTransparency_Distinguished,
RegistrationService_CreateSession,
RegistrationService_ResumeSession,
RegistrationService_RequestVerificationCode,
@@ -1636,7 +1634,6 @@ export { registerErrors,
KeyTransparency_E164SearchKey,
KeyTransparency_UsernameHashSearchKey,
KeyTransparency_Check,
KeyTransparency_Distinguished,
RegistrationService_CreateSession,
RegistrationService_ResumeSession,
RegistrationService_RequestVerificationCode,

View File

@@ -140,16 +140,15 @@ export interface Client {
*
* @param request - Key transparency client {@link Request}.
* @param store - Local key transparency storage. It will be queried for both
* the account data and the latest distinguished tree head before sending the
* server request and, if the request succeeds, will be updated with the
* search operation results.
* the account data before sending the server request and, if the request
* succeeds, will be updated with the operation results.
* @param options - options for the asynchronous operation. Optional.
*
* @returns A promise that resolves if the check succeeds and the local state has been updated
* to reflect the latest changes.
*
* @throws {KeyTransparencyError} for errors related to key transparency logic, which
* includes missing required fields in the serialized data. Retrying the search without
* includes missing required fields in the serialized data. Retrying the check without
* changing any of the arguments (including the state of the store) is unlikely to yield a
* different result.
* @throws {KeyTransparencyVerificationFailed} when it fails to
@@ -181,10 +180,6 @@ export class ClientImpl implements Client {
store: Store,
options?: Readonly<Options>
): Promise<void> {
const distinguished = await this._getLatestDistinguished(
store,
options ?? {}
);
const { abortSignal } = options ?? {};
const {
aciInfo: { aci, identityKey: aciIdentityKey },
@@ -196,50 +191,27 @@ export class ClientImpl implements Client {
e164: null,
unidentifiedAccessKey: null,
};
const accountData = await this.asyncContext.makeCancellable(
abortSignal,
Native.KeyTransparency_Check(
this.asyncContext,
this.env,
this.chatService,
aci.getServiceIdFixedWidthBinary(),
aciIdentityKey,
e164,
unidentifiedAccessKey,
usernameHash ?? null,
await store.getAccountData(aci),
distinguished,
mode === 'self',
mode === 'self' ? request.isE164Discoverable : true
)
);
const [accountData, newDistinguished] =
await this.asyncContext.makeCancellable(
abortSignal,
Native.KeyTransparency_Check(
this.asyncContext,
this.env,
this.chatService,
aci.getServiceIdFixedWidthBinary(),
aciIdentityKey,
e164,
unidentifiedAccessKey,
usernameHash ?? null,
await store.getAccountData(aci),
await store.getLastDistinguishedTreeHead(),
mode === 'self',
mode === 'self' ? request.isE164Discoverable : true
)
);
await store.setAccountData(aci, accountData);
}
private async updateDistinguished(
store: Store,
{ abortSignal }: Readonly<Options>
): Promise<Uint8Array<ArrayBuffer>> {
const bytes = await this.asyncContext.makeCancellable(
abortSignal,
Native.KeyTransparency_Distinguished(
this.asyncContext,
this.env,
this.chatService,
await store.getLastDistinguishedTreeHead()
)
);
await store.setLastDistinguishedTreeHead(bytes);
return bytes;
}
async _getLatestDistinguished(
store: Store,
options: Readonly<Options>
): Promise<Uint8Array<ArrayBuffer>> {
return (
(await store.getLastDistinguishedTreeHead()) ??
(await this.updateDistinguished(store, options))
);
if (newDistinguished.length > 0) {
await store.setLastDistinguishedTreeHead(newDistinguished);
}
}
}

View File

@@ -108,7 +108,7 @@ describe('KeyTransparency network errors', () => {
unauth._chatService,
Environment.Staging
);
const promise = client._getLatestDistinguished(new InMemoryKtStore(), {});
const promise = client.check(testRequest, new InMemoryKtStore(), {});
const request = await remote.assertReceiveIncomingRequest();
@@ -182,31 +182,36 @@ describe('KeyTransparency Integration', function (this: Mocha.Suite) {
}
expect(accountDataHistory.length).to.equal(1);
expect(store.distinguished.length).to.equal(1);
await kt.check(testRequest, store, {});
expect(accountDataHistory.length).to.equal(2);
// Distinguished tree should not have been updated
expect(store.distinguished.length).to.equal(1);
});
});
class InMemoryKtStore implements KT.Store {
storage: Map<Readonly<Aci>, Array<Readonly<Uint8Array<ArrayBuffer>>>>;
distinguished: Readonly<Uint8Array<ArrayBuffer>> | null;
distinguished: Array<Readonly<Uint8Array<ArrayBuffer>>>;
constructor() {
this.storage = new Map<Aci, Array<Readonly<Uint8Array<ArrayBuffer>>>>();
this.distinguished = null;
this.distinguished = new Array<Uint8Array<ArrayBuffer>>();
}
// eslint-disable-next-line @typescript-eslint/require-await
async getLastDistinguishedTreeHead(): Promise<Uint8Array<ArrayBuffer> | null> {
return this.distinguished;
return this.distinguished.at(-1) ?? null;
}
// eslint-disable-next-line @typescript-eslint/require-await
async setLastDistinguishedTreeHead(
bytes: Readonly<Uint8Array<ArrayBuffer>> | null
) {
this.distinguished = bytes;
if (bytes !== null) {
this.distinguished.push(bytes);
}
}
// eslint-disable-next-line @typescript-eslint/require-await

View File

@@ -43,7 +43,6 @@ exclude = [
"CPromisebool",
"CPromiseFfiCdsiLookupResponse",
"CPromiseMutPointerRegistrationService",
"CPromiseOwnedBufferOfc_uchar",
"FfiCdsiLookupResponse",
"FfiCdsiLookupResponseEntry",
"FfiChatListenerStruct",

View File

@@ -11,13 +11,11 @@ use libsignal_bridge_types::net::chat::UnauthenticatedChatConnection;
pub use libsignal_bridge_types::net::{Environment, TokioAsyncContext};
use libsignal_bridge_types::support::AsType;
use libsignal_core::{Aci, E164};
use libsignal_keytrans::{
AccountData, LastTreeHead, LocalStateUpdate, StoredAccountData, StoredTreeHead,
};
use libsignal_keytrans::{AccountData, StoredAccountData};
use libsignal_net_chat::api::RequestError;
use libsignal_net_chat::api::keytrans::{
CheckMode, Error, KeyTransparencyClient, MaybePartial, SearchKey, TreeHeadWithTimestamp,
UnauthenticatedChatApi as _, UsernameHash, check,
UsernameHash, check,
};
use libsignal_protocol::PublicKey;
use prost::{DecodeError, Message};
@@ -52,10 +50,14 @@ async fn KeyTransparency_Check(
unidentified_access_key: Option<Box<[u8]>>,
username_hash: Option<Box<[u8]>>,
account_data: Option<Box<[u8]>>,
last_distinguished_tree_head: Box<[u8]>,
last_distinguished_tree_head: Option<Box<[u8]>>,
is_self_check: bool,
is_e164_discoverable: bool,
) -> Result<Vec<u8>, RequestError<Error>> {
// Return a pair of (serialized account data, serialized distinguished)
// If the last_distinguished_tree_head was reused, serialized distinguished will be empty.
// Could have been an Option<Vec<u8>>, but that would be another layer of <> to handle in
// the bridging macro to the same effect.
) -> Result<(Vec<u8>, Vec<u8>), RequestError<Error>> {
let config = environment.into_inner().env().keytrans_config;
let username_hash = username_hash.map(UsernameHash::from);
@@ -73,9 +75,12 @@ async fn KeyTransparency_Check(
let e164_pair = make_e164_pair(e164, unidentified_access_key)?;
let last_distinguished_tree_head = try_decode_distinguished(last_distinguished_tree_head);
let last_distinguished_tree_head =
last_distinguished_tree_head.and_then(try_decode_distinguished);
let (maybe_partial_result, _updated_distinguished) = chat_connection
let previously_stored_distinguished = last_distinguished_tree_head.clone();
let (maybe_partial_result, updated_distinguished) = chat_connection
.as_typed(|chat| {
Box::pin(async move {
let kt = KeyTransparencyClient::new(*chat, config);
@@ -102,41 +107,15 @@ async fn KeyTransparency_Check(
maybe_hash_search_key,
now,
)?;
// let serialized_distinguished = updated_distinguished.into_stored(now).encode_to_vec();
Ok(serialized_account_data)
}
#[bridge_io(TokioAsyncContext)]
async fn KeyTransparency_Distinguished(
// TODO: it is currently possible to pass an env that does not match chat
environment: AsType<Environment, u8>,
chat_connection: &UnauthenticatedChatConnection,
last_distinguished_tree_head: Option<Box<[u8]>>,
) -> Result<Vec<u8>, RequestError<Error>> {
let config = environment.into_inner().env().keytrans_config;
let known_distinguished = last_distinguished_tree_head
.map(try_decode)
.transpose()
.map_err(|_| invalid_request("could not decode account data"))?
.and_then(|stored: StoredTreeHead| stored.into_last_tree_head());
let LocalStateUpdate {
tree_head,
tree_root,
monitoring_data: _,
} = chat_connection
.as_typed(|chat| {
Box::pin(async move {
let kt = KeyTransparencyClient::new(*chat, config);
kt.distinguished(known_distinguished).await
})
})
.await?;
let updated_distinguished = LastTreeHead(tree_head, tree_root).into_stored(SystemTime::now());
let serialized = updated_distinguished.encode_to_vec();
Ok(serialized)
let distinguished_to_be_stored = if let Some(known) = previously_stored_distinguished
&& known.tree_head == updated_distinguished
{
// Distinguished has not been updated, we must keep its stored_at
vec![]
} else {
updated_distinguished.into_stored(now).encode_to_vec()
};
Ok((serialized_account_data, distinguished_to_be_stored))
}
fn invalid_request(msg: &'static str) -> RequestError<Error> {

View File

@@ -1511,6 +1511,10 @@ macro_rules! ffi_result_type {
// Like Result, we can't use `:ty` here because we need the resulting tokens to be matched
// recursively. We can at least match several tokens in the second component though.
(($a:tt, $($b:tt)+)) => (ffi::PairOf<ffi_result_type!($a), ffi_result_type!($($b)+)>);
(($a:tt<$($aargs:tt),+>, $b:tt<$($bargs:tt),+>)) => (ffi::PairOf<
ffi_result_type!($a<$($aargs),+>),
ffi_result_type!($b<$($bargs),+>)
>);
(Option<($a:tt, $($b:tt)+)>) => (ffi::OptionalPairOf<ffi_result_type!($a), ffi_result_type!($($b)+)>);
(u8) => (u8);

View File

@@ -2865,6 +2865,12 @@ macro_rules! jni_result_type {
(Result<$typ:tt<$($args:tt),+> $(, $_:ty)?>) => {
$crate::jni::Throwing<jni_result_type!($typ<$($args),+>)>
};
(Result<($a:tt, $b:tt>)) => {
$crate::jni::Throwing<jni_result_type!(($a, $b))>
};
(Result<($a:tt<$($aargs:tt),+>, $b:tt<$($bargs:tt),+>) $(, $_:ty)?>) => {
$crate::jni::Throwing<jni_result_type!(($a<$($aargs),+>, $b<$($bargs),+>))>
};
(Option<u32>) => {
::jni::sys::jint
};
@@ -2886,6 +2892,9 @@ macro_rules! jni_result_type {
(($a:tt, $b:tt)) => {
$crate::jni::JavaPair<'local, $crate::jni_result_type!($a), $crate::jni_result_type!($b)>
};
(($a:tt<$($aargs:tt),+>, $b:tt<$($bargs:tt),+>)) => {
$crate::jni::JavaPair<'local, $crate::jni_result_type!($a<$($aargs),+>), $crate::jni_result_type!($b<$($bargs),+>)>
};
(bool) => {
::jni::sys::jboolean
};

View File

@@ -87,7 +87,8 @@ async fn update_distinguished_if_needed(
Ok(LastTreeHead(tree_head, tree_root))
}
#[cfg_attr(test, derive(Clone, PartialEq))]
#[cfg_attr(test, derive(PartialEq))]
#[derive(Clone)]
pub struct TreeHeadWithTimestamp {
pub tree_head: LastTreeHead,
pub stored_at_ms: u64,

View File

@@ -691,7 +691,7 @@ mod test {
}
#[tokio::test]
async fn search_with_wrong_identity_key() {
async fn search_with_wrong_identity_key_integration() {
if !kt_integration_enabled() {
println!("SKIPPED: running integration tests is not enabled");
return;

View File

@@ -100,6 +100,10 @@ extension SignalCPromiseOptionalPairOfc_charu832: PromiseStruct {
typealias Result = SignalOptionalPairOfc_charu832
}
extension SignalCPromisePairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar: PromiseStruct {
typealias Result = SignalPairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar
}
extension SignalCPromiseFfiPreKeysResponse: PromiseStruct {
typealias Result = SignalFfiPreKeysResponse
}

View File

@@ -132,9 +132,8 @@ public enum KeyTransparency {
/// - e164Info: E.164 identifying information. Optional.
/// - usernameHash: Hash of the username. Optional.
/// - store: Local key transparency storage. It will be queried for both
/// the account data and the latest distinguished tree head before sending the
/// server request and, if the request succeeds, will be updated with the
/// search operation results.
/// the account data before sending the server request and, if the
/// request succeeds, will be updated with the check results.
/// - Throws:
/// - ``SignalError/keyTransparencyError`` for errors related to key transparency logic, which
/// includes missing required fields in the serialized data. Retrying the search without
@@ -168,9 +167,9 @@ public enum KeyTransparency {
let uak = e164Info?.unidentifiedAccessKey
let accountData = await store.getAccountData(for: aciInfo.aci)
let distinguished = try await self.updateDistinguished(store)
let knownDistinguished = await store.getLastDistinguishedTreeHead()
let bytes = try await self.asyncContext.invokeAsyncFunction { promise, tokioContext in
let rawResponse = try await self.asyncContext.invokeAsyncFunction { promise, tokioContext in
try! withAllBorrowed(
self.chatConnection,
aciInfo.aci,
@@ -178,7 +177,7 @@ public enum KeyTransparency {
uak,
usernameHash,
accountData,
distinguished
knownDistinguished
) { chatHandle, aciBytes, identityKeyHandle, uakBytes, hashBytes, accDataBytes, distinguishedBytes in
signal_key_transparency_check(
promise,
@@ -197,31 +196,13 @@ public enum KeyTransparency {
)
}
}
await store.setAccountData(Data(consuming: bytes), for: aciInfo.aci)
}
let updatedAccountData = Data(consuming: rawResponse.first)
let updatedDistinguished = Data(consuming: rawResponse.second)
private func updateDistinguished(_ store: some Store) async throws -> Data {
let knownDistinguished = await store.getLastDistinguishedTreeHead()
let latestDistinguished = try await getDistinguished(knownDistinguished)
await store.setLastDistinguishedTreeHead(to: latestDistinguished)
return latestDistinguished
}
internal func getDistinguished(
_ distinguished: Data? = nil
) async throws -> Data {
let bytes = try await self.asyncContext.invokeAsyncFunction { promise, tokioContext in
try! withAllBorrowed(self.chatConnection, distinguished) { chatHandle, distinguishedBytes in
signal_key_transparency_distinguished(
promise,
tokioContext.const(),
self.environment.rawValue,
chatHandle.const(),
distinguishedBytes
)
}
await store.setAccountData(updatedAccountData, for: aciInfo.aci)
if !updatedDistinguished.isEmpty {
await store.setLastDistinguishedTreeHead(to: updatedDistinguished)
}
return Data(consuming: bytes)
}
}
}

View File

@@ -1125,6 +1125,11 @@ typedef struct {
SignalFfiLoggerDestroy destroy;
} SignalFfiLoggerStruct;
typedef struct {
SignalOwnedBuffer first;
SignalOwnedBuffer second;
} SignalPairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar;
/**
* A C callback used to report the results of Rust futures.
*
@@ -1135,10 +1140,10 @@ typedef struct {
* completed once.
*/
typedef struct {
void (*complete)(SignalFfiError *error, const SignalOwnedBuffer *result, const void *context);
void (*complete)(SignalFfiError *error, const SignalPairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar *result, const void *context);
const void *context;
SignalCancellationId cancellation_id;
} SignalCPromiseOwnedBufferOfc_uchar;
} SignalCPromisePairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar;
typedef struct {
const SignalUnauthenticatedChatConnection *raw;
@@ -2126,9 +2131,7 @@ bool signal_init_logger(SignalLogLevel max_level, SignalFfiLoggerStruct logger);
SignalFfiError *signal_key_transparency_aci_search_key(SignalOwnedBuffer *out, const SignalServiceIdFixedWidthBinaryBytes *aci);
SignalFfiError *signal_key_transparency_check(SignalCPromiseOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, const SignalServiceIdFixedWidthBinaryBytes *aci, SignalConstPointerPublicKey aci_identity_key, const char *e164, SignalOptionalBorrowedSliceOfc_uchar unidentified_access_key, SignalOptionalBorrowedSliceOfc_uchar username_hash, SignalOptionalBorrowedSliceOfc_uchar account_data, SignalBorrowedBuffer last_distinguished_tree_head, bool is_self_check, bool is_e164_discoverable);
SignalFfiError *signal_key_transparency_distinguished(SignalCPromiseOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, SignalOptionalBorrowedSliceOfc_uchar last_distinguished_tree_head);
SignalFfiError *signal_key_transparency_check(SignalCPromisePairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, const SignalServiceIdFixedWidthBinaryBytes *aci, SignalConstPointerPublicKey aci_identity_key, const char *e164, SignalOptionalBorrowedSliceOfc_uchar unidentified_access_key, SignalOptionalBorrowedSliceOfc_uchar username_hash, SignalOptionalBorrowedSliceOfc_uchar account_data, SignalOptionalBorrowedSliceOfc_uchar last_distinguished_tree_head, bool is_self_check, bool is_e164_discoverable);
SignalFfiError *signal_key_transparency_e164_search_key(SignalOwnedBuffer *out, const char *e164);

View File

@@ -236,6 +236,21 @@ typedef struct {
SignalTestingSemaphore *raw;
} SignalMutPointerTestingSemaphore;
/**
* A C callback used to report the results of Rust futures.
*
* cbindgen will produce independent C types like `SignalCPromisei32` and
* `SignalCPromiseProtocolAddress`.
*
* This derives Copy because it behaves like a C type; nevertheless, a promise should still only be
* completed once.
*/
typedef struct {
void (*complete)(SignalFfiError *error, const SignalOwnedBuffer *result, const void *context);
const void *context;
SignalRawCancellationId cancellation_id;
} SignalCPromiseOwnedBufferOfc_uchar;
typedef struct {
SignalTestingValueHolder *raw;
} SignalMutPointerTestingValueHolder;

View File

@@ -81,16 +81,6 @@ final class KeyTransparencyTests: TestCaseBase {
func connectionWasInterrupted(_: UnauthenticatedChatConnection, error: Error?) {}
}
func testUnknownDistinguished() async throws {
try self.nonHermeticTest()
let net = Net(env: .staging, userAgent: userAgent, buildVariant: .production)
let chat = try await net.connectUnauthenticatedChat()
chat.start(listener: NoOpListener())
XCTAssertNoThrow { try await chat.keyTransparencyClient.getDistinguished() }
}
func testCheck() async throws {
try self.nonHermeticTest()
@@ -108,6 +98,8 @@ final class KeyTransparencyTests: TestCaseBase {
store: store
)
XCTAssertEqual(1, store.accountData[self.testAccount.aci]!.count)
XCTAssertEqual(1, store.distinguishedTreeHeads.count)
try await chat.keyTransparencyClient.check(
for: .contact,
account: self.testAccount.aciInfo,
@@ -115,8 +107,10 @@ final class KeyTransparencyTests: TestCaseBase {
store: store
)
// Second check will send a monitor request, and should update account
// data in store
// data in store, but the distinguished tree should have been reused
// and not updated
XCTAssertEqual(2, store.accountData[self.testAccount.aci]!.count)
XCTAssertEqual(1, store.distinguishedTreeHeads.count)
}
// These testing endpoints aren't generated in device builds, to save on code size.
@@ -159,7 +153,13 @@ final class KeyTransparencyTests: TestCaseBase {
)
defer { withExtendedLifetime(chat) {} }
async let future = chat.keyTransparencyClient.getDistinguished()
let store = TestStore()
let aciInfo = self.testAccount.aciInfo
async let future: () = chat.keyTransparencyClient.check(
for: .contact,
account: aciInfo,
store: store
)
let (_, id) = try await remote.getNextIncomingRequest()