Merge branch 'AsamK:master' into master

This commit is contained in:
Adimarantis 2021-09-12 20:47:16 +02:00 committed by GitHub
commit 26372c4ab3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 365 additions and 102 deletions

View file

@ -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

View file

@ -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)

View file

@ -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()
)
}
}
}

View file

@ -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":[

View file

@ -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,

View file

@ -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);
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -5,5 +5,5 @@ import org.asamk.signal.manager.storage.recipients.RecipientId;
public interface ProfileProvider {
Profile getProfile(RecipientId address);
Profile getProfile(RecipientId recipientId);
}

View file

@ -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());
}

View file

@ -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;
}
}

View file

@ -134,7 +134,8 @@ public class Profile {
gv2,
storage,
gv1Migration,
senderKey;
senderKey,
announcementGroup;
static Capability valueOfOrNull(String value) {
try {

View file

@ -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;

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -56,6 +56,9 @@ public class Main {
e.getCause().printStackTrace();
}
status = getStatusForError(e);
} catch (Throwable e) {
e.printStackTrace();
status = 2;
}
System.exit(status);
}

View file

@ -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
) {

View file

@ -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);
}
}
}