java: Expose KeyTransparency return values as RequestResult.

This commit is contained in:
andrew-signal
2026-02-27 01:58:59 -05:00
committed by GitHub
parent f80e6cc647
commit 83ab6d3eec
12 changed files with 379 additions and 333 deletions

10
Cargo.lock generated
View File

@@ -2542,7 +2542,7 @@ dependencies = [
[[package]]
name = "libsignal-ffi"
version = "0.87.6"
version = "0.88.0"
dependencies = [
"cpufeatures",
"hex",
@@ -2563,14 +2563,14 @@ dependencies = [
[[package]]
name = "libsignal-jni"
version = "0.87.6"
version = "0.88.0"
dependencies = [
"libsignal-jni-impl",
]
[[package]]
name = "libsignal-jni-impl"
version = "0.87.6"
version = "0.88.0"
dependencies = [
"cfg-if",
"cpufeatures",
@@ -2587,7 +2587,7 @@ dependencies = [
[[package]]
name = "libsignal-jni-testing"
version = "0.87.6"
version = "0.88.0"
dependencies = [
"jni",
"libsignal-bridge-testing",
@@ -2897,7 +2897,7 @@ dependencies = [
[[package]]
name = "libsignal-node"
version = "0.87.6"
version = "0.88.0"
dependencies = [
"futures",
"libsignal-bridge",

View File

@@ -37,7 +37,7 @@ default-members = [
resolver = "2" # so that our dev-dependency features don't leak into products
[workspace.package]
version = "0.87.6"
version = "0.88.0"
authors = ["Signal Messenger LLC"]
license = "AGPL-3.0-only"
rust-version = "1.88"

View File

@@ -5,7 +5,7 @@
Pod::Spec.new do |s|
s.name = 'LibSignalClient'
s.version = '0.87.6'
s.version = '0.88.0'
s.summary = 'A Swift wrapper library for communicating with the Signal messaging service.'
s.homepage = 'https://github.com/signalapp/libsignal'

View File

@@ -1,2 +1,3 @@
v0.87.6
v0.88.0
- java: KeyTransparencyClient now returns RequestResult types

View File

@@ -23,7 +23,7 @@ repositories {
}
allprojects {
version = "0.87.6"
version = "0.88.0"
group = "org.signal"
tasks.withType(JavaCompile) {

View File

@@ -5,8 +5,10 @@
package org.signal.libsignal.keytrans;
import org.signal.libsignal.net.BadRequestError;
/** Key transparency operation failed. */
public class KeyTransparencyException extends Exception {
public class KeyTransparencyException extends Exception implements BadRequestError {
public KeyTransparencyException(String message) {
super(message);
}

View File

@@ -1,298 +0,0 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net;
import java.util.Optional;
import org.signal.libsignal.internal.CompletableFuture;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.internal.TokioAsyncContext;
import org.signal.libsignal.keytrans.Store;
import org.signal.libsignal.net.KeyTransparency.MonitorMode;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.ServiceId;
/**
* Typed API to access the key transparency subsystem using an existing unauthenticated chat
* connection.
*
* <p>Unlike {@link ChatConnection}, key transparency client does not export "raw" send/receive
* APIs, and instead uses them internally to implement high-level operations.
*
* <p>Note: {@code Store} APIs may be invoked concurrently. Here are possible strategies to make
* sure there are no thread safety violations:
*
* <ul>
* <li>Types implementing {@code Store} can be made thread safe
* <li>{@link KeyTransparencyClient} operations-completed asynchronous calls-can be serialized.
* </ul>
*
* <p>Example usage:
*
* <pre>
* var net = new Network(Network.Environment.STAGING, "key-transparency-example");
* var chat = net.connectUnauthChat(new Listener()).get();
* chat.start();
*
* KeyTransparencyClient client = chat.keyTransparencyClient();
*
* client.search(aci, identityKey, null, null, null, KT_DATA_STORE).get();
*
* </pre>
*/
public class KeyTransparencyClient {
private final TokioAsyncContext tokioAsyncContext;
private final UnauthenticatedChatConnection chatConnection;
private final Network.Environment environment;
KeyTransparencyClient(
UnauthenticatedChatConnection chat,
TokioAsyncContext tokioAsyncContext,
Network.Environment environment) {
this.chatConnection = chat;
this.tokioAsyncContext = tokioAsyncContext;
this.environment = environment;
}
/**
* Search for account information in the key transparency tree.
*
* <p>Only ACI and ACI identity key are required to identify the account.
*
* <p>If the latest distinguished tree head is not present in the store, it will be requested from
* the server prior to performing the search via {@link #updateDistinguished}.
*
* <p>This is an asynchronous operation; all the exceptions occurring during communication with
* the server will be wrapped in {@link java.util.concurrent.ExecutionException}.
*
* <p>Possible exceptions include:
*
* <ul>
* <li>{@link ChatServiceException} for errors related to communication with the server.
* Depending on the concrete subclass, client can retry the operation. See also {@link
* TimeoutException}, {@link ServerSideErrorException}, {@link UnexpectedResponseException},
* {@link AppExpiredException},
* <li>{@link RetryLaterException} when the client is being throttled. Wait for the specified
* amount of time before making new requests.
* <li>{@link NetworkException} when the network operation fails. Clients can retry the call
* after a recommended period.
* <li>{@link NetworkProtocolException} when a high level network protocol fails. For example,
* failure to establish a websocket connection.
* <li>{@link org.signal.libsignal.keytrans.KeyTransparencyException} for errors related to key
* transparency logic, which includes missing required fields in the serialized data.
* Retrying the search without changing any of the arguments (including the state of the
* store) is unlikely to yield a different result.
* <li>{@link org.signal.libsignal.keytrans.VerificationFailedException} indicates a failure to
* verify the data in key transparency server response, such as an incorrect proof or a
* wrong signature.
* </ul>
*
* @param aci the ACI of the account to be searched for. Required.
* @param aciIdentityKey {@link IdentityKey} associated with the ACI. Required.
* @param e164 string representation of an E.164 number associated with the account. Optional.
* @param unidentifiedAccessKey unidentified access key for the account. This parameter has the
* same optionality as the E.164 parameter.
* @param usernameHash hash of the username associated with the account. Optional.
* @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 data before performing the
* server request and updated with the latest information from the server response if it
* succeeds.
* @return an instance of {@link CompletableFuture} successful completion of which will indicate
* that the search request has succeeded and store has been updated with the latest account
* data.
* @throws IllegalArgumentException if the store contains corrupted data.
*/
public CompletableFuture<Void> search(
/* @NotNull */ final ServiceId.Aci aci,
/* @NotNull */ final IdentityKey aciIdentityKey,
final String e164,
final byte[] unidentifiedAccessKey,
final byte[] usernameHash,
final Store store) {
Optional<byte[]> lastDistinguishedTreeHead = store.getLastDistinguishedTreeHead();
if (lastDistinguishedTreeHead.isEmpty()) {
return this.updateDistinguished(store)
.thenCompose(
(ignored) ->
this.search(
aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, store));
}
// Decoding of the last distinguished tree head happens "eagerly" before making any network
// requests.
// It may result in an IllegalArgumentException.
try (NativeHandleGuard tokioContextGuard = this.tokioAsyncContext.guard();
NativeHandleGuard identityKeyGuard = aciIdentityKey.getPublicKey().guard();
NativeHandleGuard chatConnectionGuard = new NativeHandleGuard(chatConnection); ) {
return Native.KeyTransparency_Search(
tokioContextGuard.nativeHandle(),
this.environment.value,
chatConnectionGuard.nativeHandle(),
aci.toServiceIdFixedWidthBinary(),
identityKeyGuard.nativeHandle(),
e164,
unidentifiedAccessKey,
usernameHash,
store.getAccountData(aci).orElse(null),
lastDistinguishedTreeHead.get())
.thenApply(
(accountData) -> {
store.setAccountData(aci, accountData);
return null;
});
}
}
/**
* Request the latest distinguished tree head from the server and update it in the local store.
*
* <p>This is an asynchronous operation; all the exceptions occurring during communication with
* the server will be wrapped in {@link java.util.concurrent.ExecutionException}.
*
* <p>Possible exceptions include:
*
* <ul>
* <li>{@link ChatServiceException} for errors related to communication with the server.
* Depending on the concrete subclass, client can retry the operation. See also {@link
* TimeoutException}, {@link ServerSideErrorException}, {@link UnexpectedResponseException},
* {@link AppExpiredException},
* <li>{@link RetryLaterException} when the client is being throttled. Wait for the specified
* amount of time before making new requests.
* <li>{@link NetworkException} when the network operation fails. Clients can retry the call
* after a recommended period.
* <li>{@link NetworkProtocolException} when a high level network protocol fails. For example,
* failure to establish a websocket connection.
* <li>{@link org.signal.libsignal.keytrans.KeyTransparencyException} for errors related to key
* transparency logic. Retrying the search without changing any of the arguments (including
* the state of the store) is unlikely to yield a different result.
* </ul>
*
* @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 {@link CompletableFuture} representing the asynchronous operation, which
* does not produce any value. Successful completion of the operation results in an updated
* state of the store.
* @throws IllegalArgumentException if the store contains corrupted data.
*/
public CompletableFuture<Void> updateDistinguished(final Store store) {
byte[] lastDistinguished = store.getLastDistinguishedTreeHead().orElse(null);
try (NativeHandleGuard tokioContextGuard = this.tokioAsyncContext.guard();
NativeHandleGuard chatConnectionGuard = new NativeHandleGuard(chatConnection)) {
return Native.KeyTransparency_Distinguished(
tokioContextGuard.nativeHandle(),
this.environment.value,
chatConnectionGuard.nativeHandle(),
lastDistinguished)
.thenApply(
bytes -> {
store.setLastDistinguishedTreeHead(bytes);
return null;
});
}
}
/**
* Issue a monitor request to the key transparency service.
*
* <p>Store must contain data associated with the account being requested prior to making this
* call. Another way of putting this is: monitor cannot be called before {@link #search}.
*
* <p>If any of the monitored fields in the server response contain a version that is higher than
* the one currently in the store, the behavior depends on the mode parameter value.
*
* <ul>
* <li>{@code MonitorMode.SELF} - An exception will be thrown, no search request will be issued.
* <li>{@code MonitorMode.OTHER} - A search request will be performed automatically and, if it
* succeeds, the updated account data will be stored.
* </ul>
*
* <p>If the latest distinguished tree head is not present in the store, it will be requested from
* the server prior to performing the search via {@link #updateDistinguished}.
*
* <p>This is an asynchronous operation; all the exceptions occurring during communication with
* the server will be wrapped in {@link java.util.concurrent.ExecutionException}.
*
* <p>Possible exceptions include:
*
* <ul>
* <li>{@link ChatServiceException} for errors related to communication with the server.
* Depending on the concrete subclass, client can retry the operation. See also {@link
* TimeoutException}, {@link ServerSideErrorException}, {@link UnexpectedResponseException},
* {@link AppExpiredException},
* <li>{@link RetryLaterException} when the client is being throttled. Wait for the specified
* amount of time before making new requests.
* <li>{@link NetworkException} when the network operation fails. Clients can retry the call
* after a recommended period.
* <li>{@link NetworkProtocolException} when a high level network protocol fails. For example,
* failure to establish a websocket connection.
* <li>{@link org.signal.libsignal.keytrans.KeyTransparencyException} for errors related to key
* transparency logic, which includes missing required fields in the serialized data.
* Retrying the search without changing any of the arguments (including the state of the
* store) is unlikely to yield a different result.
* <li>{@link org.signal.libsignal.keytrans.VerificationFailedException} indicates a failure to
* verify the data in key transparency server response, such as an incorrect proof or a
* wrong signature.
* </ul>
*
* @param mode Mode of the monitor operation. See {@link MonitorMode}.
* @param aci the ACI of the account to be searched for. Required.
* @param aciIdentityKey {@link IdentityKey} associated with the ACI. Required.
* @param e164 string representation of an E.164 number associated with the account. Optional.
* @param unidentifiedAccessKey unidentified access key for the account. This parameter has the
* same optionality as the E.164 parameter.
* @param usernameHash hash of the username associated with the account. Optional.
* @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 data before performing the
* server request and updated with the latest information from the server response if it
* succeeds.
* @return an instance of {@link CompletableFuture} successful completion of which will indicate
* that the monitor request has succeeded and store has been updated with the latest account
* data.
* @throws IllegalArgumentException if the store contains corrupted data.
*/
public CompletableFuture<Void> monitor(
/* @NotNull */ final MonitorMode mode,
final ServiceId.Aci aci,
/* @NotNull */ final IdentityKey aciIdentityKey,
final String e164,
final byte[] unidentifiedAccessKey,
final byte[] usernameHash,
final Store store) {
Optional<byte[]> lastDistinguishedTreeHead = store.getLastDistinguishedTreeHead();
if (lastDistinguishedTreeHead.isEmpty()) {
return this.updateDistinguished(store)
.thenCompose(
(ignored) ->
this.monitor(
mode, aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, store));
}
try (NativeHandleGuard tokioContextGuard = this.tokioAsyncContext.guard();
NativeHandleGuard identityKeyGuard = aciIdentityKey.getPublicKey().guard();
NativeHandleGuard chatConnectionGuard = new NativeHandleGuard(chatConnection)) {
return Native.KeyTransparency_Monitor(
tokioContextGuard.nativeHandle(),
this.environment.value,
chatConnectionGuard.nativeHandle(),
aci.toServiceIdFixedWidthBinary(),
identityKeyGuard.nativeHandle(),
e164,
unidentifiedAccessKey,
usernameHash,
// Technically this is a required parameter, but passing null
// to generate the error on the Rust side.
store.getAccountData(aci).orElse(null),
lastDistinguishedTreeHead.get(),
mode == MonitorMode.SELF)
.thenApply(
(updatedAccountData) -> {
store.setAccountData(aci, updatedAccountData);
return null;
});
}
}
}

View File

@@ -0,0 +1,315 @@
//
// Copyright 2026 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.net
import org.signal.libsignal.internal.CompletableFuture
import org.signal.libsignal.internal.Native
import org.signal.libsignal.internal.NativeHandleGuard
import org.signal.libsignal.internal.TokioAsyncContext
import org.signal.libsignal.internal.mapWithCancellation
import org.signal.libsignal.keytrans.KeyTransparencyException
import org.signal.libsignal.keytrans.Store
import org.signal.libsignal.keytrans.VerificationFailedException
import org.signal.libsignal.net.KeyTransparency.MonitorMode
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.ServiceId
/**
* Typed API to access the key transparency subsystem using an existing unauthenticated chat
* connection.
*
* Unlike [ChatConnection], key transparency client does not export "raw" send/receive APIs, and
* instead uses them internally to implement high-level operations.
*
* All operations return [RequestResult]. Request-specific failures are represented as
* [RequestResult.NonSuccess] with [KeyTransparencyException]; retryable network errors as
* [RequestResult.RetryableNetworkError].
*
* Note: [Store] APIs may be invoked concurrently. Here are possible strategies to make sure there
* are no thread safety violations:
* - Types implementing [Store] can be made thread safe
* - [KeyTransparencyClient] operations-completed asynchronous calls-can be serialized.
*
* Example usage:
* ```
* val net = Network(Network.Environment.STAGING, "key-transparency-example")
* val chat = net.connectUnauthChat(Listener()).get()
* chat.start()
*
* val client = chat.keyTransparencyClient()
*
* val result = client.search(aci, identityKey, null, null, null, KT_DATA_STORE).get()
* ```
*/
public class KeyTransparencyClient internal constructor(
private val chatConnection: UnauthenticatedChatConnection,
private val tokioAsyncContext: TokioAsyncContext,
private val environment: Network.Environment,
) {
/**
* Search for account information in the key transparency tree.
*
* Only ACI and ACI identity key are required to identify the account.
*
* If the latest distinguished tree head is not present in the store, it will be requested from
* the server prior to performing the search 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,
* [ServerSideErrorException], [NetworkException], [NetworkProtocolException], and
* [TimeoutException].
* - [RequestResult.NonSuccess] with [KeyTransparencyException] for errors related to key
* transparency logic, which includes missing required fields in the serialized data.
* Retrying the search 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, such as providing an [unidentifiedAccessKey] without an [e164].
*
* @param aci the ACI of the account to be searched for. Required.
* @param aciIdentityKey [IdentityKey] associated with the ACI. Required.
* @param e164 string representation of an E.164 number associated with the account. Optional.
* @param unidentifiedAccessKey unidentified access key for the account. This parameter has the
* same optionality as the E.164 parameter.
* @param usernameHash hash of the username associated with the account. Optional.
* @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 data before performing the
* server request and updated with the latest information from the server response if it
* succeeds.
* @return an instance of [CompletableFuture] that completes with a [RequestResult] indicating
* success or containing the error details.
*/
public fun search(
aci: ServiceId.Aci,
aciIdentityKey: IdentityKey,
e164: String?,
unidentifiedAccessKey: ByteArray?,
usernameHash: ByteArray?,
store: Store,
): CompletableFuture<RequestResult<Unit, KeyTransparencyException>> {
val lastDistinguishedTreeHead =
try {
store.lastDistinguishedTreeHead
} catch (t: Throwable) {
return CompletableFuture.completedFuture(RequestResult.ApplicationError(t))
}
if (lastDistinguishedTreeHead.isEmpty) {
return updateDistinguished(store).thenCompose { result ->
when (result) {
is RequestResult.Success ->
search(aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, store)
else -> CompletableFuture.completedFuture(result)
}
}
}
return try {
NativeHandleGuard(tokioAsyncContext).use { tokioContextGuard ->
NativeHandleGuard(aciIdentityKey.publicKey).use { identityKeyGuard ->
NativeHandleGuard(chatConnection).use { chatConnectionGuard ->
Native
.KeyTransparency_Search(
tokioContextGuard.nativeHandle(),
environment.value,
chatConnectionGuard.nativeHandle(),
aci.toServiceIdFixedWidthBinary(),
identityKeyGuard.nativeHandle(),
e164,
unidentifiedAccessKey,
usernameHash,
store.getAccountData(aci).orElse(null),
lastDistinguishedTreeHead.get(),
).mapWithCancellation(
onSuccess = { accountData ->
try {
store.setAccountData(aci, accountData)
RequestResult.Success(Unit)
} catch (t: Throwable) {
RequestResult.ApplicationError(t)
}
},
onError = { err -> err.toRequestResult<KeyTransparencyException>() },
)
}
}
}
} catch (t: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(t))
}
}
/**
* 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))
}
}
/**
* Issue a monitor request to the key transparency service.
*
* Store must contain data associated with the account being requested prior to making this call.
* Another way of putting this is: monitor cannot be called before [search].
*
* If any of the monitored fields in the server response contain a version that is higher than
* the one currently in the store, the behavior depends on the mode parameter value.
* - [MonitorMode.SELF] - A [KeyTransparencyException] will be returned, no search request will
* be issued.
* - [MonitorMode.OTHER] - A 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,
* [ServerSideErrorException], [NetworkException], [NetworkProtocolException], and
* [TimeoutException].
* - [RequestResult.NonSuccess] with [KeyTransparencyException] for errors related to key
* transparency logic, which includes missing required fields in the serialized data.
* 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, such as providing an [unidentifiedAccessKey] without an [e164].
*
* @param mode Mode of the monitor operation. See [MonitorMode].
* @param aci the ACI of the account to be searched for. Required.
* @param aciIdentityKey [IdentityKey] associated with the ACI. Required.
* @param e164 string representation of an E.164 number associated with the account. Optional.
* @param unidentifiedAccessKey unidentified access key for the account. This parameter has the
* same optionality as the E.164 parameter.
* @param usernameHash hash of the username associated with the account. Optional.
* @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 data before performing the
* server request and updated with the latest information from the server response if it
* succeeds.
* @return an instance of [CompletableFuture] that completes with a [RequestResult] indicating
* success or containing the error details.
*/
public fun monitor(
mode: MonitorMode,
aci: ServiceId.Aci,
aciIdentityKey: IdentityKey,
e164: String?,
unidentifiedAccessKey: ByteArray?,
usernameHash: ByteArray?,
store: Store,
): CompletableFuture<RequestResult<Unit, KeyTransparencyException>> {
val lastDistinguishedTreeHead =
try {
store.lastDistinguishedTreeHead
} catch (t: Throwable) {
return CompletableFuture.completedFuture(RequestResult.ApplicationError(t))
}
if (lastDistinguishedTreeHead.isEmpty) {
return updateDistinguished(store).thenCompose { result ->
when (result) {
is RequestResult.Success ->
monitor(mode, aci, aciIdentityKey, e164, unidentifiedAccessKey, usernameHash, store)
else -> CompletableFuture.completedFuture(result)
}
}
}
return try {
NativeHandleGuard(tokioAsyncContext).use { tokioContextGuard ->
NativeHandleGuard(aciIdentityKey.publicKey).use { identityKeyGuard ->
NativeHandleGuard(chatConnection).use { chatConnectionGuard ->
Native
.KeyTransparency_Monitor(
tokioContextGuard.nativeHandle(),
environment.value,
chatConnectionGuard.nativeHandle(),
aci.toServiceIdFixedWidthBinary(),
identityKeyGuard.nativeHandle(),
e164,
unidentifiedAccessKey,
usernameHash,
// Technically this is a required parameter, but passing null
// to generate the error on the Rust side.
store.getAccountData(aci).orElse(null),
lastDistinguishedTreeHead.get(),
mode == MonitorMode.SELF,
).mapWithCancellation(
onSuccess = { updatedAccountData ->
try {
store.setAccountData(aci, updatedAccountData)
RequestResult.Success(Unit)
} catch (t: Throwable) {
RequestResult.ApplicationError(t)
}
},
onError = { err -> err.toRequestResult<KeyTransparencyException>() },
)
}
}
}
} catch (t: Throwable) {
CompletableFuture.completedFuture(RequestResult.ApplicationError(t))
}
}
}

View File

@@ -15,7 +15,6 @@ import org.signal.libsignal.net.KeyTransparency.MonitorMode
import org.signal.libsignal.util.TestEnvironment
import java.util.Deque
import java.util.concurrent.ExecutionException
import kotlin.test.assertFailsWith
import kotlin.test.assertIs
private fun <T> retryImpl(
@@ -65,6 +64,9 @@ class KeyTransparencyClientTest {
KeyTransparencyTest.TEST_USERNAME_HASH,
store,
).get()
.also {
assertIs<RequestResult.Success<*>>(it)
}
Assert.assertTrue(store.getLastDistinguishedTreeHead().isPresent)
Assert.assertTrue(store.getAccountData(KeyTransparencyTest.TEST_ACI).isPresent)
@@ -79,7 +81,9 @@ class KeyTransparencyClientTest {
val ktClient = connectAndGetClient(net).get()
val store = TestStore()
ktClient.updateDistinguished(store).get()
ktClient.updateDistinguished(store).get().also {
assertIs<RequestResult.Success<*>>(it)
}
Assert.assertTrue(store.getLastDistinguishedTreeHead().isPresent)
}
@@ -103,6 +107,9 @@ class KeyTransparencyClientTest {
KeyTransparencyTest.TEST_USERNAME_HASH,
store,
).get()
.also {
assertIs<RequestResult.Success<*>>(it)
}
val accountDataHistory: Deque<ByteArray?> = store.storage.get(KeyTransparencyTest.TEST_ACI)!!
@@ -119,6 +126,9 @@ class KeyTransparencyClientTest {
KeyTransparencyTest.TEST_USERNAME_HASH,
store,
).get()
.also {
assertIs<RequestResult.Success<*>>(it)
}
// Another entry in the account history after a successful monitor request
Assert.assertEquals(2, accountDataHistory.size.toLong())
}
@@ -136,7 +146,7 @@ class KeyTransparencyClientTest {
// Call to monitor before any data has been persisted in the store.
// Distinguished tree will be requested from the server, but it will fail
// due to account data missing.
try {
val result =
ktClient
.monitor(
MonitorMode.SELF,
@@ -147,12 +157,12 @@ class KeyTransparencyClientTest {
KeyTransparencyTest.TEST_USERNAME_HASH,
store,
).get()
} catch (e: ExecutionException) {
Assert.assertTrue(e.cause is KeyTransparencyException)
}
val nonSuccess = assertIs<RequestResult.NonSuccess<KeyTransparencyException>>(result)
assertIs<KeyTransparencyException>(nonSuccess.error)
}
inline fun <reified E> networkExceptionsTestImpl(
inline fun <reified E : Throwable> retryableNetworkExceptionsTestImpl(
statusCode: Int,
message: String = "",
headers: Array<String> = arrayOf(),
@@ -171,17 +181,42 @@ class KeyTransparencyClientTest {
val (_, requestId) = remote.getNextIncomingRequest().get()
remote.sendResponse(requestId, statusCode, message, headers, byteArrayOf())
val exception = assertFailsWith<ExecutionException> { responseFuture.get() }
assertIs<E>(exception.cause)
val result = responseFuture.get()
val retryable = assertIs<RequestResult.RetryableNetworkError>(result)
assertIs<E>(retryable.networkError)
}
inline fun <reified E : Throwable> applicationErrorTestImpl(
statusCode: Int,
message: String = "",
headers: Array<String> = arrayOf(),
) {
val tokio = TokioAsyncContext()
val (chat, remote) =
UnauthenticatedChatConnection.fakeConnect(
tokio,
NoOpListener(),
Network.Environment.STAGING,
)
val store = TestStore()
val responseFuture = chat.keyTransparencyClient().updateDistinguished(store)
val (_, requestId) = remote.getNextIncomingRequest().get()
remote.sendResponse(requestId, statusCode, message, headers, byteArrayOf())
val result = responseFuture.get()
val appError = assertIs<RequestResult.ApplicationError>(result)
assertIs<E>(appError.cause)
}
@Test
@Throws(ExecutionException::class, InterruptedException::class)
fun networkExceptions() {
networkExceptionsTestImpl<RetryLaterException>(429, headers = arrayOf("retry-after: 42"))
networkExceptionsTestImpl<ServerSideErrorException>(500)
retryableNetworkExceptionsTestImpl<RetryLaterException>(429, headers = arrayOf("retry-after: 42"))
retryableNetworkExceptionsTestImpl<ServerSideErrorException>(500)
// 429 without the retry-after is unexpected
networkExceptionsTestImpl<UnexpectedResponseException>(429)
applicationErrorTestImpl<UnexpectedResponseException>(429)
}
companion object {

13
node/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@signalapp/libsignal-client",
"version": "0.87.6",
"version": "0.88.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@signalapp/libsignal-client",
"version": "0.87.6",
"version": "0.88.0",
"hasInstallScript": true,
"license": "AGPL-3.0-only",
"dependencies": {
@@ -873,7 +873,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.0.tgz",
"integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.44.0",
"@typescript-eslint/types": "8.44.0",
@@ -1071,7 +1070,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1474,7 +1472,6 @@
"resolved": "https://registry.npmjs.org/chai/-/chai-6.0.1.tgz",
"integrity": "sha512-/JOoU2//6p5vCXh00FpNgtlw0LjvhGttaWc+y7wpW9yjBm3ys0dI8tSKZxIOgNruz5J0RleccatSIC3uxEZP0g==",
"dev": true,
"peer": true,
"engines": {
"node": ">=18"
}
@@ -2070,7 +2067,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz",
"integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -2230,7 +2226,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -4401,7 +4396,6 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin-prettier.js"
},
@@ -5125,7 +5119,6 @@
"resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz",
"integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==",
"dev": true,
"peer": true,
"dependencies": {
"@sinonjs/commons": "^3.0.1",
"@sinonjs/fake-timers": "^13.0.5",
@@ -5549,7 +5542,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"peer": true,
"engines": {
"node": ">=12"
},
@@ -5732,7 +5724,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@@ -1,6 +1,6 @@
{
"name": "@signalapp/libsignal-client",
"version": "0.87.6",
"version": "0.88.0",
"repository": "github:signalapp/libsignal",
"license": "AGPL-3.0-only",
"type": "module",

View File

@@ -5,4 +5,4 @@
// The value of this constant is updated by the script
// and should not be manually modified
pub const VERSION: &str = "0.87.6";
pub const VERSION: &str = "0.88.0";