mirror of
https://github.com/signalapp/libsignal.git
synced 2026-04-25 17:25:18 +02:00
java: Expose KeyTransparency return values as RequestResult.
This commit is contained in:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
v0.87.6
|
||||
v0.88.0
|
||||
|
||||
- java: KeyTransparencyClient now returns RequestResult types
|
||||
|
||||
@@ -23,7 +23,7 @@ repositories {
|
||||
}
|
||||
|
||||
allprojects {
|
||||
version = "0.87.6"
|
||||
version = "0.88.0"
|
||||
group = "org.signal"
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
13
node/package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user