From b3e3eda4f73a1e010135d1a236c32428dfcf11d2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 30 Jul 2026 21:44:34 +0200 Subject: [PATCH] Use upstream provisioning functionality --- gradle/libs.versions.toml | 4 +- lib/build.gradle.kts | 1 + .../signal/manager/helper/AccountHelper.java | 15 +- .../signal/manager/internal/ManagerImpl.java | 29 +-- .../internal/ProvisioningManagerImpl.java | 202 ++++++++++++++---- .../signal/manager/storage/SignalAccount.java | 4 +- .../org/asamk/signal/manager/util/Utils.java | 53 +---- libsignal-version | 2 +- .../java/org/asamk/signal/BaseConfig.java | 2 +- .../signal-cli/reachability-metadata.json | 106 ++++++++- 10 files changed, 302 insertions(+), 116 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9a1d808..1e23dcf0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,10 @@ [versions] slf4j = "2.0.18" +coroutines = "1.10.2" junit = "6.1.2" micronaut-json-schema = "2.1.0" micronaut-core = "5.1.10" -signal-service = "2.15.3_unofficial_150" +signal-service = "2.15.3_unofficial_151" [libraries] bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.85" @@ -19,6 +20,7 @@ slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } logback = "ch.qos.logback:logback-classic:1.6.1" +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" } sqlite = "org.xerial:sqlite-jdbc:3.53.2.1" hikari = "com.zaxxer:HikariCP:7.1.0" diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 510482e8..b5e7f315 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { implementation(libs.slf4j.api) implementation(libs.sqlite) implementation(libs.hikari) + compileOnly(libs.kotlinx.coroutines.core) testImplementation(libs.junit.jupiter) testImplementation(platform(libs.junit.jupiter.bom)) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java index 269fe9cc..4ea58220 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java @@ -17,6 +17,7 @@ import org.asamk.signal.manager.util.Utils; import org.signal.core.models.ServiceId.ACI; import org.signal.core.models.ServiceId.PNI; import org.signal.core.util.Base64; +import org.signal.core.util.crypto.DeviceNameCipher; import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.NoSessionException; @@ -40,7 +41,6 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; -import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException; import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity; import org.whispersystems.signalservice.internal.push.OutgoingPushMessage; @@ -48,6 +48,7 @@ import org.whispersystems.signalservice.internal.push.SyncMessage; import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -519,18 +520,22 @@ public class AccountHelper { } public void setDeviceName(String deviceName) { - final var privateKey = account.getAciIdentityKeyPair().getPrivateKey(); - final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); + final var encryptedDeviceName = getEncryptedDeviceName(deviceName); account.setEncryptedDeviceName(encryptedDeviceName); } public void setDeviceName(int deviceId, String deviceName) throws IOException { - final var privateKey = account.getAciIdentityKeyPair().getPrivateKey(); - final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); + final var encryptedDeviceName = getEncryptedDeviceName(deviceName); handleResponseException(dependencies.getLinkDeviceApi().setDeviceName(encryptedDeviceName, deviceId)); context.getSyncHelper().sendDeviceNameChange(deviceId); } + private String getEncryptedDeviceName(final String deviceName) { + final var identityKey = account.getAciIdentityKeyPair(); + return Base64.encodeWithoutPadding(DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), + identityKey)); + } + public void refreshDeviceName() throws IOException { final List devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi() .getDevices(cont)); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java index bd97372e..a7ef717a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ManagerImpl.java @@ -98,8 +98,10 @@ import org.asamk.signal.manager.util.StickerUtils; import org.signal.core.models.ServiceId; import org.signal.core.models.ServiceId.ACI; import org.signal.core.models.ServiceId.PNI; -import org.signal.core.util.Base64; import org.signal.core.util.Hex; +import org.signal.core.util.crypto.DeviceName; +import org.signal.core.util.crypto.DeviceNameCipher; +import org.signal.libsignal.net.LinkedDevice; import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.NoSessionException; import org.signal.libsignal.usernames.BaseUsernameException; @@ -120,10 +122,8 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage; import org.whispersystems.signalservice.api.messages.calls.OfferMessage; import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhaustedException; -import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.internal.util.Util; @@ -484,24 +484,26 @@ public class ManagerImpl implements Manager { @Override public List getLinkedDevices() throws IOException { - final List devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi() + final List devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi() .getDevices(cont)); account.setMultiDevice(devices.size() > 1); - var identityKey = account.getAciIdentityKeyPair().getPrivateKey(); + var identityKey = account.getAciIdentityKeyPair(); return devices.stream().map(d -> { - String deviceName = d.getName(); - if (deviceName != null) { + String deviceName = null; + if (d.getEncryptedName() != null && d.getEncryptedName().length > 0) { try { - deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey); - } catch (IOException e) { + deviceName = new String(DeviceNameCipher.decryptDeviceName(DeviceName.ADAPTER.decode(d.getEncryptedName()), + identityKey), StandardCharsets.UTF_8); + } catch (Exception e) { logger.debug("Failed to decrypt device name, maybe plain text?", e); + deviceName = new String(d.getEncryptedName(), StandardCharsets.UTF_8); } } final var createdAt = getPlaintextCreatedAt(d); return new Device(d.getId(), deviceName, createdAt == null ? 0 : createdAt, - d.getLastSeen(), + d.getLastSeen().toEpochMilli(), d.getId() == account.getDeviceId()); }).toList(); } @@ -521,7 +523,7 @@ public class ManagerImpl implements Manager { } } - private Long getPlaintextCreatedAt(DeviceInfo d) { + private Long getPlaintextCreatedAt(LinkedDevice d) { final var DECRYPTION_INFO = "deviceCreatedAt"; var identityKey = account.getAciIdentityKeyPair().getPrivateKey(); try { @@ -529,8 +531,9 @@ public class ManagerImpl implements Manager { var associatedData = new ByteArrayOutputStream(); associatedData.write(d.getId()); associatedData.write(ByteBuffer.allocate(4).putInt(d.getRegistrationId()).array()); - var createdAtPlaintext = identityKey.open(Base64.decode(d.getCreatedAtCiphertext() - .getBytes(StandardCharsets.UTF_8)), DECRYPTION_INFO, associatedData.toByteArray()); + var createdAtPlaintext = identityKey.open(d.getCreatedAtCiphertext(), + DECRYPTION_INFO, + associatedData.toByteArray()); return ByteBuffer.wrap(createdAtPlaintext).getLong(); } catch (Exception e) { logger.warn("Failed while reading the protobuf.", e); diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java index b90e08c3..b438a39b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java @@ -19,34 +19,57 @@ package org.asamk.signal.manager.internal; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.ProvisioningManager; import org.asamk.signal.manager.Settings; -import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.UserAlreadyExistsException; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.accounts.AccountsStore; import org.asamk.signal.manager.util.KeyUtils; +import org.signal.core.models.AccountEntropyPool; +import org.signal.core.models.ServiceId.ACI; +import org.signal.core.models.ServiceId.PNI; +import org.signal.core.models.backup.MediaRootBackupKey; +import org.signal.core.util.crypto.DeviceNameCipher; +import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKeyPair; +import org.signal.libsignal.protocol.ecc.ECPrivateKey; +import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.account.DeviceAttributes; +import org.whispersystems.signalservice.api.provisioning.ProvisioningSocket; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; -import org.whispersystems.signalservice.api.registration.ProvisioningApi; -import org.whispersystems.signalservice.api.util.DeviceNameUtil; -import org.whispersystems.signalservice.internal.push.ProvisioningSocket; -import org.whispersystems.signalservice.internal.push.PushServiceSocket; -import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; +import org.whispersystems.signalservice.internal.crypto.SecondaryProvisioningCipher; +import org.whispersystems.signalservice.internal.push.ProvisionMessage; +import java.io.Closeable; import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; import java.nio.channels.OverlappingFileLockException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; -import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType; +import kotlin.ResultKt; +import kotlin.Unit; +import kotlin.coroutines.Continuation; +import kotlin.coroutines.EmptyCoroutineContext; +import kotlin.coroutines.intrinsics.IntrinsicsKt; +import kotlin.jvm.functions.Function3; +import kotlinx.coroutines.BuildersKt; +import kotlinx.coroutines.CoroutineScope; -public class ProvisioningManagerImpl implements ProvisioningManager { +import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType; +import static org.asamk.signal.manager.util.Utils.handleResponseException; + +public class ProvisioningManagerImpl implements ProvisioningManager, Closeable { private static final Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class); @@ -56,9 +79,10 @@ public class ProvisioningManagerImpl implements ProvisioningManager { private final Consumer newManagerListener; private final AccountsStore accountsStore; - private final ProvisioningApi provisioningApi; - private final IdentityKeyPair tempIdentityKey; private final String password; + private final CompletableFuture urlFuture = new CompletableFuture<>(); + private final CompletableFuture> messageFuture = new CompletableFuture<>(); + private final Closeable socketHandle; public ProvisioningManagerImpl( PathConfig pathConfig, @@ -73,35 +97,58 @@ public class ProvisioningManagerImpl implements ProvisioningManager { this.newManagerListener = newManagerListener; this.accountsStore = accountsStore; - tempIdentityKey = KeyUtils.generateIdentityKeyPair(); + final IdentityKeyPair tempIdentityKey = KeyUtils.generateIdentityKeyPair(); password = KeyUtils.createPassword(); - final var credentialsProvider = new DynamicCredentialsProvider(null, - null, - null, - password, - SignalServiceAddress.DEFAULT_DEVICE_ID); - final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(), - credentialsProvider, - userAgent, - ServiceConfig.AUTOMATIC_NETWORK_RETRY); - final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(), - userAgent); - this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider); + + socketHandle = ProvisioningSocket.Companion.start(new ProvisioningSocket.Mode.Link(false), + tempIdentityKey, + serviceEnvironmentConfig.signalServiceConfiguration(), + (id, t) -> { + urlFuture.completeExceptionally(t); + messageFuture.completeExceptionally(t); + }, + new ProvisioningBlock()); } @Override public URI getDeviceLinkUri() throws TimeoutException, IOException { - var deviceUuid = provisioningApi.getNewDeviceUuid(); - - return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri(); + try { + var url = urlFuture.get(30, TimeUnit.SECONDS); + return new URI(url); + } catch (java.util.concurrent.TimeoutException e) { + throw new TimeoutException("Timed out waiting for provisioning URL"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting for provisioning URL", e); + } catch (ExecutionException e) { + throw new IOException("Failed to get provisioning URL", e.getCause()); + } catch (URISyntaxException e) { + throw new IOException("Invalid provisioning URL", e); + } } @Override public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException { - var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey); - var number = ret.getNumber(); - var aci = ret.getAci(); - var pni = ret.getPni(); + SecondaryProvisioningCipher.ProvisioningDecryptResult decryptResult; + try { + decryptResult = messageFuture.get(120, TimeUnit.SECONDS); + } catch (java.util.concurrent.TimeoutException e) { + throw new TimeoutException("Timed out waiting for provisioning message"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting for provisioning message", e); + } catch (ExecutionException e) { + throw new IOException("Failed to receive provisioning message", e.getCause()); + } + + if (!(decryptResult instanceof SecondaryProvisioningCipher.ProvisioningDecryptResult.Success success)) { + throw new IOException("Failed to decrypt provisioning message"); + } + var msg = success.getMessage(); + + var number = msg.number; + var aci = ACI.parseOrThrow(msg.aci, msg.aciBinary); + var pni = PNI.parseOrThrow(msg.pni, msg.pniBinary); logger.info("Received link information from {}, linking in progress ...", number); @@ -120,11 +167,28 @@ public class ProvisioningManagerImpl implements ProvisioningManager { accountsStore.updateAccount(accountPath, number, aci); } + final IdentityKeyPair aciIdentity; + final IdentityKeyPair pniIdentity; + final ProfileKey profileKey; + try { + aciIdentity = new IdentityKeyPair(new IdentityKey(msg.aciIdentityKeyPublic.toByteArray()), + new ECPrivateKey(msg.aciIdentityKeyPrivate.toByteArray())); + pniIdentity = new IdentityKeyPair(new IdentityKey(msg.pniIdentityKeyPublic.toByteArray()), + new ECPrivateKey(msg.pniIdentityKeyPrivate.toByteArray())); + profileKey = msg.profileKey == null + ? KeyUtils.createProfileKey() + : new ProfileKey(msg.profileKey.toByteArray()); + } catch (Exception e) { + throw new IOException("Invalid key material in provisioning message", e); + } + var encryptedDeviceName = deviceName == null ? null - : DeviceNameUtil.encryptDeviceName(deviceName, ret.getAciIdentity().getPrivateKey()); - // Create new account with the synced identity - var profileKey = ret.getProfileKey() == null ? KeyUtils.createProfileKey() : ret.getProfileKey(); + : DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), aciIdentity); + var accountEntropyPool = msg.accountEntropyPool == null ? null : new AccountEntropyPool(msg.accountEntropyPool); + var mediaRootBackupKey = msg.mediaRootBackupKey == null + ? null + : new MediaRootBackupKey(msg.mediaRootBackupKey.toByteArray()); SignalAccount account = null; try { @@ -142,22 +206,39 @@ public class ProvisioningManagerImpl implements ProvisioningManager { pni, password, encryptedDeviceName, - ret.getAciIdentity(), - ret.getPniIdentity(), + aciIdentity, + pniIdentity, profileKey, - ret.getAccountEntropyPool(), - ret.getMediaRootBackupKey()); + accountEntropyPool, + mediaRootBackupKey); - account.getConfigurationStore().setReadReceipts(ret.isReadReceipts()); + if (Boolean.TRUE.equals(msg.readReceipts)) { + account.getConfigurationStore().setReadReceipts(true); + } final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI)); final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI)); logger.debug("Finishing new device registration"); - var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(), - account.getAccountAttributes(null), - aciPreKeys, - pniPreKeys); + final var attrs = account.getAccountAttributes(null); + final var deviceAttributes = new DeviceAttributes(attrs.getFetchesMessages(), + attrs.getRegistrationId(), + attrs.getPniRegistrationId(), + attrs.getName(), + attrs.getCapabilities()); + final var unauthAccountManager = SignalServiceAccountManager.createWithStaticCredentials( + serviceEnvironmentConfig.signalServiceConfiguration(), + null, + null, + number, + SignalServiceAddress.DEFAULT_DEVICE_ID, + password, + userAgent, + ServiceConfig.AUTOMATIC_NETWORK_RETRY, + ServiceConfig.GROUP_MAX_SIZE); + final var registerResponse = handleResponseException(unauthAccountManager.getRegistrationApi() + .registerAsSecondaryDevice(msg.provisioningCode, deviceAttributes, aciPreKeys, pniPreKeys, null)); + final var deviceId = Integer.parseInt(registerResponse.getDeviceId()); account.finishLinking(deviceId, aciPreKeys, pniPreKeys); @@ -203,6 +284,11 @@ public class ProvisioningManagerImpl implements ProvisioningManager { } } + @Override + public void close() throws IOException { + socketHandle.close(); + } + private boolean canRelinkExistingAccount(final String accountPath) throws IOException { final SignalAccount signalAccount; try { @@ -243,4 +329,36 @@ public class ProvisioningManagerImpl implements ProvisioningManager { return false; } } + + private class ProvisioningBlock implements Function3, Continuation, Object> { + + @Override + public Object invoke( + CoroutineScope scope, + ProvisioningSocket socket, + Continuation cont + ) { + Thread.ofVirtual().start(() -> { + try { + urlFuture.complete(BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (s, c) -> socket.getProvisioningUrl(c))); + messageFuture.complete(BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (s, c) -> socket.getProvisioningMessageDecryptResult(c))); + cont.resumeWith(Unit.INSTANCE); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + failBoth(new RuntimeException(e), cont); + } catch (Throwable t) { + failBoth(t, cont); + } + }); + return IntrinsicsKt.getCOROUTINE_SUSPENDED(); + } + + private void failBoth(Throwable t, Continuation cont) { + urlFuture.completeExceptionally(t); + messageFuture.completeExceptionally(t); + cont.resumeWith(ResultKt.createFailure(t)); + } + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 014d658d..5fc7130f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -292,7 +292,7 @@ public class SignalAccount implements Closeable { final ACI aci, final PNI pni, final String password, - final String encryptedDeviceName, + final byte[] encryptedDeviceName, final IdentityKeyPair aciIdentity, final IdentityKeyPair pniIdentity, final ProfileKey profileKey, @@ -307,7 +307,7 @@ public class SignalAccount implements Closeable { getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress()); this.password = password; this.profileKey = profileKey; - this.encryptedDeviceName = encryptedDeviceName; + this.encryptedDeviceName = org.signal.core.util.Base64.encodeWithoutPadding(encryptedDeviceName); this.aciAccountData.setIdentityKeyPair(aciIdentity); this.pniAccountData.setIdentityKeyPair(pniIdentity); this.registered = false; diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index 314d8426..27ba298b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -31,8 +31,6 @@ import java.util.Map; import java.util.Optional; import java.util.Spliterator; import java.util.Spliterators; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; @@ -40,11 +38,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import kotlin.ResultKt; import kotlin.coroutines.Continuation; -import kotlin.coroutines.CoroutineContext; import kotlin.coroutines.EmptyCoroutineContext; -import kotlin.coroutines.intrinsics.IntrinsicsKt; +import kotlinx.coroutines.BuildersKt; import okio.ByteString; public class Utils { @@ -167,58 +163,15 @@ public class Utils { return NetworkResultUtil.toBasicLegacy(response); } - /** - * Bridge Kotlin suspend functions for Java callers. - */ @SuppressWarnings("unchecked") public static T runSuspendBlocking(final Function, Object> call) { - final var value = new AtomicReference(); - final var throwable = new AtomicReference(); - final var done = new CountDownLatch(1); - - final var immediate = call.apply(new Continuation<>() { - @Override - public CoroutineContext getContext() { - return EmptyCoroutineContext.INSTANCE; - } - - @Override - public void resumeWith(final Object result) { - try { - ResultKt.throwOnFailure(result); - value.set((T) result); - } catch (Throwable t) { - throwable.set(t); - } finally { - done.countDown(); - } - } - }); - - if (immediate != IntrinsicsKt.getCOROUTINE_SUSPENDED()) { - ResultKt.throwOnFailure(immediate); - return (T) immediate; - } - try { - done.await(); + return (T) BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, + (scope, cont) -> call.apply((Continuation) cont)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Interrupted while waiting for suspend function", e); } - - final var t = throwable.get(); - if (t instanceof RuntimeException e) { - throw e; - } - if (t instanceof Error e) { - throw e; - } - if (t != null) { - throw new RuntimeException(t); - } - - return value.get(); } @SuppressWarnings("unchecked") diff --git a/libsignal-version b/libsignal-version index 24d0d3e3..626c498f 100644 --- a/libsignal-version +++ b/libsignal-version @@ -1 +1 @@ -0.96.3 +0.99.1 diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java index c6d9b40f..8b818897 100644 --- a/src/main/java/org/asamk/signal/BaseConfig.java +++ b/src/main/java/org/asamk/signal/BaseConfig.java @@ -8,7 +8,7 @@ public class BaseConfig { public static final String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); static final String USER_AGENT_SIGNAL_ANDROID = Optional.ofNullable(System.getenv("SIGNAL_CLI_USER_AGENT")) - .orElse("Signal-Android/8.15.0"); + .orElse("Signal-Android/8.21.1"); static final String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + "/" + PROJECT_VERSION; diff --git a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json index 2a06aadf..b9ae5d4b 100644 --- a/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/org.asamk/signal-cli/reachability-metadata.json @@ -1163,7 +1163,8 @@ "type": "java.lang.Integer", "allDeclaredFields": true, "allDeclaredMethods": true, - "allDeclaredConstructors": true + "allDeclaredConstructors": true, + "jniAccessible": true }, { "type": "java.lang.Iterable", @@ -1198,6 +1199,7 @@ }, { "type": "java.lang.Object", + "jniAccessible": true, "methods": [ { "name": "equals", @@ -6142,9 +6144,30 @@ } ] }, + { + "type": "org.signal.libsignal.internal.LinkedDeviceInternal", + "jniAccessible": true, + "methods": [ + { + "name": "fromNative", + "parameterTypes": [ + "java.lang.Object", + "java.lang.Object", + "java.lang.Object", + "java.lang.Object", + "java.lang.Object" + ] + } + ] + }, { "type": "org.signal.libsignal.internal.NativeHandleGuard$SimpleOwner", "jniAccessible": true, + "fields": [ + { + "name": "nativeHandle" + } + ], "methods": [ { "name": "unsafeNativeHandleWithoutGuard", @@ -6152,6 +6175,9 @@ } ] }, + { + "type": "org.signal.libsignal.net.AuthenticatedChatConnection" + }, { "type": "org.signal.libsignal.net.CdsiLookupResponse", "jniAccessible": true, @@ -8053,6 +8079,31 @@ } ] }, + { + "type": "org.whispersystems.signalservice.api.account.DeviceAttributes", + "methods": [ + { + "name": "getCapabilities", + "parameterTypes": [] + }, + { + "name": "getFetchesMessages", + "parameterTypes": [] + }, + { + "name": "getName", + "parameterTypes": [] + }, + { + "name": "getPniRegistrationId", + "parameterTypes": [] + }, + { + "name": "getRegistrationId", + "parameterTypes": [] + } + ] + }, { "type": "org.whispersystems.signalservice.api.groupsv2.CredentialResponse", "allDeclaredFields": true, @@ -8179,6 +8230,26 @@ "allDeclaredMethods": true, "allDeclaredConstructors": true }, + { + "type": "org.whispersystems.signalservice.api.messages.multidevice.RegisterAsSecondaryDeviceResponse", + "fields": [ + { + "name": "deviceId" + }, + { + "name": "pni" + }, + { + "name": "uuid" + } + ], + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, { "type": "org.whispersystems.signalservice.api.profiles.SignalServiceProfile", "allDeclaredFields": true, @@ -9079,6 +9150,39 @@ "allDeclaredMethods": true, "allDeclaredConstructors": true }, + { + "type": "org.whispersystems.signalservice.internal.push.RegisterAsSecondaryDeviceRequest", + "methods": [ + { + "name": "getAccountAttributes", + "parameterTypes": [] + }, + { + "name": "getAciPqLastResortPreKey", + "parameterTypes": [] + }, + { + "name": "getAciSignedPreKey", + "parameterTypes": [] + }, + { + "name": "getGcmToken", + "parameterTypes": [] + }, + { + "name": "getPniPqLastResortPreKey", + "parameterTypes": [] + }, + { + "name": "getPniSignedPreKey", + "parameterTypes": [] + }, + { + "name": "getVerificationCode", + "parameterTypes": [] + } + ] + }, { "type": "org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson", "allDeclaredFields": true,