diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aaac4e07..5a4cf4d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: matrix: # java="25" is the LTS Java version used in reproducible builds script (default in Containerfile). # More Java versions can be added to test compatibility, eg. "26". - java: ["25"] + java: ["25", "26"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e952c6eb..e36c07bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -slf4j = "2.0.17" +slf4j = "2.0.18" junit = "6.0.3" -micronaut-json-schema = "2.0.0-M8" -micronaut-core = "4.9.3" -signal-service = "2.15.3_unofficial_145" +micronaut-json-schema = "2.0.0" +micronaut-core = "5.0.0" +signal-service = "2.15.3_unofficial_146" [libraries] bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" @@ -20,7 +20,7 @@ slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } logback = "ch.qos.logback:logback-classic:1.5.32" signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" } -sqlite = "org.xerial:sqlite-jdbc:3.53.0.0" +sqlite = "org.xerial:sqlite-jdbc:3.53.1.0" hikari = "com.zaxxer:HikariCP:7.0.2" junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d997cfc6..b1b8ef56 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c61a118f..df6a6ad7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat index e509b2dd..aa5f10b0 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -23,8 +23,8 @@ @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,7 +65,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line @@ -73,21 +73,10 @@ goto fail @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index fda3a323..dac629e7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -259,7 +259,7 @@ public interface Manager extends Closeable { RecipientIdentifier.Single recipient ) throws IOException; - SendMessageResults sendEndSessionMessage(Set recipients) throws IOException; + void sendEndSessionMessage(Set recipients) throws IOException; SendMessageResults sendMessageRequestResponse( MessageEnvelope.Sync.MessageRequestResponse.Type type, diff --git a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java index c8de254b..975e1639 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/MessageEnvelope.java @@ -157,7 +157,7 @@ public record MessageEnvelope( dataMessage.getExpiresInSeconds(), dataMessage.isExpirationUpdate(), dataMessage.isViewOnce(), - dataMessage.isEndSession(), + false, dataMessage.isProfileKeyUpdate(), dataMessage.getProfileKey().isPresent(), dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)), @@ -1028,7 +1028,7 @@ public record MessageEnvelope( final AttachmentFileProvider fileProvider, Exception exception ) { - final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null); + final var serviceId = envelope.getSourceServiceId(); final var source = !envelope.isUnidentifiedSender() && serviceId != null ? recipientResolver.resolveRecipient(serviceId) : envelope.isUnidentifiedSender() && content != null diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 92014fa3..5069749c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -109,8 +109,8 @@ public final class IncomingMessageHandler { SignalServiceContent content = null; if (!envelope.isReceipt()) { account.getIdentityKeyStore().setRetryingDecryption(true); - final var destination = getDestination(envelope).serviceId(); try { + final var destination = getDestination(envelope).serviceId(); final var cipherResult = dependencies.getCipher(destination == null || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); @@ -140,15 +140,30 @@ public final class IncomingMessageHandler { final Manager.ReceiveMessageHandler handler ) { final var actions = new ArrayList(); + if (envelope.isPreKeySignalMessage()) { + actions.add(RefreshPreKeysAction.create()); + } SignalServiceContent content = null; Exception exception = null; - envelope.getSourceServiceId().map(ServiceId::parseOrNull) - // Store uuid if we don't have it already - // uuid in envelope is sent by server - .ifPresent(serviceId -> account.getRecipientResolver().resolveRecipient(serviceId)); + if (envelope.getSourceServiceId() != null) { + // Store uuid if we don't have it already + // uuid in envelope is sent by server + account.getRecipientResolver().resolveRecipient(envelope.getSourceServiceId()); + } if (!envelope.isReceipt()) { - final var destination = getDestination(envelope).serviceId(); try { + final var destination = getDestination(envelope).serviceId(); + + if (destination == account.getPni() && envelope.getSourceServiceId() == null) { + throw new InvalidMessageException( + "Got a sealed sender message to our PNI? Invalid message, ignoring."); + } + + if (envelope.getSourceServiceId() instanceof ServiceId.PNI + && envelope.getProto().type != Envelope.Type.SERVER_DELIVERY_RECEIPT) { + throw new InvalidMessageException("Got a message from a PNI that was not a SERVER_DELIVERY_RECEIPT."); + } + final var cipherResult = dependencies.getCipher(destination == null || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); @@ -173,7 +188,13 @@ public final class IncomingMessageHandler { logger.debug("Received invalid message from blocked contact, ignoring."); } else { var serviceId = ServiceId.parseOrNull(e.getSender()); - if (serviceId != null) { + ServiceId destination; + try { + destination = getDestination(envelope).serviceId(); + } catch (InvalidMessageException ex) { + destination = null; + } + if (serviceId != null && destination != null) { final var isSelf = sender.equals(account.getSelfRecipientId()) && e.getSenderDevice() == account.getDeviceId(); logger.debug("Received invalid message, queuing renew session action."); @@ -311,7 +332,12 @@ public final class IncomingMessageHandler { final var sender = senderDeviceAddress.recipientId(); final var senderServiceId = senderDeviceAddress.serviceId(); final var senderDeviceId = senderDeviceAddress.deviceId(); - final var destination = getDestination(envelope); + final DeviceAddress destination; + try { + destination = getDestination(envelope); + } catch (InvalidMessageException e) { + throw new AssertionError(e); + } if (account.getPni().equals(destination.serviceId)) { account.getRecipientStore().markNeedsPniSignature(destination.recipientId, true); @@ -874,11 +900,6 @@ public final class IncomingMessageHandler { final var selfAddress = isSync ? source : destination; final var conversationPartnerAddress = isSync ? destination : source; - if (conversationPartnerAddress != null && message.isEndSession()) { - account.getAccountData(selfAddress.serviceId()) - .getSessionStore() - .deleteAllSessions(conversationPartnerAddress.serviceId()); - } if (message.isExpirationUpdate() || message.getBody().isPresent()) { if (message.getGroupContext().isPresent()) { final var groupContext = message.getGroupContext().get(); @@ -1047,7 +1068,7 @@ public final class IncomingMessageHandler { } private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) { - final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null); + final var serviceId = envelope.getSourceServiceId(); if (!envelope.isUnidentifiedSender() && serviceId != null) { return new SignalServiceAddress(serviceId); } else if (content != null) { @@ -1058,7 +1079,7 @@ public final class IncomingMessageHandler { } private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) { - final var serviceId = envelope.getSourceServiceId().map(ServiceId::parseOrNull).orElse(null); + final var serviceId = envelope.getSourceServiceId(); if (!envelope.isUnidentifiedSender() && serviceId != null) { return new DeviceAddress(account.getRecipientResolver().resolveRecipient(serviceId), serviceId, @@ -1070,10 +1091,13 @@ public final class IncomingMessageHandler { } } - private DeviceAddress getDestination(SignalServiceEnvelope envelope) { + private DeviceAddress getDestination(SignalServiceEnvelope envelope) throws InvalidMessageException { final var destination = envelope.getDestinationServiceId(); if (destination == null || destination.isUnknown()) { - return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId()); + throw new InvalidMessageException("Missing destination"); + } + if (!account.getAci().equals(destination) && !account.getPni().equals(destination)) { + throw new InvalidMessageException("Message not intended for this account"); } return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination), destination, diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java index 73291d9e..31f1c04d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java @@ -9,7 +9,6 @@ import org.asamk.signal.manager.jobs.CleanOldPreKeysJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.recipients.RecipientAddress; -import org.signal.core.models.ServiceId; import org.signal.core.models.ServiceId.ACI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -150,10 +149,10 @@ public class ReceiveHelper { for (final var it : batch) { SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(), it.getServerDeliveredTimestamp()); - final var recipientId = envelope1.getSourceServiceId() - .map(ServiceId::parseOrNull) - .map(s -> account.getRecipientResolver().resolveRecipient(s)) - .orElse(null); + final var sourceServiceId = envelope1.getSourceServiceId(); + final var recipientId = sourceServiceId == null + ? null + : account.getRecipientResolver().resolveRecipient(sourceServiceId); logger.trace("Storing new message from {}", recipientId); // store message on disk, before acknowledging receipt to the server cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); @@ -238,7 +237,7 @@ public class ReceiveHelper { if (exception instanceof UntrustedIdentityException) { logger.debug("Keeping message with untrusted identity in message cache"); final var address = ((UntrustedIdentityException) exception).getSender(); - if (envelope.getSourceServiceId().isEmpty() && address.aci().isPresent()) { + if (envelope.getSourceServiceId() == null && address.aci().isPresent()) { final var recipientId = account.getRecipientResolver() .resolveRecipient(ACI.parseOrThrow(address.aci().get())); try { @@ -292,7 +291,7 @@ public class ReceiveHelper { cachedMessage.delete(); return null; } - if (envelope.getSourceServiceId().isEmpty()) { + if (envelope.getSourceServiceId() == null) { final var identifier = ((UntrustedIdentityException) exception).getSender(); final var recipientId = account.getRecipientResolver() .resolveRecipient(new RecipientAddress(identifier)); 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 c2aba751..394fa894 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 @@ -1091,30 +1091,26 @@ public class ManagerImpl implements Manager { } @Override - public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { - var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); - - try { - return sendMessage(messageBuilder, - recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet()), - false); - } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { - throw new AssertionError(e); - } finally { - for (var recipient : recipients) { - final RecipientId recipientId; - try { - recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (UnregisteredRecipientException e) { - continue; - } - final var serviceId = context.getAccount() - .getRecipientAddressResolver() - .resolveRecipientAddress(recipientId) - .serviceId(); - if (serviceId.isPresent()) { - account.getAccountData(ServiceIdType.ACI).getSessionStore().deleteAllSessions(serviceId.get()); - } + public void sendEndSessionMessage(Set recipients) throws IOException { + for (var recipient : recipients) { + final RecipientId recipientId; + try { + recipientId = context.getRecipientHelper().resolveRecipient(recipient); + } catch (UnregisteredRecipientException e) { + continue; + } + final var recipientAddress = context.getAccount() + .getRecipientAddressResolver() + .resolveRecipientAddress(recipientId); + final var aciSessionStore = account.getAccountData(ServiceIdType.ACI).getSessionStore(); + final var pniSessionStore = account.getAccountData(ServiceIdType.PNI).getSessionStore(); + if (recipientAddress.aci().isPresent()) { + aciSessionStore.archiveSessions(recipientAddress.aci().get()); + pniSessionStore.archiveSessions(recipientAddress.aci().get()); + } + if (recipientAddress.pni().isPresent()) { + aciSessionStore.archiveSessions(recipientAddress.pni().get()); + pniSessionStore.archiveSessions(recipientAddress.pni().get()); } } } 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 d81078d4..78d658f4 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 @@ -368,7 +368,8 @@ public class SignalDependencies { public SignalServiceCipher getCipher(ServiceIdType serviceIdType) { final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.unidentifiedSenderTrustRoots()); - final var address = new SignalServiceAddress(credentialsProvider.getAci(), credentialsProvider.getE164()); + final var serviceId = serviceIdType == ServiceIdType.ACI ? credentialsProvider.getAci() : credentialsProvider.getPni(); + final var address = new SignalServiceAddress(serviceId, credentialsProvider.getE164()); final var deviceId = credentialsProvider.getDeviceId(); return new SignalServiceCipher(address, deviceId, diff --git a/reproducible-builds/build.Containerfile b/reproducible-builds/build.Containerfile index 38626266..3ae4902c 100644 --- a/reproducible-builds/build.Containerfile +++ b/reproducible-builds/build.Containerfile @@ -6,7 +6,7 @@ ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ENV LANG=C.UTF-8 ENV LC_CTYPE=en_US.UTF-8 RUN SNAPSHOT="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y%m%dT%H%M%SZ)" \ - && apt install -y make asciidoc-base --update --snapshot "$SNAPSHOT" --no-install-recommends --no-install-suggests + && sed -i 's/^deb /deb [snapshot=yes] /' /etc/apt/sources.list && apt update --snapshot "$SNAPSHOT" && apt install -y make asciidoc-base --snapshot "$SNAPSHOT" --no-install-recommends --no-install-suggests COPY --chmod=0700 reproducible-builds/entrypoint.sh /usr/local/bin/entrypoint.sh WORKDIR /signal-cli ENTRYPOINT [ "/usr/local/bin/entrypoint.sh", "build" ] diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index aa4bb957..c6ef5077 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -144,8 +144,7 @@ public class SendCommand implements JsonRpcLocalCommand { } try { - final var results = m.sendEndSessionMessage(singleRecipients); - outputResult(outputWriter, results); + m.sendEndSessionMessage(singleRecipients); return; } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index faf099e4..45b937ae 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -543,9 +543,8 @@ public class DbusManagerImpl implements Manager { } @Override - public SendMessageResults sendEndSessionMessage(final Set recipients) throws IOException { + public void sendEndSessionMessage(final Set recipients) throws IOException { signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList()); - return new SendMessageResults(0, Map.of()); } @Override diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 20069b9a..a9983caa 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -430,8 +430,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable { @Override public void sendEndSessionMessage(final List recipients) { try { - final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber())); - checkSendMessageResults(results); + m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber())); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } diff --git a/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java b/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java index 77e59dbd..4796ae5d 100644 --- a/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java +++ b/src/test/java/org/asamk/signal/http/SseInitialFlushTest.java @@ -326,8 +326,7 @@ class SseInitialFlushTest { } @Override - public SendMessageResults sendEndSessionMessage(Set recipients) { - return null; + public void sendEndSessionMessage(Set recipients) { } @Override diff --git a/src/test/java/org/asamk/signal/jsonrpc/SubscribeCallEventsTest.java b/src/test/java/org/asamk/signal/jsonrpc/SubscribeCallEventsTest.java index 37c00829..bfc56ed3 100644 --- a/src/test/java/org/asamk/signal/jsonrpc/SubscribeCallEventsTest.java +++ b/src/test/java/org/asamk/signal/jsonrpc/SubscribeCallEventsTest.java @@ -328,8 +328,7 @@ class SubscribeCallEventsTest { } @Override - public SendMessageResults sendEndSessionMessage(Set r) { - return null; + public void sendEndSessionMessage(Set r) { } @Override