diff --git a/CHANGELOG.md b/CHANGELOG.md index fa27507d..26b96784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,31 @@ # Changelog ## [Unreleased] + +## [0.9.0] - 2021-09-12 +**Attention**: Now requires native libsignal-client version 0.9 + ### Breaking changes -- Removed deprecated `--json` parameter, use `--output=json` instead +- Removed deprecated `--json` parameter, use global parameter `--output=json` instead - Json output format of `listGroups` command changed: - Members are now arrays of `{"number":"...","uuid":"..."}` instead of arrays of strings. + Members are now arrays of `{"number":"...","uuid":"..."}` objects instead of arrays of strings. - Removed deprecated fallback data paths, only `$XDG_DATA_HOME/signal-cli` is used now For those still using the old paths (`$HOME/.config/signal`, `$HOME/.config/textsecure`) you need to move those to the new location. ### Added - New global parameter `--trust-new-identities=always` to allow trusting any new identity key without verification -- New parameter `--device-name` for `updateAccount` command to update the device name +- New parameter `--device-name` for `updateAccount` command to change the device name (also works for the main device) +- New SignalControl DBus interface, to register/verify/link new accounts +- New `jsonRpc` command that provides a JSON-RPC based API on stdout/stdin +- Support for announcement groups +- New parameter `--set-permission-send-messages` for `updateGroup` to create an announcement group +- New `sendReceipt` command to send read and viewed receipts +- Support for receiving sender key messages, mobile apps can now send messages more efficiently with server-side fan-out to groups with signal-cli members. +- Support for reading data from remote Signal storage. Now v2 groups will be shown after linking a new device. +- New `submitRateLimitChallenge` command that can be used to lift some rate-limits by solving a captcha + +### Fixed +- Store identity key correctly when sending a message after a recipient has changed keys ## [0.8.5] - 2021-08-07 ### Added diff --git a/README.md b/README.md index 9658dcba..9a11ee6e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ signal-cli is a commandline interface for [libsignal-service-java](https://githu To be able to link to an existing Signal-Android/signal-cli instance, signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java does not yet support [provisioning as a linked device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). For registering you need a phone number where you can receive SMS or incoming calls. signal-cli is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)), that can be used to send messages from any programming language that has dbus bindings. +It also has a JSON-RPC based interface, see the [documentation](https://github.com/AsamK/signal-cli/wiki/JSON-RPC-service) for more information. ## Installation @@ -88,6 +89,10 @@ dependencies. If you have a recent gradle version installed, you can replace `./ ./gradlew distTar +5. Compile and run signal-cli: + + ./gradlew run --args="--help" + ### Building a native binary with GraalVM (EXPERIMENTAL) It is possible to build a native binary with [GraalVM](https://www.graalvm.org). @@ -97,9 +102,9 @@ This is still experimental and will not work in all situations. 2. [Install prerequisites](https://www.graalvm.org/reference-manual/native-image/#prerequisites) 3. Execute Gradle: - ./gradlew assembleNativeImage + ./gradlew nativeCompile - The binary is available at *build/native-image/signal-cli* + The binary is available at *build/native/nativeCompile/signal-cli* ## FAQ and Troubleshooting For frequently asked questions and issues have a look at the [wiki](https://github.com/AsamK/signal-cli/wiki/FAQ) diff --git a/build.gradle.kts b/build.gradle.kts index c7de229c..51b2ef75 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,9 +3,10 @@ plugins { application eclipse `check-lib-versions` + id("org.graalvm.buildtools.native") version "0.9.5" } -version = "0.8.5" +version = "0.9.0" java { sourceCompatibility = JavaVersion.VERSION_11 @@ -16,6 +17,15 @@ application { mainClass.set("org.asamk.signal.Main") } +graalvmNative { + binaries { + this["main"].run { + configurationFileDirectories.from(file("graalvm-config-dir")) + buildArgs.add("--allow-incomplete-classpath") + } + } +} + repositories { mavenLocal() mavenCentral() @@ -54,51 +64,3 @@ tasks.withType { ) } } - -tasks.withType { - val appArgs: String? by project - if (appArgs != null) { - // allow passing command-line arguments to the main application e.g.: - // $ gradle run -PappArgs="['-u', '+...', 'daemon', '--json']" - args = groovy.util.Eval.me(appArgs) as MutableList - } -} - -val assembleNativeImage by tasks.registering { - dependsOn("assemble") - - var graalVMHome = "" - doFirst { - graalVMHome = System.getenv("GRAALVM_HOME") - ?: throw GradleException("Required GRAALVM_HOME environment variable not set.") - } - - doLast { - val nativeBinaryOutputPath = "$buildDir/native-image" - val nativeBinaryName = "signal-cli" - - mkdir(nativeBinaryOutputPath) - - exec { - workingDir = File(".") - commandLine( - "$graalVMHome/bin/native-image", - "-H:Path=$nativeBinaryOutputPath", - "-H:Name=$nativeBinaryName", - "-H:JNIConfigurationFiles=graalvm-config-dir/jni-config.json", - "-H:DynamicProxyConfigurationFiles=graalvm-config-dir/proxy-config.json", - "-H:ResourceConfigurationFiles=graalvm-config-dir/resource-config.json", - "-H:ReflectionConfigurationFiles=graalvm-config-dir/reflect-config.json", - "--no-fallback", - "--allow-incomplete-classpath", - "--report-unsupported-elements-at-runtime", - "--enable-url-protocols=http,https", - "--enable-https", - "--enable-all-security-services", - "-cp", - sourceSets.main.get().runtimeClasspath.asPath, - application.mainClass.get() - ) - } - } -} diff --git a/graalvm-config-dir/jni-config.json b/graalvm-config-dir/jni-config.json index d18c13e2..8c8c30f5 100644 --- a/graalvm-config-dir/jni-config.json +++ b/graalvm-config-dir/jni-config.json @@ -17,6 +17,10 @@ "name":"java.lang.UnsatisfiedLinkError", "methods":[{"name":"","parameterTypes":["java.lang.String"] }] }, +{ + "name":"java.util.UUID", + "methods":[{"name":"","parameterTypes":["long","long"] }] +}, { "name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader" }, @@ -28,10 +32,12 @@ {"name":"getLocalRegistrationId","parameterTypes":[] }, {"name":"isTrustedIdentity","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.IdentityKey","org.whispersystems.libsignal.state.IdentityKeyStore$Direction"] }, {"name":"loadPreKey","parameterTypes":["int"] }, + {"name":"loadSenderKey","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","java.util.UUID"] }, {"name":"loadSession","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress"] }, {"name":"loadSignedPreKey","parameterTypes":["int"] }, {"name":"removePreKey","parameterTypes":["int"] }, {"name":"saveIdentity","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.IdentityKey"] }, + {"name":"storeSenderKey","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","java.util.UUID","org.whispersystems.libsignal.groups.state.SenderKeyRecord"] }, {"name":"storeSession","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.state.SessionRecord"] } ] }, @@ -66,10 +72,24 @@ "name":"org.whispersystems.libsignal.UntrustedIdentityException", "methods":[{"name":"","parameterTypes":["java.lang.String"] }] }, +{ + "name":"org.whispersystems.libsignal.groups.state.SenderKeyRecord", + "methods":[ + {"name":"","parameterTypes":["long"] }, + {"name":"nativeHandle","parameterTypes":[] } + ] +}, +{ + "name":"org.whispersystems.libsignal.groups.state.SenderKeyStore" +}, { "name":"org.whispersystems.libsignal.logging.Log", "methods":[{"name":"log","parameterTypes":["int","java.lang.String","java.lang.String"] }] }, +{ + "name":"org.whispersystems.libsignal.protocol.PlaintextContent", + "methods":[{"name":"nativeHandle","parameterTypes":[] }] +}, { "name":"org.whispersystems.libsignal.protocol.PreKeySignalMessage", "methods":[ @@ -77,6 +97,9 @@ {"name":"nativeHandle","parameterTypes":[] } ] }, +{ + "name":"org.whispersystems.libsignal.protocol.SenderKeyMessage" +}, { "name":"org.whispersystems.libsignal.protocol.SignalMessage", "methods":[ diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index 2db7538b..819eafa7 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -697,6 +697,18 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, +{ + "name":"org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore$Storage", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore$Storage$SharedSenderKey", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, { "name":"org.asamk.signal.manager.storage.stickers.StickerStore", "allDeclaredFields":true, @@ -1505,6 +1517,12 @@ "name":"org.whispersystems.signalservice.api.push.SignedPreKeyEntity$ByteArraySerializer", "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"org.whispersystems.signalservice.api.storage.StorageAuthResponse", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, { "name":"org.whispersystems.signalservice.internal.contacts.crypto.SignatureBodyEntity", "allDeclaredFields":true, 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 a7e80f7c..05700379 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -40,6 +40,7 @@ import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.IncomingMessageHandler; import org.asamk.signal.manager.helper.PinHelper; +import org.asamk.signal.manager.helper.PreKeyHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; import org.asamk.signal.manager.helper.StorageHelper; @@ -62,14 +63,11 @@ import org.asamk.signal.manager.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.fingerprint.Fingerprint; import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalSessionLock; @@ -141,6 +139,7 @@ public class Manager implements Closeable { private final GroupHelper groupHelper; private final ContactHelper contactHelper; private final IncomingMessageHandler incomingMessageHandler; + private final PreKeyHelper preKeyHelper; private final Context context; private boolean hasCaughtUpWithOldMessages = false; @@ -219,6 +218,7 @@ public class Manager implements Closeable { groupHelper, avatarStore, this::resolveSignalServiceAddress); + preKeyHelper = new PreKeyHelper(account, dependencies); this.context = new Context(account, dependencies, @@ -227,7 +227,8 @@ public class Manager implements Closeable { groupHelper, syncHelper, profileHelper, - storageHelper); + storageHelper, + preKeyHelper); var jobExecutor = new JobExecutor(context); this.incomingMessageHandler = new IncomingMessageHandler(account, @@ -238,6 +239,7 @@ public class Manager implements Closeable { contactHelper, attachmentHelper, syncHelper, + this::getRecipientProfile, jobExecutor); } @@ -249,10 +251,6 @@ public class Manager implements Closeable { return account.getSelfRecipientId(); } - private IdentityKeyPair getIdentityKeyPair() { - return account.getIdentityKeyPair(); - } - public int getDeviceId() { return account.getDeviceId(); } @@ -309,9 +307,7 @@ public class Manager implements Closeable { days); } } - if (dependencies.getAccountManager().getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { - refreshPreKeys(); - } + preKeyHelper.refreshPreKeysIfNecessary(); if (account.getUuid() == null) { account.setUuid(dependencies.getAccountManager().getOwnUuid()); } @@ -439,7 +435,7 @@ public class Manager implements Closeable { } private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { - var identityKeyPair = getIdentityKeyPair(); + var identityKeyPair = account.getIdentityKeyPair(); var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); dependencies.getAccountManager() @@ -472,29 +468,7 @@ public class Manager implements Closeable { } void refreshPreKeys() throws IOException { - var oneTimePreKeys = generatePreKeys(); - final var identityKeyPair = getIdentityKeyPair(); - var signedPreKeyRecord = generateSignedPreKey(identityKeyPair); - - dependencies.getAccountManager().setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys); - } - - private List generatePreKeys() { - final var offset = account.getPreKeyIdOffset(); - - var records = KeyUtils.generatePreKeyRecords(offset, ServiceConfig.PREKEY_BATCH_SIZE); - account.addPreKeys(records); - - return records; - } - - private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) { - final var signedPreKeyId = account.getNextSignedPreKeyId(); - - var record = KeyUtils.generateSignedPreKeyRecord(identityKeyPair, signedPreKeyId); - account.addSignedPreKey(record); - - return record; + preKeyHelper.refreshPreKeys(); } public Profile getRecipientProfile(RecipientId recipientId) { @@ -903,11 +877,11 @@ public class Manager implements Closeable { // store message on disk, before acknowledging receipt to the server cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); }); - logger.debug("New message received from server"); if (result.isPresent()) { envelope = result.get(); + logger.debug("New message received from server"); } else { - // Received indicator that server queue is empty + logger.debug("Received indicator that server queue is empty"); handleQueuedActions(queuedActions); queuedActions.clear(); @@ -1175,7 +1149,7 @@ public class Manager implements Closeable { ) { return Utils.computeSafetyNumber(capabilities.isUuid(), account.getSelfAddress(), - getIdentityKeyPair().getPublicKey(), + account.getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey); } diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/RefreshPreKeysAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/RefreshPreKeysAction.java new file mode 100644 index 00000000..82d0d290 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/RefreshPreKeysAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class RefreshPreKeysAction implements HandleAction { + + private static final RefreshPreKeysAction INSTANCE = new RefreshPreKeysAction(); + + private RefreshPreKeysAction() { + } + + public static RefreshPreKeysAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getPreKeyHelper().refreshPreKeysIfNecessary(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java new file mode 100644 index 00000000..ecd5597d --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java @@ -0,0 +1,89 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.signal.libsignal.metadata.ProtocolException; +import org.whispersystems.libsignal.protocol.CiphertextMessage; +import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; + +public class SendRetryMessageRequestAction implements HandleAction { + + private final RecipientId recipientId; + private final ProtocolException protocolException; + private final SignalServiceEnvelope envelope; + + public SendRetryMessageRequestAction( + final RecipientId recipientId, + final ProtocolException protocolException, + final SignalServiceEnvelope envelope + ) { + this.recipientId = recipientId; + this.protocolException = protocolException; + this.envelope = envelope; + } + + @Override + public void execute(Context context) throws Throwable { + context.getAccount().getSessionStore().archiveSessions(recipientId); + + int senderDevice = protocolException.getSenderDevice(); + Optional groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion( + protocolException.getGroupId().get())) : Optional.absent(); + + byte[] originalContent; + int envelopeType; + if (protocolException.getUnidentifiedSenderMessageContent().isPresent()) { + final var messageContent = protocolException.getUnidentifiedSenderMessageContent().get(); + originalContent = messageContent.getContent(); + envelopeType = messageContent.getType(); + } else { + originalContent = envelope.getContent(); + envelopeType = envelopeTypeToCiphertextMessageType(envelope.getType()); + } + + DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent, + envelopeType, + envelope.getTimestamp(), + senderDevice); + + context.getSendHelper().sendRetryReceipt(decryptionErrorMessage, recipientId, groupId); + } + + private static int envelopeTypeToCiphertextMessageType(int envelopeType) { + switch (envelopeType) { + case SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE: + return CiphertextMessage.PREKEY_TYPE; + case SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE: + return CiphertextMessage.SENDERKEY_TYPE; + case SignalServiceProtos.Envelope.Type.PLAINTEXT_CONTENT_VALUE: + return CiphertextMessage.PLAINTEXT_CONTENT_TYPE; + case SignalServiceProtos.Envelope.Type.CIPHERTEXT_VALUE: + default: + return CiphertextMessage.WHISPER_TYPE; + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final SendRetryMessageRequestAction that = (SendRetryMessageRequestAction) o; + + if (!recipientId.equals(that.recipientId)) return false; + if (!protocolException.equals(that.protocolException)) return false; + return envelope.equals(that.envelope); + } + + @Override + public int hashCode() { + int result = recipientId.hashCode(); + result = 31 * result + protocolException.hashCode(); + result = 31 * result + envelope.hashCode(); + return result; + } +} 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 e46effc0..0917a214 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 @@ -6,12 +6,14 @@ import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.TrustLevel; import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.actions.HandleAction; +import org.asamk.signal.manager.actions.RefreshPreKeysAction; import org.asamk.signal.manager.actions.RenewSessionAction; import org.asamk.signal.manager.actions.RetrieveProfileAction; import org.asamk.signal.manager.actions.RetrieveStorageDataAction; import org.asamk.signal.manager.actions.SendGroupInfoAction; import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; import org.asamk.signal.manager.actions.SendReceiptAction; +import org.asamk.signal.manager.actions.SendRetryMessageRequestAction; import org.asamk.signal.manager.actions.SendSyncBlockedListAction; import org.asamk.signal.manager.actions.SendSyncContactsAction; import org.asamk.signal.manager.actions.SendSyncGroupsAction; @@ -22,12 +24,17 @@ import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.storage.stickers.StickerPackId; +import org.signal.libsignal.metadata.ProtocolInvalidKeyException; +import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException; import org.signal.libsignal.metadata.ProtocolInvalidMessageException; +import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; +import org.signal.libsignal.metadata.SelfSendException; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; @@ -58,6 +65,7 @@ public final class IncomingMessageHandler { private final ContactHelper contactHelper; private final AttachmentHelper attachmentHelper; private final SyncHelper syncHelper; + private final ProfileProvider profileProvider; private final JobExecutor jobExecutor; public IncomingMessageHandler( @@ -69,6 +77,7 @@ public final class IncomingMessageHandler { final ContactHelper contactHelper, final AttachmentHelper attachmentHelper, final SyncHelper syncHelper, + final ProfileProvider profileProvider, final JobExecutor jobExecutor ) { this.account = account; @@ -79,6 +88,7 @@ public final class IncomingMessageHandler { this.contactHelper = contactHelper; this.attachmentHelper = attachmentHelper; this.syncHelper = syncHelper; + this.profileProvider = profileProvider; this.jobExecutor = jobExecutor; } @@ -87,6 +97,11 @@ public final class IncomingMessageHandler { final boolean ignoreAttachments, final Manager.ReceiveMessageHandler handler ) { + final List actions = new ArrayList<>(); + if (envelope.isPreKeySignalMessage()) { + actions.add(RefreshPreKeysAction.create()); + } + SignalServiceContent content = null; if (!envelope.isReceipt()) { try { @@ -100,7 +115,7 @@ public final class IncomingMessageHandler { return new Pair<>(List.of(), e); } } - final var actions = checkAndHandleMessage(envelope, content, ignoreAttachments, handler, null); + actions.addAll(checkAndHandleMessage(envelope, content, ignoreAttachments, handler, null)); return new Pair<>(actions, null); } @@ -125,11 +140,24 @@ public final class IncomingMessageHandler { actions.add(new RetrieveProfileAction(recipientId)); exception = new UntrustedIdentityException(addressResolver.resolveSignalServiceAddress(recipientId), e.getSenderDevice()); - } catch (ProtocolInvalidMessageException e) { + } catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolNoSessionException | ProtocolInvalidMessageException e) { final var sender = account.getRecipientStore().resolveRecipient(e.getSender()); - logger.debug("Received invalid message, queuing renew session action."); - actions.add(new RenewSessionAction(sender)); + final var senderProfile = profileProvider.getProfile(sender); + final var selfProfile = profileProvider.getProfile(account.getSelfRecipientId()); + if (senderProfile != null + && senderProfile.getCapabilities().contains(Profile.Capability.senderKey) + && selfProfile != null + && selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) { + logger.debug("Received invalid message, requesting message resend."); + actions.add(new SendRetryMessageRequestAction(sender, e, envelope)); + } else { + logger.debug("Received invalid message, queuing renew session action."); + actions.add(new RenewSessionAction(sender)); + } exception = e; + } catch (SelfSendException e) { + logger.debug("Dropping unidentified message from self."); + return new Pair<>(List.of(), null); } catch (Exception e) { exception = e; } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java new file mode 100644 index 00000000..f56a7055 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java @@ -0,0 +1,61 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.util.KeyUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; + +import java.io.IOException; +import java.util.List; + +public class PreKeyHelper { + + private final static Logger logger = LoggerFactory.getLogger(PreKeyHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + + public PreKeyHelper( + final SignalAccount account, final SignalDependencies dependencies + ) { + this.account = account; + this.dependencies = dependencies; + } + + public void refreshPreKeysIfNecessary() throws IOException { + if (dependencies.getAccountManager().getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { + refreshPreKeys(); + } + } + + public void refreshPreKeys() throws IOException { + var oneTimePreKeys = generatePreKeys(); + final var identityKeyPair = account.getIdentityKeyPair(); + var signedPreKeyRecord = generateSignedPreKey(identityKeyPair); + + dependencies.getAccountManager().setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys); + } + + private List generatePreKeys() { + final var offset = account.getPreKeyIdOffset(); + + var records = KeyUtils.generatePreKeyRecords(offset, ServiceConfig.PREKEY_BATCH_SIZE); + account.addPreKeys(records); + + return records; + } + + private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) { + final var signedPreKeyId = account.getNextSignedPreKeyId(); + + var record = KeyUtils.generateSignedPreKeyRecord(identityKeyPair, signedPreKeyId); + account.addSignedPreKey(record); + + return record; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java index 22915a95..5216b030 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java @@ -5,5 +5,5 @@ import org.asamk.signal.manager.storage.recipients.RecipientId; public interface ProfileProvider { - Profile getProfile(RecipientId address); + Profile getProfile(RecipientId recipientId); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 89e3eba2..c0953f1f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -13,6 +13,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.ContentHint; @@ -23,6 +24,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException; +import org.whispersystems.signalservice.api.push.exceptions.RateLimitException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import java.io.IOException; @@ -155,6 +157,25 @@ public class SendHelper { } } + public void sendRetryReceipt( + DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional groupId + ) throws IOException, UntrustedIdentityException { + var messageSender = dependencies.getMessageSender(); + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + logger.debug("Sending retry receipt for {} to {}, device: {}", + errorMessage.getTimestamp(), + recipientId, + errorMessage.getDeviceId()); + try { + messageSender.sendRetryReceipt(address, + unidentifiedAccessHelper.getAccessFor(recipientId), + groupId.transform(GroupId::serialize), + errorMessage); + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { + throw new UntrustedIdentityException(address); + } + } + public SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException { var messageSender = dependencies.getMessageSender(); @@ -285,6 +306,9 @@ public class SendHelper { } } catch (ProofRequiredException e) { return SendMessageResult.proofRequiredFailure(address, e); + } catch (RateLimitException e) { + logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage()); + return SendMessageResult.networkFailure(address); } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { return SendMessageResult.identityFailure(address, e.getIdentityKey()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java index beb41969..7dd99779 100644 --- a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java @@ -3,6 +3,7 @@ package org.asamk.signal.manager.jobs; import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.StickerPackStore; import org.asamk.signal.manager.helper.GroupHelper; +import org.asamk.signal.manager.helper.PreKeyHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; import org.asamk.signal.manager.helper.StorageHelper; @@ -19,6 +20,7 @@ public class Context { private final SyncHelper syncHelper; private final ProfileHelper profileHelper; private final StorageHelper storageHelper; + private final PreKeyHelper preKeyHelper; public Context( final SignalAccount account, @@ -28,7 +30,8 @@ public class Context { final GroupHelper groupHelper, final SyncHelper syncHelper, final ProfileHelper profileHelper, - final StorageHelper storageHelper + final StorageHelper storageHelper, + final PreKeyHelper preKeyHelper ) { this.account = account; this.dependencies = dependencies; @@ -38,6 +41,7 @@ public class Context { this.syncHelper = syncHelper; this.profileHelper = profileHelper; this.storageHelper = storageHelper; + this.preKeyHelper = preKeyHelper; } public SignalAccount getAccount() { @@ -71,4 +75,8 @@ public class Context { public StorageHelper getStorageHelper() { return storageHelper; } + + public PreKeyHelper getPreKeyHelper() { + return preKeyHelper; + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java index 87828953..d61a81b5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java @@ -134,7 +134,8 @@ public class Profile { gv2, storage, gv1Migration, - senderKey; + senderKey, + announcementGroup; static Capability valueOfOrNull(String value) { try { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientId.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientId.java index 9d22d672..f093ca33 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientId.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientId.java @@ -16,6 +16,11 @@ public class RecipientId { return id; } + @Override + public String toString() { + return "RecipientId{" + "id=" + id + '}'; + } + @Override public boolean equals(final Object o) { if (this == o) return true; diff --git a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java index f6f76c2c..7ceb07f6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java @@ -65,6 +65,10 @@ public class ProfileUtils { if (encryptedProfile.getCapabilities().isSenderKey()) { capabilities.add(Profile.Capability.senderKey); } + if (encryptedProfile.getCapabilities().isAnnouncementGroup()) { + capabilities.add(Profile.Capability.announcementGroup); + } + return capabilities; } diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index b52612be..573ade7c 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -362,6 +362,8 @@ Trust all known keys of this user, only use this for testing. *-v* VERIFIED_SAFETY_NUMBER, *--verified-safety-number* VERIFIED_SAFETY_NUMBER:: Specify the safety number of the key, only use this option if you have verified the safety number. +Can be either the plain text numbers shown in the app or the bytes from the QR-code, +encoded as base64. === updateProfile diff --git a/run_tests.sh b/run_tests.sh index 5978eed9..d306dfa9 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -11,17 +11,22 @@ if [ ! -z "$GRAALVM_HOME" ]; then export JAVA_HOME=$GRAALVM_HOME export SIGNAL_CLI_OPTS='-agentlib:native-image-agent=config-merge-dir=graalvm-config-dir/' fi -export SIGNAL_CLI="$PWD/build/install/signal-cli/bin/signal-cli" NUMBER_1="$1" NUMBER_2="$2" TEST_PIN_1=456test_pin_foo123 +NATIVE=1 PATH_TEST_CONFIG="$PWD/build/test-config" PATH_MAIN="$PATH_TEST_CONFIG/main" PATH_LINK="$PATH_TEST_CONFIG/link" -./gradlew installDist +if [ "$NATIVE" -eq 1 ]; then + SIGNAL_CLI="$PWD/build/native/nativeCompile/signal-cli" +else + ./gradlew installDist + SIGNAL_CLI="$PWD/build/install/signal-cli/bin/signal-cli" +fi run() { set -x diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index fc63b89e..26079ec6 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -56,6 +56,9 @@ public class Main { e.getCause().printStackTrace(); } status = getStatusForError(e); + } catch (Throwable e) { + e.printStackTrace(); + status = 2; } System.exit(status); } diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 15dbd1af..bc9244f8 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -8,6 +8,7 @@ import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.Util; import org.slf4j.helpers.MessageFormatter; +import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -113,6 +114,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { var typingMessage = content.getTypingMessage().get(); printTypingMessage(writer.indentedWriter(), typingMessage); } + if (content.getDecryptionErrorMessage().isPresent()) { + writer.println("Received a decryption error message (resend request)"); + var decryptionErrorMessage = content.getDecryptionErrorMessage().get(); + printDecryptionErrorMessage(writer.indentedWriter(), decryptionErrorMessage); + } } } else { writer.println("Unknown message received."); @@ -215,6 +221,15 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } } + private void printDecryptionErrorMessage( + final PlainTextWriter writer, final DecryptionErrorMessage decryptionErrorMessage + ) { + writer.println("Device id: {}", decryptionErrorMessage.getDeviceId()); + writer.println("Timestamp: {}", DateUtils.formatTimestamp(decryptionErrorMessage.getTimestamp())); + writer.println("Ratchet key: {}", + decryptionErrorMessage.getRatchetKey().isPresent() ? "is present" : "not present"); + } + private void printReceiptMessage( final PlainTextWriter writer, final SignalServiceReceiptMessage receiptMessage ) { diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java index 316f59b1..be94fb36 100644 --- a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -61,7 +61,8 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { final var writer = (PlainTextWriter) outputWriter; for (var entry : registered.entrySet()) { - writer.println("{}: {}", entry.getKey(), entry.getValue()); + final var uuid = entry.getValue().second(); + writer.println("{}: {}", entry.getKey(), uuid != null); } } }