diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 58a60d13..400aebb3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ slf4j = "2.0.18" junit = "6.1.0" micronaut-json-schema = "2.0.1" micronaut-core = "5.0.0" -signal-service = "2.15.3_unofficial_149" +signal-service = "2.15.3_unofficial_150" [libraries] bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java index 9f09f81c..0adbc7b4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java @@ -3,15 +3,15 @@ package org.asamk.signal.manager.config; import org.signal.libsignal.net.Network.Environment; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.ecc.ECPublicKey; -import org.whispersystems.signalservice.api.push.TrustStore; -import org.whispersystems.signalservice.internal.configuration.HttpProxy; -import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; -import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl; -import org.whispersystems.signalservice.internal.configuration.SignalProxy; -import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; -import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; -import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; -import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url; +import org.signal.network.config.HttpProxy; +import org.signal.network.config.SignalCdnUrl; +import org.signal.network.config.SignalCdsiUrl; +import org.signal.network.config.SignalProxy; +import org.signal.network.config.SignalServiceConfiguration; +import org.signal.network.config.SignalServiceUrl; +import org.signal.network.config.SignalStorageUrl; +import org.signal.network.config.SignalSvr2Url; +import org.signal.network.config.TrustStore; import java.util.Base64; import java.util.List; diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceEnvironmentConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceEnvironmentConfig.java index b8265191..dad06c7e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceEnvironmentConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceEnvironmentConfig.java @@ -3,7 +3,7 @@ package org.asamk.signal.manager.config; import org.asamk.signal.manager.api.ServiceEnvironment; import org.signal.libsignal.net.Network; import org.signal.libsignal.protocol.ecc.ECPublicKey; -import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.signal.network.config.SignalServiceConfiguration; import java.util.List; diff --git a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java index 19522b28..40ce0569 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java @@ -3,15 +3,15 @@ package org.asamk.signal.manager.config; import org.signal.libsignal.net.Network; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.ecc.ECPublicKey; -import org.whispersystems.signalservice.api.push.TrustStore; -import org.whispersystems.signalservice.internal.configuration.HttpProxy; -import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; -import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl; -import org.whispersystems.signalservice.internal.configuration.SignalProxy; -import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; -import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; -import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; -import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url; +import org.signal.network.config.HttpProxy; +import org.signal.network.config.SignalCdnUrl; +import org.signal.network.config.SignalCdsiUrl; +import org.signal.network.config.SignalProxy; +import org.signal.network.config.SignalServiceConfiguration; +import org.signal.network.config.SignalServiceUrl; +import org.signal.network.config.SignalStorageUrl; +import org.signal.network.config.SignalSvr2Url; +import org.signal.network.config.TrustStore; import java.util.Base64; import java.util.List; diff --git a/lib/src/main/java/org/asamk/signal/manager/config/WhisperTrustStore.java b/lib/src/main/java/org/asamk/signal/manager/config/WhisperTrustStore.java index 7add1d79..831265ec 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/WhisperTrustStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/WhisperTrustStore.java @@ -1,6 +1,6 @@ package org.asamk.signal.manager.config; -import org.whispersystems.signalservice.api.push.TrustStore; +import org.signal.network.config.TrustStore; import java.io.InputStream; 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 65d27002..269fe9cc 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 @@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; @@ -58,6 +59,7 @@ import okio.ByteString; import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID; import static org.asamk.signal.manager.util.Utils.handleResponseException; +import static org.asamk.signal.manager.util.Utils.handleResponseExceptionSuspend; import static org.whispersystems.signalservice.internal.util.Util.isEmpty; public class AccountHelper { @@ -508,8 +510,9 @@ public class AccountHelper { } } + @SuppressWarnings("unchecked") public void deleteUsername() throws IOException { - handleResponseException(dependencies.getAccountApi().deleteUsername()); + handleResponseException(dependencies.getAccountApi().deleteUsernameHash()); account.setUsernameLink(null); account.setUsername(null); logger.debug("[deleteUsername] Successfully deleted the username."); @@ -529,7 +532,8 @@ public class AccountHelper { } public void refreshDeviceName() throws IOException { - final var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices()); + final List devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi() + .getDevices(cont)); final var deviceId = account.getDeviceId(); final var device = devices.stream().filter(d -> d.id == deviceId).findFirst(); if (device.isPresent()) { @@ -569,8 +573,9 @@ public class AccountHelper { } public void removeLinkedDevices(int deviceId) throws IOException { - handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId)); - var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices()); + handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi().removeDevice(deviceId, cont)); + final List devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi() + .getDevices(cont)); account.setMultiDevice(devices.size() > 1); } @@ -605,7 +610,7 @@ public class AccountHelper { // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // If this is the primary device, other users can't send messages to this number anymore. // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. - handleResponseException(dependencies.getAccountApi().clearFcmToken()); + handleResponseExceptionSuspend(cont -> dependencies.getAccountApi().clearFcmToken(cont)); account.setRegistered(false); unregisteredListener.call(); 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 a067ecca..bd97372e 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 @@ -160,6 +160,7 @@ import okio.Utf8; import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES; import static org.asamk.signal.manager.util.Utils.handleResponseException; +import static org.asamk.signal.manager.util.Utils.handleResponseExceptionSuspend; import static org.signal.core.util.StringExtensionsKt.splitByByteLength; public class ManagerImpl implements Manager { @@ -483,7 +484,8 @@ public class ManagerImpl implements Manager { @Override public List getLinkedDevices() throws IOException { - var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices()); + final List devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi() + .getDevices(cont)); account.setMultiDevice(devices.size() > 1); var identityKey = account.getAciIdentityKeyPair().getPrivateKey(); return devices.stream().map(d -> { diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java index 6eccb005..643e81df 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalDependencies.java @@ -355,6 +355,7 @@ public class SignalDependencies { () -> preKeyRepository = new PreKeyRepository(getKeysApi(), dataStore.aci(), localProtocolAddress, + getSessionLock(), Runnable::run)); } diff --git a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java index 10f7a2a9..ba77f08a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java +++ b/lib/src/main/java/org/asamk/signal/manager/internal/SignalWebSocketHealthMonitor.java @@ -9,6 +9,7 @@ import org.whispersystems.signalservice.api.websocket.HealthMonitor; import org.whispersystems.signalservice.api.websocket.SignalWebSocket; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; +import java.time.Duration; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -103,6 +104,18 @@ final class SignalWebSocketHealthMonitor implements HealthMonitor { logger.info("Received alerts: {}", String.join(", ", strings)); } + @Override + public void onServerTimestamp(final long serverTimestamp, final boolean isIdentifiedWebsocket) { + final var skew = skewFrom(serverTimestamp); + if (skew.compareTo(Duration.ofDays(1)) > 0) { + logger.warn("Local clock is off from the server by {}, which exceeds the allowed limit..", skew); + } + } + + private Duration skewFrom(long serverTime) { + return Duration.ofMillis(Math.abs(System.currentTimeMillis() - serverTime)); + } + /** * Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If * the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated. diff --git a/lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncValidations.java b/lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncValidations.java index 45de2d25..6ec16e50 100644 --- a/lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncValidations.java +++ b/lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncValidations.java @@ -168,6 +168,11 @@ public final class StorageSyncValidations { throw new DuplicateCallLinkError(); } + ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.STICKER_PACK.getValue()); + if (ids.size() != new HashSet<>(ids).size()) { + throw new DuplicateStickerPackError(); + } + throw new DuplicateRawIdAcrossTypesError(); } @@ -217,6 +222,8 @@ public final class StorageSyncValidations { private static final class DuplicateInsertInWriteError extends Error {} + private static final class DuplicateStickerPackError extends Error {} + private static final class InsertNotPresentInFullIdSetError extends Error {} private static final class DeletePresentInFullIdSetError extends Error {} 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 04c0cbc6..314d8426 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 @@ -17,7 +17,6 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; import java.math.BigInteger; import java.net.Proxy; import java.net.ProxySelector; @@ -32,6 +31,8 @@ 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; @@ -39,6 +40,11 @@ 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 okio.ByteString; public class Utils { @@ -161,6 +167,67 @@ 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(); + } 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") + public static T handleResponseExceptionSuspend( + final Function, Object> call + ) throws IOException { + return handleResponseException((RequestResult) runSuspendBlocking((Function) call)); + } + @SuppressWarnings("unchecked") public static T handleResponseException(final RequestResult result) throws IOException { if (result instanceof RequestResult.Success success) { 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 dc0a42ee..1d4c02cd 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 @@ -268,6 +268,78 @@ } ] }, + { + "type": "com.sun.imageio.plugins.jpeg.JPEGImageReader", + "jniAccessible": true, + "methods": [ + { + "name": "acceptPixels", + "parameterTypes": [ + "int", + "boolean" + ] + }, + { + "name": "passComplete", + "parameterTypes": [] + }, + { + "name": "passStarted", + "parameterTypes": [ + "int" + ] + }, + { + "name": "pushBack", + "parameterTypes": [ + "int" + ] + }, + { + "name": "readInputData", + "parameterTypes": [ + "byte[]", + "int", + "int" + ] + }, + { + "name": "setImageData", + "parameterTypes": [ + "int", + "int", + "int", + "int", + "int", + "byte[]" + ] + }, + { + "name": "skipInputBytes", + "parameterTypes": [ + "long" + ] + }, + { + "name": "skipPastImage", + "parameterTypes": [ + "int" + ] + }, + { + "name": "warningOccurred", + "parameterTypes": [ + "int" + ] + }, + { + "name": "warningWithMessage", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, { "type": "com.sun.security.auth.module.UnixSystem", "jniAccessible": true, @@ -737,6 +809,183 @@ "allDeclaredMethods": true, "allPublicMethods": true }, + { + "type": "java.awt.GraphicsEnvironment", + "jniAccessible": true, + "methods": [ + { + "name": "isHeadless", + "parameterTypes": [] + } + ] + }, + { + "type": "java.awt.image.BufferedImage", + "jniAccessible": true, + "fields": [ + { + "name": "colorModel" + }, + { + "name": "imageType" + }, + { + "name": "raster" + } + ], + "methods": [ + { + "name": "getRGB", + "parameterTypes": [ + "int", + "int", + "int", + "int", + "int[]", + "int", + "int" + ] + }, + { + "name": "setRGB", + "parameterTypes": [ + "int", + "int", + "int", + "int", + "int[]", + "int", + "int" + ] + } + ] + }, + { + "type": "java.awt.image.ColorModel", + "jniAccessible": true, + "fields": [ + { + "name": "colorSpace" + }, + { + "name": "colorSpaceType" + }, + { + "name": "isAlphaPremultiplied" + }, + { + "name": "is_sRGB" + }, + { + "name": "nBits" + }, + { + "name": "numComponents" + }, + { + "name": "supportsAlpha" + }, + { + "name": "transparency" + } + ], + "methods": [ + { + "name": "getRGBdefault", + "parameterTypes": [] + } + ] + }, + { + "type": "java.awt.image.Raster", + "jniAccessible": true, + "fields": [ + { + "name": "dataBuffer" + }, + { + "name": "height" + }, + { + "name": "minX" + }, + { + "name": "minY" + }, + { + "name": "numBands" + }, + { + "name": "numDataElements" + }, + { + "name": "sampleModel" + }, + { + "name": "sampleModelTranslateX" + }, + { + "name": "sampleModelTranslateY" + }, + { + "name": "width" + } + ] + }, + { + "type": "java.awt.image.SampleModel", + "jniAccessible": true, + "fields": [ + { + "name": "height" + }, + { + "name": "width" + } + ], + "methods": [ + { + "name": "getPixels", + "parameterTypes": [ + "int", + "int", + "int", + "int", + "int[]", + "java.awt.image.DataBuffer" + ] + }, + { + "name": "setPixels", + "parameterTypes": [ + "int", + "int", + "int", + "int", + "int[]", + "java.awt.image.DataBuffer" + ] + } + ] + }, + { + "type": "java.awt.image.SinglePixelPackedSampleModel", + "jniAccessible": true, + "fields": [ + { + "name": "bitMasks" + }, + { + "name": "bitOffsets" + }, + { + "name": "bitSizes" + }, + { + "name": "maxBitSize" + } + ] + }, { "type": "java.io.File", "methods": [ @@ -984,6 +1233,18 @@ { "type": "java.lang.String[]" }, + { + "type": "java.lang.System", + "jniAccessible": true, + "methods": [ + { + "name": "load", + "parameterTypes": [ + "java.lang.String" + ] + } + ] + }, { "type": "java.lang.Thread", "jniAccessible": true, @@ -1455,6 +1716,33 @@ } ] }, + { + "type": "javax.imageio.plugins.jpeg.JPEGHuffmanTable", + "jniAccessible": true, + "fields": [ + { + "name": "lengths" + }, + { + "name": "values" + } + ] + }, + { + "type": "javax.imageio.plugins.jpeg.JPEGQTable", + "jniAccessible": true, + "fields": [ + { + "name": "qTable" + } + ] + }, + { + "type": "javax.imageio.spi.ImageReaderSpi" + }, + { + "type": "javax.imageio.spi.ImageWriterSpi" + }, { "type": "javax.security.auth.x500.X500Principal", "methods": [ @@ -5887,6 +6175,12 @@ { "name": "receivedQueueEmpty", "parameterTypes": [] + }, + { + "name": "receivedServerTimestamp", + "parameterTypes": [ + "long" + ] } ] }, @@ -9567,6 +9861,41 @@ } ] }, + { + "type": "sun.awt.image.ByteComponentRaster", + "jniAccessible": true, + "fields": [ + { + "name": "data" + }, + { + "name": "dataOffsets" + }, + { + "name": "pixelStride" + }, + { + "name": "scanlineStride" + }, + { + "name": "type" + } + ] + }, + { + "type": "sun.java2d.Disposer", + "jniAccessible": true, + "methods": [ + { + "name": "addRecord", + "parameterTypes": [ + "java.lang.Object", + "long", + "long" + ] + } + ] + }, { "type": "sun.launcher.LauncherHelper", "jniAccessible": true, @@ -10344,6 +10673,21 @@ { "glob": "META-INF/services/java.util.spi.ResourceBundleControlProvider" }, + { + "glob": "META-INF/services/javax.imageio.spi.ImageInputStreamSpi" + }, + { + "glob": "META-INF/services/javax.imageio.spi.ImageOutputStreamSpi" + }, + { + "glob": "META-INF/services/javax.imageio.spi.ImageReaderSpi" + }, + { + "glob": "META-INF/services/javax.imageio.spi.ImageTranscoderSpi" + }, + { + "glob": "META-INF/services/javax.imageio.spi.ImageWriterSpi" + }, { "glob": "META-INF/services/kotlin.reflect.jvm.internal.impl.km.internal.extensions.MetadataExtensions" }, @@ -10458,8 +10802,23 @@ "module": "java.base", "glob": "sun/text/resources/LineBreakIteratorData" }, + { + "module": "java.desktop", + "glob": "sun/awt/resources/awt_fr.properties" + }, + { + "module": "java.desktop", + "glob": "sun/awt/resources/awt_fr_FR.properties" + }, + { + "module": "java.desktop", + "glob": "sun/java2d/cmm/profiles/sRGB.pf" + }, { "bundle": "net.sourceforge.argparse4j.internal.ArgumentParserImpl" + }, + { + "bundle": "sun.awt.resources.awt" } ] -} +} \ No newline at end of file