mirror of
https://github.com/AsamK/signal-cli
synced 2025-09-02 20:40:38 +00:00
Merge branch 'AsamK:master' into master
This commit is contained in:
commit
26372c4ab3
21 changed files with 365 additions and 102 deletions
21
CHANGELOG.md
21
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<Jar> {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<JavaExec> {
|
||||
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<String>
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
"name":"java.lang.UnsatisfiedLinkError",
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"name":"java.util.UUID",
|
||||
"methods":[{"name":"<init>","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":"<init>","parameterTypes":["java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.libsignal.groups.state.SenderKeyRecord",
|
||||
"methods":[
|
||||
{"name":"<init>","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":[
|
||||
|
|
|
@ -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":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.api.storage.StorageAuthResponse",
|
||||
"allDeclaredFields":true,
|
||||
"allDeclaredMethods":true,
|
||||
"allDeclaredConstructors":true
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.contacts.crypto.SignatureBodyEntity",
|
||||
"allDeclaredFields":true,
|
||||
|
|
|
@ -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<PreKeyRecord> 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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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> 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;
|
||||
}
|
||||
}
|
|
@ -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<HandleAction> 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;
|
||||
}
|
||||
|
|
|
@ -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<PreKeyRecord> 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;
|
||||
}
|
||||
}
|
|
@ -5,5 +5,5 @@ import org.asamk.signal.manager.storage.recipients.RecipientId;
|
|||
|
||||
public interface ProfileProvider {
|
||||
|
||||
Profile getProfile(RecipientId address);
|
||||
Profile getProfile(RecipientId recipientId);
|
||||
}
|
||||
|
|
|
@ -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> 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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,7 +134,8 @@ public class Profile {
|
|||
gv2,
|
||||
storage,
|
||||
gv1Migration,
|
||||
senderKey;
|
||||
senderKey,
|
||||
announcementGroup;
|
||||
|
||||
static Capability valueOfOrNull(String value) {
|
||||
try {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -56,6 +56,9 @@ public class Main {
|
|||
e.getCause().printStackTrace();
|
||||
}
|
||||
status = getStatusForError(e);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
status = 2;
|
||||
}
|
||||
System.exit(status);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue