From 2cd6b2b99baf31c24e5e37a367c95c29192fd1ca Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 12 Feb 2020 20:23:04 +0100 Subject: [PATCH 001/103] Show contact name for message Sender Fixes #265 --- src/main/java/org/asamk/signal/ReceiveMessageHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 4cb09440..570631e7 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -76,7 +76,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (content == null) { System.out.println("Failed to decrypt message."); } else { - System.out.println(String.format("Sender: %s (device: %d)", content.getSender().getNumber().get(), content.getSenderDevice())); + ContactInfo sourceContact = m.getContact(content.getSender().getNumber().get()); + System.out.println(String.format("Sender: %s (device: %d)", (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + content.getSender().getNumber().get(), content.getSenderDevice())); if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); handleSignalServiceDataMessage(message); From 37ed02f4cea9b02431438025c816ca240b642678 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 12 Feb 2020 20:36:23 +0100 Subject: [PATCH 002/103] Update to libsignal 2.15.3_unofficial_2 --- build.gradle | 2 +- .../org/asamk/signal/manager/BaseConfig.java | 23 +++++++++++++++++-- .../org/asamk/signal/manager/Manager.java | 18 ++++++++++----- .../java/org/asamk/signal/manager/Utils.java | 2 +- .../asamk/signal/storage/SignalAccount.java | 4 ++++ .../signal/storage/contacts/ContactInfo.java | 6 +++++ .../signal/storage/groups/GroupInfo.java | 4 ++++ 7 files changed, 49 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index a7312bb4..9d5b33a9 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_2' compile 'org.bouncycastle:bcprov-jdk15on:1.64' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/manager/BaseConfig.java b/src/main/java/org/asamk/signal/manager/BaseConfig.java index 0f503352..f204941c 100644 --- a/src/main/java/org/asamk/signal/manager/BaseConfig.java +++ b/src/main/java/org/asamk/signal/manager/BaseConfig.java @@ -3,15 +3,22 @@ package org.asamk.signal.manager; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; +import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; +import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; + +import java.util.Collections; +import java.util.List; + +import okhttp3.Interceptor; public class BaseConfig { public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); - final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION; + final static String USER_AGENT = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + " " + PROJECT_VERSION; final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; final static int PREKEY_MINIMUM_COUNT = 20; final static int PREKEY_BATCH_SIZE = 100; @@ -19,12 +26,24 @@ public class BaseConfig { private final static String URL = "https://textsecure-service.whispersystems.org"; private final static String CDN_URL = "https://cdn.signal.org"; + private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org"; + private final static String STORAGE_URL = "https://storage.signal.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); + private final static Interceptor userAgentInterceptor = chain -> + chain.proceed(chain.request().newBuilder() + .header("User-Agent", USER_AGENT) + .build()); + + private final static List interceptors = Collections.singletonList(userAgentInterceptor); + final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration( new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, - new SignalContactDiscoveryUrl[0] + new SignalContactDiscoveryUrl[0], + new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, + new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, + interceptors ); private BaseConfig() { diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 59061068..0847e484 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -265,7 +265,7 @@ public class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), getSelfUnidentifiedAccessKey(), false); + accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false); } public void setProfileName(String name) throws IOException { @@ -396,7 +396,7 @@ public class Manager implements Signal { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); // TODO make unrestricted unidentified access configurable - accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, getSelfUnidentifiedAccessKey(), false); + accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); account.setRegistered(true); @@ -407,11 +407,12 @@ public class Manager implements Signal { } public void setRegistrationLockPin(Optional pin) throws IOException { - accountManager.setPin(pin); if (pin.isPresent()) { account.setRegistrationLockPin(pin.get()); + throw new RuntimeException("Not implemented anymore, will be replaced with KBS"); } else { account.setRegistrationLockPin(null); + accountManager.removeV1Pin(); } account.save(); } @@ -1319,6 +1320,8 @@ public class Manager implements Signal { if (g.getAvatar().isPresent()) { retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); } + syncGroup.inboxPosition = g.getInboxPosition().orNull(); + syncGroup.archived = g.isArchived(); account.getGroupStore().updateGroup(syncGroup); } } @@ -1396,6 +1399,8 @@ public class Manager implements Signal { account.getThreadStore().updateThread(thread); } contact.blocked = c.isBlocked(); + contact.inboxPosition = c.getInboxPosition().orNull(); + contact.archived = c.isArchived(); account.getContactStore().updateContact(contact); if (c.getAvatar().isPresent()) { @@ -1523,7 +1528,7 @@ public class Manager implements Signal { out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId), record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null), - Optional.fromNullable(record.color), record.blocked)); + Optional.fromNullable(record.color), record.blocked, Optional.fromNullable(record.inboxPosition), record.archived)); } } @@ -1572,7 +1577,8 @@ public class Manager implements Signal { out.write(new DeviceContact(record.getAddress(), Optional.fromNullable(record.name), createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), record.blocked, - Optional.fromNullable(info != null ? info.messageExpirationTime : null))); + Optional.fromNullable(info != null ? info.messageExpirationTime : null), + Optional.fromNullable(record.inboxPosition), record.archived)); } if (account.getProfileKey() != null) { @@ -1581,7 +1587,7 @@ public class Manager implements Signal { Optional.absent(), Optional.absent(), Optional.absent(), Optional.absent(), Optional.of(account.getProfileKey()), - false, Optional.absent())); + false, Optional.absent(), Optional.absent(), false)); } } diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index b253a2ee..eccc1656 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -82,7 +82,7 @@ class Utils { Optional preview = Optional.absent(); Optional caption = Optional.absent(); Optional blurHash = Optional.absent(); - return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, blurHash, null); + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, blurHash, null, null); } static StreamDetails createStreamDetailsFromFile(File file) throws IOException { diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 1ba78f6b..151835cb 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -291,6 +291,10 @@ public class SignalAccount { return registrationLockPin; } + public String getRegistrationLock() { + return null; // TODO implement KBS + } + public void setRegistrationLockPin(final String registrationLockPin) { this.registrationLockPin = registrationLockPin; } diff --git a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java index be69b40c..2ab2a515 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java +++ b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java @@ -22,6 +22,12 @@ public class ContactInfo { @JsonProperty(defaultValue = "false") public boolean blocked; + @JsonProperty + public Integer inboxPosition; + + @JsonProperty(defaultValue = "false") + public boolean archived; + @JsonIgnore public SignalServiceAddress getAddress() { return new SignalServiceAddress(null, number); diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java index 1a4e0ec2..0387efac 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -25,6 +25,10 @@ public class GroupInfo { public String color; @JsonProperty(defaultValue = "false") public boolean blocked; + @JsonProperty + public Integer inboxPosition; + @JsonProperty(defaultValue = "false") + public boolean archived; private long avatarId; From f3b8df789d447dcc99484c21e80e7fe7a6bc1275 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 22 Feb 2020 10:35:55 +0100 Subject: [PATCH 003/103] Print message reactions --- .../java/org/asamk/signal/ReceiveMessageHandler.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 570631e7..34d941ed 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -312,6 +312,15 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("Profile key update, key length:" + message.getProfileKey().get().length); } + if (message.getReaction().isPresent()) { + final SignalServiceDataMessage.Reaction reaction = message.getReaction().get(); + System.out.println("Reaction:"); + System.out.println(" - Emoji: " + reaction.getEmoji()); + System.out.println(" - Target author: " + reaction.getTargetAuthor().getNumber()); + System.out.println(" - Target timestamp: " + reaction.getTargetSentTimestamp()); + System.out.println(" - Is remove: " + reaction.isRemove()); + } + if (message.getQuote().isPresent()) { SignalServiceDataMessage.Quote quote = message.getQuote().get(); System.out.println("Quote: (" + quote.getId() + ")"); From 063fb95dca87b6b57996c7379b958ae05e2e5ad9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 22 Feb 2020 11:29:17 +0100 Subject: [PATCH 004/103] Add command sendReaction for emoji reactions --- man/signal-cli.1.adoc | 22 +++++ .../org/asamk/signal/commands/Commands.java | 1 + .../signal/commands/SendReactionCommand.java | 95 +++++++++++++++++++ .../org/asamk/signal/manager/Manager.java | 30 ++++++ 4 files changed, 148 insertions(+) create mode 100644 src/main/java/org/asamk/signal/commands/SendReactionCommand.java diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 35a578f4..4101986f 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -140,6 +140,28 @@ RECIPIENT:: *-e*, *--endsession*:: Clear session state and send end session message. +sendReaction +~~~~~~~~~~~~ +Send reaction to a previously received or sent message. + +RECIPIENT:: + Specify the recipients’ phone number. + +*-g* GROUP, *--group* GROUP:: + Specify the recipient group ID in base64 encoding. + +*-e* EMOJI, *--emoji* EMOJI:: + Specify the emoji, should be a single unicode grapheme cluster. + +*-a* NUMBER, *--target-author* NUMBER:: + Specify the number of the author of the message to which to react. + +*-t* TIMESTAMP, *--target-timestamp* TIMESTAMP:: + Specify the timestamp of the message to which to react. + +*-r*, *--remove*:: + Remove a reaction. + receive ~~~~~~~ Query the server for new messages. New messages are printed on standardoutput and diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 1ad0987a..24a03e3f 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -22,6 +22,7 @@ public class Commands { addCommand("removeDevice", new RemoveDeviceCommand()); addCommand("removePin", new RemovePinCommand()); addCommand("send", new SendCommand()); + addCommand("sendReaction", new SendReactionCommand()); addCommand("sendContacts", new SendContactsCommand()); addCommand("updateContact", new UpdateContactCommand()); addCommand("setPin", new SetPinCommand()); diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java new file mode 100644 index 00000000..38560642 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -0,0 +1,95 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.GroupIdFormatException; +import org.asamk.signal.GroupNotFoundException; +import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.Util; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; + +import java.io.IOException; + +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; +import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; +import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; +import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; +import static org.asamk.signal.util.ErrorUtils.handleIOException; +import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; + +public class SendReactionCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Send reaction to a previously received or sent message."); + subparser.addArgument("-g", "--group") + .help("Specify the recipient group ID."); + subparser.addArgument("recipient") + .help("Specify the recipients' phone number.") + .nargs("*"); + subparser.addArgument("-e", "--emoji") + .required(true) + .help("Specify the emoji, should be a single unicode grapheme cluster."); + subparser.addArgument("-a", "--target-author") + .required(true) + .help("Specify the number of the author of the message to which to react."); + subparser.addArgument("-t", "--target-timestamp") + .required(true) + .type(long.class) + .help("Specify the timestamp of the message to which to react."); + subparser.addArgument("-r", "--remove") + .help("Remove a reaction.") + .action(Arguments.storeTrue()); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && ns.getString("group") == null) { + System.err.println("No recipients given"); + System.err.println("Aborting sending."); + return 1; + } + + String emoji = ns.getString("emoji"); + boolean isRemove = ns.getBoolean("remove"); + SignalServiceAddress targetAuthor = new SignalServiceAddress(null, ns.getString("target_author")); + long targetTimestamp = ns.getLong("target_timestamp"); + + try { + if (ns.getString("group") != null) { + byte[] groupId = Util.decodeGroupId(ns.getString("group")); + m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId); + } else { + m.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, ns.getList("recipient")); + } + return 0; + } catch (IOException e) { + handleIOException(e); + return 3; + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); + return 3; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } catch (GroupNotFoundException e) { + handleGroupNotFoundException(e); + return 1; + } catch (NotAGroupMemberException e) { + handleNotAGroupMemberException(e); + return 1; + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + return 1; + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 0847e484..8d0d46c8 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -496,6 +496,26 @@ public class Manager implements Signal { sendMessageLegacy(messageBuilder, membersSend); } + public void sendGroupMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, + long targetSentTimestamp, byte[] groupId) + throws IOException, EncapsulatedExceptions, AttachmentInvalidException { + SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); + final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .withReaction(reaction) + .withProfileKey(account.getProfileKey()); + if (groupId != null) { + SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) + .withId(groupId) + .build(); + messageBuilder.asGroupMessage(group); + } + final GroupInfo g = getGroupForSending(groupId); + // Don't send group message to ourself + final List membersSend = new ArrayList<>(g.members); + membersSend.remove(this.username); + sendMessageLegacy(messageBuilder, membersSend); + } + public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupId) @@ -669,6 +689,16 @@ public class Manager implements Signal { sendMessageLegacy(messageBuilder, recipients); } + public void sendMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, + long targetSentTimestamp, List recipients) + throws IOException, EncapsulatedExceptions, AttachmentInvalidException { + SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); + final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .withReaction(reaction) + .withProfileKey(account.getProfileKey()); + sendMessageLegacy(messageBuilder, recipients); + } + @Override public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() From 31434ac5ec6cee7d45ab81759de7ae647f675b11 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 Mar 2020 14:32:42 +0100 Subject: [PATCH 005/103] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew.bat | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1b16c34a..a2bf1313 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat index 24467a14..9109989e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" From 0dc6b1327e1da6f5707d199f2c35eb18bee9e864 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 Mar 2020 14:37:02 +0100 Subject: [PATCH 006/103] Update libsignal-service-java - Use new ProfileKey class instead of byte array - Add capabilities (for future support of uuid and groups v2) --- build.gradle | 2 +- .../org/asamk/signal/manager/BaseConfig.java | 5 ++- .../org/asamk/signal/manager/KeyUtils.java | 10 ++++- .../org/asamk/signal/manager/Manager.java | 43 +++++++++++++------ .../asamk/signal/storage/SignalAccount.java | 20 ++++++--- 5 files changed, 57 insertions(+), 23 deletions(-) diff --git a/build.gradle b/build.gradle index 9d5b33a9..5c8984da 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_2' + compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_3' compile 'org.bouncycastle:bcprov-jdk15on:1.64' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/manager/BaseConfig.java b/src/main/java/org/asamk/signal/manager/BaseConfig.java index f204941c..edb6c201 100644 --- a/src/main/java/org/asamk/signal/manager/BaseConfig.java +++ b/src/main/java/org/asamk/signal/manager/BaseConfig.java @@ -37,13 +37,16 @@ public class BaseConfig { private final static List interceptors = Collections.singletonList(userAgentInterceptor); + private final static byte[] zkGroupServerPublicParams = new byte[]{}; + final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration( new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalContactDiscoveryUrl[0], new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, - interceptors + interceptors, + zkGroupServerPublicParams ); private BaseConfig() { diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java index 6ffc3f36..421a32f4 100644 --- a/src/main/java/org/asamk/signal/manager/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java @@ -1,6 +1,8 @@ package org.asamk.signal.manager; import org.asamk.signal.util.RandomUtils; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.util.Base64; class KeyUtils { @@ -12,8 +14,12 @@ class KeyUtils { return getSecret(52); } - static byte[] createProfileKey() { - return getSecretBytes(32); + static ProfileKey createProfileKey() { + try { + return new ProfileKey(getSecretBytes(32)); + } catch (InvalidInputException e) { + throw new AssertionError("Profile key is guaranteed to be 32 bytes here"); + } } static String createPassword() { diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 8d0d46c8..3a587257 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -41,6 +41,8 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException; 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.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; @@ -84,6 +86,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; +import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; @@ -126,6 +129,8 @@ import java.util.concurrent.TimeoutException; public class Manager implements Signal { + private static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false); + private final String settingsPath; private final String dataPath; private final String attachmentsPath; @@ -237,7 +242,7 @@ public class Manager implements Signal { if (username == null) { account = SignalAccount.createTemporaryAccount(identityKey, registrationId); } else { - byte[] profileKey = KeyUtils.createProfileKey(); + ProfileKey profileKey = KeyUtils.createProfileKey(); account = SignalAccount.create(dataPath, username, identityKey, registrationId, profileKey); account.save(); } @@ -265,7 +270,7 @@ public class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false); + accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities); } public void setProfileName(String name) throws IOException { @@ -314,9 +319,16 @@ public class Manager implements Signal { } // Create new account with the synced identity - byte[] profileKey = ret.getProfileKey(); - if (profileKey == null) { + byte[] profileKeyBytes = ret.getProfileKey(); + ProfileKey profileKey; + if (profileKeyBytes == null) { profileKey = KeyUtils.createProfileKey(); + } else { + try { + profileKey = new ProfileKey(profileKeyBytes); + } catch (InvalidInputException e) { + throw new IOException("Received invalid profileKey", e); + } } account = SignalAccount.createLinkedAccount(dataPath, username, account.getPassword(), ret.getDeviceId(), ret.getIdentity(), account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalingKey(), profileKey); @@ -354,7 +366,7 @@ public class Manager implements Signal { IdentityKeyPair identityKeyPair = account.getSignalProtocolStore().getIdentityKeyPair(); String verificationCode = accountManager.getNewDeviceVerificationCode(); - accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey()), verificationCode); + accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey().serialize()), verificationCode); account.setMultiDevice(true); account.save(); } @@ -396,7 +408,7 @@ public class Manager implements Signal { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); // TODO make unrestricted unidentified access configurable - accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false); + accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); account.setRegistered(true); @@ -502,7 +514,7 @@ public class Manager implements Signal { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withReaction(reaction) - .withProfileKey(account.getProfileKey()); + .withProfileKey(account.getProfileKey().serialize()); if (groupId != null) { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) .withId(groupId) @@ -685,7 +697,7 @@ public class Manager implements Signal { messageBuilder.withAttachments(attachmentPointers); } - messageBuilder.withProfileKey(account.getProfileKey()); + messageBuilder.withProfileKey(account.getProfileKey().serialize()); sendMessageLegacy(messageBuilder, recipients); } @@ -695,7 +707,7 @@ public class Manager implements Signal { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withReaction(reaction) - .withProfileKey(account.getProfileKey()); + .withProfileKey(account.getProfileKey().serialize()); sendMessageLegacy(messageBuilder, recipients); } @@ -1117,7 +1129,10 @@ public class Manager implements Signal { } if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { if (source.equals(username)) { - this.account.setProfileKey(message.getProfileKey().get()); + try { + this.account.setProfileKey(new ProfileKey(message.getProfileKey().get())); + } catch (InvalidInputException ignored) { + } } ContactInfo contact = account.getContactStore().getContact(source); if (contact == null) { @@ -1413,7 +1428,7 @@ public class Manager implements Signal { contact.color = c.getColor().get(); } if (c.getProfileKey().isPresent()) { - contact.profileKey = Base64.encodeBytes(c.getProfileKey().get()); + contact.profileKey = Base64.encodeBytes(c.getProfileKey().get().serialize()); } if (c.getVerified().isPresent()) { final VerifiedMessage verifiedMessage = c.getVerified().get(); @@ -1603,7 +1618,11 @@ public class Manager implements Signal { } } - byte[] profileKey = record.profileKey == null ? null : Base64.decode(record.profileKey); + ProfileKey profileKey = null; + try { + profileKey = record.profileKey == null ? null : new ProfileKey(Base64.decode(record.profileKey)); + } catch (InvalidInputException ignored) { + } out.write(new DeviceContact(record.getAddress(), Optional.fromNullable(record.name), createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), record.blocked, diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 151835cb..fd4da41f 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -16,6 +16,8 @@ import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; import org.asamk.signal.storage.threads.JsonThreadStore; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; @@ -42,7 +44,7 @@ public class SignalAccount { private String password; private String registrationLockPin; private String signalingKey; - private byte[] profileKey; + private ProfileKey profileKey; private int preKeyIdOffset; private int nextSignedPreKeyId; @@ -70,7 +72,7 @@ public class SignalAccount { return account; } - public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, byte[] profileKey) throws IOException { + public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey) throws IOException { IOUtils.createPrivateDirectories(dataPath); SignalAccount account = new SignalAccount(); @@ -87,7 +89,7 @@ public class SignalAccount { return account; } - public static SignalAccount createLinkedAccount(String dataPath, String username, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, byte[] profileKey) throws IOException { + public static SignalAccount createLinkedAccount(String dataPath, String username, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, ProfileKey profileKey) throws IOException { IOUtils.createPrivateDirectories(dataPath); SignalAccount account = new SignalAccount(); @@ -161,7 +163,11 @@ public class SignalAccount { nextSignedPreKeyId = 0; } if (rootNode.has("profileKey")) { - profileKey = Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()); + try { + profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText())); + } catch (InvalidInputException e) { + throw new IOException("Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes", e); + } } signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); @@ -203,7 +209,7 @@ public class SignalAccount { .put("signalingKey", signalingKey) .put("preKeyIdOffset", preKeyIdOffset) .put("nextSignedPreKeyId", nextSignedPreKeyId) - .put("profileKey", Base64.encodeBytes(profileKey)) + .put("profileKey", Base64.encodeBytes(profileKey.serialize())) .put("registered", registered) .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) @@ -307,11 +313,11 @@ public class SignalAccount { this.signalingKey = signalingKey; } - public byte[] getProfileKey() { + public ProfileKey getProfileKey() { return profileKey; } - public void setProfileKey(final byte[] profileKey) { + public void setProfileKey(final ProfileKey profileKey) { this.profileKey = profileKey; } From d7f7c84e6c023e96064efcfcc29a78440866218a Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 Mar 2020 14:41:11 +0100 Subject: [PATCH 007/103] Fix some inspection issues --- .../asamk/signal/commands/ReceiveCommand.java | 37 ++++++++----------- .../signal/commands/RemovePinCommand.java | 2 +- .../asamk/signal/commands/SendCommand.java | 4 +- .../signal/commands/SendReactionCommand.java | 2 +- .../org/asamk/signal/manager/Manager.java | 33 ++++++++--------- .../protocol/JsonIdentityKeyStore.java | 2 +- 6 files changed, 35 insertions(+), 45 deletions(-) diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 876b6832..d28513b5 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -37,31 +37,24 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { public int handleCommand(final Namespace ns, final Signal signal, DBusConnection dbusconnection) { if (dbusconnection != null) { try { - dbusconnection.addSigHandler(Signal.MessageReceived.class, new DBusSigHandler() { - @Override - public void handle(Signal.MessageReceived s) { - System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n", - s.getSender(), DateUtils.formatTimestamp(s.getTimestamp()), s.getMessage())); - if (s.getGroupId().length > 0) { - System.out.println("Group info:"); - System.out.println(" Id: " + Base64.encodeBytes(s.getGroupId())); - } - if (s.getAttachments().size() > 0) { - System.out.println("Attachments: "); - for (String attachment : s.getAttachments()) { - System.out.println("- Stored plaintext in: " + attachment); - } - } - System.out.println(); + dbusconnection.addSigHandler(Signal.MessageReceived.class, messageReceived -> { + System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n", + messageReceived.getSender(), DateUtils.formatTimestamp(messageReceived.getTimestamp()), messageReceived.getMessage())); + if (messageReceived.getGroupId().length > 0) { + System.out.println("Group info:"); + System.out.println(" Id: " + Base64.encodeBytes(messageReceived.getGroupId())); } - }); - dbusconnection.addSigHandler(Signal.ReceiptReceived.class, new DBusSigHandler() { - @Override - public void handle(Signal.ReceiptReceived s) { - System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n", - s.getSender(), DateUtils.formatTimestamp(s.getTimestamp()))); + if (messageReceived.getAttachments().size() > 0) { + System.out.println("Attachments: "); + for (String attachment : messageReceived.getAttachments()) { + System.out.println("- Stored plaintext in: " + attachment); + } } + System.out.println(); }); + dbusconnection.addSigHandler(Signal.ReceiptReceived.class, + receiptReceived -> System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n", + receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp())))); } catch (UnsatisfiedLinkError e) { System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); return 1; diff --git a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java index 39eb1f1b..b7de5402 100644 --- a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java @@ -21,7 +21,7 @@ public class RemovePinCommand implements LocalCommand { return 1; } try { - m.setRegistrationLockPin(Optional.absent()); + m.setRegistrationLockPin(Optional.absent()); return 0; } catch (IOException e) { System.err.println("Remove pin error: " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 7320d76d..a795cdd8 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -61,7 +61,7 @@ public class SendCommand implements DbusCommand { if (ns.getBoolean("endsession")) { try { - signal.sendEndSessionMessage(ns.getList("recipient")); + signal.sendEndSessionMessage(ns.getList("recipient")); return 0; } catch (IOException e) { handleIOException(e); @@ -98,7 +98,7 @@ public class SendCommand implements DbusCommand { byte[] groupId = Util.decodeGroupId(ns.getString("group")); signal.sendGroupMessage(messageText, attachments, groupId); } else { - signal.sendMessage(messageText, attachments, ns.getList("recipient")); + signal.sendMessage(messageText, attachments, ns.getList("recipient")); } return 0; } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 38560642..7b72caae 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -69,7 +69,7 @@ public class SendReactionCommand implements LocalCommand { byte[] groupId = Util.decodeGroupId(ns.getString("group")); m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId); } else { - m.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, ns.getList("recipient")); + m.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, ns.getList("recipient")); } return 0; } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 3a587257..69e45102 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -260,9 +260,9 @@ public class Manager implements Signal { accountManager = getSignalServiceAccountManager(); if (voiceVerification) { - accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.absent(), Optional.absent()); + accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.absent(), Optional.absent()); } else { - accountManager.requestSmsVerificationCode(false, Optional.absent(), Optional.absent()); + accountManager.requestSmsVerificationCode(false, Optional.absent(), Optional.absent()); } account.setRegistered(false); @@ -291,7 +291,7 @@ public class Manager implements Signal { // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // If this is the master device, other users can't send messages to this number anymore. // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. - accountManager.setGcmId(Optional.absent()); + accountManager.setGcmId(Optional.absent()); account.setRegistered(false); account.save(); @@ -443,7 +443,7 @@ public class Manager implements Signal { private SignalServiceMessageSender getMessageSender() { return new SignalServiceMessageSender(BaseConfig.serviceConfiguration, null, username, account.getPassword(), - account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); + account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); } private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { @@ -891,7 +891,7 @@ public class Manager implements Signal { private List> getAccessFor(Collection recipients) { List> result = new ArrayList<>(recipients.size()); for (SignalServiceAddress recipient : recipients) { - result.add(Optional.absent()); + result.add(Optional.absent()); } return result; } @@ -1217,16 +1217,13 @@ public class Manager implements Signal { Exception exception = null; final long now = new Date().getTime(); try { - envelope = messagePipe.read(timeout, unit, new SignalServiceMessagePipe.MessagePipeCallback() { - @Override - public void onMessage(SignalServiceEnvelope envelope) { - // store message on disk, before acknowledging receipt to the server - try { - File cacheFile = getMessageCacheFile(envelope.getSourceE164().get(), now, envelope.getTimestamp()); - Utils.storeEnvelope(envelope, cacheFile); - } catch (IOException e) { - System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage()); - } + envelope = messagePipe.read(timeout, unit, envelope1 -> { + // store message on disk, before acknowledging receipt to the server + try { + File cacheFile = getMessageCacheFile(envelope1.getSourceE164().get(), now, envelope1.getTimestamp()); + Utils.storeEnvelope(envelope1, cacheFile); + } catch (IOException e) { + System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage()); } }); } catch (TimeoutException e) { @@ -1633,10 +1630,10 @@ public class Manager implements Signal { if (account.getProfileKey() != null) { // Send our own profile key as well out.write(new DeviceContact(account.getSelfAddress(), - Optional.absent(), Optional.absent(), - Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent(), Optional.of(account.getProfileKey()), - false, Optional.absent(), Optional.absent(), false)); + false, Optional.absent(), Optional.absent(), false)); } } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index e6f0194d..c1381341 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -185,7 +185,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { } } - public class Identity { + public static class Identity { IdentityKey identityKey; TrustLevel trustLevel; From a6562b3b7baf7ba5621d00318e9fc52fb0b3e739 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Mar 2020 17:17:14 +0100 Subject: [PATCH 008/103] Implement sending messages using unidentified sender --- .../asamk/signal/ReceiveMessageHandler.java | 6 + .../org/asamk/signal/manager/KeyUtils.java | 4 + .../org/asamk/signal/manager/Manager.java | 118 ++++++++++++++++-- .../asamk/signal/manager/SignalProfile.java | 53 ++++++++ 4 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/asamk/signal/manager/SignalProfile.java diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 34d941ed..bf39b93f 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -5,6 +5,7 @@ import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.Util; +import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceContent; @@ -69,6 +70,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message."); System.out.println("Use 'signal-cli -u " + m.getUsername() + " listIdentities -n " + e.getName() + "', verify the key and run 'signal-cli -u " + m.getUsername() + " trust -v \"FINGER_PRINT\" " + e.getName() + "' to mark it as trusted"); System.out.println("If you don't care about security, use 'signal-cli -u " + m.getUsername() + " trust -a " + e.getName() + "' to trust it without verification"); + } else if (exception instanceof ProtocolUntrustedIdentityException) { + ProtocolUntrustedIdentityException e = (ProtocolUntrustedIdentityException) exception; + System.out.println("The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message."); + System.out.println("Use 'signal-cli -u " + m.getUsername() + " listIdentities -n " + e.getSender() + "', verify the key and run 'signal-cli -u " + m.getUsername() + " trust -v \"FINGER_PRINT\" " + e.getSender() + "' to mark it as trusted"); + System.out.println("If you don't care about security, use 'signal-cli -u " + m.getUsername() + " trust -a " + e.getSender() + "' to trust it without verification"); } else { System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass().getSimpleName() + ")"); } diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java index 421a32f4..364f1eab 100644 --- a/src/main/java/org/asamk/signal/manager/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java @@ -30,6 +30,10 @@ class KeyUtils { return getSecretBytes(16); } + static byte[] createUnrestrictedUnidentifiedAccess() { + return getSecretBytes(16); + } + private static String getSecret(int size) { byte[] secret = getSecretBytes(size); return Base64.encodeBytes(secret); diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 69e45102..fffa5013 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -41,7 +41,9 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException; import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.SelfSendException; +import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.VerificationFailedException; import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; @@ -61,6 +63,8 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; +import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; @@ -446,6 +450,25 @@ public class Manager implements Signal { account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); } + private SignalServiceProfile getRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess) throws IOException { + SignalServiceMessagePipe pipe = unidentifiedMessagePipe != null && unidentifiedAccess.isPresent() ? unidentifiedMessagePipe + : messagePipe; + + if (pipe != null) { + try { + return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).getProfile(); + } catch (IOException ignored) { + } + } + + SignalServiceMessageReceiver receiver = getMessageReceiver(); + try { + return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).getProfile(); + } catch (VerificationFailedException e) { + throw new AssertionError(e); + } + } + private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { File file = getGroupAvatarFile(groupId); if (!file.exists()) { @@ -874,31 +897,98 @@ public class Manager implements Signal { } } + private byte[] getSenderCertificate() throws IOException { + byte[] certificate = accountManager.getSenderCertificate(); + // TODO cache for a day + return certificate; + } + private byte[] getSelfUnidentifiedAccessKey() { return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey()); } - private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { - // TODO implement - return null; + private static SignalProfile decryptProfile(SignalServiceProfile encryptedProfile, ProfileKey profileKey) throws IOException { + ProfileCipher profileCipher = new ProfileCipher(profileKey); + try { + return new SignalProfile( + encryptedProfile.getIdentityKey(), + encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))), + encryptedProfile.getAvatar(), + encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(), + encryptedProfile.isUnrestrictedUnidentifiedAccess() + ); + } catch (InvalidCiphertextException e) { + return null; + } } - private Optional getAccessForSync() { - // TODO implement - return Optional.absent(); + private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) throws IOException { + ContactInfo contact = account.getContactStore().getContact(recipient.getNumber().get()); + if (contact == null || contact.profileKey == null) { + return null; + } + ProfileKey theirProfileKey; + try { + theirProfileKey = new ProfileKey(Base64.decode(contact.profileKey)); + } catch (InvalidInputException e) { + throw new AssertionError(e); + } + SignalProfile targetProfile = decryptProfile(getRecipientProfile(recipient, Optional.absent()), theirProfileKey); + + if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) { + return null; + } + + if (targetProfile.isUnrestrictedUnidentifiedAccess()) { + return KeyUtils.createUnrestrictedUnidentifiedAccess(); + } + + return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); } - private List> getAccessFor(Collection recipients) { + private Optional getAccessForSync() throws IOException { + byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); + byte[] selfUnidentifiedAccessCertificate = getSenderCertificate(); + + if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) { + return Optional.absent(); + } + + try { + return Optional.of(new UnidentifiedAccessPair( + new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate), + new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate) + )); + } catch (InvalidCertificateException e) { + return Optional.absent(); + } + } + + private List> getAccessFor(Collection recipients) throws IOException { List> result = new ArrayList<>(recipients.size()); for (SignalServiceAddress recipient : recipients) { - result.add(Optional.absent()); + result.add(getAccessFor(recipient)); } return result; } - private Optional getAccessFor(SignalServiceAddress recipient) { - // TODO implement - return Optional.absent(); + private Optional getAccessFor(SignalServiceAddress recipient) throws IOException { + byte[] recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); + byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); + byte[] selfUnidentifiedAccessCertificate = getSenderCertificate(); + + if (recipientUnidentifiedAccessKey == null || selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) { + return Optional.absent(); + } + + try { + return Optional.of(new UnidentifiedAccessPair( + new UnidentifiedAccess(recipientUnidentifiedAccessKey, selfUnidentifiedAccessCertificate), + new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate) + )); + } catch (InvalidCertificateException e) { + return Optional.absent(); + } } private void sendSyncMessage(SignalServiceSyncMessage message) @@ -945,6 +1035,12 @@ public class Manager implements Signal { return Collections.emptyList(); } + if (messagePipe == null) { + messagePipe = getMessageReceiver().createMessagePipe(); + } + if (unidentifiedMessagePipe == null) { + unidentifiedMessagePipe = getMessageReceiver().createUnidentifiedMessagePipe(); + } SignalServiceDataMessage message = null; try { SignalServiceMessageSender messageSender = getMessageSender(); diff --git a/src/main/java/org/asamk/signal/manager/SignalProfile.java b/src/main/java/org/asamk/signal/manager/SignalProfile.java new file mode 100644 index 00000000..4f529c0d --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/SignalProfile.java @@ -0,0 +1,53 @@ +package org.asamk.signal.manager; + +public class SignalProfile { + + private final String identityKey; + + private final String name; + + private final String avatar; + + private final String unidentifiedAccess; + + private final boolean unrestrictedUnidentifiedAccess; + + public SignalProfile(final String identityKey, final String name, final String avatar, final String unidentifiedAccess, final boolean unrestrictedUnidentifiedAccess) { + this.identityKey = identityKey; + this.name = name; + this.avatar = avatar; + this.unidentifiedAccess = unidentifiedAccess; + this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; + } + + public String getIdentityKey() { + return identityKey; + } + + public String getName() { + return name; + } + + public String getAvatar() { + return avatar; + } + + public String getUnidentifiedAccess() { + return unidentifiedAccess; + } + + public boolean isUnrestrictedUnidentifiedAccess() { + return unrestrictedUnidentifiedAccess; + } + + @Override + public String toString() { + return "SignalProfile{" + + "identityKey='" + identityKey + '\'' + + ", name='" + name + '\'' + + ", avatar='" + avatar + '\'' + + ", unidentifiedAccess='" + unidentifiedAccess + '\'' + + ", unrestrictedUnidentifiedAccess=" + unrestrictedUnidentifiedAccess + + '}'; + } +} From 67f6378f7f509f6cb55434bb516c154840c59bce Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Mar 2020 17:17:28 +0100 Subject: [PATCH 009/103] Fix storing received profile keys --- src/main/java/org/asamk/signal/manager/Manager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index fffa5013..ded15980 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1236,6 +1236,7 @@ public class Manager implements Signal { contact.number = source; } contact.profileKey = Base64.encodeBytes(message.getProfileKey().get()); + account.getContactStore().updateContact(contact); } if (message.getPreviews().isPresent()) { final List previews = message.getPreviews().get(); From bb06ae9d9a22896e95313d25135676e44ccb1db5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Mar 2020 18:20:52 +0100 Subject: [PATCH 010/103] Mark group as active when the user hasn't left it Fixes #269 --- .../org/asamk/signal/commands/ListGroupsCommand.java | 8 ++++---- src/main/java/org/asamk/signal/manager/Manager.java | 6 ++++-- .../org/asamk/signal/storage/groups/GroupInfo.java | 10 +++++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 9758b0e3..565bacba 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -12,13 +12,13 @@ import java.util.List; public class ListGroupsCommand implements LocalCommand { - private static void printGroup(GroupInfo group, boolean detailed) { + private static void printGroup(GroupInfo group, boolean detailed, String username) { if (detailed) { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b Members: %s", - Base64.encodeBytes(group.groupId), group.name, group.active, group.blocked, group.members)); + Base64.encodeBytes(group.groupId), group.name, group.members.contains(username), group.blocked, group.members)); } else { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", - Base64.encodeBytes(group.groupId), group.name, group.active, group.blocked)); + Base64.encodeBytes(group.groupId), group.name, group.members.contains(username), group.blocked)); } } @@ -40,7 +40,7 @@ public class ListGroupsCommand implements LocalCommand { boolean detailed = ns.getBoolean("detailed"); for (GroupInfo group : groups) { - printGroup(group, detailed); + printGroup(group, detailed, m.getUsername()); } return 0; } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index ded15980..f5bbe146 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1450,7 +1450,9 @@ public class Manager implements Signal { syncGroup.name = g.getName().get(); } syncGroup.addMembers(g.getMembers()); - syncGroup.active = g.isActive(); + if (!g.isActive()) { + syncGroup.members.remove(username); + } syncGroup.blocked = g.isBlocked(); if (g.getColor().isPresent()) { syncGroup.color = g.getColor().get(); @@ -1666,7 +1668,7 @@ public class Manager implements Signal { ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId)); out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId), - record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null), + record.members.contains(username), Optional.fromNullable(info != null ? info.messageExpirationTime : null), Optional.fromNullable(record.color), record.blocked, Optional.fromNullable(record.inboxPosition), record.archived)); } } diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java index 0387efac..cb53d3af 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -20,8 +20,6 @@ public class GroupInfo { @JsonProperty public Set members = new HashSet<>(); @JsonProperty - public boolean active; - @JsonProperty public String color; @JsonProperty(defaultValue = "false") public boolean blocked; @@ -32,17 +30,23 @@ public class GroupInfo { private long avatarId; + @JsonProperty + @JsonIgnore + private boolean active; + public GroupInfo(byte[] groupId) { this.groupId = groupId; } - public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked) { + public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived) { this.groupId = groupId; this.name = name; this.members.addAll(members); this.avatarId = avatarId; this.color = color; this.blocked = blocked; + this.inboxPosition = inboxPosition; + this.archived = archived; } @JsonIgnore From 3f315df6c8e0af0180215e19733a06f7efa1fae1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Mar 2020 11:31:55 +0100 Subject: [PATCH 011/103] Add toString method to Hex utils --- .../asamk/signal/commands/ListIdentitiesCommand.java | 2 +- src/main/java/org/asamk/signal/util/Hex.java | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index dd3e5e46..a7cf8705 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -18,7 +18,7 @@ public class ListIdentitiesCommand implements LocalCommand { private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) { String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.getIdentityKey())); System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername, - theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toStringCondensed(theirId.getFingerprint()), digits)); + theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toString(theirId.getFingerprint()), digits)); } @Override diff --git a/src/main/java/org/asamk/signal/util/Hex.java b/src/main/java/org/asamk/signal/util/Hex.java index 95b2d26f..8c6c62a2 100644 --- a/src/main/java/org/asamk/signal/util/Hex.java +++ b/src/main/java/org/asamk/signal/util/Hex.java @@ -9,6 +9,15 @@ public class Hex { private Hex() { } + public static String toString(byte[] bytes) { + StringBuffer buf = new StringBuffer(); + for (final byte aByte : bytes) { + appendHexChar(buf, aByte); + buf.append(" "); + } + return buf.toString(); + } + public static String toStringCondensed(byte[] bytes) { StringBuffer buf = new StringBuffer(); for (final byte aByte : bytes) { @@ -20,7 +29,6 @@ public class Hex { private static void appendHexChar(StringBuffer buf, int b) { buf.append(HEX_DIGITS[(b >> 4) & 0xf]); buf.append(HEX_DIGITS[b & 0xf]); - buf.append(" "); } public static byte[] toByteArray(String s) { From 23845eab4740c293617179e0f5e278f56c950fe4 Mon Sep 17 00:00:00 2001 From: Signal Stickers Date: Sun, 29 Dec 2019 16:23:51 -0500 Subject: [PATCH 012/103] Add support for uploading stickers. Closes #256 --- .../org/asamk/signal/JsonStickerPack.java | 29 ++++ .../signal/StickerPackInvalidException.java | 8 + .../org/asamk/signal/commands/Commands.java | 1 + .../commands/UploadStickerPackCommand.java | 36 +++++ .../org/asamk/signal/manager/Manager.java | 145 ++++++++++++++++++ .../java/org/asamk/signal/util/IOUtils.java | 9 ++ 6 files changed, 228 insertions(+) create mode 100644 src/main/java/org/asamk/signal/JsonStickerPack.java create mode 100644 src/main/java/org/asamk/signal/StickerPackInvalidException.java create mode 100644 src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java diff --git a/src/main/java/org/asamk/signal/JsonStickerPack.java b/src/main/java/org/asamk/signal/JsonStickerPack.java new file mode 100644 index 00000000..4594c5d1 --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonStickerPack.java @@ -0,0 +1,29 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class JsonStickerPack { + + @JsonProperty + public String title; + + @JsonProperty + public String author; + + @JsonProperty + public JsonSticker cover; + + @JsonProperty + public List stickers; + + public static class JsonSticker { + + @JsonProperty + public String emoji; + + @JsonProperty + public String file; + } +} diff --git a/src/main/java/org/asamk/signal/StickerPackInvalidException.java b/src/main/java/org/asamk/signal/StickerPackInvalidException.java new file mode 100644 index 00000000..5fea30fe --- /dev/null +++ b/src/main/java/org/asamk/signal/StickerPackInvalidException.java @@ -0,0 +1,8 @@ +package org.asamk.signal; + +public class StickerPackInvalidException extends Exception { + + public StickerPackInvalidException(String message) { + super(message); + } +} diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 24a03e3f..183b40a0 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -33,6 +33,7 @@ public class Commands { addCommand("updateGroup", new UpdateGroupCommand()); addCommand("updateProfile", new UpdateProfileCommand()); addCommand("verify", new VerifyCommand()); + addCommand("uploadStickerPack", new UploadStickerPackCommand()); } public static Map getCommands() { diff --git a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java new file mode 100644 index 00000000..d4c9e155 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java @@ -0,0 +1,36 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.StickerPackInvalidException; +import org.asamk.signal.manager.Manager; + +import java.io.IOException; + +public class UploadStickerPackCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("path") + .help("The path of the manifest.json or a zip file containing the sticker pack you wish to upload."); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + try { + String path = ns.getString("path"); + String url = m.uploadStickerPack(path); + System.out.println(""); + System.out.println("Upload complete! Sticker pack URL:"); + System.out.println(url); + return 0; + } catch (IOException e) { + System.err.println("Upload error: " + e.getMessage()); + return 3; + } catch (StickerPackInvalidException e) { + System.err.println("Invalid sticker pack: " + e.getMessage()); + return 3; + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index f5bbe146..622eb815 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -16,10 +16,14 @@ */ package org.asamk.signal.manager; +import com.fasterxml.jackson.databind.ObjectMapper; + import org.asamk.Signal; import org.asamk.signal.AttachmentInvalidException; import org.asamk.signal.GroupNotFoundException; +import org.asamk.signal.JsonStickerPack; import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.StickerPackInvalidException; import org.asamk.signal.TrustLevel; import org.asamk.signal.UserAlreadyExists; import org.asamk.signal.storage.SignalAccount; @@ -77,6 +81,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest; +import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest.StickerInfo; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; @@ -102,9 +108,13 @@ import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; +import org.whispersystems.signalservice.internal.push.StickerUploadAttributes; +import org.whispersystems.signalservice.internal.push.StickerUploadAttributesResponse; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; +import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.util.Base64; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -121,6 +131,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -130,6 +141,8 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; public class Manager implements Signal { @@ -857,6 +870,138 @@ public class Manager implements Signal { account.getThreadStore().updateThread(thread); } + public String uploadStickerPack(String path) throws IOException, StickerPackInvalidException { + JsonStickerPack pack = parseStickerPack(path); + + if (pack.stickers == null) { + throw new StickerPackInvalidException("Must set a 'stickers' field."); + } + + if (pack.stickers.isEmpty()) { + throw new StickerPackInvalidException("Must include stickers."); + } + + List stickers = new ArrayList<>(pack.stickers.size()); + for (int i = 0; i < pack.stickers.size(); i++) { + if (pack.stickers.get(i).file == null) { + throw new StickerPackInvalidException("Must set a 'file' field on each sticker."); + } + if (!stickerDataContainsPath(path, pack.stickers.get(i).file)) { + throw new StickerPackInvalidException("Could not find find " + pack.stickers.get(i).file); + } + + StickerInfo stickerInfo = new StickerInfo(i, Optional.fromNullable(pack.stickers.get(i).emoji).or("")); + stickers.add(stickerInfo); + } + + boolean uniqueCover = false; + StickerInfo cover = stickers.get(0); + if (pack.cover != null) { + if (pack.cover.file == null) { + throw new StickerPackInvalidException("Must set a 'file' field on the cover."); + } + if (!stickerDataContainsPath(path, pack.cover.file)) { + throw new StickerPackInvalidException("Could not find find cover " + pack.cover.file); + } + + uniqueCover = true; + cover = new StickerInfo(pack.stickers.size(), Optional.fromNullable(pack.cover.emoji).or("")); + } + + SignalServiceStickerManifest manifest = new SignalServiceStickerManifest( + Optional.fromNullable(pack.title).or(""), + Optional.fromNullable(pack.author).or(""), + cover, + stickers); + + SignalServiceMessageSender messageSender = new SignalServiceMessageSender( + BaseConfig.serviceConfiguration, + null, + username, + account.getPassword(), + account.getDeviceId(), + account.getSignalProtocolStore(), + BaseConfig.USER_AGENT, + account.isMultiDevice(), + Optional.fromNullable(messagePipe), + Optional.fromNullable(unidentifiedMessagePipe), + Optional.absent()); + + System.out.println("Starting upload process..."); + Pair responsePair = messageSender.getStickerUploadAttributes(stickers.size() + (uniqueCover ? 1 : 0)); + byte[] packKey = responsePair.first(); + StickerUploadAttributesResponse response = responsePair.second(); + + System.out.println("Uploading manifest..."); + messageSender.uploadStickerManifest(manifest, packKey, response.getManifest()); + + Map attrById = new HashMap<>(); + + for (StickerUploadAttributes attr : response.getStickers()) { + attrById.put(attr.getId(), attr); + } + + for (int i = 0; i < pack.stickers.size(); i++) { + System.out.println("Uploading sticker " + (i+1) + "/" + pack.stickers.size() + "..."); + StickerUploadAttributes attr = attrById.get(i); + if (attr == null) { + throw new StickerPackInvalidException("Upload attributes missing for id " + i); + } + + byte[] data = readStickerDataFromPath(path, pack.stickers.get(i).file); + messageSender.uploadSticker(new ByteArrayInputStream(data), data.length, packKey, attr); + } + + if (uniqueCover) { + System.out.println("Uploading unique cover..."); + StickerUploadAttributes attr = attrById.get(pack.stickers.size()); + if (attr == null) { + throw new StickerPackInvalidException("Upload attributes missing for cover with id " + pack.stickers.size()); + } + + byte[] data = readStickerDataFromPath(path, pack.cover.file); + messageSender.uploadSticker(new ByteArrayInputStream(data), data.length, packKey, attr); + } + + return "https://signal.art/addstickers/#pack_id=" + response.getPackId() + "&pack_key=" + Hex.toStringCondensed(packKey).replaceAll(" ", ""); + } + + private static byte[] readStickerDataFromPath(String rootPath, String subFile) throws IOException, StickerPackInvalidException { + if (rootPath.endsWith(".zip")) { + ZipFile zip = new ZipFile(rootPath); + ZipEntry entry = zip.getEntry(subFile); + return IOUtils.readFully(zip.getInputStream(entry)); + } else if (rootPath.endsWith(".json")) { + String dir = new File(rootPath).getParent(); + FileInputStream fis = new FileInputStream(new File(dir, subFile)); + return IOUtils.readFully(fis); + } else { + throw new StickerPackInvalidException("Must point to either a ZIP or JSON file."); + } + } + + private static boolean stickerDataContainsPath(String rootPath, String subFile) throws IOException { + if (rootPath.endsWith(".zip")) { + ZipFile zip = new ZipFile(rootPath); + return zip.getEntry(subFile) != null; + } else if (rootPath.endsWith(".json")) { + String dir = new File(rootPath).getParent(); + return new File(dir, subFile).exists(); + } else { + return false; + } + } + + private static JsonStickerPack parseStickerPack(String rootPath) throws IOException, StickerPackInvalidException { + if (!stickerDataContainsPath(rootPath, "manifest.json")) { + throw new StickerPackInvalidException("Could not find manifest.json"); + } + + String json = new String(readStickerDataFromPath(rootPath, "manifest.json")); + + return new ObjectMapper().readValue(json, JsonStickerPack.class); + } + private void requestSyncGroups() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); diff --git a/src/main/java/org/asamk/signal/util/IOUtils.java b/src/main/java/org/asamk/signal/util/IOUtils.java index 434669de..f21c1572 100644 --- a/src/main/java/org/asamk/signal/util/IOUtils.java +++ b/src/main/java/org/asamk/signal/util/IOUtils.java @@ -1,5 +1,8 @@ package org.asamk.signal.util; +import org.whispersystems.signalservice.internal.util.Util; + +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -35,6 +38,12 @@ public class IOUtils { return output.toString(); } + public static byte[] readFully(InputStream in) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Util.copy(in, baos); + return baos.toByteArray(); + } + public static void createPrivateDirectories(String directoryPath) throws IOException { final File file = new File(directoryPath); if (file.exists()) { From 4ff28458ffac5e6e7e5a40d17e1f65786f2b0b0e Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Mar 2020 13:18:39 +0100 Subject: [PATCH 013/103] Refactor sticker upload --- build.gradle | 2 +- .../commands/UploadStickerPackCommand.java | 2 - .../org/asamk/signal/manager/KeyUtils.java | 4 + .../org/asamk/signal/manager/Manager.java | 171 ++++++++---------- 4 files changed, 76 insertions(+), 103 deletions(-) diff --git a/build.gradle b/build.gradle index 5c8984da..55820677 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_3' + compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_4' compile 'org.bouncycastle:bcprov-jdk15on:1.64' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java index d4c9e155..fe25966c 100644 --- a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java +++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java @@ -21,8 +21,6 @@ public class UploadStickerPackCommand implements LocalCommand { try { String path = ns.getString("path"); String url = m.uploadStickerPack(path); - System.out.println(""); - System.out.println("Upload complete! Sticker pack URL:"); System.out.println(url); return 0; } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java index 364f1eab..bd093e79 100644 --- a/src/main/java/org/asamk/signal/manager/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java @@ -34,6 +34,10 @@ class KeyUtils { return getSecretBytes(16); } + static byte[] createStickerUploadKey() { + return getSecretBytes(64); + } + private static String getSecret(int size) { byte[] secret = getSecretBytes(size); return Base64.encodeBytes(secret); diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 622eb815..99b0d82a 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -81,8 +81,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; -import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest; -import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest.StickerInfo; +import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload; +import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload.StickerInfo; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; @@ -108,13 +108,10 @@ import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; -import org.whispersystems.signalservice.internal.push.StickerUploadAttributes; -import org.whispersystems.signalservice.internal.push.StickerUploadAttributesResponse; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.util.Base64; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -123,6 +120,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; @@ -131,7 +130,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -870,8 +868,42 @@ public class Manager implements Signal { account.getThreadStore().updateThread(thread); } + /** + * Upload the sticker pack from path. + * + * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file + * @return if successful, returns the URL to install the sticker pack in the signal app + */ public String uploadStickerPack(String path) throws IOException, StickerPackInvalidException { - JsonStickerPack pack = parseStickerPack(path); + SignalServiceStickerManifestUpload manifest = getSignalServiceStickerManifestUpload(path); + + SignalServiceMessageSender messageSender = getMessageSender(); + + byte[] packKey = KeyUtils.createStickerUploadKey(); + String packId = messageSender.uploadStickerManifest(manifest, packKey); + + try { + return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder.encode(packId, "utf-8") + "&pack_key=" + URLEncoder.encode(Hex.toStringCondensed(packKey), "utf-8")) + .toString(); + } catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + private SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload(final String path) throws IOException, StickerPackInvalidException { + ZipFile zip = null; + String rootPath = null; + + final File file = new File(path); + if (file.getName().endsWith(".zip")) { + zip = new ZipFile(file); + } else if (file.getName().equals("manifest.json")) { + rootPath = file.getParent(); + } else { + throw new StickerPackInvalidException("Could not find manifest.json"); + } + + JsonStickerPack pack = parseStickerPack(rootPath, zip); if (pack.stickers == null) { throw new StickerPackInvalidException("Must set a 'stickers' field."); @@ -882,126 +914,65 @@ public class Manager implements Signal { } List stickers = new ArrayList<>(pack.stickers.size()); - for (int i = 0; i < pack.stickers.size(); i++) { - if (pack.stickers.get(i).file == null) { + for (JsonStickerPack.JsonSticker sticker : pack.stickers) { + if (sticker.file == null) { throw new StickerPackInvalidException("Must set a 'file' field on each sticker."); } - if (!stickerDataContainsPath(path, pack.stickers.get(i).file)) { - throw new StickerPackInvalidException("Could not find find " + pack.stickers.get(i).file); + + Pair data; + try { + data = getInputStreamAndLength(rootPath, zip, sticker.file); + } catch (IOException ignored) { + throw new StickerPackInvalidException("Could not find find " + sticker.file); } - StickerInfo stickerInfo = new StickerInfo(i, Optional.fromNullable(pack.stickers.get(i).emoji).or("")); + StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or("")); stickers.add(stickerInfo); } - boolean uniqueCover = false; - StickerInfo cover = stickers.get(0); + StickerInfo cover = null; if (pack.cover != null) { if (pack.cover.file == null) { throw new StickerPackInvalidException("Must set a 'file' field on the cover."); } - if (!stickerDataContainsPath(path, pack.cover.file)) { - throw new StickerPackInvalidException("Could not find find cover " + pack.cover.file); + + Pair data; + try { + data = getInputStreamAndLength(rootPath, zip, pack.cover.file); + } catch (IOException ignored) { + throw new StickerPackInvalidException("Could not find find " + pack.cover.file); } - uniqueCover = true; - cover = new StickerInfo(pack.stickers.size(), Optional.fromNullable(pack.cover.emoji).or("")); + cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or("")); } - SignalServiceStickerManifest manifest = new SignalServiceStickerManifest( - Optional.fromNullable(pack.title).or(""), - Optional.fromNullable(pack.author).or(""), + return new SignalServiceStickerManifestUpload( + pack.title, + pack.author, cover, stickers); - - SignalServiceMessageSender messageSender = new SignalServiceMessageSender( - BaseConfig.serviceConfiguration, - null, - username, - account.getPassword(), - account.getDeviceId(), - account.getSignalProtocolStore(), - BaseConfig.USER_AGENT, - account.isMultiDevice(), - Optional.fromNullable(messagePipe), - Optional.fromNullable(unidentifiedMessagePipe), - Optional.absent()); - - System.out.println("Starting upload process..."); - Pair responsePair = messageSender.getStickerUploadAttributes(stickers.size() + (uniqueCover ? 1 : 0)); - byte[] packKey = responsePair.first(); - StickerUploadAttributesResponse response = responsePair.second(); - - System.out.println("Uploading manifest..."); - messageSender.uploadStickerManifest(manifest, packKey, response.getManifest()); - - Map attrById = new HashMap<>(); - - for (StickerUploadAttributes attr : response.getStickers()) { - attrById.put(attr.getId(), attr); - } - - for (int i = 0; i < pack.stickers.size(); i++) { - System.out.println("Uploading sticker " + (i+1) + "/" + pack.stickers.size() + "..."); - StickerUploadAttributes attr = attrById.get(i); - if (attr == null) { - throw new StickerPackInvalidException("Upload attributes missing for id " + i); - } - - byte[] data = readStickerDataFromPath(path, pack.stickers.get(i).file); - messageSender.uploadSticker(new ByteArrayInputStream(data), data.length, packKey, attr); - } - - if (uniqueCover) { - System.out.println("Uploading unique cover..."); - StickerUploadAttributes attr = attrById.get(pack.stickers.size()); - if (attr == null) { - throw new StickerPackInvalidException("Upload attributes missing for cover with id " + pack.stickers.size()); - } - - byte[] data = readStickerDataFromPath(path, pack.cover.file); - messageSender.uploadSticker(new ByteArrayInputStream(data), data.length, packKey, attr); - } - - return "https://signal.art/addstickers/#pack_id=" + response.getPackId() + "&pack_key=" + Hex.toStringCondensed(packKey).replaceAll(" ", ""); } - private static byte[] readStickerDataFromPath(String rootPath, String subFile) throws IOException, StickerPackInvalidException { - if (rootPath.endsWith(".zip")) { - ZipFile zip = new ZipFile(rootPath); - ZipEntry entry = zip.getEntry(subFile); - return IOUtils.readFully(zip.getInputStream(entry)); - } else if (rootPath.endsWith(".json")) { - String dir = new File(rootPath).getParent(); - FileInputStream fis = new FileInputStream(new File(dir, subFile)); - return IOUtils.readFully(fis); + private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException, StickerPackInvalidException { + InputStream inputStream; + if (zip != null) { + inputStream = zip.getInputStream(zip.getEntry("manifest.json")); } else { - throw new StickerPackInvalidException("Must point to either a ZIP or JSON file."); + inputStream = new FileInputStream((new File(rootPath, "manifest.json"))); } + return new ObjectMapper().readValue(inputStream, JsonStickerPack.class); } - private static boolean stickerDataContainsPath(String rootPath, String subFile) throws IOException { - if (rootPath.endsWith(".zip")) { - ZipFile zip = new ZipFile(rootPath); - return zip.getEntry(subFile) != null; - } else if (rootPath.endsWith(".json")) { - String dir = new File(rootPath).getParent(); - return new File(dir, subFile).exists(); + private static Pair getInputStreamAndLength(final String rootPath, final ZipFile zip, final String subfile) throws IOException { + if (zip != null) { + final ZipEntry entry = zip.getEntry(subfile); + return new Pair<>(zip.getInputStream(entry), entry.getSize()); } else { - return false; + final File file = new File(rootPath, subfile); + return new Pair<>(new FileInputStream(file), file.length()); } } - private static JsonStickerPack parseStickerPack(String rootPath) throws IOException, StickerPackInvalidException { - if (!stickerDataContainsPath(rootPath, "manifest.json")) { - throw new StickerPackInvalidException("Could not find manifest.json"); - } - - String json = new String(readStickerDataFromPath(rootPath, "manifest.json")); - - return new ObjectMapper().readValue(json, JsonStickerPack.class); - } - private void requestSyncGroups() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); From 4f1ee8347564d4c26a36df6b959ea69f6406abe1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Mar 2020 13:25:39 +0100 Subject: [PATCH 014/103] Reformat project --- .idea/codeStyles/Project.xml | 22 +++++++++ .../org/asamk/signal/JsonSyncMessage.java | 47 +++++++++---------- .../asamk/signal/commands/BlockCommand.java | 1 + .../signal/commands/ListContactsCommand.java | 2 + .../asamk/signal/commands/ReceiveCommand.java | 3 +- .../signal/commands/SendContactsCommand.java | 1 + .../asamk/signal/commands/UnblockCommand.java | 1 + 7 files changed, 51 insertions(+), 26 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 9f37145b..4953eaca 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -4,6 +4,28 @@ diff --git a/src/main/java/org/asamk/signal/JsonSyncMessage.java b/src/main/java/org/asamk/signal/JsonSyncMessage.java index a6ecb459..326ec4ed 100644 --- a/src/main/java/org/asamk/signal/JsonSyncMessage.java +++ b/src/main/java/org/asamk/signal/JsonSyncMessage.java @@ -4,14 +4,13 @@ import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; - import java.util.ArrayList; import java.util.List; enum JsonSyncMessageType { - CONTACTS_SYNC, - GROUPS_SYNC, - REQUEST_SYNC + CONTACTS_SYNC, + GROUPS_SYNC, + REQUEST_SYNC } class JsonSyncMessage { @@ -22,25 +21,25 @@ class JsonSyncMessage { JsonSyncMessageType type; JsonSyncMessage(SignalServiceSyncMessage syncMessage) { - if (syncMessage.getSent().isPresent()) { - this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get()); - } - if (syncMessage.getBlockedList().isPresent()) { - this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size()); - for (SignalServiceAddress address : syncMessage.getBlockedList().get().getAddresses()) { - this.blockedNumbers.add(address.getNumber().get()); - } - } - if (syncMessage.getRead().isPresent()) { - this.readMessages = syncMessage.getRead().get(); - } + if (syncMessage.getSent().isPresent()) { + this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get()); + } + if (syncMessage.getBlockedList().isPresent()) { + this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size()); + for (SignalServiceAddress address : syncMessage.getBlockedList().get().getAddresses()) { + this.blockedNumbers.add(address.getNumber().get()); + } + } + if (syncMessage.getRead().isPresent()) { + this.readMessages = syncMessage.getRead().get(); + } - if (syncMessage.getContacts().isPresent()) { - this.type = JsonSyncMessageType.CONTACTS_SYNC; - } else if (syncMessage.getGroups().isPresent()) { - this.type = JsonSyncMessageType.GROUPS_SYNC; - } else if (syncMessage.getRequest().isPresent()) { - this.type = JsonSyncMessageType.REQUEST_SYNC; - } - } + if (syncMessage.getContacts().isPresent()) { + this.type = JsonSyncMessageType.CONTACTS_SYNC; + } else if (syncMessage.getGroups().isPresent()) { + this.type = JsonSyncMessageType.GROUPS_SYNC; + } else if (syncMessage.getRequest().isPresent()) { + this.type = JsonSyncMessageType.REQUEST_SYNC; + } + } } diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index a49fc798..305c5df2 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.GroupIdFormatException; import org.asamk.signal.GroupNotFoundException; import org.asamk.signal.manager.Manager; diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index 1d2b7b31..24d6898c 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -5,9 +5,11 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.contacts.ContactInfo; + import java.util.List; public class ListContactsCommand implements LocalCommand { + @Override public void attachToSubparser(final Subparser subparser) { } diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index d28513b5..ffc59530 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -10,7 +10,6 @@ import org.asamk.signal.ReceiveMessageHandler; import org.asamk.signal.manager.Manager; import org.asamk.signal.util.DateUtils; import org.freedesktop.dbus.DBusConnection; -import org.freedesktop.dbus.DBusSigHandler; import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.util.Base64; @@ -54,7 +53,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { }); dbusconnection.addSigHandler(Signal.ReceiptReceived.class, receiptReceived -> System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n", - receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp())))); + receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp())))); } catch (UnsatisfiedLinkError e) { System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); return 1; diff --git a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java index 523292ab..20e81a60 100644 --- a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index be745cb0..2fad39a5 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.GroupIdFormatException; import org.asamk.signal.GroupNotFoundException; import org.asamk.signal.manager.Manager; From 8a44b3777455a3b123a0f813ebd0df652821b908 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Mar 2020 14:48:52 +0100 Subject: [PATCH 015/103] Add documentation for sticker upload --- man/signal-cli.1.adoc | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 4101986f..98a54756 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -287,6 +287,34 @@ sendContacts Send a synchronization message with the local contacts list to all linked devices. This command should only be used if this is the master device. +uploadStickerPack +~~~~~~~~~~~~~~~~~ +Upload a new sticker pack, consisting of a manifest file and the stickers in WebP +format (maximum size for a sticker file is 100KiB). +The required manifest.json has the following format: + +```json +{ + "title": "", + "author": "", + "cover": { // Optional cover, by default the first sticker is used as cover + "file": "", + "emoji": "" + }, + "stickers": [ + { + "file": "", + "emoji": "" + } + ... + ] +} +``` + +PATH:: + The path of the manifest.json or a zip file containing the sticker pack you + wish to upload. + daemon ~~~~~~ signal-cli can run in daemon mode and provides an experimental dbus interface. For From e2b7bda65ba728a0a747bb0bac6fbe1e56fd2de8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Mar 2020 17:09:56 +0100 Subject: [PATCH 016/103] Use SignalServiceAddress in more places --- src/main/java/org/asamk/Signal.java | 10 +- .../asamk/signal/ReceiveMessageHandler.java | 10 +- .../signal/commands/QuitGroupCommand.java | 5 + .../asamk/signal/commands/SendCommand.java | 8 + .../signal/commands/SendReactionCommand.java | 5 + .../signal/commands/UpdateGroupCommand.java | 5 + .../org/asamk/signal/manager/Manager.java | 156 +++++++++--------- .../java/org/asamk/signal/manager/Utils.java | 22 --- .../asamk/signal/storage/SignalAccount.java | 8 +- .../protocol/JsonIdentityKeyStore.java | 2 +- .../org/asamk/signal/util/ErrorUtils.java | 7 + 11 files changed, 124 insertions(+), 114 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 8c9c525f..90d02758 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -13,13 +13,13 @@ import java.util.List; public interface Signal extends DBusInterface { - void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; + void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException; - void sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; + void sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException; - void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions; + void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException; - void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; + void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException, InvalidNumberException; String getContactName(String number) throws InvalidNumberException; @@ -35,7 +35,7 @@ public interface Signal extends DBusInterface { List getGroupMembers(byte[] groupId); - byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException; + byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException; boolean isRegistered(); diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index bf39b93f..62e3a76d 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -109,7 +109,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("Received sync read messages list"); for (ReadMessage rm : syncMessage.getRead().get()) { ContactInfo fromContact = m.getContact(rm.getSender().getNumber().get()); - System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender().getNumber() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp())); + System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender().getNumber().get() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp())); } } if (syncMessage.getRequest().isPresent()) { @@ -144,7 +144,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("Blocked numbers:"); final BlockedListMessage blockedList = syncMessage.getBlockedList().get(); for (SignalServiceAddress address : blockedList.getAddresses()) { - System.out.println(" - " + address.getNumber()); + System.out.println(" - " + address.getNumber().get()); } } if (syncMessage.getVerified().isPresent()) { @@ -168,7 +168,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (syncMessage.getViewOnceOpen().isPresent()) { final ViewOnceOpenMessage viewOnceOpenMessage = syncMessage.getViewOnceOpen().get(); System.out.println("Received sync message with view once open message:"); - System.out.println(" - Sender:" + viewOnceOpenMessage.getSender().getNumber()); + System.out.println(" - Sender:" + viewOnceOpenMessage.getSender().getNumber().get()); System.out.println(" - Timestamp:" + viewOnceOpenMessage.getTimestamp()); } if (syncMessage.getStickerPackOperations().isPresent()) { @@ -322,7 +322,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final SignalServiceDataMessage.Reaction reaction = message.getReaction().get(); System.out.println("Reaction:"); System.out.println(" - Emoji: " + reaction.getEmoji()); - System.out.println(" - Target author: " + reaction.getTargetAuthor().getNumber()); + System.out.println(" - Target author: " + reaction.getTargetAuthor().getNumber().get()); System.out.println(" - Target timestamp: " + reaction.getTargetSentTimestamp()); System.out.println(" - Is remove: " + reaction.isRemove()); } @@ -330,7 +330,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (message.getQuote().isPresent()) { SignalServiceDataMessage.Quote quote = message.getQuote().get(); System.out.println("Quote: (" + quote.getId() + ")"); - System.out.println(" Author: " + quote.getAuthor().getNumber()); + System.out.println(" Author: " + quote.getAuthor().getNumber().get()); System.out.println(" Text: " + quote.getText()); if (quote.getAttachments().size() > 0) { System.out.println(" Attachments: "); diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 6e53cb2a..38bc79ac 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -9,6 +9,7 @@ import org.asamk.signal.NotAGroupMemberException; import org.asamk.signal.manager.Manager; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; @@ -17,6 +18,7 @@ import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; import static org.asamk.signal.util.ErrorUtils.handleIOException; +import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException; import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class QuitGroupCommand implements LocalCommand { @@ -56,6 +58,9 @@ public class QuitGroupCommand implements LocalCommand { } catch (GroupIdFormatException e) { handleGroupIdFormatException(e); return 1; + } catch (InvalidNumberException e) { + handleInvalidNumberException(e); + return 1; } } } diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index a795cdd8..ab7ca246 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -13,6 +13,7 @@ import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; import java.nio.charset.Charset; @@ -25,6 +26,7 @@ import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; import static org.asamk.signal.util.ErrorUtils.handleIOException; +import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException; import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class SendCommand implements DbusCommand { @@ -75,6 +77,9 @@ public class SendCommand implements DbusCommand { } catch (DBusExecutionException e) { handleDBusExecutionException(e); return 1; + } catch (InvalidNumberException e) { + handleInvalidNumberException(e); + return 1; } } @@ -126,6 +131,9 @@ public class SendCommand implements DbusCommand { } catch (GroupIdFormatException e) { handleGroupIdFormatException(e); return 1; + } catch (InvalidNumberException e) { + handleInvalidNumberException(e); + return 1; } } } diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 7b72caae..ffb30195 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -11,6 +11,7 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; @@ -19,6 +20,7 @@ import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; import static org.asamk.signal.util.ErrorUtils.handleIOException; +import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException; import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class SendReactionCommand implements LocalCommand { @@ -90,6 +92,9 @@ public class SendReactionCommand implements LocalCommand { } catch (GroupIdFormatException e) { handleGroupIdFormatException(e); return 1; + } catch (InvalidNumberException e) { + handleInvalidNumberException(e); + return 1; } } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 66071cb0..63f5252e 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -10,6 +10,7 @@ import org.asamk.signal.GroupNotFoundException; import org.asamk.signal.NotAGroupMemberException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.util.Base64; import java.io.IOException; @@ -20,6 +21,7 @@ import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; import static org.asamk.signal.util.ErrorUtils.handleIOException; +import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException; import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class UpdateGroupCommand implements DbusCommand { @@ -88,6 +90,9 @@ public class UpdateGroupCommand implements DbusCommand { } catch (GroupIdFormatException e) { handleGroupIdFormatException(e); return 1; + } catch (InvalidNumberException e) { + handleInvalidNumberException(e); + return 1; } } } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 99b0d82a..a8be8ad8 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -171,10 +171,6 @@ public class Manager implements Signal { return username; } - private SignalServiceAddress getSelfAddress() { - return new SignalServiceAddress(null, username); - } - private SignalServiceAccountManager getSignalServiceAccountManager() { return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, null, account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer); } @@ -453,11 +449,11 @@ public class Manager implements Signal { } private SignalServiceMessageReceiver getMessageReceiver() { - return new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, null, username, account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer); + return new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer); } private SignalServiceMessageSender getMessageSender() { - return new SignalServiceMessageSender(BaseConfig.serviceConfiguration, null, username, account.getPassword(), + return new SignalServiceMessageSender(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); } @@ -504,7 +500,7 @@ public class Manager implements Signal { throw new GroupNotFoundException(groupId); } for (String member : g.members) { - if (member.equals(this.username)) { + if (member.equals(account.getUsername())) { return g; } } @@ -518,7 +514,7 @@ public class Manager implements Signal { @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) - throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments)); @@ -536,15 +532,15 @@ public class Manager implements Signal { final GroupInfo g = getGroupForSending(groupId); + final Collection membersSend = getSignalServiceAddresses(g.members); // Don't send group message to ourself - final List membersSend = new ArrayList<>(g.members); - membersSend.remove(this.username); + membersSend.remove(account.getSelfAddress()); sendMessageLegacy(messageBuilder, membersSend); } public void sendGroupMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, long targetSentTimestamp, byte[] groupId) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withReaction(reaction) @@ -556,13 +552,13 @@ public class Manager implements Signal { messageBuilder.asGroupMessage(group); } final GroupInfo g = getGroupForSending(groupId); + final Collection membersSend = getSignalServiceAddresses(g.members); // Don't send group message to ourself - final List membersSend = new ArrayList<>(g.members); - membersSend.remove(this.username); + membersSend.remove(account.getSelfAddress()); sendMessageLegacy(messageBuilder, membersSend); } - public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { + public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, InvalidNumberException { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupId) .build(); @@ -571,18 +567,18 @@ public class Manager implements Signal { .asGroupMessage(group); final GroupInfo g = getGroupForSending(groupId); - g.members.remove(this.username); + g.members.remove(account.getUsername()); account.getGroupStore().updateGroup(g); - sendMessageLegacy(messageBuilder, g.members); + sendMessageLegacy(messageBuilder, getSignalServiceAddresses(g.members)); } - private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException { GroupInfo g; if (groupId == null) { // Create new group g = new GroupInfo(KeyUtils.createGroupId()); - g.members.add(username); + g.members.add(account.getUsername()); } else { g = getGroupForSending(groupId); } @@ -594,13 +590,7 @@ public class Manager implements Signal { if (members != null) { Set newMembers = new HashSet<>(); for (String member : members) { - try { - member = Utils.canonicalizeNumber(member, username); - } catch (InvalidNumberException e) { - System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage()); - System.err.println("Aborting…"); - System.exit(1); - } + member = Utils.canonicalizeNumber(member, account.getUsername()); if (g.members.contains(member)) { continue; } @@ -629,29 +619,27 @@ public class Manager implements Signal { SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); + final Collection membersSend = getSignalServiceAddresses(g.members); // Don't send group message to ourself - final List membersSend = new ArrayList<>(g.members); - membersSend.remove(this.username); + membersSend.remove(account.getSelfAddress()); sendMessageLegacy(messageBuilder, membersSend); return g.groupId; } - private void sendUpdateGroupMessage(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions { + private void sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions { if (groupId == null) { return; } GroupInfo g = getGroupForSending(groupId); - if (!g.members.contains(recipient)) { + if (!g.members.contains(recipient.getNumber().get())) { return; } SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); // Send group message only to the recipient who requested it - final List membersSend = new ArrayList<>(); - membersSend.add(recipient); - sendMessageLegacy(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, Collections.singleton(recipient)); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) { @@ -680,7 +668,7 @@ public class Manager implements Signal { return messageBuilder; } - private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions { + private void sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions { if (groupId == null) { return; } @@ -697,14 +685,12 @@ public class Manager implements Signal { } // Send group info request message to the recipient who sent us a message with this groupId - final List membersSend = new ArrayList<>(); - membersSend.add(recipient); - sendMessageLegacy(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, Collections.singleton(recipient)); } @Override public void sendMessage(String message, List attachments, String recipient) - throws EncapsulatedExceptions, AttachmentInvalidException, IOException { + throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException { List recipients = new ArrayList<>(1); recipients.add(recipient); sendMessage(message, attachments, recipients); @@ -713,7 +699,7 @@ public class Manager implements Signal { @Override public void sendMessage(String messageText, List attachments, List recipients) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { List attachmentStreams = Utils.getSignalServiceAttachments(attachments); @@ -732,30 +718,30 @@ public class Manager implements Signal { messageBuilder.withAttachments(attachmentPointers); } messageBuilder.withProfileKey(account.getProfileKey().serialize()); - sendMessageLegacy(messageBuilder, recipients); + sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); } public void sendMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, long targetSentTimestamp, List recipients) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withReaction(reaction) .withProfileKey(account.getProfileKey().serialize()); - sendMessageLegacy(messageBuilder, recipients); + sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); } @Override - public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { + public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException { SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asEndSessionMessage(); - sendMessageLegacy(messageBuilder, recipients); + sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); } @Override public String getContactName(String number) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, username); + String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername()); ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber); if (contact == null) { return ""; @@ -766,7 +752,7 @@ public class Manager implements Signal { @Override public void setContactName(String number, String name) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, username); + String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername()); ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber); if (contact == null) { contact = new ContactInfo(); @@ -782,7 +768,7 @@ public class Manager implements Signal { @Override public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException { - number = Utils.canonicalizeNumber(number, username); + number = Utils.canonicalizeNumber(number, account.getUsername()); ContactInfo contact = account.getContactStore().getContact(number); if (contact == null) { contact = new ContactInfo(); @@ -840,7 +826,7 @@ public class Manager implements Signal { } @Override - public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException { if (groupId.length == 0) { groupId = null; } @@ -858,9 +844,6 @@ public class Manager implements Signal { /** * Change the expiration timer for a thread (number of groupId) - * - * @param numberOrGroupId - * @param messageExpirationTimer */ public void setExpirationTimer(String numberOrGroupId, int messageExpirationTimer) { ThreadInfo thread = account.getThreadStore().getThread(numberOrGroupId); @@ -953,7 +936,7 @@ public class Manager implements Signal { stickers); } - private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException, StickerPackInvalidException { + private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException { InputStream inputStream; if (zip != null) { inputStream = zip.getInputStream(zip.getEntry("manifest.json")); @@ -1121,7 +1104,7 @@ public class Manager implements Signal { /** * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult. */ - private void sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) + private void sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) throws EncapsulatedExceptions, IOException { List results = sendMessage(messageBuilder, recipients); @@ -1143,14 +1126,24 @@ public class Manager implements Signal { } } - private List sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) - throws IOException { - Set recipientsTS = Utils.getSignalServiceAddresses(recipients, username); - if (recipientsTS == null) { - account.save(); - return Collections.emptyList(); - } + private Collection getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { + final Set signalServiceAddresses = new HashSet<>(numbers.size()); + final String username = account.getUsername(); + for (String number : numbers) { + String canonicalizedNumber = Utils.canonicalizeNumber(number, username); + if (canonicalizedNumber.equals(username)) { + signalServiceAddresses.add(account.getSelfAddress()); + } else { + // TODO get corresponding uuid + signalServiceAddresses.add(new SignalServiceAddress(null, canonicalizedNumber)); + } + } + return signalServiceAddresses; + } + + private List sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) + throws IOException { if (messagePipe == null) { messagePipe = getMessageReceiver().createMessagePipe(); } @@ -1165,7 +1158,7 @@ public class Manager implements Signal { if (message.getGroupInfo().isPresent()) { try { final boolean isRecipientUpdate = false; - List result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), isRecipientUpdate, message); + List result = messageSender.sendMessage(new ArrayList<>(recipients), getAccessFor(recipients), isRecipientUpdate, message); for (SendMessageResult r : result) { if (r.getIdentityFailure() != null) { account.getSignalProtocolStore().saveIdentity(r.getAddress().getNumber().get(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED); @@ -1176,8 +1169,8 @@ public class Manager implements Signal { account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); return Collections.emptyList(); } - } else if (recipientsTS.size() == 1 && recipientsTS.contains(getSelfAddress())) { - SignalServiceAddress recipient = getSelfAddress(); + } else if (recipients.size() == 1 && recipients.contains(account.getSelfAddress())) { + SignalServiceAddress recipient = account.getSelfAddress(); final Optional unidentifiedAccess = getAccessFor(recipient); SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient), message.getTimestamp(), @@ -1187,7 +1180,7 @@ public class Manager implements Signal { false); SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); - List results = new ArrayList<>(recipientsTS.size()); + List results = new ArrayList<>(recipients.size()); try { messageSender.sendMessage(syncMessage, unidentifiedAccess); } catch (UntrustedIdentityException e) { @@ -1197,8 +1190,8 @@ public class Manager implements Signal { return results; } else { // Send to all individually, so sync messages are sent correctly - List results = new ArrayList<>(recipientsTS.size()); - for (SignalServiceAddress address : recipientsTS) { + List results = new ArrayList<>(recipients.size()); + for (SignalServiceAddress address : recipients) { ThreadInfo thread = account.getThreadStore().getThread(address.getNumber().get()); if (thread != null) { messageBuilder.withExpiration(thread.messageExpirationTime); @@ -1218,7 +1211,7 @@ public class Manager implements Signal { } } finally { if (message != null && message.isEndSession()) { - for (SignalServiceAddress recipient : recipientsTS) { + for (SignalServiceAddress recipient : recipients) { handleEndSession(recipient.getNumber().get()); } } @@ -1227,7 +1220,7 @@ public class Manager implements Signal { } private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException, UnsupportedDataMessageException { - SignalServiceCipher cipher = new SignalServiceCipher(getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator()); + SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator()); try { return cipher.decrypt(envelope); } catch (ProtocolUntrustedIdentityException e) { @@ -1241,7 +1234,7 @@ public class Manager implements Signal { account.getSignalProtocolStore().deleteAllSessions(source); } - private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, SignalServiceAddress destination, boolean ignoreAttachments) { + private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) { String threadId; if (message.getGroupInfo().isPresent()) { SignalServiceGroup groupInfo = message.getGroupInfo().get(); @@ -1291,7 +1284,7 @@ public class Manager implements Signal { e.printStackTrace(); } } else { - group.members.remove(source); + group.members.remove(source.getNumber().get()); account.getGroupStore().updateGroup(group); } break; @@ -1311,11 +1304,11 @@ public class Manager implements Signal { if (isSync) { threadId = destination.getNumber().get(); } else { - threadId = source; + threadId = source.getNumber().get(); } } if (message.isEndSession()) { - handleEndSession(isSync ? destination.getNumber().get() : source); + handleEndSession(isSync ? destination.getNumber().get() : source.getNumber().get()); } if (message.isExpirationUpdate() || message.getBody().isPresent()) { ThreadInfo thread = account.getThreadStore().getThread(threadId); @@ -1340,16 +1333,16 @@ public class Manager implements Signal { } } if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { - if (source.equals(username)) { + if (source.equals(account.getSelfAddress())) { try { this.account.setProfileKey(new ProfileKey(message.getProfileKey().get())); } catch (InvalidInputException ignored) { } } - ContactInfo contact = account.getContactStore().getContact(source); + ContactInfo contact = account.getContactStore().getContact(source.getNumber().get()); if (contact == null) { contact = new ContactInfo(); - contact.number = source; + contact.number = source.getNumber().get(); } contact.profileKey = Base64.encodeBytes(message.getProfileKey().get()); account.getContactStore().updateContact(contact); @@ -1516,14 +1509,14 @@ public class Manager implements Signal { } if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - handleSignalServiceDataMessage(message, false, sender.getNumber().get(), getSelfAddress(), ignoreAttachments); + handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments); } if (content.getSyncMessage().isPresent()) { account.setMultiDevice(true); SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) { SentTranscriptMessage message = syncMessage.getSent().get(); - handleSignalServiceDataMessage(message.getMessage(), true, sender.getNumber().get(), message.getDestination().orNull(), ignoreAttachments); + handleSignalServiceDataMessage(message.getMessage(), true, sender, message.getDestination().orNull(), ignoreAttachments); } if (syncMessage.getRequest().isPresent()) { RequestMessage rm = syncMessage.getRequest().get(); @@ -1567,7 +1560,10 @@ public class Manager implements Signal { } syncGroup.addMembers(g.getMembers()); if (!g.isActive()) { - syncGroup.members.remove(username); + syncGroup.members.remove(account.getUsername()); + } else { + // Add ourself to the member set as it's marked as active + syncGroup.members.add(account.getUsername()); } syncGroup.blocked = g.isBlocked(); if (g.getColor().isPresent()) { @@ -1784,7 +1780,7 @@ public class Manager implements Signal { ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId)); out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId), - record.members.contains(username), Optional.fromNullable(info != null ? info.messageExpirationTime : null), + record.members.contains(account.getUsername()), Optional.fromNullable(info != null ? info.messageExpirationTime : null), Optional.fromNullable(record.color), record.blocked, Optional.fromNullable(record.inboxPosition), record.archived)); } } @@ -1910,7 +1906,7 @@ public class Manager implements Signal { } public Pair> getIdentities(String number) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, username); + String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername()); return new Pair<>(canonicalizedNumber, account.getSignalProtocolStore().getIdentities(canonicalizedNumber)); } @@ -1995,7 +1991,7 @@ public class Manager implements Signal { } public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) { - return Utils.computeSafetyNumber(username, getIdentity(), theirUsername, theirIdentityKey); + return Utils.computeSafetyNumber(account.getUsername(), getIdentity(), theirUsername, theirIdentityKey); } public interface ReceiveMessageHandler { diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index eccc1656..4396e0ca 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -35,12 +35,9 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.file.Files; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import static org.whispersystems.signalservice.internal.util.Util.isEmpty; @@ -149,29 +146,10 @@ class Utils { return new DeviceLinkInfo(deviceIdentifier, deviceKey); } - static Set getSignalServiceAddresses(Collection recipients, String localNumber) { - Set recipientsTS = new HashSet<>(recipients.size()); - for (String recipient : recipients) { - try { - recipientsTS.add(getPushAddress(recipient, localNumber)); - } catch (InvalidNumberException e) { - System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - return null; - } - } - return recipientsTS; - } - static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { return PhoneNumberFormatter.formatNumber(number, localNumber); } - private static SignalServiceAddress getPushAddress(String number, String localNumber) throws InvalidNumberException { - String e164number = canonicalizeNumber(number, localNumber); - return new SignalServiceAddress(null, e164number); - } - static SignalServiceEnvelope loadEnvelope(File file) throws IOException { try (FileInputStream f = new FileInputStream(file)) { DataInputStream in = new DataInputStream(f); diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index fd4da41f..08ed5139 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -32,6 +32,7 @@ import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.util.Collection; +import java.util.UUID; public class SignalAccount { @@ -39,6 +40,7 @@ public class SignalAccount { private FileChannel fileChannel; private FileLock lock; private String username; + private UUID uuid; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private boolean isMultiDevice = false; private String password; @@ -277,8 +279,12 @@ public class SignalAccount { return username; } + public UUID getUuid() { + return uuid; + } + public SignalServiceAddress getSelfAddress() { - return new SignalServiceAddress(null, username); + return new SignalServiceAddress(uuid, username); } public int getDeviceId() { diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index c1381341..53d6bf23 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -55,7 +55,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { * * @param name User name, i.e. phone number * @param identityKey The user's public key - * @param trustLevel + * @param trustLevel Level of trust: untrusted, trusted, trusted and verified * @param added Added timestamp, if null and the key is newly added, the current time is used. */ public boolean saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel, Date added) { diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 38f1986e..44d98cd2 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -8,6 +8,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; @@ -59,4 +60,10 @@ public class ErrorUtils { System.err.println(e.getMessage()); System.err.println("Aborting sending."); } + + public static void handleInvalidNumberException(InvalidNumberException e) { + System.err.println("Failed to parse recipient: " + e.getMessage()); + System.err.println(e.getMessage()); + System.err.println("Aborting sending."); + } } From 0ce64dc923e515c8dfa75858d132fe4c021559d9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Mar 2020 17:10:11 +0100 Subject: [PATCH 017/103] Use lambda for ThreadLocal --- .../java/org/asamk/signal/util/RandomUtils.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/asamk/signal/util/RandomUtils.java b/src/main/java/org/asamk/signal/util/RandomUtils.java index d0463b47..19c3f18c 100644 --- a/src/main/java/org/asamk/signal/util/RandomUtils.java +++ b/src/main/java/org/asamk/signal/util/RandomUtils.java @@ -5,17 +5,14 @@ import java.security.SecureRandom; public class RandomUtils { - private static final ThreadLocal LOCAL_RANDOM = new ThreadLocal() { - @Override - protected SecureRandom initialValue() { - SecureRandom rand = getSecureRandomUnseeded(); + private static final ThreadLocal LOCAL_RANDOM = ThreadLocal.withInitial(() -> { + SecureRandom rand = getSecureRandomUnseeded(); - // Let the SecureRandom seed it self initially - rand.nextBoolean(); + // Let the SecureRandom seed it self initially + rand.nextBoolean(); - return rand; - } - }; + return rand; + }); private static SecureRandom getSecureRandomUnseeded() { try { From eb0648828a7670988ec1097a7fa4e895e6c5ed9a Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Mar 2020 17:10:44 +0100 Subject: [PATCH 018/103] Show recipient of sent sync message also when destination is not present --- src/main/java/org/asamk/signal/ReceiveMessageHandler.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 62e3a76d..1460aa61 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -129,6 +129,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { String dest = sentTranscriptMessage.getDestination().get().getNumber().get(); ContactInfo destContact = m.getContact(dest); to = (destContact == null ? "" : "“" + destContact.name + "” ") + dest; + } else if (sentTranscriptMessage.getRecipients().size() > 0) { + StringBuilder toBuilder = new StringBuilder(); + for (SignalServiceAddress dest : sentTranscriptMessage.getRecipients()) { + ContactInfo destContact = m.getContact(dest.getNumber().get()); + toBuilder.append(destContact == null ? "" : "“" + destContact.name + "” ").append(dest.getNumber().get()).append(" "); + } + to = toBuilder.toString(); } else { to = "Unknown"; } From a4e1d697884ea492f1f41c44b45e09b228255eb6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Mar 2020 17:43:37 +0100 Subject: [PATCH 019/103] Store contact uuids in contact store --- .../org/asamk/signal/manager/Manager.java | 28 ++++---- .../signal/storage/contacts/ContactInfo.java | 15 ++++- .../storage/contacts/JsonContactsStore.java | 64 ++++++------------- 3 files changed, 47 insertions(+), 60 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index a8be8ad8..8364888c 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -742,7 +742,7 @@ public class Manager implements Signal { @Override public String getContactName(String number) throws InvalidNumberException { String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername()); - ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber); + ContactInfo contact = account.getContactStore().getContact(new SignalServiceAddress(null, canonicalizedNumber)); if (contact == null) { return ""; } else { @@ -753,10 +753,10 @@ public class Manager implements Signal { @Override public void setContactName(String number, String name) throws InvalidNumberException { String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername()); - ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber); + final SignalServiceAddress address = new SignalServiceAddress(null, canonicalizedNumber); + ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { - contact = new ContactInfo(); - contact.number = canonicalizedNumber; + contact = new ContactInfo(address); System.err.println("Add contact " + canonicalizedNumber + " named " + name); } else { System.err.println("Updating contact " + canonicalizedNumber + " name " + contact.name + " -> " + name); @@ -769,10 +769,10 @@ public class Manager implements Signal { @Override public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException { number = Utils.canonicalizeNumber(number, account.getUsername()); - ContactInfo contact = account.getContactStore().getContact(number); + final SignalServiceAddress address = new SignalServiceAddress(null, number); + ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { - contact = new ContactInfo(); - contact.number = number; + contact = new ContactInfo(address); System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + number); } else { System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + number); @@ -1022,7 +1022,7 @@ public class Manager implements Signal { } private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) throws IOException { - ContactInfo contact = account.getContactStore().getContact(recipient.getNumber().get()); + ContactInfo contact = account.getContactStore().getContact(recipient); if (contact == null || contact.profileKey == null) { return null; } @@ -1339,10 +1339,9 @@ public class Manager implements Signal { } catch (InvalidInputException ignored) { } } - ContactInfo contact = account.getContactStore().getContact(source.getNumber().get()); + ContactInfo contact = account.getContactStore().getContact(source); if (contact == null) { - contact = new ContactInfo(); - contact.number = source.getNumber().get(); + contact = new ContactInfo(source); } contact.profileKey = Base64.encodeBytes(message.getProfileKey().get()); account.getContactStore().updateContact(contact); @@ -1624,10 +1623,9 @@ public class Manager implements Signal { if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { account.setProfileKey(c.getProfileKey().get()); } - ContactInfo contact = account.getContactStore().getContact(c.getAddress().getNumber().get()); + ContactInfo contact = account.getContactStore().getContact(c.getAddress()); if (contact == null) { - contact = new ContactInfo(); - contact.number = c.getAddress().getNumber().get(); + contact = new ContactInfo(c.getAddress()); } if (c.getName().isPresent()) { contact.name = c.getName().get(); @@ -1894,7 +1892,7 @@ public class Manager implements Signal { } public ContactInfo getContact(String number) { - return account.getContactStore().getContact(number); + return account.getContactStore().getContact(new SignalServiceAddress(null, number)); } public GroupInfo getGroup(byte[] groupId) { diff --git a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java index 2ab2a515..b5dadd1a 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java +++ b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import java.util.UUID; + public class ContactInfo { @JsonProperty @@ -13,6 +15,9 @@ public class ContactInfo { @JsonProperty public String number; + @JsonProperty + public UUID uuid; + @JsonProperty public String color; @@ -28,8 +33,16 @@ public class ContactInfo { @JsonProperty(defaultValue = "false") public boolean archived; + public ContactInfo() { + } + + public ContactInfo(SignalServiceAddress address) { + this.number = address.getNumber().orNull(); + this.uuid = address.getUuid().orNull(); + } + @JsonIgnore public SignalServiceAddress getAddress() { - return new SignalServiceAddress(null, number); + return new SignalServiceAddress(uuid, number); } } diff --git a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java index c10dfbb7..86514bc1 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java +++ b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java @@ -1,41 +1,40 @@ package org.asamk.signal.storage.contacts; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.io.IOException; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; public class JsonContactsStore { - private static final ObjectMapper jsonProcessor = new ObjectMapper(); @JsonProperty("contacts") - @JsonSerialize(using = JsonContactsStore.MapToListSerializer.class) - @JsonDeserialize(using = ContactsDeserializer.class) - private Map contacts = new HashMap<>(); + private List contacts = new ArrayList<>(); public void updateContact(ContactInfo contact) { - contacts.put(contact.number, contact); + final SignalServiceAddress contactAddress = contact.getAddress(); + for (int i = 0; i < contacts.size(); i++) { + if (contacts.get(i).getAddress().matches(contactAddress)) { + contacts.set(i, contact); + return; + } + } + + contacts.add(contact); } - public ContactInfo getContact(String number) { - return contacts.get(number); + public ContactInfo getContact(SignalServiceAddress address) { + for (ContactInfo contact : contacts) { + if (contact.getAddress().matches(address)) { + return contact; + } + } + return null; } public List getContacts() { - return new ArrayList<>(contacts.values()); + return new ArrayList<>(contacts); } /** @@ -44,27 +43,4 @@ public class JsonContactsStore { public void clear() { contacts.clear(); } - - private static class MapToListSerializer extends JsonSerializer> { - - @Override - public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { - jgen.writeObject(value.values()); - } - } - - private static class ContactsDeserializer extends JsonDeserializer> { - - @Override - public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { - Map contacts = new HashMap<>(); - JsonNode node = jsonParser.getCodec().readTree(jsonParser); - for (JsonNode n : node) { - ContactInfo c = jsonProcessor.treeToValue(n, ContactInfo.class); - contacts.put(c.number, c); - } - - return contacts; - } - } } From f982d2752e7531ea69296e09309715fdff9e0b2c Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Mar 2020 19:08:41 +0100 Subject: [PATCH 020/103] Store group member uuids in group store The member list is now stored as a mixed list of strings and objects, e.g.: "members": [ "+XXXX", { "number": "+XXXX", "uuid": "XXX-XX" } ] --- src/main/java/org/asamk/Signal.java | 2 +- .../signal/commands/ListGroupsCommand.java | 9 +- .../signal/commands/QuitGroupCommand.java | 5 - .../org/asamk/signal/manager/Manager.java | 78 +++++------ .../signal/storage/groups/GroupInfo.java | 126 ++++++++++++++++-- 5 files changed, 159 insertions(+), 61 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 90d02758..528116b1 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -19,7 +19,7 @@ public interface Signal extends DBusInterface { void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException; - void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException, InvalidNumberException; + void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; String getContactName(String number) throws InvalidNumberException; diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 565bacba..0baa8744 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -6,19 +6,20 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.groups.GroupInfo; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.util.Base64; import java.util.List; public class ListGroupsCommand implements LocalCommand { - private static void printGroup(GroupInfo group, boolean detailed, String username) { + private static void printGroup(GroupInfo group, boolean detailed, SignalServiceAddress address) { if (detailed) { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b Members: %s", - Base64.encodeBytes(group.groupId), group.name, group.members.contains(username), group.blocked, group.members)); + Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked, group.getMembersE164())); } else { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", - Base64.encodeBytes(group.groupId), group.name, group.members.contains(username), group.blocked)); + Base64.encodeBytes(group.groupId), group.name, group.isMember(address), group.blocked)); } } @@ -40,7 +41,7 @@ public class ListGroupsCommand implements LocalCommand { boolean detailed = ns.getBoolean("detailed"); for (GroupInfo group : groups) { - printGroup(group, detailed, m.getUsername()); + printGroup(group, detailed, m.getSelfAddress()); } return 0; } diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 38bc79ac..6e53cb2a 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -9,7 +9,6 @@ import org.asamk.signal.NotAGroupMemberException; import org.asamk.signal.manager.Manager; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; @@ -18,7 +17,6 @@ import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; import static org.asamk.signal.util.ErrorUtils.handleIOException; -import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException; import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class QuitGroupCommand implements LocalCommand { @@ -58,9 +56,6 @@ public class QuitGroupCommand implements LocalCommand { } catch (GroupIdFormatException e) { handleGroupIdFormatException(e); return 1; - } catch (InvalidNumberException e) { - handleInvalidNumberException(e); - return 1; } } } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 8364888c..7a28f7bc 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -171,6 +171,10 @@ public class Manager implements Signal { return username; } + public SignalServiceAddress getSelfAddress() { + return account.getSelfAddress(); + } + private SignalServiceAccountManager getSignalServiceAccountManager() { return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, null, account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer); } @@ -499,12 +503,10 @@ public class Manager implements Signal { if (g == null) { throw new GroupNotFoundException(groupId); } - for (String member : g.members) { - if (member.equals(account.getUsername())) { - return g; - } + if (!g.isMember(account.getSelfAddress())) { + throw new NotAGroupMemberException(groupId, g.name); } - throw new NotAGroupMemberException(groupId, g.name); + return g; } public List getGroups() { @@ -514,7 +516,7 @@ public class Manager implements Signal { @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) - throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException { + throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments)); @@ -532,15 +534,12 @@ public class Manager implements Signal { final GroupInfo g = getGroupForSending(groupId); - final Collection membersSend = getSignalServiceAddresses(g.members); - // Don't send group message to ourself - membersSend.remove(account.getSelfAddress()); - sendMessageLegacy(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } public void sendGroupMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, long targetSentTimestamp, byte[] groupId) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { + throws IOException, EncapsulatedExceptions, AttachmentInvalidException { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withReaction(reaction) @@ -552,13 +551,10 @@ public class Manager implements Signal { messageBuilder.asGroupMessage(group); } final GroupInfo g = getGroupForSending(groupId); - final Collection membersSend = getSignalServiceAddresses(g.members); - // Don't send group message to ourself - membersSend.remove(account.getSelfAddress()); - sendMessageLegacy(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, InvalidNumberException { + public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupId) .build(); @@ -567,18 +563,18 @@ public class Manager implements Signal { .asGroupMessage(group); final GroupInfo g = getGroupForSending(groupId); - g.members.remove(account.getUsername()); + g.removeMember(account.getSelfAddress()); account.getGroupStore().updateGroup(g); - sendMessageLegacy(messageBuilder, getSignalServiceAddresses(g.members)); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException { + private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { GroupInfo g; if (groupId == null) { // Create new group g = new GroupInfo(KeyUtils.createGroupId()); - g.members.add(account.getUsername()); + g.addMembers(Collections.singleton(account.getSelfAddress())); } else { g = getGroupForSending(groupId); } @@ -588,25 +584,26 @@ public class Manager implements Signal { } if (members != null) { - Set newMembers = new HashSet<>(); - for (String member : members) { - member = Utils.canonicalizeNumber(member, account.getUsername()); - if (g.members.contains(member)) { + final Set newE164Members = new HashSet<>(); + for (SignalServiceAddress member : members) { + if (g.isMember(member) || !member.getNumber().isPresent()) { continue; } - newMembers.add(member); - g.members.add(member); + newE164Members.add(member.getNumber().get()); } - final List contacts = accountManager.getContacts(newMembers); - if (contacts.size() != newMembers.size()) { + + final List contacts = accountManager.getContacts(newE164Members); + if (contacts.size() != newE164Members.size()) { // Some of the new members are not registered on Signal for (ContactTokenDetails contact : contacts) { - newMembers.remove(contact.getNumber()); + newE164Members.remove(contact.getNumber()); } - System.err.println("Failed to add members " + Util.join(", ", newMembers) + " to group: Not registered on Signal"); + System.err.println("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal"); System.err.println("Aborting…"); System.exit(1); } + + g.addMembers(members); } if (avatarFile != null) { @@ -619,10 +616,7 @@ public class Manager implements Signal { SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); - final Collection membersSend = getSignalServiceAddresses(g.members); - // Don't send group message to ourself - membersSend.remove(account.getSelfAddress()); - sendMessageLegacy(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); return g.groupId; } @@ -632,7 +626,7 @@ public class Manager implements Signal { } GroupInfo g = getGroupForSending(groupId); - if (!g.members.contains(recipient.getNumber().get())) { + if (!g.isMember(recipient)) { return; } @@ -819,9 +813,9 @@ public class Manager implements Signal { public List getGroupMembers(byte[] groupId) { GroupInfo group = getGroup(groupId); if (group == null) { - return new ArrayList<>(); + return Collections.emptyList(); } else { - return new ArrayList<>(group.members); + return new ArrayList<>(group.getMembersE164()); } } @@ -839,7 +833,7 @@ public class Manager implements Signal { if (avatar.isEmpty()) { avatar = null; } - return sendUpdateGroupMessage(groupId, name, members, avatar); + return sendUpdateGroupMessage(groupId, name, members == null ? null : getSignalServiceAddresses(members), avatar); } /** @@ -1284,7 +1278,7 @@ public class Manager implements Signal { e.printStackTrace(); } } else { - group.members.remove(source.getNumber().get()); + group.removeMember(source); account.getGroupStore().updateGroup(group); } break; @@ -1559,10 +1553,10 @@ public class Manager implements Signal { } syncGroup.addMembers(g.getMembers()); if (!g.isActive()) { - syncGroup.members.remove(account.getUsername()); + syncGroup.removeMember(account.getSelfAddress()); } else { // Add ourself to the member set as it's marked as active - syncGroup.members.add(account.getUsername()); + syncGroup.addMembers(Collections.singleton(account.getSelfAddress())); } syncGroup.blocked = g.isBlocked(); if (g.getColor().isPresent()) { @@ -1778,7 +1772,7 @@ public class Manager implements Signal { ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId)); out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId), - record.members.contains(account.getUsername()), Optional.fromNullable(info != null ? info.messageExpirationTime : null), + record.isMember(account.getSelfAddress()), Optional.fromNullable(info != null ? info.messageExpirationTime : null), Optional.fromNullable(record.color), record.blocked, Optional.fromNullable(record.inboxPosition), record.archived)); } } diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java index cb53d3af..21ba910f 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -2,15 +2,29 @@ package org.asamk.signal.storage.groups; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.Set; +import java.util.UUID; public class GroupInfo { + private static final ObjectMapper jsonProcessor = new ObjectMapper(); + @JsonProperty public final byte[] groupId; @@ -18,7 +32,9 @@ public class GroupInfo { public String name; @JsonProperty - public Set members = new HashSet<>(); + @JsonDeserialize(using = MembersDeserializer.class) + @JsonSerialize(using = MembersSerializer.class) + public Set members = new HashSet<>(); @JsonProperty public String color; @JsonProperty(defaultValue = "false") @@ -38,7 +54,7 @@ public class GroupInfo { this.groupId = groupId; } - public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived) { + public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived) { this.groupId = groupId; this.name = name; this.members.addAll(members); @@ -56,16 +72,108 @@ public class GroupInfo { @JsonIgnore public Set getMembers() { - Set addresses = new HashSet<>(members.size()); - for (String member : members) { - addresses.add(new SignalServiceAddress(null, member)); - } - return addresses; + return members; } - public void addMembers(Collection members) { + @JsonIgnore + public Set getMembersE164() { + Set membersE164 = new HashSet<>(); for (SignalServiceAddress member : members) { - this.members.add(member.getNumber().get()); + if (!member.getNumber().isPresent()) { + continue; + } + membersE164.add(member.getNumber().get()); + } + return membersE164; + } + + @JsonIgnore + public Set getMembersWithout(SignalServiceAddress address) { + Set members = new HashSet<>(this.members.size()); + for (SignalServiceAddress member : this.members) { + if (!member.matches(address)) { + members.add(member); + } + } + return members; + } + + public void addMembers(Collection addresses) { + for (SignalServiceAddress address : addresses) { + removeMember(address); + this.members.add(address); + } + } + + public void removeMember(SignalServiceAddress address) { + this.members.removeIf(member -> member.matches(address)); + } + + @JsonIgnore + public boolean isMember(SignalServiceAddress address) { + for (SignalServiceAddress member : this.members) { + if (member.matches(address)) { + return true; + } + } + return false; + } + + private static final class JsonSignalServiceAddress { + + @JsonProperty + private UUID uuid; + + @JsonProperty + private String number; + + JsonSignalServiceAddress(@JsonProperty("uuid") final UUID uuid, @JsonProperty("number") final String number) { + this.uuid = uuid; + this.number = number; + } + + JsonSignalServiceAddress(SignalServiceAddress address) { + this.uuid = address.getUuid().orNull(); + this.number = address.getNumber().orNull(); + } + + SignalServiceAddress toSignalServiceAddress() { + return new SignalServiceAddress(uuid, number); + } + } + + private static class MembersSerializer extends JsonSerializer> { + + @Override + public void serialize(final Set value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { + jgen.writeStartArray(value.size()); + for (SignalServiceAddress address : value) { + if (address.getUuid().isPresent()) { + jgen.writeObject(new JsonSignalServiceAddress(address)); + } else { + jgen.writeString(address.getNumber().get()); + } + } + jgen.writeEndArray(); + } + } + + private static class MembersDeserializer extends JsonDeserializer> { + + @Override + public Set deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + Set addresses = new HashSet<>(); + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + for (JsonNode n : node) { + if (n.isTextual()) { + addresses.add(new SignalServiceAddress(null, n.textValue())); + } else { + JsonSignalServiceAddress address = jsonProcessor.treeToValue(n, JsonSignalServiceAddress.class); + addresses.add(address.toSignalServiceAddress()); + } + } + + return addresses; } } } From b62694dbc75c2b52ce5fc946805f61f244f43dc0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Mar 2020 20:28:15 +0100 Subject: [PATCH 021/103] Remove ThreadStore and store message expiration time in group/contact store To match the implemenation of Signal-Android --- .../org/asamk/signal/manager/Manager.java | 98 +++++++++---------- .../asamk/signal/storage/SignalAccount.java | 35 ++++--- .../signal/storage/contacts/ContactInfo.java | 3 + .../signal/storage/groups/GroupInfo.java | 5 +- ...dStore.java => LegacyJsonThreadStore.java} | 4 +- 5 files changed, 75 insertions(+), 70 deletions(-) rename src/main/java/org/asamk/signal/storage/threads/{JsonThreadStore.java => LegacyJsonThreadStore.java} (95%) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 7a28f7bc..976d62a9 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -31,7 +31,6 @@ import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.JsonGroupStore; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; -import org.asamk.signal.storage.threads.ThreadInfo; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.signal.libsignal.metadata.InvalidMetadataMessageException; @@ -527,13 +526,11 @@ public class Manager implements Signal { .build(); messageBuilder.asGroupMessage(group); } - ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(groupId)); - if (thread != null) { - messageBuilder.withExpiration(thread.messageExpirationTime); - } final GroupInfo g = getGroupForSending(groupId); + messageBuilder.withExpiration(g.messageExpirationTime); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } @@ -651,15 +648,9 @@ public class Manager implements Signal { } } - SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()); - - ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(g.groupId)); - if (thread != null) { - messageBuilder.withExpiration(thread.messageExpirationTime); - } - - return messageBuilder; + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()) + .withExpiration(g.messageExpirationTime); } private void sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions { @@ -673,11 +664,6 @@ public class Manager implements Signal { SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()); - ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(groupId)); - if (thread != null) { - messageBuilder.withExpiration(thread.messageExpirationTime); - } - // Send group info request message to the recipient who sent us a message with this groupId sendMessageLegacy(messageBuilder, Collections.singleton(recipient)); } @@ -837,12 +823,21 @@ public class Manager implements Signal { } /** - * Change the expiration timer for a thread (number of groupId) + * Change the expiration timer for a contact */ - public void setExpirationTimer(String numberOrGroupId, int messageExpirationTimer) { - ThreadInfo thread = account.getThreadStore().getThread(numberOrGroupId); - thread.messageExpirationTime = messageExpirationTimer; - account.getThreadStore().updateThread(thread); + public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) { + ContactInfo c = account.getContactStore().getContact(address); + c.messageExpirationTime = messageExpirationTimer; + account.getContactStore().updateContact(c); + } + + /** + * Change the expiration timer for a group + */ + public void setExpirationTimer(byte[] groupId, int messageExpirationTimer) { + GroupInfo g = account.getGroupStore().getGroup(groupId); + g.messageExpirationTime = messageExpirationTimer; + account.getGroupStore().updateGroup(g); } /** @@ -1186,9 +1181,9 @@ public class Manager implements Signal { // Send to all individually, so sync messages are sent correctly List results = new ArrayList<>(recipients.size()); for (SignalServiceAddress address : recipients) { - ThreadInfo thread = account.getThreadStore().getThread(address.getNumber().get()); - if (thread != null) { - messageBuilder.withExpiration(thread.messageExpirationTime); + ContactInfo contact = account.getContactStore().getContact(address); + if (contact != null) { + messageBuilder.withExpiration(contact.messageExpirationTime); } else { messageBuilder.withExpiration(0); } @@ -1229,10 +1224,8 @@ public class Manager implements Signal { } private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) { - String threadId; if (message.getGroupInfo().isPresent()) { SignalServiceGroup groupInfo = message.getGroupInfo().get(); - threadId = Base64.encodeBytes(groupInfo.getGroupId()); GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId()); switch (groupInfo.getType()) { case UPDATE: @@ -1294,25 +1287,30 @@ public class Manager implements Signal { } break; } - } else { - if (isSync) { - threadId = destination.getNumber().get(); - } else { - threadId = source.getNumber().get(); - } } if (message.isEndSession()) { handleEndSession(isSync ? destination.getNumber().get() : source.getNumber().get()); } if (message.isExpirationUpdate() || message.getBody().isPresent()) { - ThreadInfo thread = account.getThreadStore().getThread(threadId); - if (thread == null) { - thread = new ThreadInfo(); - thread.id = threadId; - } - if (thread.messageExpirationTime != message.getExpiresInSeconds()) { - thread.messageExpirationTime = message.getExpiresInSeconds(); - account.getThreadStore().updateThread(thread); + if (message.getGroupInfo().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupInfo().get(); + GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId()); + if (group == null) { + group = new GroupInfo(groupInfo.getGroupId()); + } + if (group.messageExpirationTime != message.getExpiresInSeconds()) { + group.messageExpirationTime = message.getExpiresInSeconds(); + account.getGroupStore().updateGroup(group); + } + } else { + ContactInfo contact = account.getContactStore().getContact(isSync ? destination : source); + if (contact == null) { + contact = new ContactInfo(isSync ? destination : source); + } + if (contact.messageExpirationTime != message.getExpiresInSeconds()) { + contact.messageExpirationTime = message.getExpiresInSeconds(); + account.getContactStore().updateContact(contact); + } } } if (message.getAttachments().isPresent() && !ignoreAttachments) { @@ -1635,13 +1633,7 @@ public class Manager implements Signal { account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (c.getExpirationTimer().isPresent()) { - ThreadInfo thread = account.getThreadStore().getThread(c.getAddress().getNumber().get()); - if (thread == null) { - thread = new ThreadInfo(); - thread.id = c.getAddress().getNumber().get(); - } - thread.messageExpirationTime = c.getExpirationTimer().get(); - account.getThreadStore().updateThread(thread); + contact.messageExpirationTime = c.getExpirationTimer().get(); } contact.blocked = c.isBlocked(); contact.inboxPosition = c.getInboxPosition().orNull(); @@ -1769,10 +1761,9 @@ public class Manager implements Signal { try (OutputStream fos = new FileOutputStream(groupsFile)) { DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos); for (GroupInfo record : account.getGroupStore().getGroups()) { - ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId)); out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId), - record.isMember(account.getSelfAddress()), Optional.fromNullable(info != null ? info.messageExpirationTime : null), + record.isMember(account.getSelfAddress()), Optional.of(record.messageExpirationTime), Optional.fromNullable(record.color), record.blocked, Optional.fromNullable(record.inboxPosition), record.archived)); } } @@ -1805,7 +1796,6 @@ public class Manager implements Signal { DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); for (ContactInfo record : account.getContactStore().getContacts()) { VerifiedMessage verifiedMessage = null; - ThreadInfo info = account.getThreadStore().getThread(record.number); if (getIdentities().containsKey(record.number)) { JsonIdentityKeyStore.Identity currentIdentity = null; for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) { @@ -1826,7 +1816,7 @@ public class Manager implements Signal { out.write(new DeviceContact(record.getAddress(), Optional.fromNullable(record.name), createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), record.blocked, - Optional.fromNullable(info != null ? info.messageExpirationTime : null), + Optional.of(record.messageExpirationTime), Optional.fromNullable(record.inboxPosition), record.archived)); } diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 08ed5139..6d83f78f 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -10,10 +10,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.contacts.JsonContactsStore; +import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.JsonGroupStore; import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; -import org.asamk.signal.storage.threads.JsonThreadStore; +import org.asamk.signal.storage.threads.LegacyJsonThreadStore; +import org.asamk.signal.storage.threads.ThreadInfo; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.signal.zkgroup.InvalidInputException; @@ -55,7 +58,6 @@ public class SignalAccount { private JsonSignalProtocolStore signalProtocolStore; private JsonGroupStore groupStore; private JsonContactsStore contactStore; - private JsonThreadStore threadStore; private SignalAccount() { jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect @@ -84,7 +86,6 @@ public class SignalAccount { account.profileKey = profileKey; account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); account.groupStore = new JsonGroupStore(); - account.threadStore = new JsonThreadStore(); account.contactStore = new JsonContactsStore(); account.registered = false; @@ -104,7 +105,6 @@ public class SignalAccount { account.signalingKey = signalingKey; account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); account.groupStore = new JsonGroupStore(); - account.threadStore = new JsonThreadStore(); account.contactStore = new JsonContactsStore(); account.registered = true; account.isMultiDevice = true; @@ -191,10 +191,24 @@ public class SignalAccount { } JsonNode threadStoreNode = rootNode.get("threadStore"); if (threadStoreNode != null) { - threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class); - } - if (threadStore == null) { - threadStore = new JsonThreadStore(); + LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class); + // Migrate thread info to group and contact store + for (ThreadInfo thread : threadStore.getThreads()) { + try { + ContactInfo contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id)); + if (contactInfo != null) { + contactInfo.messageExpirationTime = thread.messageExpirationTime; + contactStore.updateContact(contactInfo); + } else { + GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id)); + if (groupInfo != null) { + groupInfo.messageExpirationTime = thread.messageExpirationTime; + groupStore.updateGroup(groupInfo); + } + } + } catch (Exception ignored) { + } + } } } @@ -216,7 +230,6 @@ public class SignalAccount { .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) .putPOJO("contactStore", contactStore) - .putPOJO("threadStore", threadStore) ; try { synchronized (fileChannel) { @@ -271,10 +284,6 @@ public class SignalAccount { return contactStore; } - public JsonThreadStore getThreadStore() { - return threadStore; - } - public String getUsername() { return username; } diff --git a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java index b5dadd1a..4d3a5e95 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java +++ b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java @@ -21,6 +21,9 @@ public class ContactInfo { @JsonProperty public String color; + @JsonProperty(defaultValue = "0") + public int messageExpirationTime; + @JsonProperty public String profileKey; diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java index 21ba910f..e3a45420 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -37,6 +37,8 @@ public class GroupInfo { public Set members = new HashSet<>(); @JsonProperty public String color; + @JsonProperty(defaultValue = "0") + public int messageExpirationTime; @JsonProperty(defaultValue = "false") public boolean blocked; @JsonProperty @@ -54,7 +56,7 @@ public class GroupInfo { this.groupId = groupId; } - public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived) { + public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked, @JsonProperty("inboxPosition") Integer inboxPosition, @JsonProperty("archived") boolean archived, @JsonProperty("messageExpirationTime") int messageExpirationTime) { this.groupId = groupId; this.name = name; this.members.addAll(members); @@ -63,6 +65,7 @@ public class GroupInfo { this.blocked = blocked; this.inboxPosition = inboxPosition; this.archived = archived; + this.messageExpirationTime = messageExpirationTime; } @JsonIgnore diff --git a/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java b/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java similarity index 95% rename from src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java rename to src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java index a4a89ccd..267d712c 100644 --- a/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java +++ b/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java @@ -18,12 +18,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -public class JsonThreadStore { +public class LegacyJsonThreadStore { private static final ObjectMapper jsonProcessor = new ObjectMapper(); @JsonProperty("threads") - @JsonSerialize(using = JsonThreadStore.MapToListSerializer.class) + @JsonSerialize(using = MapToListSerializer.class) @JsonDeserialize(using = ThreadsDeserializer.class) private Map threads = new HashMap<>(); From 1b56485fc8ce1d82d1e5e8aaeca82bcd39bc7c26 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Mar 2020 20:49:38 +0100 Subject: [PATCH 022/103] Send delivery receipt for data messages that need it With the unidentified sender messages, the Signal server cannot do this automatically anymore. --- .../java/org/asamk/signal/manager/Manager.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 976d62a9..9ff1540e 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -80,6 +80,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload; import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload.StickerInfo; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; @@ -668,6 +669,14 @@ public class Manager implements Signal { sendMessageLegacy(messageBuilder, Collections.singleton(recipient)); } + private void sendReceipt(SignalServiceAddress remoteAddress, long messageId) throws IOException, UntrustedIdentityException { + SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, + Collections.singletonList(messageId), + System.currentTimeMillis()); + + getMessageSender().sendReceipt(remoteAddress, getAccessFor(remoteAddress), receiptMessage); + } + @Override public void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException { @@ -1500,6 +1509,15 @@ public class Manager implements Signal { } if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); + + if (content.isNeedsReceipt()) { + try { + sendReceipt(sender, message.getTimestamp()); + } catch (IOException | UntrustedIdentityException e) { + e.printStackTrace(); + } + } + handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments); } if (content.getSyncMessage().isPresent()) { From 6665dc0e486d8e8dda8884e9bf17d781fd614614 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Mar 2020 21:56:51 +0100 Subject: [PATCH 023/103] Set uuid after verify and linking and request it at startup for existing clients --- build.gradle | 2 +- .../org/asamk/signal/manager/Manager.java | 20 +++++++++++++------ .../asamk/signal/storage/SignalAccount.java | 16 ++++++++++++++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 55820677..1b88f353 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_4' + compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_5' compile 'org.bouncycastle:bcprov-jdk15on:1.64' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 9ff1540e..0aada703 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -137,6 +137,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.zip.ZipEntry; @@ -176,7 +177,7 @@ public class Manager implements Signal { } private SignalServiceAccountManager getSignalServiceAccountManager() { - return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, null, account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer); + return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer); } private IdentityKey getIdentity() { @@ -215,9 +216,15 @@ public class Manager implements Signal { accountManager = getSignalServiceAccountManager(); try { - if (account.isRegistered() && accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) { - refreshPreKeys(); - account.save(); + if (account.isRegistered()) { + if (accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) { + refreshPreKeys(); + account.save(); + } + if (account.getUuid() == null) { + account.setUuid(accountManager.getOwnUuid()); + account.save(); + } } } catch (AuthorizationFailedException e) { System.err.println("Authorization failed, was the number registered elsewhere?"); @@ -345,7 +352,7 @@ public class Manager implements Signal { throw new IOException("Received invalid profileKey", e); } } - account = SignalAccount.createLinkedAccount(dataPath, username, account.getPassword(), ret.getDeviceId(), ret.getIdentity(), account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalingKey(), profileKey); + account = SignalAccount.createLinkedAccount(dataPath, username, ret.getUuid(), account.getPassword(), ret.getDeviceId(), ret.getIdentity(), account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalingKey(), profileKey); refreshPreKeys(); @@ -423,10 +430,11 @@ public class Manager implements Signal { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); // TODO make unrestricted unidentified access configurable - accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities); + UUID uuid = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); account.setRegistered(true); + account.setUuid(uuid); account.setRegistrationLockPin(pin); refreshPreKeys(); diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 6d83f78f..ebf7a846 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -92,13 +92,14 @@ public class SignalAccount { return account; } - public static SignalAccount createLinkedAccount(String dataPath, String username, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, ProfileKey profileKey) throws IOException { + public static SignalAccount createLinkedAccount(String dataPath, String username, UUID uuid, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, ProfileKey profileKey) throws IOException { IOUtils.createPrivateDirectories(dataPath); SignalAccount account = new SignalAccount(); account.openFileChannel(getFileName(dataPath, username)); account.username = username; + account.uuid = uuid; account.password = password; account.profileKey = profileKey; account.deviceId = deviceId; @@ -140,6 +141,14 @@ public class SignalAccount { rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel)); } + JsonNode uuidNode = rootNode.get("uuid"); + if (uuidNode != null && !uuidNode.isNull()) { + try { + uuid = UUID.fromString(uuidNode.asText()); + } catch (IllegalArgumentException e) { + throw new IOException("Config file contains an invalid uuid, needs to be a valid UUID", e); + } + } JsonNode node = rootNode.get("deviceId"); if (node != null) { deviceId = node.asInt(); @@ -218,6 +227,7 @@ public class SignalAccount { } ObjectNode rootNode = jsonProcessor.createObjectNode(); rootNode.put("username", username) + .put("uuid", uuid == null ? null : uuid.toString()) .put("deviceId", deviceId) .put("isMultiDevice", isMultiDevice) .put("password", password) @@ -292,6 +302,10 @@ public class SignalAccount { return uuid; } + public void setUuid(final UUID uuid) { + this.uuid = uuid; + } + public SignalServiceAddress getSelfAddress() { return new SignalServiceAddress(uuid, username); } From 20bf605e36be0892d05b3ae11954aa7dd0784e23 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 24 Mar 2020 14:27:12 +0100 Subject: [PATCH 024/103] Only store our own profile key in contact list, if a contact entry already exists --- .../org/asamk/signal/manager/Manager.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 0aada703..90e981c2 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1342,18 +1342,24 @@ public class Manager implements Signal { } } if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { - if (source.equals(account.getSelfAddress())) { + if (source.matches(account.getSelfAddress())) { try { this.account.setProfileKey(new ProfileKey(message.getProfileKey().get())); } catch (InvalidInputException ignored) { } + ContactInfo contact = account.getContactStore().getContact(source); + if (contact != null) { + contact.profileKey = Base64.encodeBytes(message.getProfileKey().get()); + account.getContactStore().updateContact(contact); + } + } else { + ContactInfo contact = account.getContactStore().getContact(source); + if (contact == null) { + contact = new ContactInfo(source); + } + contact.profileKey = Base64.encodeBytes(message.getProfileKey().get()); + account.getContactStore().updateContact(contact); } - ContactInfo contact = account.getContactStore().getContact(source); - if (contact == null) { - contact = new ContactInfo(source); - } - contact.profileKey = Base64.encodeBytes(message.getProfileKey().get()); - account.getContactStore().updateContact(contact); } if (message.getPreviews().isPresent()) { final List previews = message.getPreviews().get(); From 26aa31edc266a81752ebe07d6ae03d538b38678b Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 24 Mar 2020 14:29:10 +0100 Subject: [PATCH 025/103] Only send our profile key to recipient who are in our contact list --- src/main/java/org/asamk/signal/manager/Manager.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 90e981c2..caba52ac 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -548,8 +548,7 @@ public class Manager implements Signal { throws IOException, EncapsulatedExceptions, AttachmentInvalidException { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() - .withReaction(reaction) - .withProfileKey(account.getProfileKey().serialize()); + .withReaction(reaction); if (groupId != null) { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) .withId(groupId) @@ -714,7 +713,6 @@ public class Manager implements Signal { messageBuilder.withAttachments(attachmentPointers); } - messageBuilder.withProfileKey(account.getProfileKey().serialize()); sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); } @@ -723,8 +721,7 @@ public class Manager implements Signal { throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() - .withReaction(reaction) - .withProfileKey(account.getProfileKey().serialize()); + .withReaction(reaction); sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); } @@ -1201,8 +1198,10 @@ public class Manager implements Signal { ContactInfo contact = account.getContactStore().getContact(address); if (contact != null) { messageBuilder.withExpiration(contact.messageExpirationTime); + messageBuilder.withProfileKey(account.getProfileKey().serialize()); } else { messageBuilder.withExpiration(0); + messageBuilder.withProfileKey(null); } message = messageBuilder.build(); try { From efa1c43b0bdeb6e7b12d252492194448543d70fb Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 24 Mar 2020 14:34:00 +0100 Subject: [PATCH 026/103] Get uuids from contact store when resolving numbers --- src/main/java/org/asamk/signal/manager/Manager.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index caba52ac..c928ec17 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1138,8 +1138,11 @@ public class Manager implements Signal { if (canonicalizedNumber.equals(username)) { signalServiceAddresses.add(account.getSelfAddress()); } else { - // TODO get corresponding uuid - signalServiceAddresses.add(new SignalServiceAddress(null, canonicalizedNumber)); + SignalServiceAddress address = new SignalServiceAddress(null, canonicalizedNumber); + ContactInfo contact = account.getContactStore().getContact(address); + signalServiceAddresses.add(contact == null + ? address + : contact.getAddress()); } } return signalServiceAddresses; From 9546a793086c35133b88f9df8f48a37166445994 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 24 Mar 2020 15:14:20 +0100 Subject: [PATCH 027/103] Print better error message for captcha required error --- src/main/java/org/asamk/signal/commands/RegisterCommand.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java index 2e2b7c4f..e95487bf 100644 --- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java @@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; +import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; import java.io.IOException; @@ -22,6 +23,9 @@ public class RegisterCommand implements LocalCommand { try { m.register(ns.getBoolean("voice")); return 0; + } catch (CaptchaRequiredException e) { + System.err.println("Captcha required for verification (" + e.getMessage() + ")"); + return 1; } catch (IOException e) { System.err.println("Request verify error: " + e.getMessage()); return 3; From 995de3ef5b311d2296c4435c380b55c4c82c5175 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 24 Mar 2020 18:15:21 +0100 Subject: [PATCH 028/103] Improve behavior, when authorization fails - register command should still be possible, to regain authorization - reset uuid after registering, otherwise the verify request will fail --- src/main/java/org/asamk/signal/Main.java | 7 ++++++ .../org/asamk/signal/manager/Manager.java | 23 ++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 81065c77..5e37bf3b 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -38,6 +38,7 @@ import org.asamk.signal.util.SecurityProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; +import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.File; @@ -105,6 +106,12 @@ public class Main { ts = m; try { m.init(); + } catch (AuthorizationFailedException e) { + if (!"register".equals(ns.getString("command"))) { + // Register command should still be possible, if current authorization fails + System.err.println("Authorization failed, was the number registered elsewhere?"); + return 2; + } } catch (Exception e) { System.err.println("Error loading state file: " + e.getMessage()); return 2; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index c928ec17..3cf345c8 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -99,7 +99,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; @@ -215,20 +214,15 @@ public class Manager implements Signal { migrateLegacyConfigs(); accountManager = getSignalServiceAccountManager(); - try { - if (account.isRegistered()) { - if (accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) { - refreshPreKeys(); - account.save(); - } - if (account.getUuid() == null) { - account.setUuid(accountManager.getOwnUuid()); - account.save(); - } + if (account.isRegistered()) { + if (accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) { + refreshPreKeys(); + account.save(); + } + if (account.getUuid() == null) { + account.setUuid(accountManager.getOwnUuid()); + account.save(); } - } catch (AuthorizationFailedException e) { - System.err.println("Authorization failed, was the number registered elsewhere?"); - throw e; } } @@ -279,6 +273,7 @@ public class Manager implements Signal { createNewIdentity(); } account.setPassword(KeyUtils.createPassword()); + account.setUuid(null); accountManager = getSignalServiceAccountManager(); if (voiceVerification) { From 5df8f32820f3b4b0f5c5eae69bf9281b5fb462ff Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 24 Mar 2020 18:16:05 +0100 Subject: [PATCH 029/103] Mark our own identity key as trusted initially to match Signal-Android behavior --- src/main/java/org/asamk/signal/manager/Manager.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 3cf345c8..94d0e929 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -258,6 +258,8 @@ public class Manager implements Signal { if (username == null) { account = SignalAccount.createTemporaryAccount(identityKey, registrationId); } else { + account.getSignalProtocolStore().saveIdentity(username, identityKey.getPublicKey(), TrustLevel.TRUSTED_VERIFIED); + ProfileKey profileKey = KeyUtils.createProfileKey(); account = SignalAccount.create(dataPath, username, identityKey, registrationId, profileKey); account.save(); From d50dc69f084a1684d4194fb78606ab7a22154707 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 24 Mar 2020 19:30:21 +0100 Subject: [PATCH 030/103] Remove unused methods from LegacyJsonThreadStore --- .../signal/storage/threads/LegacyJsonThreadStore.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java b/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java index 267d712c..6bea1bfe 100644 --- a/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java +++ b/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java @@ -27,14 +27,6 @@ public class LegacyJsonThreadStore { @JsonDeserialize(using = ThreadsDeserializer.class) private Map threads = new HashMap<>(); - public void updateThread(ThreadInfo thread) { - threads.put(thread.id, thread); - } - - public ThreadInfo getThread(String id) { - return threads.get(id); - } - public List getThreads() { return new ArrayList<>(threads.values()); } From 286070c054d617f376c88a9b24efa9c5cffaef6a Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Mar 2020 20:34:12 +0100 Subject: [PATCH 031/103] Use legacy sender certificate, until uuid support is complete --- src/main/java/org/asamk/signal/manager/Manager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 94d0e929..b418d310 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -997,7 +997,9 @@ public class Manager implements Signal { } private byte[] getSenderCertificate() throws IOException { - byte[] certificate = accountManager.getSenderCertificate(); + // TODO support UUID capable sender certificates + // byte[] certificate = accountManager.getSenderCertificate(); + byte[] certificate = accountManager.getSenderCertificateLegacy(); // TODO cache for a day return certificate; } From 416f43b225f20e6b40292d00e05ddd9733f3cc2d Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 27 Mar 2020 15:33:27 +0100 Subject: [PATCH 032/103] Fix potential crash that could happen when legacy thread.id is null or empty --- src/main/java/org/asamk/signal/storage/SignalAccount.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index ebf7a846..e806794f 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -203,6 +203,9 @@ public class SignalAccount { LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class); // Migrate thread info to group and contact store for (ThreadInfo thread : threadStore.getThreads()) { + if (thread.id == null || thread.id.isEmpty()) { + continue; + } try { ContactInfo contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id)); if (contactInfo != null) { From 7e5aec6e15ac104a62a375754bf4d2d21f55ee3a Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 29 Mar 2020 12:51:32 +0200 Subject: [PATCH 033/103] Store uuids in identity and session store --- .../asamk/signal/ReceiveMessageHandler.java | 2 +- .../commands/ListIdentitiesCommand.java | 20 +- .../signal/commands/SendReactionCommand.java | 3 +- .../asamk/signal/commands/TrustCommand.java | 18 +- .../org/asamk/signal/manager/BaseConfig.java | 3 + .../org/asamk/signal/manager/Manager.java | 263 ++++++++++-------- .../java/org/asamk/signal/manager/Utils.java | 47 +++- .../asamk/signal/storage/SignalAccount.java | 5 + .../protocol/JsonIdentityKeyStore.java | 187 +++++++++---- .../storage/protocol/JsonSessionStore.java | 137 ++++++--- .../protocol/JsonSignalProtocolStore.java | 29 +- .../signal/storage/protocol/SessionInfo.java | 18 ++ .../SignalServiceAddressResolver.java | 13 + src/main/java/org/asamk/signal/util/Util.java | 16 ++ 14 files changed, 510 insertions(+), 251 deletions(-) create mode 100644 src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java create mode 100644 src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 1460aa61..baf456bd 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -158,7 +158,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("Received sync message with verified identities:"); final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified()); - String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey())); + String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey())); System.out.println(" " + safetyNumber); } if (syncMessage.getConfiguration().isPresent()) { diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index a7cf8705..529c7c30 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -7,17 +7,15 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.util.Hex; import org.asamk.signal.util.Util; -import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.List; -import java.util.Map; public class ListIdentitiesCommand implements LocalCommand { - private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) { - String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.getIdentityKey())); - System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername, + private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity theirId) { + String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey())); + System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirId.getAddress().getNumber().orNull(), theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toString(theirId.getFingerprint()), digits)); } @@ -34,17 +32,15 @@ public class ListIdentitiesCommand implements LocalCommand { return 1; } if (ns.get("number") == null) { - for (Map.Entry> keys : m.getIdentities().entrySet()) { - for (JsonIdentityKeyStore.Identity id : keys.getValue()) { - printIdentityFingerprint(m, keys.getKey(), id); - } + for (JsonIdentityKeyStore.Identity identity : m.getIdentities()) { + printIdentityFingerprint(m, identity); } } else { String number = ns.getString("number"); try { - Pair> key = m.getIdentities(number); - for (JsonIdentityKeyStore.Identity id : key.second()) { - printIdentityFingerprint(m, key.first(), id); + List identities = m.getIdentities(number); + for (JsonIdentityKeyStore.Identity id : identities) { + printIdentityFingerprint(m, id); } } catch (InvalidNumberException e) { System.out.println("Invalid number: " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index ffb30195..eb1327ac 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -9,7 +9,6 @@ import org.asamk.signal.GroupNotFoundException; import org.asamk.signal.NotAGroupMemberException; import org.asamk.signal.manager.Manager; import org.asamk.signal.util.Util; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -63,7 +62,7 @@ public class SendReactionCommand implements LocalCommand { String emoji = ns.getString("emoji"); boolean isRemove = ns.getBoolean("remove"); - SignalServiceAddress targetAuthor = new SignalServiceAddress(null, ns.getString("target_author")); + String targetAuthor = ns.getString("target_author"); long targetTimestamp = ns.getLong("target_timestamp"); try { diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index f2744545..a507e265 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -6,7 +6,9 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Hex; +import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.Locale; @@ -50,13 +52,25 @@ public class TrustCommand implements LocalCommand { System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); return 1; } - boolean res = m.trustIdentityVerified(number, fingerprintBytes); + boolean res; + try { + res = m.trustIdentityVerified(number, fingerprintBytes); + } catch (InvalidNumberException e) { + ErrorUtils.handleInvalidNumberException(e); + return 1; + } if (!res) { System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); return 1; } } else if (fingerprint.length() == 60) { - boolean res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint); + boolean res; + try { + res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint); + } catch (InvalidNumberException e) { + ErrorUtils.handleInvalidNumberException(e); + return 1; + } if (!res) { System.err.println("Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); return 1; diff --git a/src/main/java/org/asamk/signal/manager/BaseConfig.java b/src/main/java/org/asamk/signal/manager/BaseConfig.java index edb6c201..1461d99e 100644 --- a/src/main/java/org/asamk/signal/manager/BaseConfig.java +++ b/src/main/java/org/asamk/signal/manager/BaseConfig.java @@ -1,5 +1,6 @@ package org.asamk.signal.manager; +import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; @@ -49,6 +50,8 @@ public class BaseConfig { zkGroupServerPublicParams ); + static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false); + private BaseConfig() { } } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index b418d310..dcc01d94 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -106,6 +106,7 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; import org.whispersystems.signalservice.internal.util.Hex; @@ -133,7 +134,6 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -144,8 +144,6 @@ import java.util.zip.ZipFile; public class Manager implements Signal { - private static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false); - private final String settingsPath; private final String dataPath; private final String attachmentsPath; @@ -192,6 +190,10 @@ public class Manager implements Signal { } private String getMessageCachePath(String sender) { + if (sender == null || sender.isEmpty()) { + return getMessageCachePath(); + } + return getMessageCachePath() + "/" + sender.replace("/", "_"); } @@ -210,6 +212,7 @@ public class Manager implements Signal { return; } account = SignalAccount.load(dataPath, username); + account.setResolver(this::resolveSignalServiceAddress); migrateLegacyConfigs(); @@ -257,11 +260,11 @@ public class Manager implements Signal { int registrationId = KeyHelper.generateRegistrationId(false); if (username == null) { account = SignalAccount.createTemporaryAccount(identityKey, registrationId); + account.setResolver(this::resolveSignalServiceAddress); } else { - account.getSignalProtocolStore().saveIdentity(username, identityKey.getPublicKey(), TrustLevel.TRUSTED_VERIFIED); - ProfileKey profileKey = KeyUtils.createProfileKey(); account = SignalAccount.create(dataPath, username, identityKey, registrationId, profileKey); + account.setResolver(this::resolveSignalServiceAddress); account.save(); } } @@ -289,7 +292,7 @@ public class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities); + accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, BaseConfig.capabilities); } public void setProfileName(String name) throws IOException { @@ -350,6 +353,7 @@ public class Manager implements Signal { } } account = SignalAccount.createLinkedAccount(dataPath, username, ret.getUuid(), account.getPassword(), ret.getDeviceId(), ret.getIdentity(), account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalingKey(), profileKey); + account.setResolver(this::resolveSignalServiceAddress); refreshPreKeys(); @@ -427,12 +431,13 @@ public class Manager implements Signal { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); // TODO make unrestricted unidentified access configurable - UUID uuid = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities); + UUID uuid = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, BaseConfig.capabilities); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); account.setRegistered(true); account.setUuid(uuid); account.setRegistrationLockPin(pin); + account.getSignalProtocolStore().saveIdentity(account.getSelfAddress(), account.getSignalProtocolStore().getIdentityKeyPair().getPublicKey(), TrustLevel.TRUSTED_VERIFIED); refreshPreKeys(); account.save(); @@ -540,10 +545,10 @@ public class Manager implements Signal { sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - public void sendGroupMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, + public void sendGroupMessageReaction(String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, byte[] groupId) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException { - SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); + throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { + SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withReaction(reaction); if (groupId != null) { @@ -713,10 +718,10 @@ public class Manager implements Signal { sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); } - public void sendMessageReaction(String emoji, boolean remove, SignalServiceAddress targetAuthor, + public void sendMessageReaction(String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients) throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { - SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, targetAuthor, targetSentTimestamp); + SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withReaction(reaction); sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); @@ -732,8 +737,7 @@ public class Manager implements Signal { @Override public String getContactName(String number) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername()); - ContactInfo contact = account.getContactStore().getContact(new SignalServiceAddress(null, canonicalizedNumber)); + ContactInfo contact = account.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number)); if (contact == null) { return ""; } else { @@ -743,14 +747,13 @@ public class Manager implements Signal { @Override public void setContactName(String number, String name) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername()); - final SignalServiceAddress address = new SignalServiceAddress(null, canonicalizedNumber); + final SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number); ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { contact = new ContactInfo(address); - System.err.println("Add contact " + canonicalizedNumber + " named " + name); + System.err.println("Add contact " + contact.number + " named " + name); } else { - System.err.println("Updating contact " + canonicalizedNumber + " name " + contact.name + " -> " + name); + System.err.println("Updating contact " + contact.number + " name " + contact.name + " -> " + name); } contact.name = name; account.getContactStore().updateContact(contact); @@ -759,14 +762,16 @@ public class Manager implements Signal { @Override public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException { - number = Utils.canonicalizeNumber(number, account.getUsername()); - final SignalServiceAddress address = new SignalServiceAddress(null, number); + setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked); + } + + private void setContactBlocked(SignalServiceAddress address, boolean blocked) { ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { contact = new ContactInfo(address); - System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + number); + System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + address.getNumber().orNull()); } else { - System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + number); + System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + address.getNumber().orNull()); } contact.blocked = blocked; account.getContactStore().updateContact(contact); @@ -996,10 +1001,16 @@ public class Manager implements Signal { } } - private byte[] getSenderCertificate() throws IOException { + private byte[] getSenderCertificate() { // TODO support UUID capable sender certificates // byte[] certificate = accountManager.getSenderCertificate(); - byte[] certificate = accountManager.getSenderCertificateLegacy(); + byte[] certificate; + try { + certificate = accountManager.getSenderCertificateLegacy(); + } catch (IOException e) { + System.err.println("Failed to get sender certificate: " + e); + return null; + } // TODO cache for a day return certificate; } @@ -1023,7 +1034,7 @@ public class Manager implements Signal { } } - private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) throws IOException { + private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { ContactInfo contact = account.getContactStore().getContact(recipient); if (contact == null || contact.profileKey == null) { return null; @@ -1031,10 +1042,16 @@ public class Manager implements Signal { ProfileKey theirProfileKey; try { theirProfileKey = new ProfileKey(Base64.decode(contact.profileKey)); - } catch (InvalidInputException e) { + } catch (InvalidInputException | IOException e) { throw new AssertionError(e); } - SignalProfile targetProfile = decryptProfile(getRecipientProfile(recipient, Optional.absent()), theirProfileKey); + SignalProfile targetProfile; + try { + targetProfile = decryptProfile(getRecipientProfile(recipient, Optional.absent()), theirProfileKey); + } catch (IOException e) { + System.err.println("Failed to get recipient profile: " + e); + return null; + } if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) { return null; @@ -1047,7 +1064,7 @@ public class Manager implements Signal { return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); } - private Optional getAccessForSync() throws IOException { + private Optional getAccessForSync() { byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); byte[] selfUnidentifiedAccessCertificate = getSenderCertificate(); @@ -1065,7 +1082,7 @@ public class Manager implements Signal { } } - private List> getAccessFor(Collection recipients) throws IOException { + private List> getAccessFor(Collection recipients) { List> result = new ArrayList<>(recipients.size()); for (SignalServiceAddress recipient : recipients) { result.add(getAccessFor(recipient)); @@ -1073,7 +1090,7 @@ public class Manager implements Signal { return result; } - private Optional getAccessFor(SignalServiceAddress recipient) throws IOException { + private Optional getAccessFor(SignalServiceAddress recipient) { byte[] recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); byte[] selfUnidentifiedAccessCertificate = getSenderCertificate(); @@ -1092,13 +1109,23 @@ public class Manager implements Signal { } } + private Optional getUnidentifiedAccess(SignalServiceAddress recipient) { + Optional unidentifiedAccess = getAccessFor(recipient); + + if (unidentifiedAccess.isPresent()) { + return unidentifiedAccess.get().getTargetUnidentifiedAccess(); + } + + return Optional.absent(); + } + private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { SignalServiceMessageSender messageSender = getMessageSender(); try { messageSender.sendMessage(message, getAccessForSync()); } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); throw e; } } @@ -1116,11 +1143,11 @@ public class Manager implements Signal { for (SendMessageResult result : results) { if (result.isUnregisteredFailure()) { - unregisteredUsers.add(new UnregisteredUserException(result.getAddress().getNumber().get(), null)); + unregisteredUsers.add(new UnregisteredUserException(result.getAddress().getLegacyIdentifier(), null)); } else if (result.isNetworkFailure()) { - networkExceptions.add(new NetworkFailureException(result.getAddress().getNumber().get(), null)); + networkExceptions.add(new NetworkFailureException(result.getAddress().getLegacyIdentifier(), null)); } else if (result.getIdentityFailure() != null) { - untrustedIdentities.add(new UntrustedIdentityException("Untrusted", result.getAddress().getNumber().get(), result.getIdentityFailure().getIdentityKey())); + untrustedIdentities.add(new UntrustedIdentityException("Untrusted", result.getAddress().getLegacyIdentifier(), result.getIdentityFailure().getIdentityKey())); } } if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) { @@ -1130,19 +1157,9 @@ public class Manager implements Signal { private Collection getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { final Set signalServiceAddresses = new HashSet<>(numbers.size()); - final String username = account.getUsername(); for (String number : numbers) { - String canonicalizedNumber = Utils.canonicalizeNumber(number, username); - if (canonicalizedNumber.equals(username)) { - signalServiceAddresses.add(account.getSelfAddress()); - } else { - SignalServiceAddress address = new SignalServiceAddress(null, canonicalizedNumber); - ContactInfo contact = account.getContactStore().getContact(address); - signalServiceAddresses.add(contact == null - ? address - : contact.getAddress()); - } + signalServiceAddresses.add(canonicalizeAndResolveSignalServiceAddress(number)); } return signalServiceAddresses; } @@ -1166,12 +1183,12 @@ public class Manager implements Signal { List result = messageSender.sendMessage(new ArrayList<>(recipients), getAccessFor(recipients), isRecipientUpdate, message); for (SendMessageResult r : result) { if (r.getIdentityFailure() != null) { - account.getSignalProtocolStore().saveIdentity(r.getAddress().getNumber().get(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(r.getAddress(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED); } } return result; } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); return Collections.emptyList(); } } else if (recipients.size() == 1 && recipients.contains(account.getSelfAddress())) { @@ -1189,7 +1206,7 @@ public class Manager implements Signal { try { messageSender.sendMessage(syncMessage, unidentifiedAccess); } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); results.add(SendMessageResult.identityFailure(recipient, e.getIdentityKey())); } return results; @@ -1210,7 +1227,7 @@ public class Manager implements Signal { SendMessageResult result = messageSender.sendMessage(address, getAccessFor(address), message); results.add(result); } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(e.getIdentifier(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); results.add(SendMessageResult.identityFailure(address, e.getIdentityKey())); } } @@ -1219,7 +1236,7 @@ public class Manager implements Signal { } finally { if (message != null && message.isEndSession()) { for (SignalServiceAddress recipient : recipients) { - handleEndSession(recipient.getNumber().get()); + handleEndSession(recipient); } } account.save(); @@ -1237,7 +1254,7 @@ public class Manager implements Signal { } } - private void handleEndSession(String source) { + private void handleEndSession(SignalServiceAddress source) { account.getSignalProtocolStore().deleteAllSessions(source); } @@ -1307,7 +1324,7 @@ public class Manager implements Signal { } } if (message.isEndSession()) { - handleEndSession(isSync ? destination.getNumber().get() : source.getNumber().get()); + handleEndSession(isSync ? destination : source); } if (message.isExpirationUpdate() || message.getBody().isPresent()) { if (message.getGroupInfo().isPresent()) { @@ -1384,6 +1401,7 @@ public class Manager implements Signal { } for (final File dir : Objects.requireNonNull(cachePath.listFiles())) { if (!dir.isDirectory()) { + retryFailedReceivedMessage(handler, ignoreAttachments, dir); continue; } @@ -1391,38 +1409,42 @@ public class Manager implements Signal { if (!fileEntry.isFile()) { continue; } - SignalServiceEnvelope envelope; - try { - envelope = Utils.loadEnvelope(fileEntry); - if (envelope == null) { - continue; - } - } catch (IOException e) { - e.printStackTrace(); - continue; - } - SignalServiceContent content = null; - if (!envelope.isReceipt()) { - try { - content = decryptMessage(envelope); - } catch (Exception e) { - continue; - } - handleMessage(envelope, content, ignoreAttachments); - } - account.save(); - handler.handleMessage(envelope, content, null); - try { - Files.delete(fileEntry.toPath()); - } catch (IOException e) { - System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); - } + retryFailedReceivedMessage(handler, ignoreAttachments, fileEntry); } // Try to delete directory if empty dir.delete(); } } + private void retryFailedReceivedMessage(final ReceiveMessageHandler handler, final boolean ignoreAttachments, final File fileEntry) { + SignalServiceEnvelope envelope; + try { + envelope = Utils.loadEnvelope(fileEntry); + if (envelope == null) { + return; + } + } catch (IOException e) { + e.printStackTrace(); + return; + } + SignalServiceContent content = null; + if (!envelope.isReceipt()) { + try { + content = decryptMessage(envelope); + } catch (Exception e) { + return; + } + handleMessage(envelope, content, ignoreAttachments); + } + account.save(); + handler.handleMessage(envelope, content, null); + try { + Files.delete(fileEntry.toPath()); + } catch (IOException e) { + System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); + } + } + public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); final SignalServiceMessageReceiver messageReceiver = getMessageReceiver(); @@ -1496,7 +1518,7 @@ public class Manager implements Signal { } else { return false; } - ContactInfo sourceContact = getContact(source.getNumber().get()); + ContactInfo sourceContact = account.getContactStore().getContact(source); if (sourceContact != null && sourceContact.blocked) { return true; } @@ -1617,13 +1639,7 @@ public class Manager implements Signal { if (syncMessage.getBlockedList().isPresent()) { final BlockedListMessage blockedListMessage = syncMessage.getBlockedList().get(); for (SignalServiceAddress address : blockedListMessage.getAddresses()) { - if (address.getNumber().isPresent()) { - try { - setContactBlocked(address.getNumber().get(), true); - } catch (InvalidNumberException e) { - e.printStackTrace(); - } - } + setContactBlocked(address, true); } for (byte[] groupId : blockedListMessage.getGroupIds()) { try { @@ -1663,7 +1679,7 @@ public class Manager implements Signal { } if (c.getVerified().isPresent()) { final VerifiedMessage verifiedMessage = c.getVerified().get(); - account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + account.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (c.getExpirationTimer().isPresent()) { contact.messageExpirationTime = c.getExpirationTimer().get(); @@ -1692,7 +1708,7 @@ public class Manager implements Signal { } if (syncMessage.getVerified().isPresent()) { final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); - account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination().getNumber().get(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + account.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (syncMessage.getConfiguration().isPresent()) { // TODO @@ -1829,16 +1845,9 @@ public class Manager implements Signal { DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); for (ContactInfo record : account.getContactStore().getContacts()) { VerifiedMessage verifiedMessage = null; - if (getIdentities().containsKey(record.number)) { - JsonIdentityKeyStore.Identity currentIdentity = null; - for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) { - if (currentIdentity == null || id.getDateAdded().after(currentIdentity.getDateAdded())) { - currentIdentity = id; - } - } - if (currentIdentity != null) { - verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime()); - } + JsonIdentityKeyStore.Identity currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress()); + if (currentIdentity != null) { + verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime()); } ProfileKey profileKey = null; @@ -1909,20 +1918,19 @@ public class Manager implements Signal { } public ContactInfo getContact(String number) { - return account.getContactStore().getContact(new SignalServiceAddress(null, number)); + return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number)); } public GroupInfo getGroup(byte[] groupId) { return account.getGroupStore().getGroup(groupId); } - public Map> getIdentities() { + public List getIdentities() { return account.getSignalProtocolStore().getIdentities(); } - public Pair> getIdentities(String number) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, account.getUsername()); - return new Pair<>(canonicalizedNumber, account.getSignalProtocolStore().getIdentities(canonicalizedNumber)); + public List getIdentities(String number) throws InvalidNumberException { + return account.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number)); } /** @@ -1931,8 +1939,9 @@ public class Manager implements Signal { * @param name username of the identity * @param fingerprint Fingerprint */ - public boolean trustIdentityVerified(String name, byte[] fingerprint) { - List ids = account.getSignalProtocolStore().getIdentities(name); + public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException { + SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } @@ -1941,9 +1950,9 @@ public class Manager implements Signal { continue; } - account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); try { - sendVerifiedMessage(new SignalServiceAddress(null, name), id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); } catch (IOException | UntrustedIdentityException e) { e.printStackTrace(); } @@ -1959,19 +1968,20 @@ public class Manager implements Signal { * @param name username of the identity * @param safetyNumber Safety number */ - public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) { - List ids = account.getSignalProtocolStore().getIdentities(name); + public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException { + SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } for (JsonIdentityKeyStore.Identity id : ids) { - if (!safetyNumber.equals(computeSafetyNumber(name, id.getIdentityKey()))) { + if (!safetyNumber.equals(computeSafetyNumber(address, id.getIdentityKey()))) { continue; } - account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); try { - sendVerifiedMessage(new SignalServiceAddress(null, name), id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); } catch (IOException | UntrustedIdentityException e) { e.printStackTrace(); } @@ -1987,15 +1997,16 @@ public class Manager implements Signal { * @param name username of the identity */ public boolean trustIdentityAllKeys(String name) { - List ids = account.getSignalProtocolStore().getIdentities(name); + SignalServiceAddress address = resolveSignalServiceAddress(name); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } for (JsonIdentityKeyStore.Identity id : ids) { if (id.getTrustLevel() == TrustLevel.UNTRUSTED) { - account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); + account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); try { - sendVerifiedMessage(new SignalServiceAddress(null, name), id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); + sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); } catch (IOException | UntrustedIdentityException e) { e.printStackTrace(); } @@ -2005,8 +2016,26 @@ public class Manager implements Signal { return true; } - public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) { - return Utils.computeSafetyNumber(account.getUsername(), getIdentity(), theirUsername, theirIdentityKey); + public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + return Utils.computeSafetyNumber(account.getSelfAddress(), getIdentity(), theirAddress, theirIdentityKey); + } + + public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException { + String canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier : Util.canonicalizeNumber(identifier, account.getUsername()); + return resolveSignalServiceAddress(canonicalizedNumber); + } + + public SignalServiceAddress resolveSignalServiceAddress(String identifier) { + SignalServiceAddress address = Util.getSignalServiceAddressFromIdentifier(identifier); + if (address.matches(account.getSelfAddress())) { + return account.getSelfAddress(); + } + + ContactInfo contactInfo = account.getContactStore().getContact(address); + if (contactInfo == null) { + return address; + } + return contactInfo.getAddress(); } public interface ReceiveMessageHandler { diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index 4396e0ca..0b74f01d 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -13,9 +13,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.InvalidNumberException; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.StreamDetails; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.util.Base64; import java.io.BufferedInputStream; @@ -38,6 +37,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import static org.whispersystems.signalservice.internal.util.Util.isEmpty; @@ -146,19 +146,19 @@ class Utils { return new DeviceLinkInfo(deviceIdentifier, deviceKey); } - static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { - return PhoneNumberFormatter.formatNumber(number, localNumber); - } - static SignalServiceEnvelope loadEnvelope(File file) throws IOException { try (FileInputStream f = new FileInputStream(file)) { DataInputStream in = new DataInputStream(f); int version = in.readInt(); - if (version > 2) { + if (version > 3) { return null; } int type = in.readInt(); String source = in.readUTF(); + UUID sourceUuid = null; + if (version >= 3) { + sourceUuid = UuidUtil.parseOrNull(in.readUTF()); + } int sourceDevice = in.readInt(); if (version == 1) { // read legacy relay field @@ -186,16 +186,20 @@ class Utils { uuid = null; } } - return new SignalServiceEnvelope(type, Optional.of(new SignalServiceAddress(null, source)), sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid); + Optional addressOptional = sourceUuid == null && source.isEmpty() + ? Optional.absent() + : Optional.of(new SignalServiceAddress(sourceUuid, source)); + return new SignalServiceEnvelope(type, addressOptional, sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid); } } static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { try (FileOutputStream f = new FileOutputStream(file)) { try (DataOutputStream out = new DataOutputStream(f)) { - out.writeInt(2); // version + out.writeInt(3); // version out.writeInt(envelope.getType()); - out.writeUTF(envelope.getSourceE164().get()); + out.writeUTF(envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : ""); + out.writeUTF(envelope.getSourceUuid().isPresent() ? envelope.getSourceUuid().get() : ""); out.writeInt(envelope.getSourceDevice()); out.writeLong(envelope.getTimestamp()); if (envelope.hasContent()) { @@ -234,10 +238,25 @@ class Utils { return outputFile; } - static String computeSafetyNumber(String ownUsername, IdentityKey ownIdentityKey, String theirUsername, IdentityKey theirIdentityKey) { - // Version 1: E164 user - // Version 2: UUID user - Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(1, ownUsername.getBytes(), ownIdentityKey, theirUsername.getBytes(), theirIdentityKey); + static String computeSafetyNumber(SignalServiceAddress ownAddress, IdentityKey ownIdentityKey, SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + int version; + byte[] ownId; + byte[] theirId; + + if (BaseConfig.capabilities.isUuid() + && ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) { + // Version 2: UUID user + version = 2; + ownId = UuidUtil.toByteArray(ownAddress.getUuid().get()); + theirId = UuidUtil.toByteArray(theirAddress.getUuid().get()); + } else { + // Version 1: E164 user + version = 1; + ownId = ownAddress.getNumber().get().getBytes(); + theirId = theirAddress.getNumber().get().getBytes(); + } + + Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(version, ownId, ownIdentityKey, theirId, theirIdentityKey); return fingerprint.getDisplayableFingerprint().getDisplayText(); } diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index e806794f..73f79a48 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -15,6 +15,7 @@ import org.asamk.signal.storage.contacts.JsonContactsStore; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.JsonGroupStore; import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; +import org.asamk.signal.storage.protocol.SignalServiceAddressResolver; import org.asamk.signal.storage.threads.LegacyJsonThreadStore; import org.asamk.signal.storage.threads.ThreadInfo; import org.asamk.signal.util.IOUtils; @@ -273,6 +274,10 @@ public class SignalAccount { } } + public void setResolver(final SignalServiceAddressResolver resolver) { + signalProtocolStore.setResolver(resolver); + } + public void addPreKeys(Collection records) { for (PreKeyRecord record : records) { signalProtocolStore.storePreKey(record.getId(), record); diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index 53d6bf23..ddb69096 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -9,32 +9,48 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.asamk.signal.TrustLevel; +import org.asamk.signal.util.Util; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.IdentityKeyStore; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.util.Base64; import java.io.IOException; import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.UUID; public class JsonIdentityKeyStore implements IdentityKeyStore { - private final Map> trustedKeys = new HashMap<>(); + private final List identities = new ArrayList<>(); private final IdentityKeyPair identityKeyPair; private final int localRegistrationId; + private SignalServiceAddressResolver resolver; + public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) { this.identityKeyPair = identityKeyPair; this.localRegistrationId = localRegistrationId; } + public void setResolver(final SignalServiceAddressResolver resolver) { + this.resolver = resolver; + } + + private SignalServiceAddress resolveSignalServiceAddress(String identifier) { + if (resolver != null) { + return resolver.resolveSignalServiceAddress(identifier); + } else { + return Util.getSignalServiceAddressFromIdentifier(identifier); + } + } + @Override public IdentityKeyPair getIdentityKeyPair() { return identityKeyPair; @@ -47,85 +63,116 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { @Override public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { - return saveIdentity(address.getName(), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null); + return saveIdentity(resolveSignalServiceAddress(address.getName()), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null); } /** - * Adds or updates the given identityKey for the user name and sets the trustLevel and added timestamp. + * Adds the given identityKey for the user name and sets the trustLevel and added timestamp. + * If the identityKey already exists, the trustLevel and added timestamp are NOT updated. * - * @param name User name, i.e. phone number - * @param identityKey The user's public key - * @param trustLevel Level of trust: untrusted, trusted, trusted and verified - * @param added Added timestamp, if null and the key is newly added, the current time is used. + * @param serviceAddress User address, i.e. phone number and/or uuid + * @param identityKey The user's public key + * @param trustLevel Level of trust: untrusted, trusted, trusted and verified + * @param added Added timestamp, if null and the key is newly added, the current time is used. */ - public boolean saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel, Date added) { - List identities = trustedKeys.get(name); - if (identities == null) { - identities = new ArrayList<>(); - trustedKeys.put(name, identities); - } else { - for (Identity id : identities) { - if (!id.identityKey.equals(identityKey)) - continue; - - if (id.trustLevel.compareTo(trustLevel) < 0) { - id.trustLevel = trustLevel; - } - if (added != null) { - id.added = added; - } - return true; + public boolean saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added) { + for (Identity id : identities) { + if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) { + continue; } + + if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) { + id.address = serviceAddress; + } + // Identity already exists, not updating the trust level + return true; } - identities.add(new Identity(identityKey, trustLevel, added != null ? added : new Date())); + + identities.add(new Identity(serviceAddress, identityKey, trustLevel, added != null ? added : new Date())); return false; } + /** + * Update trustLevel for the given identityKey for the user name. + * + * @param serviceAddress User address, i.e. phone number and/or uuid + * @param identityKey The user's public key + * @param trustLevel Level of trust: untrusted, trusted, trusted and verified + */ + public void setIdentityTrustLevel(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) { + for (Identity id : identities) { + if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) { + continue; + } + + if (!id.address.getUuid().isPresent() || !id.address.getNumber().isPresent()) { + id.address = serviceAddress; + } + id.trustLevel = trustLevel; + return; + } + + identities.add(new Identity(serviceAddress, identityKey, trustLevel, new Date())); + } + @Override public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { // TODO implement possibility for different handling of incoming/outgoing trust decisions - List identities = trustedKeys.get(address.getName()); - if (identities == null) { - // Trust on first use - return true; - } + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + boolean trustOnFirstUse = true; for (Identity id : identities) { + if (!id.address.matches(serviceAddress)) { + continue; + } + if (id.identityKey.equals(identityKey)) { return id.isTrusted(); + } else { + trustOnFirstUse = false; } } - return false; + return trustOnFirstUse; } @Override public IdentityKey getIdentity(SignalProtocolAddress address) { - List identities = trustedKeys.get(address.getName()); - if (identities == null || identities.size() == 0) { - return null; - } + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + Identity identity = getIdentity(serviceAddress); + return identity == null ? null : identity.getIdentityKey(); + } + public Identity getIdentity(SignalServiceAddress serviceAddress) { long maxDate = 0; Identity maxIdentity = null; - for (Identity id : identities) { + for (Identity id : this.identities) { + if (!id.address.matches(serviceAddress)) { + continue; + } + final long time = id.getDateAdded().getTime(); if (maxIdentity == null || maxDate <= time) { maxDate = time; maxIdentity = id; } } - return maxIdentity.getIdentityKey(); + return maxIdentity; } - public Map> getIdentities() { + public List getIdentities() { // TODO deep copy - return trustedKeys; + return identities; } - public List getIdentities(String name) { - // TODO deep copy - return trustedKeys.get(name); + public List getIdentities(SignalServiceAddress serviceAddress) { + List identities = new ArrayList<>(); + for (Identity identity : this.identities) { + if (identity.address.matches(serviceAddress)) { + identities.add(identity); + } + } + return identities; } public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer { @@ -143,12 +190,26 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { JsonNode trustedKeysNode = node.get("trustedKeys"); if (trustedKeysNode.isArray()) { for (JsonNode trustedKey : trustedKeysNode) { - String trustedKeyName = trustedKey.get("name").asText(); + String trustedKeyName = trustedKey.has("name") + ? trustedKey.get("name").asText() + : null; + + if (UuidUtil.isUuid(trustedKeyName)) { + // Ignore identities that were incorrectly created with UUIDs as name + continue; + } + + UUID uuid = trustedKey.hasNonNull("uuid") + ? UuidUtil.parseOrNull(trustedKey.get("uuid").asText()) + : null; + final SignalServiceAddress serviceAddress = uuid == null + ? Util.getSignalServiceAddressFromIdentifier(trustedKeyName) + : new SignalServiceAddress(uuid, trustedKeyName); try { IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0); TrustLevel trustLevel = trustedKey.has("trustLevel") ? TrustLevel.fromInt(trustedKey.get("trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED; Date added = trustedKey.has("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp").asLong()) : new Date(); - keyStore.saveIdentity(trustedKeyName, id, trustLevel, added); + keyStore.saveIdentity(serviceAddress, id, trustLevel, added); } catch (InvalidKeyException | IOException e) { System.out.println(String.format("Error while decoding key for: %s", trustedKeyName)); } @@ -170,15 +231,18 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId()); json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize())); json.writeArrayFieldStart("trustedKeys"); - for (Map.Entry> trustedKey : jsonIdentityKeyStore.trustedKeys.entrySet()) { - for (Identity id : trustedKey.getValue()) { - json.writeStartObject(); - json.writeStringField("name", trustedKey.getKey()); - json.writeStringField("identityKey", Base64.encodeBytes(id.identityKey.serialize())); - json.writeNumberField("trustLevel", id.trustLevel.ordinal()); - json.writeNumberField("addedTimestamp", id.added.getTime()); - json.writeEndObject(); + for (Identity trustedKey : jsonIdentityKeyStore.identities) { + json.writeStartObject(); + if (trustedKey.getAddress().getNumber().isPresent()) { + json.writeStringField("name", trustedKey.getAddress().getNumber().get()); } + if (trustedKey.getAddress().getUuid().isPresent()) { + json.writeStringField("uuid", trustedKey.getAddress().getUuid().get().toString()); + } + json.writeStringField("identityKey", Base64.encodeBytes(trustedKey.identityKey.serialize())); + json.writeNumberField("trustLevel", trustedKey.trustLevel.ordinal()); + json.writeNumberField("addedTimestamp", trustedKey.added.getTime()); + json.writeEndObject(); } json.writeEndArray(); json.writeEndObject(); @@ -187,22 +251,33 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { public static class Identity { + SignalServiceAddress address; IdentityKey identityKey; TrustLevel trustLevel; Date added; - public Identity(IdentityKey identityKey, TrustLevel trustLevel) { + public Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) { + this.address = address; this.identityKey = identityKey; this.trustLevel = trustLevel; this.added = new Date(); } - Identity(IdentityKey identityKey, TrustLevel trustLevel, Date added) { + Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) { + this.address = address; this.identityKey = identityKey; this.trustLevel = trustLevel; this.added = added; } + public SignalServiceAddress getAddress() { + return address; + } + + public void setAddress(final SignalServiceAddress address) { + this.address = address; + } + boolean isTrusted() { return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index f7bbf204..90229575 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -8,51 +8,68 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; +import org.asamk.signal.util.Util; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionStore; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.util.Base64; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Map; +import java.util.UUID; class JsonSessionStore implements SessionStore { - private final Map sessions = new HashMap<>(); + private final List sessions = new ArrayList<>(); + + private SignalServiceAddressResolver resolver; public JsonSessionStore() { - } - private void addSessions(Map sessions) { - this.sessions.putAll(sessions); + public void setResolver(final SignalServiceAddressResolver resolver) { + this.resolver = resolver; } - @Override - public synchronized SessionRecord loadSession(SignalProtocolAddress remoteAddress) { - try { - if (containsSession(remoteAddress)) { - return new SessionRecord(sessions.get(remoteAddress)); - } else { - return new SessionRecord(); - } - } catch (IOException e) { - throw new AssertionError(e); + private SignalServiceAddress resolveSignalServiceAddress(String identifier) { + if (resolver != null) { + return resolver.resolveSignalServiceAddress(identifier); + } else { + return Util.getSignalServiceAddressFromIdentifier(identifier); } } @Override - public synchronized List getSubDeviceSessions(String name) { - List deviceIds = new LinkedList<>(); + public synchronized SessionRecord loadSession(SignalProtocolAddress address) { + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + for (SessionInfo info : sessions) { + if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) { + try { + return new SessionRecord(info.sessionRecord); + } catch (IOException e) { + System.err.println("Failed to load session, resetting session: " + e); + final SessionRecord sessionRecord = new SessionRecord(); + info.sessionRecord = sessionRecord.serialize(); + return sessionRecord; + } + } + } - for (SignalProtocolAddress key : sessions.keySet()) { - if (key.getName().equals(name) && - key.getDeviceId() != 1) { - deviceIds.add(key.getDeviceId()); + return new SessionRecord(); + } + + @Override + public synchronized List getSubDeviceSessions(String name) { + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name); + + List deviceIds = new LinkedList<>(); + for (SessionInfo info : sessions) { + if (info.address.matches(serviceAddress) && info.deviceId != 1) { + deviceIds.add(info.deviceId); } } @@ -61,26 +78,45 @@ class JsonSessionStore implements SessionStore { @Override public synchronized void storeSession(SignalProtocolAddress address, SessionRecord record) { - sessions.put(address, record.serialize()); + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + for (SessionInfo info : sessions) { + if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) { + if (!info.address.getUuid().isPresent() || !info.address.getNumber().isPresent()) { + info.address = serviceAddress; + } + info.sessionRecord = record.serialize(); + return; + } + } + + sessions.add(new SessionInfo(serviceAddress, address.getDeviceId(), record.serialize())); } @Override public synchronized boolean containsSession(SignalProtocolAddress address) { - return sessions.containsKey(address); + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + for (SessionInfo info : sessions) { + if (info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()) { + return true; + } + } + return false; } @Override public synchronized void deleteSession(SignalProtocolAddress address) { - sessions.remove(address); + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); + sessions.removeIf(info -> info.address.matches(serviceAddress) && info.deviceId == address.getDeviceId()); } @Override public synchronized void deleteAllSessions(String name) { - for (SignalProtocolAddress key : new ArrayList<>(sessions.keySet())) { - if (key.getName().equals(name)) { - sessions.remove(key); - } - } + SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name); + deleteAllSessions(serviceAddress); + } + + public synchronized void deleteAllSessions(SignalServiceAddress serviceAddress) { + sessions.removeIf(info -> info.address.matches(serviceAddress)); } public static class JsonSessionStoreDeserializer extends JsonDeserializer { @@ -89,23 +125,36 @@ class JsonSessionStore implements SessionStore { public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - Map sessionMap = new HashMap<>(); + JsonSessionStore sessionStore = new JsonSessionStore(); + if (node.isArray()) { for (JsonNode session : node) { - String sessionName = session.get("name").asText(); + String sessionName = session.has("name") + ? session.get("name").asText() + : null; + if (UuidUtil.isUuid(sessionName)) { + // Ignore sessions that were incorrectly created with UUIDs as name + continue; + } + + UUID uuid = session.hasNonNull("uuid") + ? UuidUtil.parseOrNull(session.get("uuid").asText()) + : null; + final SignalServiceAddress serviceAddress = uuid == null + ? Util.getSignalServiceAddressFromIdentifier(sessionName) + : new SignalServiceAddress(uuid, sessionName); + final int deviceId = session.get("deviceId").asInt(); + final String record = session.get("record").asText(); try { - sessionMap.put(new SignalProtocolAddress(sessionName, session.get("deviceId").asInt()), Base64.decode(session.get("record").asText())); + SessionInfo sessionInfo = new SessionInfo(serviceAddress, deviceId, Base64.decode(record)); + sessionStore.sessions.add(sessionInfo); } catch (IOException e) { System.out.println(String.format("Error while decoding session for: %s", sessionName)); } } } - JsonSessionStore sessionStore = new JsonSessionStore(); - sessionStore.addSessions(sessionMap); - return sessionStore; - } } @@ -114,14 +163,20 @@ class JsonSessionStore implements SessionStore { @Override public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { json.writeStartArray(); - for (Map.Entry preKey : jsonSessionStore.sessions.entrySet()) { + for (SessionInfo sessionInfo : jsonSessionStore.sessions) { json.writeStartObject(); - json.writeStringField("name", preKey.getKey().getName()); - json.writeNumberField("deviceId", preKey.getKey().getDeviceId()); - json.writeStringField("record", Base64.encodeBytes(preKey.getValue())); + if (sessionInfo.address.getNumber().isPresent()) { + json.writeStringField("name", sessionInfo.address.getNumber().get()); + } + if (sessionInfo.address.getUuid().isPresent()) { + json.writeStringField("uuid", sessionInfo.address.getUuid().get().toString()); + } + json.writeNumberField("deviceId", sessionInfo.deviceId); + json.writeStringField("record", Base64.encodeBytes(sessionInfo.sessionRecord)); json.writeEndObject(); } json.writeEndArray(); } } + } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java index 65ee4a6e..c7079078 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java @@ -13,9 +13,9 @@ import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.List; -import java.util.Map; public class JsonSignalProtocolStore implements SignalProtocolStore { @@ -56,6 +56,11 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId); } + public void setResolver(final SignalServiceAddressResolver resolver) { + sessionStore.setResolver(resolver); + identityKeyStore.setResolver(resolver); + } + @Override public IdentityKeyPair getIdentityKeyPair() { return identityKeyStore.getIdentityKeyPair(); @@ -71,16 +76,20 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { return identityKeyStore.saveIdentity(address, identityKey); } - public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel) { - identityKeyStore.saveIdentity(name, identityKey, trustLevel, null); + public void saveIdentity(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) { + identityKeyStore.saveIdentity(serviceAddress, identityKey, trustLevel, null); } - public Map> getIdentities() { + public void setIdentityTrustLevel(SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel) { + identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel); + } + + public List getIdentities() { return identityKeyStore.getIdentities(); } - public List getIdentities(String name) { - return identityKeyStore.getIdentities(name); + public List getIdentities(SignalServiceAddress serviceAddress) { + return identityKeyStore.getIdentities(serviceAddress); } @Override @@ -93,6 +102,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { return identityKeyStore.getIdentity(address); } + public JsonIdentityKeyStore.Identity getIdentity(SignalServiceAddress serviceAddress) { + return identityKeyStore.getIdentity(serviceAddress); + } + @Override public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { return preKeyStore.loadPreKey(preKeyId); @@ -143,6 +156,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { sessionStore.deleteAllSessions(name); } + public void deleteAllSessions(SignalServiceAddress serviceAddress) { + sessionStore.deleteAllSessions(serviceAddress); + } + @Override public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { return signedPreKeyStore.loadSignedPreKey(signedPreKeyId); diff --git a/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java b/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java new file mode 100644 index 00000000..00221233 --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java @@ -0,0 +1,18 @@ +package org.asamk.signal.storage.protocol; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public class SessionInfo { + + public SignalServiceAddress address; + + public int deviceId; + + public byte[] sessionRecord; + + public SessionInfo(final SignalServiceAddress address, final int deviceId, final byte[] sessionRecord) { + this.address = address; + this.deviceId = deviceId; + this.sessionRecord = sessionRecord; + } +} diff --git a/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java b/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java new file mode 100644 index 00000000..b1c5fb38 --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java @@ -0,0 +1,13 @@ +package org.asamk.signal.storage.protocol; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public interface SignalServiceAddressResolver { + + /** + * Get a SignalServiceAddress with number and/or uuid from an identifier name. + * + * @param identifier can be either a serialized uuid or a e164 phone number + */ + SignalServiceAddress resolveSignalServiceAddress(String identifier); +} diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 01d8b2b1..847abcc2 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -3,6 +3,10 @@ package org.asamk.signal.util; import com.fasterxml.jackson.databind.JsonNode; import org.asamk.signal.GroupIdFormatException; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.util.Base64; import java.io.IOException; @@ -51,4 +55,16 @@ public class Util { throw new GroupIdFormatException(groupId, e); } } + + public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { + return PhoneNumberFormatter.formatNumber(number, localNumber); + } + + public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) { + if (UuidUtil.isUuid(identifier)) { + return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null); + } else { + return new SignalServiceAddress(null, identifier); + } + } } From ff18b1bf99ebb34c76ad57e0b1ed8dbfee135b3d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 29 Mar 2020 14:45:11 +0200 Subject: [PATCH 034/103] Get untrusted identity from inner exception Fixes #283 --- .../org/asamk/signal/ReceiveMessageHandler.java | 6 ------ src/main/java/org/asamk/signal/manager/Manager.java | 13 ++++++++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index baf456bd..faf8deec 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -5,7 +5,6 @@ import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.Util; -import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceContent; @@ -70,11 +69,6 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message."); System.out.println("Use 'signal-cli -u " + m.getUsername() + " listIdentities -n " + e.getName() + "', verify the key and run 'signal-cli -u " + m.getUsername() + " trust -v \"FINGER_PRINT\" " + e.getName() + "' to mark it as trusted"); System.out.println("If you don't care about security, use 'signal-cli -u " + m.getUsername() + " trust -a " + e.getName() + "' to trust it without verification"); - } else if (exception instanceof ProtocolUntrustedIdentityException) { - ProtocolUntrustedIdentityException e = (ProtocolUntrustedIdentityException) exception; - System.out.println("The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message."); - System.out.println("Use 'signal-cli -u " + m.getUsername() + " listIdentities -n " + e.getSender() + "', verify the key and run 'signal-cli -u " + m.getUsername() + " trust -v \"FINGER_PRINT\" " + e.getSender() + "' to mark it as trusted"); - System.out.println("If you don't care about security, use 'signal-cli -u " + m.getUsername() + " trust -a " + e.getSender() + "' to trust it without verification"); } else { System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass().getSimpleName() + ")"); } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index dcc01d94..1d55d886 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1243,14 +1243,17 @@ public class Manager implements Signal { } } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException, UnsupportedDataMessageException { + private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, org.whispersystems.libsignal.UntrustedIdentityException { SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator()); try { return cipher.decrypt(envelope); } catch (ProtocolUntrustedIdentityException e) { - // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else -// account.getSignalProtocolStore().saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED); - throw e; + if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) { + org.whispersystems.libsignal.UntrustedIdentityException identityException = (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause(); + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(identityException.getName()), identityException.getUntrustedIdentity(), TrustLevel.UNTRUSTED); + throw identityException; + } + throw new AssertionError(e); } } @@ -1489,7 +1492,7 @@ public class Manager implements Signal { if (!isMessageBlocked(envelope, content)) { handler.handleMessage(envelope, content, exception); } - if (!(exception instanceof ProtocolUntrustedIdentityException)) { + if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) { File cacheFile = null; try { cacheFile = getMessageCacheFile(envelope.getSourceE164().get(), now, envelope.getTimestamp()); From b3870d6281974822570ff6441fc18031f376ebd4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 29 Mar 2020 14:49:01 +0200 Subject: [PATCH 035/103] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 58695 -> 58694 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f3d88b1c2faf2fc91d853cd5d4242b5547257070..490fda8577df6c95960ba7077c43220e5bb2c0d9 100644 GIT binary patch delta 6577 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 MViXjGx&OBR0?kH%b^rhX delta 6547 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 Date: Sun, 29 Mar 2020 15:26:28 +0200 Subject: [PATCH 036/103] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1b88f353..c79688df 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_1_8 mainClassName = 'org.asamk.signal.Main' -version = '0.6.5' +version = '0.6.6' compileJava.options.encoding = 'UTF-8' From b68575dd1610373343cd3731fbb98707304975d1 Mon Sep 17 00:00:00 2001 From: signal-stickers <59041698+signal-stickers@users.noreply.github.com> Date: Mon, 30 Mar 2020 15:19:17 -0400 Subject: [PATCH 037/103] Use correct sticker pack key length of 32. (#288) * Use correct sticker pack key length of 32. * Update dependencies Co-authored-by: AsamK --- build.gradle | 2 +- src/main/java/org/asamk/signal/manager/KeyUtils.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index c79688df..0c842875 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_5' + compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_6' compile 'org.bouncycastle:bcprov-jdk15on:1.64' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java index bd093e79..fff8179c 100644 --- a/src/main/java/org/asamk/signal/manager/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java @@ -35,7 +35,7 @@ class KeyUtils { } static byte[] createStickerUploadKey() { - return getSecretBytes(64); + return getSecretBytes(32); } private static String getSecret(int size) { From e684a902bb40e56286fad3991c8f6cd44534b588 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 30 Mar 2020 22:00:41 +0200 Subject: [PATCH 038/103] Update dependencies --- build.gradle | 2 +- .../java/org/asamk/signal/JsonDataMessage.java | 6 ++++-- .../signal/JsonDbusReceiveMessageHandler.java | 7 ++++--- .../org/asamk/signal/ReceiveMessageHandler.java | 4 ++-- .../java/org/asamk/signal/manager/Manager.java | 14 +++++++------- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index 0c842875..e6cad8c8 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_6' + compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_7' compile 'org.bouncycastle:bcprov-jdk15on:1.64' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/JsonDataMessage.java b/src/main/java/org/asamk/signal/JsonDataMessage.java index 34f6249e..efd8e53e 100644 --- a/src/main/java/org/asamk/signal/JsonDataMessage.java +++ b/src/main/java/org/asamk/signal/JsonDataMessage.java @@ -2,6 +2,7 @@ package org.asamk.signal; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import java.util.ArrayList; import java.util.List; @@ -16,8 +17,9 @@ class JsonDataMessage { JsonDataMessage(SignalServiceDataMessage dataMessage) { this.timestamp = dataMessage.getTimestamp(); - if (dataMessage.getGroupInfo().isPresent()) { - this.groupInfo = new JsonGroupInfo(dataMessage.getGroupInfo().get()); + if (dataMessage.getGroupContext().isPresent() && dataMessage.getGroupContext().get().getGroupV1().isPresent()) { + SignalServiceGroup groupInfo = dataMessage.getGroupContext().get().getGroupV1().get(); + this.groupInfo = new JsonGroupInfo(groupInfo); } if (dataMessage.getBody().isPresent()) { this.message = dataMessage.getBody().get(); diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 6b26ea0e..14aa1fbd 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -40,8 +40,8 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { SignalServiceDataMessage message = content.getDataMessage().get(); if (!message.isEndSession() && - !(message.getGroupInfo().isPresent() && - message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) { + !(message.getGroupContext().isPresent() && + message.getGroupContext().get().getGroupV1Type() != SignalServiceGroup.Type.DELIVER)) { List attachments = new ArrayList<>(); if (message.getAttachments().isPresent()) { for (SignalServiceAttachment attachment : message.getAttachments().get()) { @@ -56,7 +56,8 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { objectPath, message.getTimestamp(), envelope.isUnidentifiedSender() || !envelope.hasSource() ? content.getSender().getNumber().get() : envelope.getSourceE164().get(), - message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0], + message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent() + ? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0], message.getBody().isPresent() ? message.getBody().get() : "", attachments)); } catch (DBusException e) { diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index faf8deec..fe3dd669 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -254,8 +254,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (message.getBody().isPresent()) { System.out.println("Body: " + message.getBody().get()); } - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); + if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); System.out.println("Group info:"); System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) { diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 1d55d886..ccb2fbce 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1177,7 +1177,7 @@ public class Manager implements Signal { SignalServiceMessageSender messageSender = getMessageSender(); message = messageBuilder.build(); - if (message.getGroupInfo().isPresent()) { + if (message.getGroupContext().isPresent()) { try { final boolean isRecipientUpdate = false; List result = messageSender.sendMessage(new ArrayList<>(recipients), getAccessFor(recipients), isRecipientUpdate, message); @@ -1262,8 +1262,8 @@ public class Manager implements Signal { } private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) { - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); + if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId()); switch (groupInfo.getType()) { case UPDATE: @@ -1330,8 +1330,8 @@ public class Manager implements Signal { handleEndSession(isSync ? destination : source); } if (message.isExpirationUpdate() || message.getBody().isPresent()) { - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); + if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId()); if (group == null) { group = new GroupInfo(groupInfo.getGroupId()); @@ -1528,8 +1528,8 @@ public class Manager implements Signal { if (content != null && content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); + if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); GroupInfo group = getGroup(groupInfo.getGroupId()); if (groupInfo.getType() == SignalServiceGroup.Type.DELIVER && group != null && group.blocked) { return true; From d49d536c32f5d44eaf50f3ac20dfd8e72e94c985 Mon Sep 17 00:00:00 2001 From: narodnik Date: Fri, 3 Apr 2020 13:16:57 +0200 Subject: [PATCH 039/103] Add a new sync dbus message which shows messages you sent. Necessary for having synchronized chats where you want your message to appear. Format is similar to receive message dbus except instead of sender, it has sender (source) and receiver (destination). (#289) --- src/main/java/org/asamk/Signal.java | 43 +++++++++++ .../signal/JsonDbusReceiveMessageHandler.java | 76 ++++++++++++++----- .../asamk/signal/commands/ReceiveCommand.java | 17 ++++- 3 files changed, 114 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 528116b1..f8b353c7 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -96,4 +96,47 @@ public interface Signal extends DBusInterface { return sender; } } + + class SyncMessageReceived extends DBusSignal { + private long timestamp; + private String source; + private String destination; + private byte[] groupId; + private String message; + private List attachments; + + public SyncMessageReceived(String objectpath, long timestamp, String source, String destination, byte[] groupId, String message, List attachments) throws DBusException { + super(objectpath, timestamp, source, destination, groupId, message, attachments); + this.timestamp = timestamp; + this.source = source; + this.destination = destination; + this.groupId = groupId; + this.message = message; + this.attachments = attachments; + } + + public long getTimestamp() { + return timestamp; + } + + public String getSource() { + return source; + } + + public String getDestination() { + return destination; + } + + public byte[] getGroupId() { + return groupId; + } + + public String getMessage() { + return message; + } + + public List getAttachments() { + return attachments; + } + } } diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 14aa1fbd..3ba65f7d 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -9,6 +9,9 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.ArrayList; import java.util.List; @@ -36,35 +39,66 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { } catch (DBusException e) { e.printStackTrace(); } - } else if (content != null && content.getDataMessage().isPresent()) { - SignalServiceDataMessage message = content.getDataMessage().get(); + } else if (content != null) { + if (content.getDataMessage().isPresent()) { + SignalServiceDataMessage message = content.getDataMessage().get(); - if (!message.isEndSession() && - !(message.getGroupContext().isPresent() && - message.getGroupContext().get().getGroupV1Type() != SignalServiceGroup.Type.DELIVER)) { - List attachments = new ArrayList<>(); - if (message.getAttachments().isPresent()) { - for (SignalServiceAttachment attachment : message.getAttachments().get()) { - if (attachment.isPointer()) { - attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath()); + if (message.getBody().isPresent()) + System.out.println(message.getBody().get()); + + if (!message.isEndSession() && + !(message.getGroupContext().isPresent() && + message.getGroupContext().get().getGroupV1Type() != SignalServiceGroup.Type.DELIVER)) { + try { + conn.sendSignal(new Signal.MessageReceived( + objectPath, + message.getTimestamp(), + envelope.isUnidentifiedSender() || !envelope.hasSource() ? content.getSender().getNumber().get() : envelope.getSourceE164().get(), + message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent() + ? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0], + message.getBody().isPresent() ? message.getBody().get() : "", + JsonDbusReceiveMessageHandler.getAttachments(message, m))); + } catch (DBusException e) { + e.printStackTrace(); + } + } + } else if (content.getSyncMessage().isPresent()) { + SignalServiceSyncMessage sync_message = content.getSyncMessage().get(); + if (sync_message.getSent().isPresent()) { + SentTranscriptMessage transcript = sync_message.getSent().get(); + + if (!envelope.isUnidentifiedSender() && envelope.hasSource() && (transcript.getDestination().isPresent() || transcript.getMessage().getGroupContext().isPresent())) { + SignalServiceDataMessage message = transcript.getMessage(); + + try { + conn.sendSignal(new Signal.SyncMessageReceived( + objectPath, + transcript.getTimestamp(), + envelope.getSourceAddress().getNumber().get(), + transcript.getDestination().isPresent() ? transcript.getDestination().get().getNumber().get() : "", + message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent() + ? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0], + message.getBody().isPresent() ? message.getBody().get() : "", + JsonDbusReceiveMessageHandler.getAttachments(message, m))); + } catch (DBusException e) { + e.printStackTrace(); } } } + } + } + } - try { - conn.sendSignal(new Signal.MessageReceived( - objectPath, - message.getTimestamp(), - envelope.isUnidentifiedSender() || !envelope.hasSource() ? content.getSender().getNumber().get() : envelope.getSourceE164().get(), - message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent() - ? message.getGroupContext().get().getGroupV1().get().getGroupId() : new byte[0], - message.getBody().isPresent() ? message.getBody().get() : "", - attachments)); - } catch (DBusException e) { - e.printStackTrace(); + static private List getAttachments(SignalServiceDataMessage message, Manager m) { + List attachments = new ArrayList<>(); + if (message.getAttachments().isPresent()) { + for (SignalServiceAttachment attachment : message.getAttachments().get()) { + if (attachment.isPointer()) { + attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath()); } } } + return attachments; } @Override diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index ffc59530..42ab7327 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -53,7 +53,22 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { }); dbusconnection.addSigHandler(Signal.ReceiptReceived.class, receiptReceived -> System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n", - receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp())))); + receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp())))); + dbusconnection.addSigHandler(Signal.SyncMessageReceived.class, syncReceived -> { + System.out.print(String.format("Sync Envelope from: %s to: %s\nTimestamp: %s\nBody: %s\n", + syncReceived.getSource(), syncReceived.getDestination(), DateUtils.formatTimestamp(syncReceived.getTimestamp()), syncReceived.getMessage())); + if (syncReceived.getGroupId().length > 0) { + System.out.println("Group info:"); + System.out.println(" Id: " + Base64.encodeBytes(syncReceived.getGroupId())); + } + if (syncReceived.getAttachments().size() > 0) { + System.out.println("Attachments: "); + for (String attachment : syncReceived.getAttachments()) { + System.out.println("- Stored plaintext in: " + attachment); + } + } + System.out.println(); + }); } catch (UnsatisfiedLinkError e) { System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); return 1; From 320e126eebc4e3ebac5144197e640508704eeb0c Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 3 Apr 2020 11:56:26 +0200 Subject: [PATCH 040/103] Add RecipientStore to resolve all identifiers to SignalServiceAddress Should fix #290 --- .../org/asamk/signal/manager/Manager.java | 38 +++++--- .../java/org/asamk/signal/manager/Utils.java | 3 + .../asamk/signal/storage/SignalAccount.java | 41 +++++++++ .../storage/contacts/JsonContactsStore.java | 6 ++ .../signal/storage/groups/GroupInfo.java | 3 + .../storage/protocol/JsonSessionStore.java | 6 +- .../protocol/JsonSignalProtocolStore.java | 6 +- .../storage/protocol/RecipientStore.java | 87 +++++++++++++++++++ 8 files changed, 174 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index ccb2fbce..f1316d64 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -139,6 +139,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -1287,7 +1288,10 @@ public class Manager implements Signal { } if (groupInfo.getMembers().isPresent()) { - group.addMembers(groupInfo.getMembers().get()); + group.addMembers(groupInfo.getMembers().get() + .stream() + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toSet())); } account.getGroupStore().updateGroup(group); @@ -1326,8 +1330,9 @@ public class Manager implements Signal { break; } } + final SignalServiceAddress conversationPartnerAddress = isSync ? destination : source; if (message.isEndSession()) { - handleEndSession(isSync ? destination : source); + handleEndSession(conversationPartnerAddress); } if (message.isExpirationUpdate() || message.getBody().isPresent()) { if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) { @@ -1341,9 +1346,9 @@ public class Manager implements Signal { account.getGroupStore().updateGroup(group); } } else { - ContactInfo contact = account.getContactStore().getContact(isSync ? destination : source); + ContactInfo contact = account.getContactStore().getContact(conversationPartnerAddress); if (contact == null) { - contact = new ContactInfo(isSync ? destination : source); + contact = new ContactInfo(conversationPartnerAddress); } if (contact.messageExpirationTime != message.getExpiresInSeconds()) { contact.messageExpirationTime = message.getExpiresInSeconds(); @@ -1607,7 +1612,10 @@ public class Manager implements Signal { if (g.getName().isPresent()) { syncGroup.name = g.getName().get(); } - syncGroup.addMembers(g.getMembers()); + syncGroup.addMembers(g.getMembers() + .stream() + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toSet())); if (!g.isActive()) { syncGroup.removeMember(account.getSelfAddress()); } else { @@ -1642,7 +1650,7 @@ public class Manager implements Signal { if (syncMessage.getBlockedList().isPresent()) { final BlockedListMessage blockedListMessage = syncMessage.getBlockedList().get(); for (SignalServiceAddress address : blockedListMessage.getAddresses()) { - setContactBlocked(address, true); + setContactBlocked(resolveSignalServiceAddress(address), true); } for (byte[] groupId : blockedListMessage.getGroupIds()) { try { @@ -1667,9 +1675,10 @@ public class Manager implements Signal { if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { account.setProfileKey(c.getProfileKey().get()); } - ContactInfo contact = account.getContactStore().getContact(c.getAddress()); + final SignalServiceAddress address = resolveSignalServiceAddress(c.getAddress()); + ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { - contact = new ContactInfo(c.getAddress()); + contact = new ContactInfo(address); } if (c.getName().isPresent()) { contact.name = c.getName().get(); @@ -1711,7 +1720,7 @@ public class Manager implements Signal { } if (syncMessage.getVerified().isPresent()) { final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); - account.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + account.getSignalProtocolStore().setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage.getDestination()), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (syncMessage.getConfiguration().isPresent()) { // TODO @@ -2030,15 +2039,16 @@ public class Manager implements Signal { public SignalServiceAddress resolveSignalServiceAddress(String identifier) { SignalServiceAddress address = Util.getSignalServiceAddressFromIdentifier(identifier); + + return resolveSignalServiceAddress(address); + } + + public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { if (address.matches(account.getSelfAddress())) { return account.getSelfAddress(); } - ContactInfo contactInfo = account.getContactStore().getContact(address); - if (contactInfo == null) { - return address; - } - return contactInfo.getAddress(); + return account.getRecipientStore().resolveServiceAddress(address); } public interface ReceiveMessageHandler { diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index 0b74f01d..449681d2 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -252,6 +252,9 @@ class Utils { } else { // Version 1: E164 user version = 1; + if (!ownAddress.getNumber().isPresent() || !theirAddress.getNumber().isPresent()) { + return "INVALID ID"; + } ownId = ownAddress.getNumber().get().getBytes(); theirId = theirAddress.getNumber().get().getBytes(); } diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 73f79a48..f03bac3a 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -14,7 +14,10 @@ import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.contacts.JsonContactsStore; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.JsonGroupStore; +import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; +import org.asamk.signal.storage.protocol.RecipientStore; +import org.asamk.signal.storage.protocol.SessionInfo; import org.asamk.signal.storage.protocol.SignalServiceAddressResolver; import org.asamk.signal.storage.threads.LegacyJsonThreadStore; import org.asamk.signal.storage.threads.ThreadInfo; @@ -37,6 +40,7 @@ import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.util.Collection; import java.util.UUID; +import java.util.stream.Collectors; public class SignalAccount { @@ -59,6 +63,7 @@ public class SignalAccount { private JsonSignalProtocolStore signalProtocolStore; private JsonGroupStore groupStore; private JsonContactsStore contactStore; + private RecipientStore recipientStore; private SignalAccount() { jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect @@ -88,6 +93,7 @@ public class SignalAccount { account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); account.groupStore = new JsonGroupStore(); account.contactStore = new JsonContactsStore(); + account.recipientStore = new RecipientStore(); account.registered = false; return account; @@ -108,6 +114,7 @@ public class SignalAccount { account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); account.groupStore = new JsonGroupStore(); account.contactStore = new JsonContactsStore(); + account.recipientStore = new RecipientStore(); account.registered = true; account.isMultiDevice = true; @@ -199,6 +206,35 @@ public class SignalAccount { if (contactStore == null) { contactStore = new JsonContactsStore(); } + + JsonNode recipientStoreNode = rootNode.get("recipientStore"); + if (recipientStoreNode != null) { + recipientStore = jsonProcessor.convertValue(recipientStoreNode, RecipientStore.class); + } + if (recipientStore == null) { + recipientStore = new RecipientStore(); + + recipientStore.resolveServiceAddress(getSelfAddress()); + + for (ContactInfo contact : contactStore.getContacts()) { + recipientStore.resolveServiceAddress(contact.getAddress()); + } + + for (GroupInfo group : groupStore.getGroups()) { + group.members = group.members.stream() + .map(m -> recipientStore.resolveServiceAddress(m)) + .collect(Collectors.toSet()); + } + + for (SessionInfo session : signalProtocolStore.getSessions()) { + session.address = recipientStore.resolveServiceAddress(session.address); + } + + for (JsonIdentityKeyStore.Identity identity : signalProtocolStore.getIdentities()) { + identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress())); + } + } + JsonNode threadStoreNode = rootNode.get("threadStore"); if (threadStoreNode != null) { LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class); @@ -244,6 +280,7 @@ public class SignalAccount { .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) .putPOJO("contactStore", contactStore) + .putPOJO("recipientStore", recipientStore) ; try { synchronized (fileChannel) { @@ -302,6 +339,10 @@ public class SignalAccount { return contactStore; } + public RecipientStore getRecipientStore() { + return recipientStore; + } + public String getUsername() { return username; } diff --git a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java index 86514bc1..bb81b0c9 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java +++ b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java @@ -27,6 +27,12 @@ public class JsonContactsStore { public ContactInfo getContact(SignalServiceAddress address) { for (ContactInfo contact : contacts) { if (contact.getAddress().matches(address)) { + if (contact.uuid == null) { + contact.uuid = address.getUuid().orNull(); + } else if (contact.number == null) { + contact.number = address.getNumber().orNull(); + } + return contact; } } diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java index e3a45420..4b0adcd0 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -103,6 +103,9 @@ public class GroupInfo { public void addMembers(Collection addresses) { for (SignalServiceAddress address : addresses) { + if (this.members.contains(address)) { + continue; + } removeMember(address); this.members.add(address); } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index 90229575..5ce99742 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -62,6 +62,10 @@ class JsonSessionStore implements SessionStore { return new SessionRecord(); } + public synchronized List getSessions() { + return sessions; + } + @Override public synchronized List getSubDeviceSessions(String name) { SignalServiceAddress serviceAddress = resolveSignalServiceAddress(name); @@ -158,7 +162,7 @@ class JsonSessionStore implements SessionStore { } } - public static class JsonPreKeyStoreSerializer extends JsonSerializer { + public static class JsonSessionStoreSerializer extends JsonSerializer { @Override public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java index c7079078..3dc15cca 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java @@ -26,7 +26,7 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { @JsonProperty("sessionStore") @JsonDeserialize(using = JsonSessionStore.JsonSessionStoreDeserializer.class) - @JsonSerialize(using = JsonSessionStore.JsonPreKeyStoreSerializer.class) + @JsonSerialize(using = JsonSessionStore.JsonSessionStoreSerializer.class) private JsonSessionStore sessionStore; @JsonProperty("signedPreKeyStore") @@ -131,6 +131,10 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { return sessionStore.loadSession(address); } + public List getSessions() { + return sessionStore.getSessions(); + } + @Override public List getSubDeviceSessions(String name) { return sessionStore.getSubDeviceSessions(name); diff --git a/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java b/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java new file mode 100644 index 00000000..4943bd36 --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java @@ -0,0 +1,87 @@ +package org.asamk.signal.storage.protocol; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class RecipientStore { + + @JsonProperty("recipientStore") + @JsonDeserialize(using = RecipientStoreDeserializer.class) + @JsonSerialize(using = RecipientStoreSerializer.class) + private final Set addresses = new HashSet<>(); + + public RecipientStore() { + } + + public SignalServiceAddress resolveServiceAddress(SignalServiceAddress serviceAddress) { + if (addresses.contains(serviceAddress)) { + // If the Set already contains the exact address with UUID and Number, + // we can just return it here. + return serviceAddress; + } + + for (SignalServiceAddress address : addresses) { + if (address.matches(serviceAddress)) { + return address; + } + } + + if (serviceAddress.getNumber().isPresent() && serviceAddress.getUuid().isPresent()) { + addresses.add(serviceAddress); + } + + return serviceAddress; + } + + public static class RecipientStoreDeserializer extends JsonDeserializer> { + + @Override + public Set deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + Set addresses = new HashSet<>(); + + if (node.isArray()) { + for (JsonNode recipient : node) { + String recipientName = recipient.get("name").asText(); + UUID uuid = UuidUtil.parseOrThrow(recipient.get("uuid").asText()); + final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, recipientName); + addresses.add(serviceAddress); + } + } + + return addresses; + } + } + + public static class RecipientStoreSerializer extends JsonSerializer> { + + @Override + public void serialize(Set addresses, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { + json.writeStartArray(); + for (SignalServiceAddress address : addresses) { + json.writeStartObject(); + json.writeStringField("name", address.getNumber().get()); + json.writeStringField("uuid", address.getUuid().get().toString()); + json.writeEndObject(); + } + json.writeEndArray(); + } + } +} From f51f0cbbcf0c7aab9bf725b4f3831544f50c4e14 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 3 Apr 2020 14:02:18 +0200 Subject: [PATCH 041/103] Return message timestamp after sucessfully sending a message Fixes #104 --- src/main/java/org/asamk/Signal.java | 6 +++--- .../org/asamk/signal/commands/SendCommand.java | 6 ++++-- .../java/org/asamk/signal/manager/Manager.java | 17 ++++++++++------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index f8b353c7..f0e01dd8 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -13,13 +13,13 @@ import java.util.List; public interface Signal extends DBusInterface { - void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException; + long sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException; - void sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException; + long sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException; void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException; - void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; + long sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; String getContactName(String number) throws InvalidNumberException; diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index ab7ca246..f431d708 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -99,12 +99,14 @@ public class SendCommand implements DbusCommand { if (attachments == null) { attachments = new ArrayList<>(); } + long timestamp; if (ns.getString("group") != null) { byte[] groupId = Util.decodeGroupId(ns.getString("group")); - signal.sendGroupMessage(messageText, attachments, groupId); + timestamp = signal.sendGroupMessage(messageText, attachments, groupId); } else { - signal.sendMessage(messageText, attachments, ns.getList("recipient")); + timestamp = signal.sendMessage(messageText, attachments, ns.getList("recipient")); } + System.out.println(timestamp); return 0; } catch (IOException e) { handleIOException(e); diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index f1316d64..06350982 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -525,7 +525,7 @@ public class Manager implements Signal { } @Override - public void sendGroupMessage(String messageText, List attachments, + public long sendGroupMessage(String messageText, List attachments, byte[] groupId) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); @@ -543,7 +543,7 @@ public class Manager implements Signal { messageBuilder.withExpiration(g.messageExpirationTime); - sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); + return sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } public void sendGroupMessageReaction(String emoji, boolean remove, String targetAuthor, @@ -688,15 +688,15 @@ public class Manager implements Signal { } @Override - public void sendMessage(String message, List attachments, String recipient) + public long sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException { List recipients = new ArrayList<>(1); recipients.add(recipient); - sendMessage(message, attachments, recipients); + return sendMessage(message, attachments, recipients); } @Override - public void sendMessage(String messageText, List attachments, + public long sendMessage(String messageText, List attachments, List recipients) throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); @@ -716,7 +716,7 @@ public class Manager implements Signal { messageBuilder.withAttachments(attachmentPointers); } - sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); + return sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); } public void sendMessageReaction(String emoji, boolean remove, String targetAuthor, @@ -1134,8 +1134,10 @@ public class Manager implements Signal { /** * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult. */ - private void sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) + private long sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) throws EncapsulatedExceptions, IOException { + final long timestamp = System.currentTimeMillis(); + messageBuilder.withTimestamp(timestamp); List results = sendMessage(messageBuilder, recipients); List untrustedIdentities = new LinkedList<>(); @@ -1154,6 +1156,7 @@ public class Manager implements Signal { if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) { throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions); } + return timestamp; } private Collection getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { From e04c45766d50d6a4e2d3c084145e2115b38bef5c Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 3 Apr 2020 14:27:18 +0200 Subject: [PATCH 042/103] Rename fingerprint to safety number Fixes #92 --- man/signal-cli.1.adoc | 8 +++---- .../asamk/signal/commands/TrustCommand.java | 22 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 98a54756..3cd260b3 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -226,9 +226,9 @@ number:: *-a*, *--trust-all-known-keys*:: Trust all known keys of this user, only use this for testing. -*-v* VERIFIED_FINGERPRINT, *--verified-fingerprint* VERIFIED_FINGERPRINT:: - Specify the safety number or fingerprint of the key, only use this option if you have verified - the fingerprint. +*-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. updateProfile ~~~~~~~~~~~~~ @@ -355,7 +355,7 @@ Send a message to a group:: signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID Trust new key, after having verified it:: - signal-cli -u USERNAME trust -v FINGER_PRINT NUMBER + signal-cli -u USERNAME trust -v SAFETY_NUMBER NUMBER Trust new key, without having verified it. Only use this if you don't care about security:: signal-cli -u USERNAME trust -a NUMBER diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index a507e265..2780dc46 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -23,8 +23,8 @@ public class TrustCommand implements LocalCommand { mutTrust.addArgument("-a", "--trust-all-known-keys") .help("Trust all known keys of this user, only use this for testing.") .action(Arguments.storeTrue()); - mutTrust.addArgument("-v", "--verified-fingerprint") - .help("Specify the fingerprint of the key, only use this option if you have verified the fingerprint."); + mutTrust.addArgument("-v", "--verified-safety-number", "--verified-fingerprint") + .help("Specify the safety number of the key, only use this option if you have verified the safety number."); } @Override @@ -41,13 +41,13 @@ public class TrustCommand implements LocalCommand { return 1; } } else { - String fingerprint = ns.getString("verified_fingerprint"); - if (fingerprint != null) { - fingerprint = fingerprint.replaceAll(" ", ""); - if (fingerprint.length() == 66) { + String safetyNumber = ns.getString("verified_safety_number"); + if (safetyNumber != null) { + safetyNumber = safetyNumber.replaceAll(" ", ""); + if (safetyNumber.length() == 66) { byte[] fingerprintBytes; try { - fingerprintBytes = Hex.toByteArray(fingerprint.toLowerCase(Locale.ROOT)); + fingerprintBytes = Hex.toByteArray(safetyNumber.toLowerCase(Locale.ROOT)); } catch (Exception e) { System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); return 1; @@ -63,10 +63,10 @@ public class TrustCommand implements LocalCommand { System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); return 1; } - } else if (fingerprint.length() == 60) { + } else if (safetyNumber.length() == 60) { boolean res; try { - res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint); + res = m.trustIdentityVerifiedSafetyNumber(number, safetyNumber); } catch (InvalidNumberException e) { ErrorUtils.handleInvalidNumberException(e); return 1; @@ -76,11 +76,11 @@ public class TrustCommand implements LocalCommand { return 1; } } else { - System.err.println("Fingerprint has invalid format, either specify the old hex fingerprint or the new safety number"); + System.err.println("Safety number has invalid format, either specify the old hex fingerprint or the new safety number"); return 1; } } else { - System.err.println("You need to specify the fingerprint you have verified with -v FINGERPRINT"); + System.err.println("You need to specify the fingerprint/safety number you have verified with -v SAFETY_NUMBER"); return 1; } } From ae41d0c5026fe868c6198e1005344fc78b6e0a2c Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 3 Apr 2020 16:22:10 +0200 Subject: [PATCH 043/103] Output json when receiving messages from dbus and --json parameter is given Fixes #138 --- .../signal/JsonDbusReceiveMessageHandler.java | 25 +++-- src/main/java/org/asamk/signal/JsonError.java | 10 -- .../signal/JsonReceiveMessageHandler.java | 2 + .../asamk/signal/commands/ReceiveCommand.java | 96 ++++++++++++++----- .../signal/{ => json}/JsonAttachment.java | 6 +- .../signal/{ => json}/JsonCallMessage.java | 2 +- .../signal/{ => json}/JsonDataMessage.java | 24 ++++- .../java/org/asamk/signal/json/JsonError.java | 10 ++ .../signal/{ => json}/JsonGroupInfo.java | 6 +- .../{ => json}/JsonMessageEnvelope.java | 23 ++++- .../signal/{ => json}/JsonReceiptMessage.java | 2 +- .../{ => json}/JsonSyncDataMessage.java | 8 +- .../signal/{ => json}/JsonSyncMessage.java | 7 +- .../signal/{ => manager}/JsonStickerPack.java | 4 +- .../org/asamk/signal/manager/Manager.java | 1 - 15 files changed, 176 insertions(+), 50 deletions(-) delete mode 100644 src/main/java/org/asamk/signal/JsonError.java rename src/main/java/org/asamk/signal/{ => json}/JsonAttachment.java (88%) rename src/main/java/org/asamk/signal/{ => json}/JsonCallMessage.java (97%) rename src/main/java/org/asamk/signal/{ => json}/JsonDataMessage.java (60%) create mode 100644 src/main/java/org/asamk/signal/json/JsonError.java rename src/main/java/org/asamk/signal/{ => json}/JsonGroupInfo.java (87%) rename src/main/java/org/asamk/signal/{ => json}/JsonMessageEnvelope.java (71%) rename src/main/java/org/asamk/signal/{ => json}/JsonReceiptMessage.java (95%) rename src/main/java/org/asamk/signal/{ => json}/JsonSyncDataMessage.java (67%) rename src/main/java/org/asamk/signal/{ => json}/JsonSyncMessage.java (89%) rename src/main/java/org/asamk/signal/{ => manager}/JsonStickerPack.java (88%) diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 3ba65f7d..ecb54d07 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -9,9 +9,9 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import java.util.ArrayList; import java.util.List; @@ -40,12 +40,25 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { e.printStackTrace(); } } else if (content != null) { - if (content.getDataMessage().isPresent()) { + if (content.getReceiptMessage().isPresent()) { + final SignalServiceReceiptMessage receiptMessage = content.getReceiptMessage().get(); + if (receiptMessage.isDeliveryReceipt()) { + final String sender = !envelope.isUnidentifiedSender() && envelope.hasSource() ? envelope.getSourceE164().get() : content.getSender().getNumber().get(); + for (long timestamp : receiptMessage.getTimestamps()) { + try { + conn.sendSignal(new Signal.ReceiptReceived( + objectPath, + timestamp, + sender + )); + } catch (DBusException e) { + e.printStackTrace(); + } + } + } + } else if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - if (message.getBody().isPresent()) - System.out.println(message.getBody().get()); - if (!message.isEndSession() && !(message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1Type() != SignalServiceGroup.Type.DELIVER)) { diff --git a/src/main/java/org/asamk/signal/JsonError.java b/src/main/java/org/asamk/signal/JsonError.java deleted file mode 100644 index 5ef2cd7c..00000000 --- a/src/main/java/org/asamk/signal/JsonError.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.asamk.signal; - -class JsonError { - - String message; - - JsonError(Throwable exception) { - this.message = exception.getMessage(); - } -} diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java index 1aea2327..5aa57f44 100644 --- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.asamk.signal.json.JsonError; +import org.asamk.signal.json.JsonMessageEnvelope; import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 42ab7327..f85aea8b 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -1,5 +1,11 @@ package org.asamk.signal.commands; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; @@ -7,6 +13,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; import org.asamk.signal.JsonReceiveMessageHandler; import org.asamk.signal.ReceiveMessageHandler; +import org.asamk.signal.json.JsonMessageEnvelope; import org.asamk.signal.manager.Manager; import org.asamk.signal.util.DateUtils; import org.freedesktop.dbus.DBusConnection; @@ -34,9 +41,27 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { } public int handleCommand(final Namespace ns, final Signal signal, DBusConnection dbusconnection) { - if (dbusconnection != null) { - try { - dbusconnection.addSigHandler(Signal.MessageReceived.class, messageReceived -> { + final ObjectMapper jsonProcessor; + if (ns.getBoolean("json")) { + jsonProcessor = new ObjectMapper(); + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect + jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + } else { + jsonProcessor = null; + } + try { + dbusconnection.addSigHandler(Signal.MessageReceived.class, messageReceived -> { + if (jsonProcessor != null) { + JsonMessageEnvelope envelope = new JsonMessageEnvelope(messageReceived); + ObjectNode result = jsonProcessor.createObjectNode(); + result.putPOJO("envelope", envelope); + try { + jsonProcessor.writeValue(System.out, result); + System.out.println(); + } catch (IOException e) { + e.printStackTrace(); + } + } else { System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n", messageReceived.getSender(), DateUtils.formatTimestamp(messageReceived.getTimestamp()), messageReceived.getMessage())); if (messageReceived.getGroupId().length > 0) { @@ -50,11 +75,39 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { } } System.out.println(); - }); - dbusconnection.addSigHandler(Signal.ReceiptReceived.class, - receiptReceived -> System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n", - receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp())))); - dbusconnection.addSigHandler(Signal.SyncMessageReceived.class, syncReceived -> { + } + }); + + dbusconnection.addSigHandler(Signal.ReceiptReceived.class, + receiptReceived -> { + if (jsonProcessor != null) { + JsonMessageEnvelope envelope = new JsonMessageEnvelope(receiptReceived); + ObjectNode result = jsonProcessor.createObjectNode(); + result.putPOJO("envelope", envelope); + try { + jsonProcessor.writeValue(System.out, result); + System.out.println(); + } catch (IOException e) { + e.printStackTrace(); + } + } else { + System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n", + receiptReceived.getSender(), DateUtils.formatTimestamp(receiptReceived.getTimestamp()))); + } + }); + + dbusconnection.addSigHandler(Signal.SyncMessageReceived.class, syncReceived -> { + if (jsonProcessor != null) { + JsonMessageEnvelope envelope = new JsonMessageEnvelope(syncReceived); + ObjectNode result = jsonProcessor.createObjectNode(); + result.putPOJO("envelope", envelope); + try { + jsonProcessor.writeValue(System.out, result); + System.out.println(); + } catch (IOException e) { + e.printStackTrace(); + } + } else { System.out.print(String.format("Sync Envelope from: %s to: %s\nTimestamp: %s\nBody: %s\n", syncReceived.getSource(), syncReceived.getDestination(), DateUtils.formatTimestamp(syncReceived.getTimestamp()), syncReceived.getMessage())); if (syncReceived.getGroupId().length > 0) { @@ -68,23 +121,22 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { } } System.out.println(); - }); - } catch (UnsatisfiedLinkError e) { - System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); - return 1; - } catch (DBusException e) { - e.printStackTrace(); - return 1; - } - while (true) { - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - return 0; } + }); + } catch (UnsatisfiedLinkError e) { + System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); + return 1; + } catch (DBusException e) { + e.printStackTrace(); + return 1; + } + while (true) { + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + return 0; } } - return 0; } @Override diff --git a/src/main/java/org/asamk/signal/JsonAttachment.java b/src/main/java/org/asamk/signal/json/JsonAttachment.java similarity index 88% rename from src/main/java/org/asamk/signal/JsonAttachment.java rename to src/main/java/org/asamk/signal/json/JsonAttachment.java index 58165639..8a405fc4 100644 --- a/src/main/java/org/asamk/signal/JsonAttachment.java +++ b/src/main/java/org/asamk/signal/json/JsonAttachment.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.json; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -24,4 +24,8 @@ class JsonAttachment { } } } + + JsonAttachment(String filename) { + this.filename = filename; + } } diff --git a/src/main/java/org/asamk/signal/JsonCallMessage.java b/src/main/java/org/asamk/signal/json/JsonCallMessage.java similarity index 97% rename from src/main/java/org/asamk/signal/JsonCallMessage.java rename to src/main/java/org/asamk/signal/json/JsonCallMessage.java index 2c8518f9..c1b1d443 100644 --- a/src/main/java/org/asamk/signal/JsonCallMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonCallMessage.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.json; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; import org.whispersystems.signalservice.api.messages.calls.BusyMessage; diff --git a/src/main/java/org/asamk/signal/JsonDataMessage.java b/src/main/java/org/asamk/signal/json/JsonDataMessage.java similarity index 60% rename from src/main/java/org/asamk/signal/JsonDataMessage.java rename to src/main/java/org/asamk/signal/json/JsonDataMessage.java index efd8e53e..fc8538aa 100644 --- a/src/main/java/org/asamk/signal/JsonDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonDataMessage.java @@ -1,11 +1,13 @@ -package org.asamk.signal; +package org.asamk.signal.json; +import org.asamk.Signal; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; class JsonDataMessage { @@ -34,4 +36,24 @@ class JsonDataMessage { this.attachments = new ArrayList<>(); } } + + public JsonDataMessage(Signal.MessageReceived messageReceived) { + timestamp = messageReceived.getTimestamp(); + message = messageReceived.getMessage(); + groupInfo = new JsonGroupInfo(messageReceived.getGroupId()); + attachments = messageReceived.getAttachments() + .stream() + .map(JsonAttachment::new) + .collect(Collectors.toList()); + } + + public JsonDataMessage(Signal.SyncMessageReceived messageReceived) { + timestamp = messageReceived.getTimestamp(); + message = messageReceived.getMessage(); + groupInfo = new JsonGroupInfo(messageReceived.getGroupId()); + attachments = messageReceived.getAttachments() + .stream() + .map(JsonAttachment::new) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/org/asamk/signal/json/JsonError.java b/src/main/java/org/asamk/signal/json/JsonError.java new file mode 100644 index 00000000..29d85c8b --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonError.java @@ -0,0 +1,10 @@ +package org.asamk.signal.json; + +public class JsonError { + + String message; + + public JsonError(Throwable exception) { + this.message = exception.getMessage(); + } +} diff --git a/src/main/java/org/asamk/signal/JsonGroupInfo.java b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java similarity index 87% rename from src/main/java/org/asamk/signal/JsonGroupInfo.java rename to src/main/java/org/asamk/signal/json/JsonGroupInfo.java index 5678b896..572623e4 100644 --- a/src/main/java/org/asamk/signal/JsonGroupInfo.java +++ b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.json; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -27,4 +27,8 @@ class JsonGroupInfo { } this.type = groupInfo.getType().toString(); } + + JsonGroupInfo(byte[] groupId) { + this.groupId = Base64.encodeBytes(groupId); + } } diff --git a/src/main/java/org/asamk/signal/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java similarity index 71% rename from src/main/java/org/asamk/signal/JsonMessageEnvelope.java rename to src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index 9a275970..3279d941 100644 --- a/src/main/java/org/asamk/signal/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -1,10 +1,11 @@ -package org.asamk.signal; +package org.asamk.signal.json; +import org.asamk.Signal; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -class JsonMessageEnvelope { +public class JsonMessageEnvelope { String source; int sourceDevice; @@ -44,4 +45,22 @@ class JsonMessageEnvelope { } } } + + public JsonMessageEnvelope(Signal.MessageReceived messageReceived) { + source = messageReceived.getSender(); + timestamp = messageReceived.getTimestamp(); + dataMessage = new JsonDataMessage(messageReceived); + } + + public JsonMessageEnvelope(Signal.ReceiptReceived receiptReceived) { + source = receiptReceived.getSender(); + timestamp = receiptReceived.getTimestamp(); + isReceipt = true; + } + + public JsonMessageEnvelope(Signal.SyncMessageReceived messageReceived) { + source = messageReceived.getSource(); + timestamp = messageReceived.getTimestamp(); + syncMessage = new JsonSyncMessage(messageReceived); + } } diff --git a/src/main/java/org/asamk/signal/JsonReceiptMessage.java b/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java similarity index 95% rename from src/main/java/org/asamk/signal/JsonReceiptMessage.java rename to src/main/java/org/asamk/signal/json/JsonReceiptMessage.java index fd875af5..1b896053 100644 --- a/src/main/java/org/asamk/signal/JsonReceiptMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonReceiptMessage.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.json; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; diff --git a/src/main/java/org/asamk/signal/JsonSyncDataMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java similarity index 67% rename from src/main/java/org/asamk/signal/JsonSyncDataMessage.java rename to src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java index b72fb26d..d253b197 100644 --- a/src/main/java/org/asamk/signal/JsonSyncDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java @@ -1,5 +1,6 @@ -package org.asamk.signal; +package org.asamk.signal.json; +import org.asamk.Signal; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; class JsonSyncDataMessage extends JsonDataMessage { @@ -12,4 +13,9 @@ class JsonSyncDataMessage extends JsonDataMessage { this.destination = transcriptMessage.getDestination().get().getNumber().get(); } } + + JsonSyncDataMessage(Signal.SyncMessageReceived messageReceived) { + super(messageReceived); + destination = messageReceived.getDestination(); + } } diff --git a/src/main/java/org/asamk/signal/JsonSyncMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncMessage.java similarity index 89% rename from src/main/java/org/asamk/signal/JsonSyncMessage.java rename to src/main/java/org/asamk/signal/json/JsonSyncMessage.java index 326ec4ed..27766bda 100644 --- a/src/main/java/org/asamk/signal/JsonSyncMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncMessage.java @@ -1,5 +1,6 @@ -package org.asamk.signal; +package org.asamk.signal.json; +import org.asamk.Signal; import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -42,4 +43,8 @@ class JsonSyncMessage { this.type = JsonSyncMessageType.REQUEST_SYNC; } } + + JsonSyncMessage(Signal.SyncMessageReceived messageReceived) { + sentMessage = new JsonSyncDataMessage(messageReceived); + } } diff --git a/src/main/java/org/asamk/signal/JsonStickerPack.java b/src/main/java/org/asamk/signal/manager/JsonStickerPack.java similarity index 88% rename from src/main/java/org/asamk/signal/JsonStickerPack.java rename to src/main/java/org/asamk/signal/manager/JsonStickerPack.java index 4594c5d1..a7e5eb7f 100644 --- a/src/main/java/org/asamk/signal/JsonStickerPack.java +++ b/src/main/java/org/asamk/signal/manager/JsonStickerPack.java @@ -1,10 +1,10 @@ -package org.asamk.signal; +package org.asamk.signal.manager; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; -public class JsonStickerPack { +class JsonStickerPack { @JsonProperty public String title; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 06350982..11c49d84 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.asamk.Signal; import org.asamk.signal.AttachmentInvalidException; import org.asamk.signal.GroupNotFoundException; -import org.asamk.signal.JsonStickerPack; import org.asamk.signal.NotAGroupMemberException; import org.asamk.signal.StickerPackInvalidException; import org.asamk.signal.TrustLevel; From 6ca695b65e72daf06e6079b774f7b6f6b1e25990 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 3 Apr 2020 18:00:27 +0200 Subject: [PATCH 044/103] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e6cad8c8..157e8601 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_1_8 mainClassName = 'org.asamk.signal.Main' -version = '0.6.6' +version = '0.6.7' compileJava.options.encoding = 'UTF-8' From 19b01ff2e9cd45335e59f064acb3731478d0b16d Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 21 Apr 2020 20:33:23 +0200 Subject: [PATCH 045/103] Also catch IllegalArgumentException when sending messages during receive It's necessary to keep receiving messages if a session state is corrupted. e.g: Exception in thread "main" java.lang.IllegalArgumentException: Empty key at java.base/javax.crypto.spec.SecretKeySpec.(Unknown Source) at org.whispersystems.libsignal.ratchet.ChainKey.getBaseMaterial(ChainKey.java:57) at org.whispersystems.libsignal.ratchet.ChainKey.getMessageKeys(ChainKey.java:47) at org.whispersystems.libsignal.SessionCipher.encrypt(SessionCipher.java:97) at org.signal.libsignal.metadata.SealedSessionCipher.encrypt(SealedSessionCipher.java:70) at org.whispersystems.signalservice.api.crypto.SignalServiceCipher.encrypt(SignalServiceCipher.java:86) at org.whispersystems.signalservice.api.SignalServiceMessageSender.getEncryptedMessage(SignalServiceMessageSender.java:1456) at org.whispersystems.signalservice.api.SignalServiceMessageSender.getEncryptedMessages(SignalServiceMessageSender.java:1406) at org.whispersystems.signalservice.api.SignalServiceMessageSender.sendMessage(SignalServiceMessageSender.java:1276) at org.whispersystems.signalservice.api.SignalServiceMessageSender.sendReceipt(SignalServiceMessageSender.java:206) at org.asamk.signal.manager.Manager.sendReceipt(Manager.java:686) at org.asamk.signal.manager.Manager.handleMessage(Manager.java:1562) at org.asamk.signal.manager.Manager.receiveMessages(Manager.java:1496) at org.asamk.signal.commands.ReceiveCommand.handleCommand(ReceiveCommand.java:160) at org.asamk.signal.Main.handleCommands(Main.java:137) at org.asamk.signal.Main.main(Main.java:60) --- src/main/java/org/asamk/signal/manager/Manager.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 11c49d84..cec04431 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1560,7 +1560,7 @@ public class Manager implements Signal { if (content.isNeedsReceipt()) { try { sendReceipt(sender, message.getTimestamp()); - } catch (IOException | UntrustedIdentityException e) { + } catch (IOException | UntrustedIdentityException | IllegalArgumentException e) { e.printStackTrace(); } } @@ -1579,21 +1579,21 @@ public class Manager implements Signal { if (rm.isContactsRequest()) { try { sendContacts(); - } catch (UntrustedIdentityException | IOException e) { + } catch (UntrustedIdentityException | IOException | IllegalArgumentException e) { e.printStackTrace(); } } if (rm.isGroupsRequest()) { try { sendGroups(); - } catch (UntrustedIdentityException | IOException e) { + } catch (UntrustedIdentityException | IOException | IllegalArgumentException e) { e.printStackTrace(); } } if (rm.isBlockedListRequest()) { try { sendBlockedList(); - } catch (UntrustedIdentityException | IOException e) { + } catch (UntrustedIdentityException | IOException | IllegalArgumentException e) { e.printStackTrace(); } } From 08749fcee090f0b36f5899e1207a3333798c8a59 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 21 Apr 2020 20:36:28 +0200 Subject: [PATCH 046/103] When sending an end session message clear local session store also if sending message fails --- src/main/java/org/asamk/signal/manager/Manager.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index cec04431..b6964917 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -732,7 +732,15 @@ public class Manager implements Signal { SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asEndSessionMessage(); - sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); + final Collection signalServiceAddresses = getSignalServiceAddresses(recipients); + try { + sendMessageLegacy(messageBuilder, signalServiceAddresses); + } catch (Exception e) { + for (SignalServiceAddress address : signalServiceAddresses) { + handleEndSession(address); + } + throw e; + } } @Override From 207075c236104dd746a15c76c3a45066df0e4e93 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 6 May 2020 08:37:30 +0200 Subject: [PATCH 047/103] Update README.md Closes #286 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b4bed3b8..31a12c94 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Important: The USERNAME (your phone number) must include the country calling cod You can register Signal using a land line number. In this case you can skip SMS verification process and jump directly to the voice call verification by adding the --voice switch at the end of above register command. -* Verify the number using the code received via SMS or voice +* Verify the number using the code received via SMS or voice, optionally add `--pin PIN_CODE` if you've added a pin code to your account signal-cli -u USERNAME verify CODE From 00777a469c7ec152555a2e92eaf13b8dd0bf43f0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 6 May 2020 09:13:49 +0200 Subject: [PATCH 048/103] Switch to hypfvieh dbus-java Removes transitive dependency on libmatthew-unix-java Fixes #285 --- build.gradle | 6 ++---- src/main/java/org/asamk/Signal.java | 5 +++-- .../org/asamk/signal/DbusReceiveMessageHandler.java | 2 +- .../asamk/signal/JsonDbusReceiveMessageHandler.java | 10 +++++----- src/main/java/org/asamk/signal/Main.java | 8 ++++---- .../java/org/asamk/signal/commands/DaemonCommand.java | 8 ++++---- .../org/asamk/signal/commands/ExtendedDbusCommand.java | 2 +- .../java/org/asamk/signal/commands/ReceiveCommand.java | 2 +- src/main/java/org/asamk/signal/manager/Manager.java | 6 ++++++ 9 files changed, 27 insertions(+), 22 deletions(-) diff --git a/build.gradle b/build.gradle index 157e8601..0fc3d1f9 100644 --- a/build.gradle +++ b/build.gradle @@ -13,9 +13,6 @@ compileJava.options.encoding = 'UTF-8' repositories { mavenLocal() - maven { - url "https://raw.github.com/AsamK/maven/master/releases/" - } mavenCentral() } @@ -23,7 +20,8 @@ dependencies { compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_7' compile 'org.bouncycastle:bcprov-jdk15on:1.64' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' - compile 'org.freedesktop.dbus:dbus-java:2.7.0' + compile 'com.github.hypfvieh:dbus-java:3.2.0' + compile 'org.slf4j:slf4j-nop:1.7.30' } jar { diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index f0e01dd8..654f365f 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -2,9 +2,9 @@ package org.asamk; import org.asamk.signal.AttachmentInvalidException; import org.asamk.signal.GroupNotFoundException; -import org.freedesktop.dbus.DBusInterface; -import org.freedesktop.dbus.DBusSignal; import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.messages.DBusSignal; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -98,6 +98,7 @@ public interface Signal extends DBusInterface { } class SyncMessageReceived extends DBusSignal { + private long timestamp; private String source; private String destination; diff --git a/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java index cebabc18..8fb11a59 100644 --- a/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java @@ -1,7 +1,7 @@ package org.asamk.signal; import org.asamk.signal.manager.Manager; -import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnection; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index ecb54d07..0c5775db 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -2,7 +2,7 @@ package org.asamk.signal; import org.asamk.Signal; import org.asamk.signal.manager.Manager; -import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceContent; @@ -31,7 +31,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { static void sendReceivedMessageToDbus(SignalServiceEnvelope envelope, SignalServiceContent content, DBusConnection conn, final String objectPath, Manager m) { if (envelope.isReceipt()) { try { - conn.sendSignal(new Signal.ReceiptReceived( + conn.sendMessage(new Signal.ReceiptReceived( objectPath, envelope.getTimestamp(), !envelope.isUnidentifiedSender() && envelope.hasSource() ? envelope.getSourceE164().get() : content.getSender().getNumber().get() @@ -46,7 +46,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { final String sender = !envelope.isUnidentifiedSender() && envelope.hasSource() ? envelope.getSourceE164().get() : content.getSender().getNumber().get(); for (long timestamp : receiptMessage.getTimestamps()) { try { - conn.sendSignal(new Signal.ReceiptReceived( + conn.sendMessage(new Signal.ReceiptReceived( objectPath, timestamp, sender @@ -63,7 +63,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { !(message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1Type() != SignalServiceGroup.Type.DELIVER)) { try { - conn.sendSignal(new Signal.MessageReceived( + conn.sendMessage(new Signal.MessageReceived( objectPath, message.getTimestamp(), envelope.isUnidentifiedSender() || !envelope.hasSource() ? content.getSender().getNumber().get() : envelope.getSourceE164().get(), @@ -84,7 +84,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { SignalServiceDataMessage message = transcript.getMessage(); try { - conn.sendSignal(new Signal.SyncMessageReceived( + conn.sendMessage(new Signal.SyncMessageReceived( objectPath, transcript.getTimestamp(), envelope.getSourceAddress().getNumber().get(), diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 5e37bf3b..38c7a68d 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -36,7 +36,7 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.SecurityProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -76,11 +76,11 @@ public class Main { if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { try { m = null; - int busType; + DBusConnection.DBusBusType busType; if (ns.getBoolean("dbus_system")) { - busType = DBusConnection.SYSTEM; + busType = DBusConnection.DBusBusType.SYSTEM; } else { - busType = DBusConnection.SESSION; + busType = DBusConnection.DBusBusType.SESSION; } dBusConn = DBusConnection.getConnection(busType); ts = dBusConn.getRemoteObject( diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 85fee723..11805b44 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -7,7 +7,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.DbusReceiveMessageHandler; import org.asamk.signal.JsonDbusReceiveMessageHandler; import org.asamk.signal.manager.Manager; -import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import java.io.IOException; @@ -41,11 +41,11 @@ public class DaemonCommand implements LocalCommand { DBusConnection conn = null; try { try { - int busType; + DBusConnection.DBusBusType busType; if (ns.getBoolean("system")) { - busType = DBusConnection.SYSTEM; + busType = DBusConnection.DBusBusType.SYSTEM; } else { - busType = DBusConnection.SESSION; + busType = DBusConnection.DBusBusType.SESSION; } conn = DBusConnection.getConnection(busType); conn.exportObject(SIGNAL_OBJECTPATH, m); diff --git a/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java b/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java index b7f70dee..f9cd9de8 100644 --- a/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java +++ b/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java @@ -3,7 +3,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import org.asamk.Signal; -import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnection; public interface ExtendedDbusCommand extends Command { diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index f85aea8b..bc3acbde 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -16,7 +16,7 @@ import org.asamk.signal.ReceiveMessageHandler; import org.asamk.signal.json.JsonMessageEnvelope; import org.asamk.signal.manager.Manager; import org.asamk.signal.util.DateUtils; -import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.util.Base64; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index b6964917..ad770617 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.asamk.Signal; import org.asamk.signal.AttachmentInvalidException; +import org.asamk.signal.DbusConfig; import org.asamk.signal.GroupNotFoundException; import org.asamk.signal.NotAGroupMemberException; import org.asamk.signal.StickerPackInvalidException; @@ -1825,6 +1826,11 @@ public class Manager implements Signal { return false; } + @Override + public String getObjectPath() { + return null; + } + private void sendGroups() throws IOException, UntrustedIdentityException { File groupsFile = IOUtils.createTempFile(); From d8ef312b5f94bf297808dff199a1116746910201 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 6 May 2020 09:18:48 +0200 Subject: [PATCH 049/103] Remove version check which isn't working correctly --- build.gradle | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/build.gradle b/build.gradle index 0fc3d1f9..c6cf9697 100644 --- a/build.gradle +++ b/build.gradle @@ -41,41 +41,3 @@ run { args Eval.me(appArgs) } } - -// Find any 3rd party libraries which have released new versions -// to the central Maven repo since we last upgraded. -// http://daniel.gredler.net/2011/08/08/gradle-keeping-libraries-up-to-date/ -task checkLibVersions { - doLast { - def checked = [:] - allprojects { - configurations.each { configuration -> - configuration.allDependencies.each { dependency -> - def version = dependency.version - if (!version.contains('SNAPSHOT') && !checked[dependency]) { - def group = dependency.group - def path = group.replace('.', '/') - def name = dependency.name - def url = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml" - try { - def metadata = new XmlSlurper().parseText(url.toURL().text) - def versions = metadata.versioning.versions.version.collect { it.text() } - versions.removeAll { it.toLowerCase().contains('alpha') } - versions.removeAll { it.toLowerCase().contains('beta') } - versions.removeAll { it.toLowerCase().contains('rc') } - def newest = versions.max() - if (version != newest) { - println "$group:$name $version -> $newest" - } - } catch (FileNotFoundException e) { - logger.debug "Unable to download $url: $e.message" - } catch (org.xml.sax.SAXParseException e) { - logger.debug "Unable to parse $url: $e.message" - } - checked[dependency] = true - } - } - } - } - } -} From a486b752e81eacc607db434dc36234a28983a4c3 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 6 May 2020 09:24:54 +0200 Subject: [PATCH 050/103] Improve asciidoc formatting of the man page --- man/signal-cli.1.adoc | 338 ++++++++++++++++++++---------------------- 1 file changed, 162 insertions(+), 176 deletions(-) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 3cd260b3..ecd7ac9f 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -5,276 +5,268 @@ vim:set ts=4 sw=4 tw=82 noet: = signal-cli (1) -Name ----- +== Name + signal-cli - A commandline and dbus interface for the Signal messenger -Synopsis --------- +== Synopsis + *signal-cli* [--config CONFIG] [-h | -v | -u USERNAME | --dbus | --dbus-system] command [command-options] -Description ------------ +== Description -signal-cli is a commandline interface for libsignal-service-java. It supports -registering, verifying, sending and receiving messages. For registering you need a -phone number where you can receive SMS or incoming calls. -signal-cli was primarily developed to be used on servers to notify admins of -important events. For this use-case, it has a dbus interface, that can be used to -send messages from any programming language that has dbus bindings. +signal-cli is a commandline interface for libsignal-service-java. +It supports registering, verifying, sending and receiving messages. +For registering you need a phone number where you can receive SMS or incoming calls. +signal-cli was primarily developed to be used on servers to notify admins of important events. +For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings. -Options -------- +== Options *-h*, *--help*:: - Show help message and quit. +Show help message and quit. *-v*, *--version*:: - Print the version and quit. +Print the version and quit. *--config* CONFIG:: - Set the path, where to store the config. - Make sure you have full read/write access to the given directory. - (Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`)) +Set the path, where to store the config. +Make sure you have full read/write access to the given directory. +(Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`)) *-u* USERNAME, *--username* USERNAME:: - Specify your phone number, that will be your identifier. - The phone number must include the country calling code, i.e. the number must - start with a "+" sign. +Specify your phone number, that will be your identifier. +The phone number must include the country calling code, i.e. the number must start with a "+" sign. *--dbus*:: - Make request via user dbus. +Make request via user dbus. *--dbus-system*:: - Make request via system dbus. +Make request via system dbus. -Commands --------- +== Commands -register -~~~~~~~~ -Register a phone number with SMS or voice verification. Use the verify command to -complete the verification. +=== register + +Register a phone number with SMS or voice verification. +Use the verify command to complete the verification. *-v*, *--voice*:: - The verification should be done over voice, not SMS. +The verification should be done over voice, not SMS. + +=== verify -verify -~~~~~~ Verify the number using the code received via SMS or voice. VERIFICATIONCODE:: - The verification code. +The verification code. *-p* PIN, *--pin* PIN:: - The registration lock PIN, that was set by the user. Only required if a PIN was set. +The registration lock PIN, that was set by the user. +Only required if a PIN was set. + +=== unregister -unregister -~~~~~~~~~~ Disable push support for this device, i.e. this device won't receive any more messages. If this is the master device, other users can't send messages to this number anymore. Use "updateAccount" to undo this. To remove a linked device, use "removeDevice" from the master device. -updateAccount -~~~~~~~~~~~~~ +=== updateAccount + Update the account attributes on the signal server. Can fix problems with receiving messages. -setPin -~~~~~~ +=== setPin + Set a registration lock pin, to prevent others from registering this number. REGISTRATION_LOCK_PIN:: - The registration lock PIN, that will be required for new registrations (resets after 7 days of inactivity) +The registration lock PIN, that will be required for new registrations (resets after 7 days of inactivity) + +=== removePin -removePin -~~~~~~~~~ Remove the registration lock pin. -link -~~~~ -Link to an existing device, instead of registering a new number. This shows a -"tsdevice:/…" URI. If you want to connect to another signal-cli instance, you can -just use this URI. If you want to link to an Android/iOS device, create a QR code -with the URI (e.g. with qrencode) and scan that in the Signal app. +=== link + +Link to an existing device, instead of registering a new number. +This shows a "tsdevice:/…" URI. If you want to connect to another signal-cli instance, you can just use this URI. If you want to link to an Android/iOS device, create a QR code with the URI (e.g. with qrencode) and scan that in the Signal app. *-n* NAME, *--name* NAME:: - Optionally specify a name to describe this new device. By default "cli" will - be used. +Optionally specify a name to describe this new device. +By default "cli" will be used. -addDevice -~~~~~~~~~ -Link another device to this device. Only works, if this is the master device. +=== addDevice + +Link another device to this device. +Only works, if this is the master device. *--uri* URI:: - Specify the uri contained in the QR code shown by the new device. +Specify the uri contained in the QR code shown by the new device. + +=== listDevices -listDevices -~~~~~~~~~~~ Show a list of connected devices. -removeDevice -~~~~~~~~~~~~ -Remove a connected device. Only works, if this is the master device. +=== removeDevice + +Remove a connected device. +Only works, if this is the master device. *-d* DEVICEID, *--deviceId* DEVICEID:: - Specify the device you want to remove. Use listDevices to see the deviceIds. +Specify the device you want to remove. +Use listDevices to see the deviceIds. + +=== send -send -~~~~ Send a message to another user or group. RECIPIENT:: - Specify the recipients’ phone number. +Specify the recipients’ phone number. *-g* GROUP, *--group* GROUP:: - Specify the recipient group ID in base64 encoding. +Specify the recipient group ID in base64 encoding. *-m* MESSAGE, *--message* MESSAGE:: - Specify the message, if missing, standard input is used. +Specify the message, if missing, standard input is used. *-a* [ATTACHMENT [ATTACHMENT ...]], *--attachment* [ATTACHMENT [ATTACHMENT ...]]:: - Add one or more files as attachment. +Add one or more files as attachment. *-e*, *--endsession*:: - Clear session state and send end session message. +Clear session state and send end session message. + +=== sendReaction -sendReaction -~~~~~~~~~~~~ Send reaction to a previously received or sent message. RECIPIENT:: - Specify the recipients’ phone number. +Specify the recipients’ phone number. *-g* GROUP, *--group* GROUP:: - Specify the recipient group ID in base64 encoding. +Specify the recipient group ID in base64 encoding. *-e* EMOJI, *--emoji* EMOJI:: - Specify the emoji, should be a single unicode grapheme cluster. +Specify the emoji, should be a single unicode grapheme cluster. *-a* NUMBER, *--target-author* NUMBER:: - Specify the number of the author of the message to which to react. +Specify the number of the author of the message to which to react. *-t* TIMESTAMP, *--target-timestamp* TIMESTAMP:: - Specify the timestamp of the message to which to react. +Specify the timestamp of the message to which to react. *-r*, *--remove*:: - Remove a reaction. +Remove a reaction. -receive -~~~~~~~ -Query the server for new messages. New messages are printed on standardoutput and -attachments are downloaded to the config directory. +=== receive + +Query the server for new messages. +New messages are printed on standardoutput and attachments are downloaded to the config directory. *-t* TIMEOUT, *--timeout* TIMEOUT:: - Number of seconds to wait for new messages (negative values disable timeout). - Default is 5 seconds. +Number of seconds to wait for new messages (negative values disable timeout). +Default is 5 seconds. *--ignore-attachments*:: - Don’t download attachments of received messages. +Don’t download attachments of received messages. *--json*:: - Output received messages in json format, one object per line. +Output received messages in json format, one object per line. + +=== updateGroup -updateGroup -~~~~~~~~~~~ Create or update a group. *-g* GROUP, *--group* GROUP:: - Specify the recipient group ID in base64 encoding. If not specified, a new - group with a new random ID is generated. +Specify the recipient group ID in base64 encoding. +If not specified, a new group with a new random ID is generated. *-n* NAME, *--name* NAME:: - Specify the new group name. +Specify the new group name. *-a* AVATAR, *--avatar* AVATAR:: - Specify a new group avatar image file. +Specify a new group avatar image file. *-m* [MEMBER [MEMBER ...]], *--member* [MEMBER [MEMBER ...]]:: - Specify one or more members to add to the group. +Specify one or more members to add to the group. + +=== quitGroup -quitGroup -~~~~~~~~~ Send a quit group message to all group members and remove self from member list. *-g* GROUP, *--group* GROUP:: - Specify the recipient group ID in base64 encoding. +Specify the recipient group ID in base64 encoding. + +=== listGroups -listGroups -~~~~~~~~~~~ Show a list of known groups. *-d*, *--detailed*:: - Include the list of members of each group. +Include the list of members of each group. -listIdentities -~~~~~~~~~~~~~~ -List all known identity keys and their trust status, fingerprint and safety -number. +=== listIdentities + +List all known identity keys and their trust status, fingerprint and safety number. *-n* NUMBER, *--number* NUMBER:: - Only show identity keys for the given phone number. +Only show identity keys for the given phone number. -trust -~~~~~ -Set the trust level of a given number. The first time a key for a number is seen, -it is trusted by default (TOFU). If the key changes, the new key must be trusted -manually. +=== trust + +Set the trust level of a given number. +The first time a key for a number is seen, it is trusted by default (TOFU). +If the key changes, the new key must be trusted manually. number:: - Specify the phone number, for which to set the trust. +Specify the phone number, for which to set the trust. *-a*, *--trust-all-known-keys*:: - Trust all known keys of this user, only use this for testing. +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. +Specify the safety number of the key, only use this option if you have verified the safety number. + +=== updateProfile -updateProfile -~~~~~~~~~~~~~ Update the name and/or avatar image visible by message recipients for the current users. -The profile is stored encrypted on the Signal servers. The decryption key is sent -with every outgoing messages (excluding group messages). +The profile is stored encrypted on the Signal servers. +The decryption key is sent with every outgoing messages (excluding group messages). *--name*:: - New name visible by message recipients. +New name visible by message recipients. *--avatar*:: - Path to the new avatar visible by message recipients. +Path to the new avatar visible by message recipients. *--remove-avatar*:: - Remove the avatar visible by message recipients. +Remove the avatar visible by message recipients. -updateContact -~~~~~~~~~~~~~ -Update the info associated to a number on our contact list. This change is only -local but can be synchronized to other devices by using `sendContacts` (see -below). +=== updateContact + +Update the info associated to a number on our contact list. +This change is only local but can be synchronized to other devices by using `sendContacts` (see below). If the contact doesn't exist yet, it will be added. NUMBER:: - Specify the contact phone number. +Specify the contact phone number. *-n*, *--name*:: - Specify the new name for this contact. +Specify the new name for this contact. -block -~~~~~ -Block the given contacts or groups (no messages will be received). This change is only -local but can be synchronized to other devices by using `sendContacts` (see -below). +=== block + +Block the given contacts or groups (no messages will be received). +This change is only local but can be synchronized to other devices by using `sendContacts` (see below). [CONTACT [CONTACT ...]]:: - Specify the phone numbers of contacts that should be blocked. +Specify the phone numbers of contacts that should be blocked. *-g* [GROUP [GROUP ...]], *--group* [GROUP [GROUP ...]]:: - Specify the group IDs that should be blocked in base64 encoding. +Specify the group IDs that should be blocked in base64 encoding. -unblock -~~~~~~~ -Unblock the given contacts or groups (messages will be received again). This change is only -local but can be synchronized to other devices by using `sendContacts` (see -below). +=== unblock + +Unblock the given contacts or groups (messages will be received again). +This change is only local but can be synchronized to other devices by using `sendContacts` (see below). [CONTACT [CONTACT ...]]:: Specify the phone numbers of contacts that should be unblocked. @@ -282,18 +274,18 @@ Specify the phone numbers of contacts that should be unblocked. *-g* [GROUP [GROUP ...]], *--group* [GROUP [GROUP ...]]:: Specify the group IDs that should be unblocked in base64 encoding. -sendContacts -~~~~~~~~~~~~ +=== sendContacts + Send a synchronization message with the local contacts list to all linked devices. This command should only be used if this is the master device. -uploadStickerPack -~~~~~~~~~~~~~~~~~ -Upload a new sticker pack, consisting of a manifest file and the stickers in WebP -format (maximum size for a sticker file is 100KiB). +=== uploadStickerPack + +Upload a new sticker pack, consisting of a manifest file and the stickers in WebP format (maximum size for a sticker file is 100KiB). The required manifest.json has the following format: -```json +[source,json] +---- { "title": "", "author": "", @@ -309,61 +301,57 @@ The required manifest.json has the following format: ... ] } -``` +---- PATH:: - The path of the manifest.json or a zip file containing the sticker pack you - wish to upload. +The path of the manifest.json or a zip file containing the sticker pack you wish to upload. -daemon -~~~~~~ -signal-cli can run in daemon mode and provides an experimental dbus interface. For -dbus support you need jni/unix-java.so installed on your system (Debian: +=== daemon + +signal-cli can run in daemon mode and provides an experimental dbus interface. +For dbus support you need jni/unix-java.so installed on your system (Debian: libunixsocket-java ArchLinux: libmatthew-unix-java (AUR)). *--system*:: - Use DBus system bus instead of user bus. +Use DBus system bus instead of user bus. *--ignore-attachments*:: - Don’t download attachments of received messages. +Don’t download attachments of received messages. - -Examples --------- +== Examples Register a number (with SMS verification):: - signal-cli -u USERNAME register +signal-cli -u USERNAME register Verify the number using the code received via SMS or voice:: - signal-cli -u USERNAME verify CODE +signal-cli -u USERNAME verify CODE Send a message to one or more recipients:: - signal-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] +signal-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] Pipe the message content from another process:: - uname -a | signal-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]] +uname -a | signal-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]] Create a group:: - signal-cli -u USERNAME updateGroup -n "Group name" -m [MEMBER [MEMBER ...]] +signal-cli -u USERNAME updateGroup -n "Group name" -m [MEMBER [MEMBER ...]] Add member to a group:: - signal-cli -u USERNAME updateGroup -g GROUP_ID -m "NEW_MEMBER" +signal-cli -u USERNAME updateGroup -g GROUP_ID -m "NEW_MEMBER" Leave a group:: - signal-cli -u USERNAME quitGroup -g GROUP_ID +signal-cli -u USERNAME quitGroup -g GROUP_ID Send a message to a group:: - signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID +signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID Trust new key, after having verified it:: - signal-cli -u USERNAME trust -v SAFETY_NUMBER NUMBER +signal-cli -u USERNAME trust -v SAFETY_NUMBER NUMBER Trust new key, without having verified it. Only use this if you don't care about security:: - signal-cli -u USERNAME trust -a NUMBER +signal-cli -u USERNAME trust -a NUMBER -Files ------ -The password and cryptographic keys are created when registering and stored in the -current users home directory, the directory can be changed with *--config*: +== Files + +The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--config*: `$XDG_DATA_HOME/signal-cli/` (`$HOME/.local/share/signal-cli/`) @@ -373,10 +361,8 @@ For legacy users, the old config directories are used as a fallback: $HOME/.config/textsecure/ +== Authors -Authors -------- - -Maintained by AsamK , who is assisted by other open -source contributors. For more information about signal-cli development, see +Maintained by AsamK , who is assisted by other open source contributors. +For more information about signal-cli development, see . From 26840a2f0fb1042483fe58d6161948865881dee6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 6 May 2020 20:11:26 +0200 Subject: [PATCH 051/103] Update dependencies --- build.gradle | 2 +- .../signal/JsonDbusReceiveMessageHandler.java | 2 +- .../asamk/signal/ReceiveMessageHandler.java | 4 +- .../org/asamk/signal/json/JsonAttachment.java | 2 +- .../org/asamk/signal/manager/BaseConfig.java | 19 ++++++- .../org/asamk/signal/manager/Manager.java | 55 ++++++++++++------- .../java/org/asamk/signal/manager/Utils.java | 5 +- 7 files changed, 60 insertions(+), 29 deletions(-) diff --git a/build.gradle b/build.gradle index c6cf9697..90289456 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_7' + compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_8' compile 'org.bouncycastle:bcprov-jdk15on:1.64' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'com.github.hypfvieh:dbus-java:3.2.0' diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 0c5775db..0728b871 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -107,7 +107,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { if (message.getAttachments().isPresent()) { for (SignalServiceAttachment attachment : message.getAttachments().get()) { if (attachment.isPointer()) { - attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath()); + attachments.add(m.getAttachmentFile(attachment.asPointer().getRemoteId()).getAbsolutePath()); } } } diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index fe3dd669..898f382c 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -358,12 +358,12 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); if (attachment.isPointer()) { final SignalServiceAttachmentPointer pointer = attachment.asPointer(); - System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length); + System.out.println(" Id: " + pointer.getRemoteId() + " Key length: " + pointer.getKey().length); System.out.println(" Filename: " + (pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-")); System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); System.out.println(" Voice note: " + (pointer.getVoiceNote() ? "yes" : "no")); System.out.println(" Dimensions: " + pointer.getWidth() + "x" + pointer.getHeight()); - File file = m.getAttachmentFile(pointer.getId()); + File file = m.getAttachmentFile(pointer.getRemoteId()); if (file.exists()) { System.out.println(" Stored plaintext in: " + file); } diff --git a/src/main/java/org/asamk/signal/json/JsonAttachment.java b/src/main/java/org/asamk/signal/json/JsonAttachment.java index 8a405fc4..1949171a 100644 --- a/src/main/java/org/asamk/signal/json/JsonAttachment.java +++ b/src/main/java/org/asamk/signal/json/JsonAttachment.java @@ -15,7 +15,7 @@ class JsonAttachment { final SignalServiceAttachmentPointer pointer = attachment.asPointer(); if (attachment.isPointer()) { - this.id = String.valueOf(pointer.getId()); + this.id = String.valueOf(pointer.getRemoteId()); if (pointer.getFileName().isPresent()) { this.filename = pointer.getFileName().get(); } diff --git a/src/main/java/org/asamk/signal/manager/BaseConfig.java b/src/main/java/org/asamk/signal/manager/BaseConfig.java index 1461d99e..ade0288d 100644 --- a/src/main/java/org/asamk/signal/manager/BaseConfig.java +++ b/src/main/java/org/asamk/signal/manager/BaseConfig.java @@ -1,5 +1,6 @@ package org.asamk.signal.manager; +import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; @@ -10,8 +11,11 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import okhttp3.Dns; import okhttp3.Interceptor; public class BaseConfig { @@ -27,10 +31,13 @@ public class BaseConfig { private final static String URL = "https://textsecure-service.whispersystems.org"; private final static String CDN_URL = "https://cdn.signal.org"; + private final static String CDN2_URL = "https://cdn2.signal.org"; private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org"; private final static String STORAGE_URL = "https://storage.signal.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); + private final static Optional dns = Optional.absent(); + private final static Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request().newBuilder() .header("User-Agent", USER_AGENT) @@ -42,16 +49,24 @@ public class BaseConfig { final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration( new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, - new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, + makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}), new SignalContactDiscoveryUrl[0], new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, interceptors, + dns, zkGroupServerPublicParams ); - static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false); + static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false, false); private BaseConfig() { } + + private static Map makeSignalCdnUrlMapFor(SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls) { + Map result = new HashMap<>(); + result.put(0, cdn0Urls); + result.put(2, cdn2Urls); + return Collections.unmodifiableMap(result); + } } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index ad770617..4588aaea 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.asamk.Signal; import org.asamk.signal.AttachmentInvalidException; -import org.asamk.signal.DbusConfig; import org.asamk.signal.GroupNotFoundException; import org.asamk.signal.NotAGroupMemberException; import org.asamk.signal.StickerPackInvalidException; @@ -47,6 +46,7 @@ import org.signal.libsignal.metadata.SelfSendException; import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.VerificationFailedException; +import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; @@ -75,6 +75,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -100,6 +101,7 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -109,6 +111,7 @@ import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; +import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.util.Base64; @@ -125,11 +128,13 @@ import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.Deque; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -236,7 +241,7 @@ public class Manager implements Signal { if (JsonGroupStore.groupsWithLegacyAvatarId.size() > 0) { for (GroupInfo g : JsonGroupStore.groupsWithLegacyAvatarId) { File avatarFile = getGroupAvatarFile(g.groupId); - File attachmentFile = getAttachmentFile(g.getAvatarId()); + File attachmentFile = getAttachmentFile(new SignalServiceAttachmentRemoteId(g.getAvatarId())); if (!avatarFile.exists() && attachmentFile.exists()) { try { IOUtils.createPrivateDirectories(avatarsPath); @@ -432,8 +437,10 @@ public class Manager implements Signal { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); // TODO make unrestricted unidentified access configurable - UUID uuid = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, BaseConfig.capabilities); + VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, BaseConfig.capabilities); + UUID uuid = UuidUtil.parseOrNull(response.getUuid()); + // TODO response.isStorageCapable() //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); account.setRegistered(true); account.setUuid(uuid); @@ -450,7 +457,7 @@ public class Manager implements Signal { throw new RuntimeException("Not implemented anymore, will be replaced with KBS"); } else { account.setRegistrationLockPin(null); - accountManager.removeV1Pin(); + accountManager.removeRegistrationLockV1(); } account.save(); } @@ -464,12 +471,17 @@ public class Manager implements Signal { } private SignalServiceMessageReceiver getMessageReceiver() { - return new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer); + // TODO implement ZkGroup support + final ClientZkProfileOperations clientZkProfileOperations = null; + return new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer, clientZkProfileOperations); } private SignalServiceMessageSender getMessageSender() { + // TODO implement ZkGroup support + final ClientZkProfileOperations clientZkProfileOperations = null; + final boolean attachmentsV3 = false; return new SignalServiceMessageSender(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), - account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); + account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), attachmentsV3, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations); } private SignalServiceProfile getRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess) throws IOException { @@ -1288,8 +1300,8 @@ public class Manager implements Signal { if (avatar.isPointer()) { try { retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId); - } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage()); + } catch (IOException | InvalidMessageException | MissingConfigurationException e) { + System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getRemoteId() + "): " + e.getMessage()); } } } @@ -1372,8 +1384,8 @@ public class Manager implements Signal { if (attachment.isPointer()) { try { retrieveAttachment(attachment.asPointer()); - } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage()); + } catch (IOException | InvalidMessageException | MissingConfigurationException e) { + System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getRemoteId() + "): " + e.getMessage()); } } } @@ -1405,8 +1417,8 @@ public class Manager implements Signal { SignalServiceAttachmentPointer attachment = preview.getImage().get().asPointer(); try { retrieveAttachment(attachment); - } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve attachment (" + attachment.getId() + "): " + e.getMessage()); + } catch (IOException | InvalidMessageException | MissingConfigurationException e) { + System.err.println("Failed to retrieve attachment (" + attachment.getRemoteId() + "): " + e.getMessage()); } } } @@ -1482,7 +1494,8 @@ public class Manager implements Signal { envelope = messagePipe.read(timeout, unit, envelope1 -> { // store message on disk, before acknowledging receipt to the server try { - File cacheFile = getMessageCacheFile(envelope1.getSourceE164().get(), now, envelope1.getTimestamp()); + String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : ""; + File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp()); Utils.storeEnvelope(envelope1, cacheFile); } catch (IOException e) { System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage()); @@ -1744,7 +1757,7 @@ public class Manager implements Signal { return new File(avatarsPath, "contact-" + number); } - private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException { + private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException, MissingConfigurationException { IOUtils.createPrivateDirectories(avatarsPath); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); @@ -1759,7 +1772,7 @@ public class Manager implements Signal { return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_")); } - private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException { + private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException, MissingConfigurationException { IOUtils.createPrivateDirectories(avatarsPath); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); @@ -1770,16 +1783,16 @@ public class Manager implements Signal { } } - public File getAttachmentFile(long attachmentId) { - return new File(attachmentsPath, attachmentId + ""); + public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { + return new File(attachmentsPath, attachmentId.toString()); } - private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { + private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException, MissingConfigurationException { IOUtils.createPrivateDirectories(attachmentsPath); - return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true); + return retrieveAttachment(pointer, getAttachmentFile(pointer.getRemoteId()), true); } - private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException { + private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException, MissingConfigurationException { if (storePreview && pointer.getPreview().isPresent()) { File previewFile = new File(outputFile + ".preview"); try (OutputStream output = new FileOutputStream(previewFile)) { @@ -1816,7 +1829,7 @@ public class Manager implements Signal { return outputFile; } - private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { + private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException, MissingConfigurationException { final SignalServiceMessageReceiver messageReceiver = getMessageReceiver(); return messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE); } diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index 449681d2..28dd7a07 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -15,6 +15,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import org.whispersystems.util.Base64; import java.io.BufferedInputStream; @@ -76,10 +77,12 @@ class Utils { final long attachmentSize = attachmentFile.length(); final String mime = getFileMimeType(attachmentFile); // TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option + final long uploadTimestamp = System.currentTimeMillis(); Optional preview = Optional.absent(); Optional caption = Optional.absent(); Optional blurHash = Optional.absent(); - return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, blurHash, null, null); + final Optional resumableUploadSpec = Optional.absent(); + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, uploadTimestamp, caption, blurHash, null, null, resumableUploadSpec); } static StreamDetails createStreamDetailsFromFile(File file) throws IOException { From 916d0e3cf1713d91c0d8c511c0f31986caad22b7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 May 2020 12:41:49 +0200 Subject: [PATCH 052/103] Don't send group info request after receiving QUIT for unknown group The sender has quit the group so he won't respond to the info request anyway --- src/main/java/org/asamk/signal/manager/Manager.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 4588aaea..f76642c9 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1329,13 +1329,7 @@ public class Manager implements Signal { } break; case QUIT: - if (group == null) { - try { - sendGroupInfoRequest(groupInfo.getGroupId(), source); - } catch (IOException | EncapsulatedExceptions e) { - e.printStackTrace(); - } - } else { + if (group != null) { group.removeMember(source); account.getGroupStore().updateGroup(group); } From 06caf4ebb3e43f0d7f1be810244bba799cd66625 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 11 May 2020 11:49:02 +0200 Subject: [PATCH 053/103] Update dependencies --- build.gradle | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 90289456..8e1c985f 100644 --- a/build.gradle +++ b/build.gradle @@ -17,11 +17,11 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.15.3_unofficial_8' - compile 'org.bouncycastle:bcprov-jdk15on:1.64' - compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' - compile 'com.github.hypfvieh:dbus-java:3.2.0' - compile 'org.slf4j:slf4j-nop:1.7.30' + implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_8' + implementation 'org.bouncycastle:bcprov-jdk15on:1.65' + implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1' + implementation 'com.github.hypfvieh:dbus-java:3.2.1' + implementation 'org.slf4j:slf4j-nop:1.7.30' } jar { @@ -41,3 +41,36 @@ run { args Eval.me(appArgs) } } + +// Find any 3rd party libraries which have released new versions +// to the central Maven repo since we last upgraded. +task checkLibVersions { + doLast { + def checked = [:] + allprojects { + configurations.each { configuration -> + configuration.allDependencies.each { dependency -> + def version = dependency.version + if (!checked[dependency]) { + def group = dependency.group + def path = group.replace('.', '/') + def name = dependency.name + def url = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml" + try { + def metadata = new XmlSlurper().parseText(url.toURL().text) + def newest = metadata.versioning.latest; + if ("$version" != "$newest") { + println "UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}" + } + } catch (FileNotFoundException e) { + logger.debug "Unable to download $url: $e.message" + } catch (org.xml.sax.SAXParseException e) { + logger.debug "Unable to parse $url: $e.message" + } + checked[dependency] = true + } + } + } + } + } +} From 8163a42d3afa269242537227be2a0fe87633ea1f Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 11 May 2020 14:51:55 +0200 Subject: [PATCH 054/103] Split manager ServiceConfig from BaseConfig --- .../java/org/asamk/signal/BaseConfig.java | 12 +++++ src/main/java/org/asamk/signal/Main.java | 4 +- .../org/asamk/signal/manager/Manager.java | 32 +++++++------ .../{BaseConfig.java => ServiceConfig.java} | 46 +++++++++---------- .../java/org/asamk/signal/manager/Utils.java | 4 +- 5 files changed, 55 insertions(+), 43 deletions(-) create mode 100644 src/main/java/org/asamk/signal/BaseConfig.java rename src/main/java/org/asamk/signal/manager/{BaseConfig.java => ServiceConfig.java} (62%) diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java new file mode 100644 index 00000000..afafc7d7 --- /dev/null +++ b/src/main/java/org/asamk/signal/BaseConfig.java @@ -0,0 +1,12 @@ +package org.asamk.signal; + +public class BaseConfig { + + public final static String PROJECT_NAME = BaseConfig.class.getPackage().getImplementationTitle(); + public final static String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); + + final static String USER_AGENT = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + " " + PROJECT_VERSION; + + private BaseConfig() { + } +} diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 38c7a68d..b2defd81 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -31,8 +31,8 @@ import org.asamk.signal.commands.Commands; import org.asamk.signal.commands.DbusCommand; import org.asamk.signal.commands.ExtendedDbusCommand; import org.asamk.signal.commands.LocalCommand; -import org.asamk.signal.manager.BaseConfig; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.ServiceConfig; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.SecurityProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -102,7 +102,7 @@ public class Main { dataPath = getDefaultDataPath(); } - m = new Manager(username, dataPath); + m = new Manager(username, dataPath, ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT), BaseConfig.USER_AGENT); ts = m; try { m.init(); diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index f76642c9..ab4ac6cb 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -109,6 +109,7 @@ import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; @@ -128,13 +129,11 @@ import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; -import java.util.Deque; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -155,6 +154,8 @@ public class Manager implements Signal { private final String attachmentsPath; private final String avatarsPath; private final SleepTimer timer = new UptimeSleepTimer(); + private final SignalServiceConfiguration serviceConfiguration; + private final String userAgent; private SignalAccount account; private String username; @@ -162,13 +163,14 @@ public class Manager implements Signal { private SignalServiceMessagePipe messagePipe = null; private SignalServiceMessagePipe unidentifiedMessagePipe = null; - public Manager(String username, String settingsPath) { + public Manager(String username, String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) { this.username = username; this.settingsPath = settingsPath; this.dataPath = this.settingsPath + "/data"; this.attachmentsPath = this.settingsPath + "/attachments"; this.avatarsPath = this.settingsPath + "/avatars"; - + this.serviceConfiguration = serviceConfiguration; + this.userAgent = userAgent; } public String getUsername() { @@ -180,7 +182,7 @@ public class Manager implements Signal { } private SignalServiceAccountManager getSignalServiceAccountManager() { - return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer); + return new SignalServiceAccountManager(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), userAgent, timer); } private IdentityKey getIdentity() { @@ -224,7 +226,7 @@ public class Manager implements Signal { accountManager = getSignalServiceAccountManager(); if (account.isRegistered()) { - if (accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) { + if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { refreshPreKeys(); account.save(); } @@ -298,7 +300,7 @@ public class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, BaseConfig.capabilities); + accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities); } public void setProfileName(String name) throws IOException { @@ -401,10 +403,10 @@ public class Manager implements Signal { } private List generatePreKeys() { - List records = new ArrayList<>(BaseConfig.PREKEY_BATCH_SIZE); + List records = new ArrayList<>(ServiceConfig.PREKEY_BATCH_SIZE); final int offset = account.getPreKeyIdOffset(); - for (int i = 0; i < BaseConfig.PREKEY_BATCH_SIZE; i++) { + for (int i = 0; i < ServiceConfig.PREKEY_BATCH_SIZE; i++) { int preKeyId = (offset + i) % Medium.MAX_VALUE; ECKeyPair keyPair = Curve.generateKeyPair(); PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair); @@ -437,7 +439,7 @@ public class Manager implements Signal { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); // TODO make unrestricted unidentified access configurable - VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, BaseConfig.capabilities); + VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities); UUID uuid = UuidUtil.parseOrNull(response.getUuid()); // TODO response.isStorageCapable() @@ -473,15 +475,15 @@ public class Manager implements Signal { private SignalServiceMessageReceiver getMessageReceiver() { // TODO implement ZkGroup support final ClientZkProfileOperations clientZkProfileOperations = null; - return new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer, clientZkProfileOperations); + return new SignalServiceMessageReceiver(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalingKey(), userAgent, null, timer, clientZkProfileOperations); } private SignalServiceMessageSender getMessageSender() { // TODO implement ZkGroup support final ClientZkProfileOperations clientZkProfileOperations = null; final boolean attachmentsV3 = false; - return new SignalServiceMessageSender(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), - account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), attachmentsV3, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations); + return new SignalServiceMessageSender(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), + account.getDeviceId(), account.getSignalProtocolStore(), userAgent, account.isMultiDevice(), attachmentsV3, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations); } private SignalServiceProfile getRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess) throws IOException { @@ -1801,7 +1803,7 @@ public class Manager implements Signal { final SignalServiceMessageReceiver messageReceiver = getMessageReceiver(); File tmpFile = IOUtils.createTempFile(); - try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE)) { + try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE)) { try (OutputStream output = new FileOutputStream(outputFile)) { byte[] buffer = new byte[4096]; int read; @@ -1825,7 +1827,7 @@ public class Manager implements Signal { private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException, MissingConfigurationException { final SignalServiceMessageReceiver messageReceiver = getMessageReceiver(); - return messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE); + return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); } @Override diff --git a/src/main/java/org/asamk/signal/manager/BaseConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java similarity index 62% rename from src/main/java/org/asamk/signal/manager/BaseConfig.java rename to src/main/java/org/asamk/signal/manager/ServiceConfig.java index ade0288d..f1323087 100644 --- a/src/main/java/org/asamk/signal/manager/BaseConfig.java +++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java @@ -18,12 +18,8 @@ import java.util.Map; import okhttp3.Dns; import okhttp3.Interceptor; -public class BaseConfig { +public class ServiceConfig { - public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); - public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); - - final static String USER_AGENT = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + " " + PROJECT_VERSION; final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; final static int PREKEY_MINIMUM_COUNT = 20; final static int PREKEY_BATCH_SIZE = 100; @@ -38,29 +34,28 @@ public class BaseConfig { private final static Optional dns = Optional.absent(); - private final static Interceptor userAgentInterceptor = chain -> - chain.proceed(chain.request().newBuilder() - .header("User-Agent", USER_AGENT) - .build()); - - private final static List interceptors = Collections.singletonList(userAgentInterceptor); - private final static byte[] zkGroupServerPublicParams = new byte[]{}; - final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration( - new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, - makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}), - new SignalContactDiscoveryUrl[0], - new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, - new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, - interceptors, - dns, - zkGroupServerPublicParams - ); - static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false, false); - private BaseConfig() { + public static SignalServiceConfiguration createDefaultServiceConfiguration(String userAgent) { + final Interceptor userAgentInterceptor = chain -> + chain.proceed(chain.request().newBuilder() + .header("User-Agent", userAgent) + .build()); + + final List interceptors = Collections.singletonList(userAgentInterceptor); + + return new SignalServiceConfiguration( + new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, + makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}), + new SignalContactDiscoveryUrl[0], + new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, + new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, + interceptors, + dns, + zkGroupServerPublicParams + ); } private static Map makeSignalCdnUrlMapFor(SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls) { @@ -69,4 +64,7 @@ public class BaseConfig { result.put(2, cdn2Urls); return Collections.unmodifiableMap(result); } + + private ServiceConfig() { + } } diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index 28dd7a07..a5b37b05 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -97,7 +97,7 @@ class Utils { static CertificateValidator getCertificateValidator() { try { - ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0); + ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0); return new CertificateValidator(unidentifiedSenderTrustRoot); } catch (InvalidKeyException | IOException e) { throw new AssertionError(e); @@ -246,7 +246,7 @@ class Utils { byte[] ownId; byte[] theirId; - if (BaseConfig.capabilities.isUuid() + if (ServiceConfig.capabilities.isUuid() && ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) { // Version 2: UUID user version = 2; From a02031aa807d45816398955c9667215baf5d06dc Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 11 May 2020 18:07:37 +0200 Subject: [PATCH 055/103] Refactor Manager to always have a valid SignalAccount instance Extract ProvisioningManager to link new devices --- src/main/java/org/asamk/signal/Main.java | 34 ++-- .../asamk/signal/commands/LinkCommand.java | 10 +- .../signal/commands/ProvisioningCommand.java | 10 + .../asamk/signal/commands/VerifyCommand.java | 4 - .../org/asamk/signal/manager/Manager.java | 185 +++++++----------- .../org/asamk/signal/manager/PathConfig.java | 34 ++++ .../signal/manager/ProvisioningManager.java | 102 ++++++++++ .../asamk/signal/storage/SignalAccount.java | 9 - 8 files changed, 239 insertions(+), 149 deletions(-) create mode 100644 src/main/java/org/asamk/signal/commands/ProvisioningCommand.java create mode 100644 src/main/java/org/asamk/signal/manager/PathConfig.java create mode 100644 src/main/java/org/asamk/signal/manager/ProvisioningManager.java diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index b2defd81..0958b9a8 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -31,7 +31,9 @@ import org.asamk.signal.commands.Commands; import org.asamk.signal.commands.DbusCommand; import org.asamk.signal.commands.ExtendedDbusCommand; import org.asamk.signal.commands.LocalCommand; +import org.asamk.signal.commands.ProvisioningCommand; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.ProvisioningManager; import org.asamk.signal.manager.ServiceConfig; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.SecurityProvider; @@ -69,13 +71,13 @@ public class Main { private static int handleCommands(Namespace ns) { final String username = ns.getString("username"); - Manager m; + Manager m = null; + ProvisioningManager pm = null; Signal ts; DBusConnection dBusConn = null; try { if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { try { - m = null; DBusConnection.DBusBusType busType; if (ns.getBoolean("dbus_system")) { busType = DBusConnection.DBusBusType.SYSTEM; @@ -102,19 +104,23 @@ public class Main { dataPath = getDefaultDataPath(); } - m = new Manager(username, dataPath, ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT), BaseConfig.USER_AGENT); - ts = m; - try { - m.init(); - } catch (AuthorizationFailedException e) { - if (!"register".equals(ns.getString("command"))) { - // Register command should still be possible, if current authorization fails - System.err.println("Authorization failed, was the number registered elsewhere?"); + if (username == null) { + pm = new ProvisioningManager(dataPath, ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT), BaseConfig.USER_AGENT); + ts = null; + } else { + try { + m = Manager.init(username, dataPath, ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT), BaseConfig.USER_AGENT); + } catch (AuthorizationFailedException e) { + if (!"register".equals(ns.getString("command"))) { + // Register command should still be possible, if current authorization fails + System.err.println("Authorization failed, was the number registered elsewhere?"); + return 2; + } + } catch (Throwable e) { + System.err.println("Error loading state file: " + e.getMessage()); return 2; } - } catch (Exception e) { - System.err.println("Error loading state file: " + e.getMessage()); - return 2; + ts = m; } } @@ -135,6 +141,8 @@ public class Main { } else { if (command instanceof LocalCommand) { return ((LocalCommand) command).handleCommand(ns, m); + } else if (command instanceof ProvisioningCommand) { + return ((ProvisioningCommand) command).handleCommand(ns, pm); } else if (command instanceof DbusCommand) { return ((DbusCommand) command).handleCommand(ns, ts); } else { diff --git a/src/main/java/org/asamk/signal/commands/LinkCommand.java b/src/main/java/org/asamk/signal/commands/LinkCommand.java index 2a2d4c4b..917b674b 100644 --- a/src/main/java/org/asamk/signal/commands/LinkCommand.java +++ b/src/main/java/org/asamk/signal/commands/LinkCommand.java @@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.UserAlreadyExists; -import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.ProvisioningManager; import org.whispersystems.libsignal.InvalidKeyException; import java.io.IOException; @@ -12,7 +12,7 @@ import java.util.concurrent.TimeoutException; import static org.asamk.signal.util.ErrorUtils.handleAssertionError; -public class LinkCommand implements LocalCommand { +public class LinkCommand implements ProvisioningCommand { @Override public void attachToSubparser(final Subparser subparser) { @@ -21,15 +21,15 @@ public class LinkCommand implements LocalCommand { } @Override - public int handleCommand(final Namespace ns, final Manager m) { + public int handleCommand(final Namespace ns, final ProvisioningManager m) { String deviceName = ns.getString("name"); if (deviceName == null) { deviceName = "cli"; } try { System.out.println(m.getDeviceLinkUri()); - m.finishDeviceLink(deviceName); - System.out.println("Associated with: " + m.getUsername()); + String username = m.finishDeviceLink(deviceName); + System.out.println("Associated with: " + username); } catch (TimeoutException e) { System.err.println("Link request timed out, please try again."); return 3; diff --git a/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java b/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java new file mode 100644 index 00000000..12a612ff --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java @@ -0,0 +1,10 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; + +import org.asamk.signal.manager.ProvisioningManager; + +public interface ProvisioningCommand extends Command { + + int handleCommand(Namespace ns, ProvisioningManager m); +} diff --git a/src/main/java/org/asamk/signal/commands/VerifyCommand.java b/src/main/java/org/asamk/signal/commands/VerifyCommand.java index 9d99c0d2..0f336325 100644 --- a/src/main/java/org/asamk/signal/commands/VerifyCommand.java +++ b/src/main/java/org/asamk/signal/commands/VerifyCommand.java @@ -20,10 +20,6 @@ public class VerifyCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.userHasKeys()) { - System.err.println("User has no keys, first call register."); - return 1; - } if (m.isRegistered()) { System.err.println("User registration is already verified"); return 1; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index ab4ac6cb..aa5c2dba 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -24,7 +24,6 @@ import org.asamk.signal.GroupNotFoundException; import org.asamk.signal.NotAGroupMemberException; import org.asamk.signal.StickerPackInvalidException; import org.asamk.signal.TrustLevel; -import org.asamk.signal.UserAlreadyExists; import org.asamk.signal.storage.SignalAccount; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; @@ -149,44 +148,40 @@ import java.util.zip.ZipFile; public class Manager implements Signal { - private final String settingsPath; - private final String dataPath; - private final String attachmentsPath; - private final String avatarsPath; private final SleepTimer timer = new UptimeSleepTimer(); private final SignalServiceConfiguration serviceConfiguration; private final String userAgent; - private SignalAccount account; - private String username; + private final SignalAccount account; + private final PathConfig pathConfig; private SignalServiceAccountManager accountManager; private SignalServiceMessagePipe messagePipe = null; private SignalServiceMessagePipe unidentifiedMessagePipe = null; - public Manager(String username, String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) { - this.username = username; - this.settingsPath = settingsPath; - this.dataPath = this.settingsPath + "/data"; - this.attachmentsPath = this.settingsPath + "/attachments"; - this.avatarsPath = this.settingsPath + "/avatars"; + public Manager(SignalAccount account, PathConfig pathConfig, SignalServiceConfiguration serviceConfiguration, String userAgent) { + this.account = account; + this.pathConfig = pathConfig; this.serviceConfiguration = serviceConfiguration; this.userAgent = userAgent; + this.accountManager = createSignalServiceAccountManager(); + + this.account.setResolver(this::resolveSignalServiceAddress); } public String getUsername() { - return username; + return account.getUsername(); } public SignalServiceAddress getSelfAddress() { return account.getSelfAddress(); } - private SignalServiceAccountManager getSignalServiceAccountManager() { + private SignalServiceAccountManager createSignalServiceAccountManager() { return new SignalServiceAccountManager(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), userAgent, timer); } - private IdentityKey getIdentity() { - return account.getSignalProtocolStore().getIdentityKeyPair().getPublicKey(); + private IdentityKeyPair getIdentityKeyPair() { + return account.getSignalProtocolStore().getIdentityKeyPair(); } public int getDeviceId() { @@ -194,7 +189,7 @@ public class Manager implements Signal { } private String getMessageCachePath() { - return this.dataPath + "/" + username + ".d/msg-cache"; + return pathConfig.getDataPath() + "/" + account.getUsername() + ".d/msg-cache"; } private String getMessageCachePath(String sender) { @@ -211,30 +206,28 @@ public class Manager implements Signal { return new File(cachePath + "/" + now + "_" + timestamp); } - public boolean userHasKeys() { - return account != null && account.getSignalProtocolStore() != null; - } + public static Manager init(String username, String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) throws IOException { + PathConfig pathConfig = PathConfig.createDefault(settingsPath); - public void init() throws IOException { - if (!SignalAccount.userExists(dataPath, username)) { - return; + if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { + IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair(); + int registrationId = KeyHelper.generateRegistrationId(false); + + ProfileKey profileKey = KeyUtils.createProfileKey(); + SignalAccount account = SignalAccount.create(pathConfig.getDataPath(), username, identityKey, registrationId, profileKey); + account.save(); + + return new Manager(account, pathConfig, serviceConfiguration, userAgent); } - account = SignalAccount.load(dataPath, username); - account.setResolver(this::resolveSignalServiceAddress); - migrateLegacyConfigs(); + SignalAccount account = SignalAccount.load(pathConfig.getDataPath(), username); - accountManager = getSignalServiceAccountManager(); - if (account.isRegistered()) { - if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { - refreshPreKeys(); - account.save(); - } - if (account.getUuid() == null) { - account.setUuid(accountManager.getOwnUuid()); - account.save(); - } - } + Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent); + + m.migrateLegacyConfigs(); + m.checkAccountState(); + + return m; } private void migrateLegacyConfigs() { @@ -246,7 +239,7 @@ public class Manager implements Signal { File attachmentFile = getAttachmentFile(new SignalServiceAttachmentRemoteId(g.getAvatarId())); if (!avatarFile.exists() && attachmentFile.exists()) { try { - IOUtils.createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { // Ignore @@ -263,31 +256,29 @@ public class Manager implements Signal { } } - private void createNewIdentity() throws IOException { - IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair(); - int registrationId = KeyHelper.generateRegistrationId(false); - if (username == null) { - account = SignalAccount.createTemporaryAccount(identityKey, registrationId); - account.setResolver(this::resolveSignalServiceAddress); - } else { - ProfileKey profileKey = KeyUtils.createProfileKey(); - account = SignalAccount.create(dataPath, username, identityKey, registrationId, profileKey); - account.setResolver(this::resolveSignalServiceAddress); - account.save(); + private void checkAccountState() throws IOException { + if (account.isRegistered()) { + if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { + refreshPreKeys(); + account.save(); + } + if (account.getUuid() == null) { + account.setUuid(accountManager.getOwnUuid()); + account.save(); + } } } public boolean isRegistered() { - return account != null && account.isRegistered(); + return account.isRegistered(); } public void register(boolean voiceVerification) throws IOException { - if (account == null) { - createNewIdentity(); - } account.setPassword(KeyUtils.createPassword()); + + // Resetting UUID, because registering doesn't work otherwise account.setUuid(null); - accountManager = getSignalServiceAccountManager(); + accountManager = createSignalServiceAccountManager(); if (voiceVerification) { accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.absent(), Optional.absent()); @@ -327,52 +318,6 @@ public class Manager implements Signal { account.save(); } - public String getDeviceLinkUri() throws TimeoutException, IOException { - if (account == null) { - createNewIdentity(); - } - account.setPassword(KeyUtils.createPassword()); - accountManager = getSignalServiceAccountManager(); - String uuid = accountManager.getNewDeviceUuid(); - - return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(uuid, getIdentity().getPublicKey())); - } - - public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { - account.setSignalingKey(KeyUtils.createSignalingKey()); - SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(account.getSignalProtocolStore().getIdentityKeyPair(), account.getSignalingKey(), false, true, account.getSignalProtocolStore().getLocalRegistrationId(), deviceName); - - username = ret.getNumber(); - // TODO do this check before actually registering - if (SignalAccount.userExists(dataPath, username)) { - throw new UserAlreadyExists(username, SignalAccount.getFileName(dataPath, username)); - } - - // Create new account with the synced identity - byte[] profileKeyBytes = ret.getProfileKey(); - ProfileKey profileKey; - if (profileKeyBytes == null) { - profileKey = KeyUtils.createProfileKey(); - } else { - try { - profileKey = new ProfileKey(profileKeyBytes); - } catch (InvalidInputException e) { - throw new IOException("Received invalid profileKey", e); - } - } - account = SignalAccount.createLinkedAccount(dataPath, username, ret.getUuid(), account.getPassword(), ret.getDeviceId(), ret.getIdentity(), account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalingKey(), profileKey); - account.setResolver(this::resolveSignalServiceAddress); - - refreshPreKeys(); - - requestSyncGroups(); - requestSyncContacts(); - requestSyncBlocked(); - requestSyncConfiguration(); - - account.save(); - } - public List getLinkedDevices() throws IOException { List devices = accountManager.getDevices(); account.setMultiDevice(devices.size() > 1); @@ -394,7 +339,7 @@ public class Manager implements Signal { } private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { - IdentityKeyPair identityKeyPair = account.getSignalProtocolStore().getIdentityKeyPair(); + IdentityKeyPair identityKeyPair = getIdentityKeyPair(); String verificationCode = accountManager.getNewDeviceVerificationCode(); accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey().serialize()), verificationCode); @@ -447,7 +392,7 @@ public class Manager implements Signal { account.setRegistered(true); account.setUuid(uuid); account.setRegistrationLockPin(pin); - account.getSignalProtocolStore().saveIdentity(account.getSelfAddress(), account.getSignalProtocolStore().getIdentityKeyPair().getPublicKey(), TrustLevel.TRUSTED_VERIFIED); + account.getSignalProtocolStore().saveIdentity(account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), TrustLevel.TRUSTED_VERIFIED); refreshPreKeys(); account.save(); @@ -464,12 +409,12 @@ public class Manager implements Signal { account.save(); } - private void refreshPreKeys() throws IOException { + void refreshPreKeys() throws IOException { List oneTimePreKeys = generatePreKeys(); - final IdentityKeyPair identityKeyPair = account.getSignalProtocolStore().getIdentityKeyPair(); + final IdentityKeyPair identityKeyPair = getIdentityKeyPair(); SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(identityKeyPair); - accountManager.setPreKeys(getIdentity(), signedPreKeyRecord, oneTimePreKeys); + accountManager.setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys); } private SignalServiceMessageReceiver getMessageReceiver() { @@ -629,7 +574,7 @@ public class Manager implements Signal { } if (avatarFile != null) { - IOUtils.createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); File aFile = getGroupAvatarFile(g.groupId); Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } @@ -984,7 +929,7 @@ public class Manager implements Signal { } } - private void requestSyncGroups() throws IOException { + void requestSyncGroups() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { @@ -994,7 +939,7 @@ public class Manager implements Signal { } } - private void requestSyncContacts() throws IOException { + void requestSyncContacts() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { @@ -1004,7 +949,7 @@ public class Manager implements Signal { } } - private void requestSyncBlocked() throws IOException { + void requestSyncBlocked() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { @@ -1014,7 +959,7 @@ public class Manager implements Signal { } } - private void requestSyncConfiguration() throws IOException { + void requestSyncConfiguration() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { @@ -1750,11 +1695,11 @@ public class Manager implements Signal { } private File getContactAvatarFile(String number) { - return new File(avatarsPath, "contact-" + number); + return new File(pathConfig.getAvatarsPath(), "contact-" + number); } private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException, MissingConfigurationException { - IOUtils.createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); return retrieveAttachment(pointer, getContactAvatarFile(number), false); @@ -1765,11 +1710,11 @@ public class Manager implements Signal { } private File getGroupAvatarFile(byte[] groupId) { - return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_")); + return new File(pathConfig.getAvatarsPath(), "group-" + Base64.encodeBytes(groupId).replace("/", "_")); } private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException, MissingConfigurationException { - IOUtils.createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false); @@ -1780,11 +1725,11 @@ public class Manager implements Signal { } public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { - return new File(attachmentsPath, attachmentId.toString()); + return new File(pathConfig.getAttachmentsPath(), attachmentId.toString()); } private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException, MissingConfigurationException { - IOUtils.createPrivateDirectories(attachmentsPath); + IOUtils.createPrivateDirectories(pathConfig.getAttachmentsPath()); return retrieveAttachment(pointer, getAttachmentFile(pointer.getRemoteId()), true); } @@ -2054,7 +1999,11 @@ public class Manager implements Signal { } public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - return Utils.computeSafetyNumber(account.getSelfAddress(), getIdentity(), theirAddress, theirIdentityKey); + return Utils.computeSafetyNumber(account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey); + } + + void saveAccount() { + account.save(); } public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException { diff --git a/src/main/java/org/asamk/signal/manager/PathConfig.java b/src/main/java/org/asamk/signal/manager/PathConfig.java new file mode 100644 index 00000000..2c2d938a --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/PathConfig.java @@ -0,0 +1,34 @@ +package org.asamk.signal.manager; + +public class PathConfig { + + private final String dataPath; + private final String attachmentsPath; + private final String avatarsPath; + + public static PathConfig createDefault(final String settingsPath) { + return new PathConfig( + settingsPath + "/data", + settingsPath + "/attachments", + settingsPath + "/avatars" + ); + } + + private PathConfig(final String dataPath, final String attachmentsPath, final String avatarsPath) { + this.dataPath = dataPath; + this.attachmentsPath = attachmentsPath; + this.avatarsPath = avatarsPath; + } + + public String getDataPath() { + return dataPath; + } + + public String getAttachmentsPath() { + return attachmentsPath; + } + + public String getAvatarsPath() { + return avatarsPath; + } +} diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java new file mode 100644 index 00000000..61d3315f --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -0,0 +1,102 @@ +/* + Copyright (C) 2015-2020 AsamK and contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package org.asamk.signal.manager; + +import org.asamk.signal.UserAlreadyExists; +import org.asamk.signal.storage.SignalAccount; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.util.KeyHelper; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.SleepTimer; +import org.whispersystems.signalservice.api.util.UptimeSleepTimer; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +public class ProvisioningManager { + + private final PathConfig pathConfig; + private final SignalServiceConfiguration serviceConfiguration; + private final String userAgent; + + private final SignalServiceAccountManager accountManager; + private final IdentityKeyPair identityKey; + private final int registrationId; + private final String password; + + public ProvisioningManager(String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) { + this.pathConfig = PathConfig.createDefault(settingsPath); + this.serviceConfiguration = serviceConfiguration; + this.userAgent = userAgent; + + identityKey = KeyHelper.generateIdentityKeyPair(); + registrationId = KeyHelper.generateRegistrationId(false); + password = KeyUtils.createPassword(); + final SleepTimer timer = new UptimeSleepTimer(); + accountManager = new SignalServiceAccountManager(serviceConfiguration, null, null, password, SignalServiceAddress.DEFAULT_DEVICE_ID, userAgent, timer); + } + + public String getDeviceLinkUri() throws TimeoutException, IOException { + String deviceUuid = accountManager.getNewDeviceUuid(); + + return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(deviceUuid, identityKey.getPublicKey().getPublicKey())); + } + + public String finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { + String signalingKey = KeyUtils.createSignalingKey(); + SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(identityKey, signalingKey, false, true, registrationId, deviceName); + + String username = ret.getNumber(); + // TODO do this check before actually registering + if (SignalAccount.userExists(pathConfig.getDataPath(), username)) { + throw new UserAlreadyExists(username, SignalAccount.getFileName(pathConfig.getDataPath(), username)); + } + + // Create new account with the synced identity + byte[] profileKeyBytes = ret.getProfileKey(); + ProfileKey profileKey; + if (profileKeyBytes == null) { + profileKey = KeyUtils.createProfileKey(); + } else { + try { + profileKey = new ProfileKey(profileKeyBytes); + } catch (InvalidInputException e) { + throw new IOException("Received invalid profileKey", e); + } + } + SignalAccount account = SignalAccount.createLinkedAccount(pathConfig.getDataPath(), username, ret.getUuid(), password, ret.getDeviceId(), ret.getIdentity(), registrationId, signalingKey, profileKey); + account.save(); + + Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent); + + m.refreshPreKeys(); + + m.requestSyncGroups(); + m.requestSyncContacts(); + m.requestSyncBlocked(); + m.requestSyncConfiguration(); + + m.saveAccount(); + + return username; + } +} diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index f03bac3a..16ad2122 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -121,15 +121,6 @@ public class SignalAccount { return account; } - public static SignalAccount createTemporaryAccount(IdentityKeyPair identityKey, int registrationId) { - SignalAccount account = new SignalAccount(); - - account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); - account.registered = false; - - return account; - } - public static String getFileName(String dataPath, String username) { return dataPath + "/" + username; } From 87f65de0c56735397fd6f68e77d6fe5cc915675d Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 11 May 2020 18:31:22 +0200 Subject: [PATCH 056/103] Save account state after ending session even if sending the message has failed --- src/main/java/org/asamk/signal/manager/Manager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index aa5c2dba..1340cd0a 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -699,6 +699,7 @@ public class Manager implements Signal { for (SignalServiceAddress address : signalServiceAddresses) { handleEndSession(address); } + account.save(); throw e; } } From d520023fc76a522650b7561f2a4fc7a95fb5a04d Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 13 May 2020 23:33:40 +0200 Subject: [PATCH 057/103] Refactor Manager and SignalAccount to implement Closeable Should make sure that file lock and web socket connections are closed reliably. --- src/main/java/org/asamk/signal/Main.java | 183 ++++++++++-------- .../org/asamk/signal/manager/Manager.java | 118 +++++------ .../signal/manager/ProvisioningManager.java | 21 +- .../asamk/signal/storage/SignalAccount.java | 66 ++++--- 4 files changed, 224 insertions(+), 164 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 0958b9a8..16d9e0dc 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -44,6 +44,7 @@ import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedE import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.File; +import java.io.IOException; import java.security.Security; import java.util.Map; @@ -71,94 +72,122 @@ public class Main { private static int handleCommands(Namespace ns) { final String username = ns.getString("username"); - Manager m = null; - ProvisioningManager pm = null; - Signal ts; - DBusConnection dBusConn = null; - try { - if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { - try { - DBusConnection.DBusBusType busType; - if (ns.getBoolean("dbus_system")) { - busType = DBusConnection.DBusBusType.SYSTEM; - } else { - busType = DBusConnection.DBusBusType.SESSION; - } - dBusConn = DBusConnection.getConnection(busType); - ts = dBusConn.getRemoteObject( + + if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { + try { + DBusConnection.DBusBusType busType; + if (ns.getBoolean("dbus_system")) { + busType = DBusConnection.DBusBusType.SYSTEM; + } else { + busType = DBusConnection.DBusBusType.SESSION; + } + try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) { + Signal ts = dBusConn.getRemoteObject( DbusConfig.SIGNAL_BUSNAME, DbusConfig.SIGNAL_OBJECTPATH, Signal.class); - } catch (UnsatisfiedLinkError e) { - System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); - return 1; - } catch (DBusException e) { - e.printStackTrace(); - if (dBusConn != null) { - dBusConn.disconnect(); - } - return 3; - } - } else { - String dataPath = ns.getString("config"); - if (isEmpty(dataPath)) { - dataPath = getDefaultDataPath(); - } - if (username == null) { - pm = new ProvisioningManager(dataPath, ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT), BaseConfig.USER_AGENT); - ts = null; - } else { - try { - m = Manager.init(username, dataPath, ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT), BaseConfig.USER_AGENT); - } catch (AuthorizationFailedException e) { - if (!"register".equals(ns.getString("command"))) { - // Register command should still be possible, if current authorization fails - System.err.println("Authorization failed, was the number registered elsewhere?"); - return 2; - } - } catch (Throwable e) { - System.err.println("Error loading state file: " + e.getMessage()); + return handleCommands(ns, ts, dBusConn); + } + } catch (UnsatisfiedLinkError e) { + System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); + return 1; + } catch (DBusException | IOException e) { + e.printStackTrace(); + return 3; + } + } else { + String dataPath = ns.getString("config"); + if (isEmpty(dataPath)) { + dataPath = getDefaultDataPath(); + } + + if (username == null) { + ProvisioningManager pm = new ProvisioningManager(dataPath, ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT), BaseConfig.USER_AGENT); + return handleCommands(ns, pm); + } + + Manager manager; + try { + manager = Manager.init(username, dataPath, ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT), BaseConfig.USER_AGENT); + } catch (Throwable e) { + System.err.println("Error loading state file: " + e.getMessage()); + return 2; + } + + try (Manager m = manager) { + try { + m.checkAccountState(); + } catch (AuthorizationFailedException e) { + if (!"register".equals(ns.getString("command"))) { + // Register command should still be possible, if current authorization fails + System.err.println("Authorization failed, was the number registered elsewhere?"); return 2; } - ts = m; + } catch (IOException e) { + System.err.println("Error while checking account: " + e.getMessage()); + return 2; } - } - String commandKey = ns.getString("command"); - final Map commands = Commands.getCommands(); - if (commands.containsKey(commandKey)) { - Command command = commands.get(commandKey); - - if (dBusConn != null) { - if (command instanceof ExtendedDbusCommand) { - return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn); - } else if (command instanceof DbusCommand) { - return ((DbusCommand) command).handleCommand(ns, ts); - } else { - System.err.println(commandKey + " is not yet implemented via dbus"); - return 1; - } - } else { - if (command instanceof LocalCommand) { - return ((LocalCommand) command).handleCommand(ns, m); - } else if (command instanceof ProvisioningCommand) { - return ((ProvisioningCommand) command).handleCommand(ns, pm); - } else if (command instanceof DbusCommand) { - return ((DbusCommand) command).handleCommand(ns, ts); - } else { - System.err.println(commandKey + " is only works via dbus"); - return 1; - } - } - } - return 0; - } finally { - if (dBusConn != null) { - dBusConn.disconnect(); + return handleCommands(ns, m); + } catch (IOException e) { + e.printStackTrace(); + return 3; } } } + private static int handleCommands(Namespace ns, Signal ts, DBusConnection dBusConn) { + String commandKey = ns.getString("command"); + final Map commands = Commands.getCommands(); + if (commands.containsKey(commandKey)) { + Command command = commands.get(commandKey); + + if (command instanceof ExtendedDbusCommand) { + return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn); + } else if (command instanceof DbusCommand) { + return ((DbusCommand) command).handleCommand(ns, ts); + } else { + System.err.println(commandKey + " is not yet implemented via dbus"); + return 1; + } + } + return 0; + } + + private static int handleCommands(Namespace ns, ProvisioningManager pm) { + String commandKey = ns.getString("command"); + final Map commands = Commands.getCommands(); + if (commands.containsKey(commandKey)) { + Command command = commands.get(commandKey); + + if (command instanceof ProvisioningCommand) { + return ((ProvisioningCommand) command).handleCommand(ns, pm); + } else { + System.err.println(commandKey + " only works with a username"); + return 1; + } + } + return 0; + } + + private static int handleCommands(Namespace ns, Manager m) { + String commandKey = ns.getString("command"); + final Map commands = Commands.getCommands(); + if (commands.containsKey(commandKey)) { + Command command = commands.get(commandKey); + + if (command instanceof LocalCommand) { + return ((LocalCommand) command).handleCommand(ns, m); + } else if (command instanceof DbusCommand) { + return ((DbusCommand) command).handleCommand(ns, m); + } else if (command instanceof ExtendedDbusCommand) { + System.err.println(commandKey + " only works via dbus"); + } + return 1; + } + return 0; + } + /** * Uses $XDG_DATA_HOME/signal-cli if it exists, or if none of the legacy directories exist: * - $HOME/.config/signal diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 1340cd0a..7d15b8a1 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -115,6 +115,7 @@ import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.util.Base64; +import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -146,7 +147,7 @@ import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -public class Manager implements Signal { +public class Manager implements Signal, Closeable { private final SleepTimer timer = new UptimeSleepTimer(); private final SignalServiceConfiguration serviceConfiguration; @@ -225,7 +226,6 @@ public class Manager implements Signal { Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent); m.migrateLegacyConfigs(); - m.checkAccountState(); return m; } @@ -256,7 +256,7 @@ public class Manager implements Signal { } } - private void checkAccountState() throws IOException { + public void checkAccountState() throws IOException { if (account.isRegistered()) { if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { refreshPreKeys(); @@ -1422,63 +1422,56 @@ public class Manager implements Signal { retryFailedReceivedMessages(handler, ignoreAttachments); final SignalServiceMessageReceiver messageReceiver = getMessageReceiver(); - try { - if (messagePipe == null) { - messagePipe = messageReceiver.createMessagePipe(); - } + if (messagePipe == null) { + messagePipe = messageReceiver.createMessagePipe(); + } - while (true) { - SignalServiceEnvelope envelope; - SignalServiceContent content = null; - Exception exception = null; - final long now = new Date().getTime(); - try { - envelope = messagePipe.read(timeout, unit, envelope1 -> { - // store message on disk, before acknowledging receipt to the server - try { - String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : ""; - File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp()); - Utils.storeEnvelope(envelope1, cacheFile); - } catch (IOException e) { - System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage()); - } - }); - } catch (TimeoutException e) { - if (returnOnTimeout) - return; - continue; - } catch (InvalidVersionException e) { - System.err.println("Ignoring error: " + e.getMessage()); - continue; - } - if (!envelope.isReceipt()) { + while (true) { + SignalServiceEnvelope envelope; + SignalServiceContent content = null; + Exception exception = null; + final long now = new Date().getTime(); + try { + envelope = messagePipe.read(timeout, unit, envelope1 -> { + // store message on disk, before acknowledging receipt to the server try { - content = decryptMessage(envelope); - } catch (Exception e) { - exception = e; - } - handleMessage(envelope, content, ignoreAttachments); - } - account.save(); - if (!isMessageBlocked(envelope, content)) { - handler.handleMessage(envelope, content, exception); - } - if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) { - File cacheFile = null; - try { - cacheFile = getMessageCacheFile(envelope.getSourceE164().get(), now, envelope.getTimestamp()); - Files.delete(cacheFile.toPath()); - // Try to delete directory if empty - new File(getMessageCachePath()).delete(); + String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : ""; + File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp()); + Utils.storeEnvelope(envelope1, cacheFile); } catch (IOException e) { - System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage()); + System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage()); } - } + }); + } catch (TimeoutException e) { + if (returnOnTimeout) + return; + continue; + } catch (InvalidVersionException e) { + System.err.println("Ignoring error: " + e.getMessage()); + continue; } - } finally { - if (messagePipe != null) { - messagePipe.shutdown(); - messagePipe = null; + if (!envelope.isReceipt()) { + try { + content = decryptMessage(envelope); + } catch (Exception e) { + exception = e; + } + handleMessage(envelope, content, ignoreAttachments); + } + account.save(); + if (!isMessageBlocked(envelope, content)) { + handler.handleMessage(envelope, content, exception); + } + if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) { + File cacheFile = null; + try { + cacheFile = getMessageCacheFile(envelope.getSourceE164().get(), now, envelope.getTimestamp()); + Files.delete(cacheFile.toPath()); + // Try to delete directory if empty + new File(getMessageCachePath()).delete(); + } catch (IOException e) { + System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage()); + } } } } @@ -2026,6 +2019,21 @@ public class Manager implements Signal { return account.getRecipientStore().resolveServiceAddress(address); } + @Override + public void close() throws IOException { + if (messagePipe != null) { + messagePipe.shutdown(); + messagePipe = null; + } + + if (unidentifiedMessagePipe != null) { + unidentifiedMessagePipe.shutdown(); + unidentifiedMessagePipe = null; + } + + account.close(); + } + public interface ReceiveMessageHandler { void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e); diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 61d3315f..e7693f21 100644 --- a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -83,19 +83,22 @@ public class ProvisioningManager { throw new IOException("Received invalid profileKey", e); } } - SignalAccount account = SignalAccount.createLinkedAccount(pathConfig.getDataPath(), username, ret.getUuid(), password, ret.getDeviceId(), ret.getIdentity(), registrationId, signalingKey, profileKey); - account.save(); - Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent); + try (SignalAccount account = SignalAccount.createLinkedAccount(pathConfig.getDataPath(), username, ret.getUuid(), password, ret.getDeviceId(), ret.getIdentity(), registrationId, signalingKey, profileKey)) { + account.save(); - m.refreshPreKeys(); + try (Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent)) { - m.requestSyncGroups(); - m.requestSyncContacts(); - m.requestSyncBlocked(); - m.requestSyncConfiguration(); + m.refreshPreKeys(); - m.saveAccount(); + m.requestSyncGroups(); + m.requestSyncContacts(); + m.requestSyncBlocked(); + m.requestSyncConfiguration(); + + m.saveAccount(); + } + } return username; } diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 16ad2122..94935441 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -29,9 +29,11 @@ import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Medium; +import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.util.Base64; +import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; @@ -42,11 +44,11 @@ import java.util.Collection; import java.util.UUID; import java.util.stream.Collectors; -public class SignalAccount { +public class SignalAccount implements Closeable { private final ObjectMapper jsonProcessor = new ObjectMapper(); - private FileChannel fileChannel; - private FileLock lock; + private final FileChannel fileChannel; + private final FileLock lock; private String username; private UUID uuid; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; @@ -65,7 +67,9 @@ public class SignalAccount { private JsonContactsStore contactStore; private RecipientStore recipientStore; - private SignalAccount() { + private SignalAccount(final FileChannel fileChannel, final FileLock lock) { + this.fileChannel = fileChannel; + this.lock = lock; jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); @@ -75,18 +79,28 @@ public class SignalAccount { } public static SignalAccount load(String dataPath, String username) throws IOException { - SignalAccount account = new SignalAccount(); - IOUtils.createPrivateDirectories(dataPath); - account.openFileChannel(getFileName(dataPath, username)); - account.load(); - return account; + final String fileName = getFileName(dataPath, username); + final Pair pair = openFileChannel(fileName); + try { + SignalAccount account = new SignalAccount(pair.first(), pair.second()); + account.load(); + return account; + } catch (Throwable e) { + pair.second().close(); + pair.first().close(); + throw e; + } } public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey) throws IOException { IOUtils.createPrivateDirectories(dataPath); + String fileName = getFileName(dataPath, username); + if (!new File(fileName).exists()) { + IOUtils.createPrivateFile(fileName); + } - SignalAccount account = new SignalAccount(); - account.openFileChannel(getFileName(dataPath, username)); + final Pair pair = openFileChannel(fileName); + SignalAccount account = new SignalAccount(pair.first(), pair.second()); account.username = username; account.profileKey = profileKey; @@ -101,9 +115,13 @@ public class SignalAccount { public static SignalAccount createLinkedAccount(String dataPath, String username, UUID uuid, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, ProfileKey profileKey) throws IOException { IOUtils.createPrivateDirectories(dataPath); + String fileName = getFileName(dataPath, username); + if (!new File(fileName).exists()) { + IOUtils.createPrivateFile(fileName); + } - SignalAccount account = new SignalAccount(); - account.openFileChannel(getFileName(dataPath, username)); + final Pair pair = openFileChannel(fileName); + SignalAccount account = new SignalAccount(pair.first(), pair.second()); account.username = username; account.uuid = uuid; @@ -285,21 +303,15 @@ public class SignalAccount { } } - private void openFileChannel(String fileName) throws IOException { - if (fileChannel != null) { - return; - } - - if (!new File(fileName).exists()) { - IOUtils.createPrivateFile(fileName); - } - fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel(); - lock = fileChannel.tryLock(); + private static Pair openFileChannel(String fileName) throws IOException { + FileChannel fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel(); + FileLock lock = fileChannel.tryLock(); if (lock == null) { System.err.println("Config file is in use by another instance, waiting…"); lock = fileChannel.lock(); System.err.println("Config file lock acquired."); } + return new Pair<>(fileChannel, lock); } public void setResolver(final SignalServiceAddressResolver resolver) { @@ -413,4 +425,12 @@ public class SignalAccount { public void setMultiDevice(final boolean multiDevice) { isMultiDevice = multiDevice; } + + @Override + public void close() throws IOException { + synchronized (fileChannel) { + lock.close(); + fileChannel.close(); + } + } } From 28be77572fd0e0f5857ed1900de849ea3f31c1da Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 14 May 2020 19:14:20 +0200 Subject: [PATCH 058/103] Update dependency --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8e1c985f..2f0b38c1 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } dependencies { - implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_8' + implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_9' implementation 'org.bouncycastle:bcprov-jdk15on:1.65' implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1' implementation 'com.github.hypfvieh:dbus-java:3.2.1' From 063f2fb2944a94e0a84863df5ee001e62217d64c Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 14 May 2020 19:47:35 +0200 Subject: [PATCH 059/103] Update dependencies --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2f0b38c1..19882c3d 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } dependencies { - implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_9' + implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_10' implementation 'org.bouncycastle:bcprov-jdk15on:1.65' implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1' implementation 'com.github.hypfvieh:dbus-java:3.2.1' From 0287272a3b9924f3ba9aeded570a32841d3e1196 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 14 May 2020 23:44:24 +0200 Subject: [PATCH 060/103] Make fields final for DBusSignal classes --- src/main/java/org/asamk/Signal.java | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 654f365f..d56dbc9c 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -41,11 +41,11 @@ public interface Signal extends DBusInterface { class MessageReceived extends DBusSignal { - private long timestamp; - private String sender; - private byte[] groupId; - private String message; - private List attachments; + private final long timestamp; + private final String sender; + private final byte[] groupId; + private final String message; + private final List attachments; public MessageReceived(String objectpath, long timestamp, String sender, byte[] groupId, String message, List attachments) throws DBusException { super(objectpath, timestamp, sender, groupId, message, attachments); @@ -79,8 +79,8 @@ public interface Signal extends DBusInterface { class ReceiptReceived extends DBusSignal { - private long timestamp; - private String sender; + private final long timestamp; + private final String sender; public ReceiptReceived(String objectpath, long timestamp, String sender) throws DBusException { super(objectpath, timestamp, sender); @@ -99,12 +99,12 @@ public interface Signal extends DBusInterface { class SyncMessageReceived extends DBusSignal { - private long timestamp; - private String source; - private String destination; - private byte[] groupId; - private String message; - private List attachments; + private final long timestamp; + private final String source; + private final String destination; + private final byte[] groupId; + private final String message; + private final List attachments; public SyncMessageReceived(String objectpath, long timestamp, String source, String destination, byte[] groupId, String message, List attachments) throws DBusException { super(objectpath, timestamp, source, destination, groupId, message, attachments); From d08508e6efbb0f993a6b7bd633d6406d0575db34 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 15 May 2020 16:55:14 +0200 Subject: [PATCH 061/103] Move Exception classes to corresponding package --- src/main/java/org/asamk/Signal.java | 4 +-- .../asamk/signal/commands/BlockCommand.java | 4 +-- .../asamk/signal/commands/LinkCommand.java | 2 +- .../signal/commands/QuitGroupCommand.java | 6 ++--- .../asamk/signal/commands/SendCommand.java | 8 +++--- .../signal/commands/SendReactionCommand.java | 6 ++--- .../asamk/signal/commands/UnblockCommand.java | 4 +-- .../signal/commands/UpdateGroupCommand.java | 8 +++--- .../commands/UploadStickerPackCommand.java | 2 +- .../AttachmentInvalidException.java | 6 ++--- .../{ => manager}/GroupNotFoundException.java | 5 ++-- .../org/asamk/signal/manager/Manager.java | 25 ++++++++----------- .../NotAGroupMemberException.java | 5 ++-- .../signal/manager/ProvisioningManager.java | 1 - .../StickerPackInvalidException.java | 2 +- .../signal/{ => manager}/TrustLevel.java | 2 +- .../{ => manager}/UserAlreadyExists.java | 2 +- .../java/org/asamk/signal/manager/Utils.java | 1 - .../protocol/JsonIdentityKeyStore.java | 2 +- .../protocol/JsonSignalProtocolStore.java | 2 +- .../org/asamk/signal/util/ErrorUtils.java | 5 ++-- .../{ => util}/GroupIdFormatException.java | 2 +- src/main/java/org/asamk/signal/util/Util.java | 1 - 23 files changed, 46 insertions(+), 59 deletions(-) rename src/main/java/org/asamk/signal/{ => manager}/AttachmentInvalidException.java (57%) rename src/main/java/org/asamk/signal/{ => manager}/GroupNotFoundException.java (51%) rename src/main/java/org/asamk/signal/{ => manager}/NotAGroupMemberException.java (58%) rename src/main/java/org/asamk/signal/{ => manager}/StickerPackInvalidException.java (81%) rename src/main/java/org/asamk/signal/{ => manager}/TrustLevel.java (97%) rename src/main/java/org/asamk/signal/{ => manager}/UserAlreadyExists.java (92%) rename src/main/java/org/asamk/signal/{ => util}/GroupIdFormatException.java (89%) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index d56dbc9c..c1acf13e 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,7 +1,7 @@ package org.asamk; -import org.asamk.signal.AttachmentInvalidException; -import org.asamk.signal.GroupNotFoundException; +import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.GroupNotFoundException; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.messages.DBusSignal; diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 305c5df2..05f5c9ce 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -3,9 +3,9 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; +import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; diff --git a/src/main/java/org/asamk/signal/commands/LinkCommand.java b/src/main/java/org/asamk/signal/commands/LinkCommand.java index 917b674b..45f59082 100644 --- a/src/main/java/org/asamk/signal/commands/LinkCommand.java +++ b/src/main/java/org/asamk/signal/commands/LinkCommand.java @@ -3,8 +3,8 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.UserAlreadyExists; import org.asamk.signal.manager.ProvisioningManager; +import org.asamk.signal.manager.UserAlreadyExists; import org.whispersystems.libsignal.InvalidKeyException; import java.io.IOException; diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 6e53cb2a..6db230f5 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -3,10 +3,10 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; -import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index f431d708..a805a12c 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -5,10 +5,10 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.AttachmentInvalidException; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; -import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.GroupNotFoundException; +import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index eb1327ac..7e748866 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -4,10 +4,10 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; -import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.util.InvalidNumberException; diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index 2fad39a5..a95aa328 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -3,9 +3,9 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; +import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 63f5252e..7251a185 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -4,10 +4,10 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.AttachmentInvalidException; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; -import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.GroupNotFoundException; +import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.util.InvalidNumberException; diff --git a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java index fe25966c..77df2b22 100644 --- a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java +++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java @@ -3,8 +3,8 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.StickerPackInvalidException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.StickerPackInvalidException; import java.io.IOException; diff --git a/src/main/java/org/asamk/signal/AttachmentInvalidException.java b/src/main/java/org/asamk/signal/manager/AttachmentInvalidException.java similarity index 57% rename from src/main/java/org/asamk/signal/AttachmentInvalidException.java rename to src/main/java/org/asamk/signal/manager/AttachmentInvalidException.java index 839c7940..78fba6e0 100644 --- a/src/main/java/org/asamk/signal/AttachmentInvalidException.java +++ b/src/main/java/org/asamk/signal/manager/AttachmentInvalidException.java @@ -1,8 +1,6 @@ -package org.asamk.signal; +package org.asamk.signal.manager; -import org.freedesktop.dbus.exceptions.DBusExecutionException; - -public class AttachmentInvalidException extends DBusExecutionException { +public class AttachmentInvalidException extends Exception { public AttachmentInvalidException(String message) { super(message); diff --git a/src/main/java/org/asamk/signal/GroupNotFoundException.java b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java similarity index 51% rename from src/main/java/org/asamk/signal/GroupNotFoundException.java rename to src/main/java/org/asamk/signal/manager/GroupNotFoundException.java index f09014a0..0c0d6d2d 100644 --- a/src/main/java/org/asamk/signal/GroupNotFoundException.java +++ b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java @@ -1,9 +1,8 @@ -package org.asamk.signal; +package org.asamk.signal.manager; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.util.Base64; -public class GroupNotFoundException extends DBusExecutionException { +public class GroupNotFoundException extends Exception { public GroupNotFoundException(byte[] groupId) { super("Group not found: " + Base64.encodeBytes(groupId)); diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 7d15b8a1..9d79c468 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -19,11 +19,6 @@ package org.asamk.signal.manager; import com.fasterxml.jackson.databind.ObjectMapper; import org.asamk.Signal; -import org.asamk.signal.AttachmentInvalidException; -import org.asamk.signal.GroupNotFoundException; -import org.asamk.signal.NotAGroupMemberException; -import org.asamk.signal.StickerPackInvalidException; -import org.asamk.signal.TrustLevel; import org.asamk.signal.storage.SignalAccount; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; @@ -486,7 +481,7 @@ public class Manager implements Signal, Closeable { @Override public long sendGroupMessage(String messageText, List attachments, byte[] groupId) - throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments)); @@ -507,7 +502,7 @@ public class Manager implements Signal, Closeable { public void sendGroupMessageReaction(String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, byte[] groupId) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { + throws IOException, EncapsulatedExceptions, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withReaction(reaction); @@ -521,7 +516,7 @@ public class Manager implements Signal, Closeable { sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { + public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, NotAGroupMemberException { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupId) .build(); @@ -536,7 +531,7 @@ public class Manager implements Signal, Closeable { sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { GroupInfo g; if (groupId == null) { // Create new group @@ -587,7 +582,7 @@ public class Manager implements Signal, Closeable { return g.groupId; } - private void sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions { + private void sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { if (groupId == null) { return; } @@ -603,7 +598,7 @@ public class Manager implements Signal, Closeable { sendMessageLegacy(messageBuilder, Collections.singleton(recipient)); } - private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) { + private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) throws AttachmentInvalidException { SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) .withId(g.groupId) .withName(g.name) @@ -680,7 +675,7 @@ public class Manager implements Signal, Closeable { public void sendMessageReaction(String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { + throws IOException, EncapsulatedExceptions, InvalidNumberException { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp); final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withReaction(reaction); @@ -791,7 +786,7 @@ public class Manager implements Signal, Closeable { } @Override - public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException { + public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { if (groupId.length == 0) { groupId = null; } @@ -1286,9 +1281,9 @@ public class Manager implements Signal, Closeable { if (group != null) { try { sendUpdateGroupMessage(groupInfo.getGroupId(), source); - } catch (IOException | EncapsulatedExceptions e) { + } catch (IOException | EncapsulatedExceptions | AttachmentInvalidException e) { e.printStackTrace(); - } catch (NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException e) { // We have left this group, so don't send a group update message } } diff --git a/src/main/java/org/asamk/signal/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java similarity index 58% rename from src/main/java/org/asamk/signal/NotAGroupMemberException.java rename to src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java index cfdc7855..8c0e9be0 100644 --- a/src/main/java/org/asamk/signal/NotAGroupMemberException.java +++ b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java @@ -1,9 +1,8 @@ -package org.asamk.signal; +package org.asamk.signal.manager; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.util.Base64; -public class NotAGroupMemberException extends DBusExecutionException { +public class NotAGroupMemberException extends Exception { public NotAGroupMemberException(byte[] groupId, String groupName) { super("User is not a member in group: " + groupName + " (" + Base64.encodeBytes(groupId) + ")"); diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index e7693f21..db6f4238 100644 --- a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -16,7 +16,6 @@ */ package org.asamk.signal.manager; -import org.asamk.signal.UserAlreadyExists; import org.asamk.signal.storage.SignalAccount; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; diff --git a/src/main/java/org/asamk/signal/StickerPackInvalidException.java b/src/main/java/org/asamk/signal/manager/StickerPackInvalidException.java similarity index 81% rename from src/main/java/org/asamk/signal/StickerPackInvalidException.java rename to src/main/java/org/asamk/signal/manager/StickerPackInvalidException.java index 5fea30fe..52869acd 100644 --- a/src/main/java/org/asamk/signal/StickerPackInvalidException.java +++ b/src/main/java/org/asamk/signal/manager/StickerPackInvalidException.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.manager; public class StickerPackInvalidException extends Exception { diff --git a/src/main/java/org/asamk/signal/TrustLevel.java b/src/main/java/org/asamk/signal/manager/TrustLevel.java similarity index 97% rename from src/main/java/org/asamk/signal/TrustLevel.java rename to src/main/java/org/asamk/signal/manager/TrustLevel.java index 5eaf960a..c9fa7a5e 100644 --- a/src/main/java/org/asamk/signal/TrustLevel.java +++ b/src/main/java/org/asamk/signal/manager/TrustLevel.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.manager; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; diff --git a/src/main/java/org/asamk/signal/UserAlreadyExists.java b/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java similarity index 92% rename from src/main/java/org/asamk/signal/UserAlreadyExists.java rename to src/main/java/org/asamk/signal/manager/UserAlreadyExists.java index 28836f28..a07c455b 100644 --- a/src/main/java/org/asamk/signal/UserAlreadyExists.java +++ b/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.manager; public class UserAlreadyExists extends Exception { diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index a5b37b05..894bd58d 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -1,6 +1,5 @@ package org.asamk.signal.manager; -import org.asamk.signal.AttachmentInvalidException; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index ddb69096..fcb71c5e 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -8,7 +8,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; -import org.asamk.signal.TrustLevel; +import org.asamk.signal.manager.TrustLevel; import org.asamk.signal.util.Util; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java index 3dc15cca..9a8802b4 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.asamk.signal.TrustLevel; +import org.asamk.signal.manager.TrustLevel; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyIdException; diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 44d98cd2..53a39205 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -1,8 +1,7 @@ package org.asamk.signal.util; -import org.asamk.signal.GroupIdFormatException; -import org.asamk.signal.GroupNotFoundException; -import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.manager.GroupNotFoundException; +import org.asamk.signal.manager.NotAGroupMemberException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; diff --git a/src/main/java/org/asamk/signal/GroupIdFormatException.java b/src/main/java/org/asamk/signal/util/GroupIdFormatException.java similarity index 89% rename from src/main/java/org/asamk/signal/GroupIdFormatException.java rename to src/main/java/org/asamk/signal/util/GroupIdFormatException.java index 62add535..5a5c4570 100644 --- a/src/main/java/org/asamk/signal/GroupIdFormatException.java +++ b/src/main/java/org/asamk/signal/util/GroupIdFormatException.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.util; import java.io.IOException; diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 847abcc2..d6b467b0 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -2,7 +2,6 @@ package org.asamk.signal.util; import com.fasterxml.jackson.databind.JsonNode; -import org.asamk.signal.GroupIdFormatException; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; From 1e0aa8929d059b0740da496f6f4ac62159e75141 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 15 May 2020 17:38:32 +0200 Subject: [PATCH 062/103] Separate DbusSignal implementation from Manager --- src/main/java/org/asamk/Signal.java | 73 +++++-- src/main/java/org/asamk/signal/Main.java | 3 +- .../asamk/signal/commands/DaemonCommand.java | 3 +- .../asamk/signal/commands/SendCommand.java | 81 +++---- .../signal/commands/UpdateGroupCommand.java | 83 +++---- .../org/asamk/signal/dbus/DbusSignalImpl.java | 205 ++++++++++++++++++ .../org/asamk/signal/manager/Manager.java | 59 +---- .../org/asamk/signal/util/ErrorUtils.java | 7 - 8 files changed, 332 insertions(+), 182 deletions(-) create mode 100644 src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index c1acf13e..a93d6d86 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,33 +1,33 @@ package org.asamk; -import org.asamk.signal.manager.AttachmentInvalidException; -import org.asamk.signal.manager.GroupNotFoundException; import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.messages.DBusSignal; -import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; -import org.whispersystems.signalservice.api.util.InvalidNumberException; -import java.io.IOException; import java.util.List; +/** + * DBus interface for the org.asamk.Signal service. + * Including emitted Signals and returned Errors. + */ public interface Signal extends DBusInterface { - long sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException; + long sendMessage(String message, List attachments, String recipient) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber; - long sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException; + long sendMessage(String message, List attachments, List recipients) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity; - void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException; + void sendEndSessionMessage(List recipients) throws Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity; - long sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; + long sendGroupMessage(String message, List attachments, byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid, Error.UnregisteredUser, Error.UntrustedIdentity; - String getContactName(String number) throws InvalidNumberException; + String getContactName(String number) throws Error.InvalidNumber; - void setContactName(String number, String name) throws InvalidNumberException; + void setContactName(String number, String name) throws Error.InvalidNumber; - void setContactBlocked(String number, boolean blocked) throws InvalidNumberException; + void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber; - void setGroupBlocked(byte[] groupId, boolean blocked) throws GroupNotFoundException; + void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound; List getGroupIds(); @@ -35,7 +35,7 @@ public interface Signal extends DBusInterface { List getGroupMembers(byte[] groupId); - byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException; + byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.UnregisteredUser, Error.UntrustedIdentity; boolean isRegistered(); @@ -140,4 +140,49 @@ public interface Signal extends DBusInterface { return attachments; } } + + interface Error { + + class AttachmentInvalid extends DBusExecutionException { + + public AttachmentInvalid(final String message) { + super(message); + } + } + + class Failure extends DBusExecutionException { + + public Failure(final String message) { + super(message); + } + } + + class GroupNotFound extends DBusExecutionException { + + public GroupNotFound(final String message) { + super(message); + } + } + + class InvalidNumber extends DBusExecutionException { + + public InvalidNumber(final String message) { + super(message); + } + } + + class UnregisteredUser extends DBusExecutionException { + + public UnregisteredUser(final String message) { + super(message); + } + } + + class UntrustedIdentity extends DBusExecutionException { + + public UntrustedIdentity(final String message) { + super(message); + } + } + } } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 16d9e0dc..562267c4 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -32,6 +32,7 @@ import org.asamk.signal.commands.DbusCommand; import org.asamk.signal.commands.ExtendedDbusCommand; import org.asamk.signal.commands.LocalCommand; import org.asamk.signal.commands.ProvisioningCommand; +import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.ProvisioningManager; import org.asamk.signal.manager.ServiceConfig; @@ -179,7 +180,7 @@ public class Main { if (command instanceof LocalCommand) { return ((LocalCommand) command).handleCommand(ns, m); } else if (command instanceof DbusCommand) { - return ((DbusCommand) command).handleCommand(ns, m); + return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m)); } else if (command instanceof ExtendedDbusCommand) { System.err.println(commandKey + " only works via dbus"); } diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 11805b44..2b983851 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -6,6 +6,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.DbusReceiveMessageHandler; import org.asamk.signal.JsonDbusReceiveMessageHandler; +import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; @@ -48,7 +49,7 @@ public class DaemonCommand implements LocalCommand { busType = DBusConnection.DBusBusType.SESSION; } conn = DBusConnection.getConnection(busType); - conn.exportObject(SIGNAL_OBJECTPATH, m); + conn.exportObject(SIGNAL_OBJECTPATH, new DbusSignalImpl(m)); conn.requestBusName(SIGNAL_BUSNAME); } catch (UnsatisfiedLinkError e) { System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index a805a12c..43166b5b 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -5,15 +5,10 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.manager.AttachmentInvalidException; -import org.asamk.signal.manager.GroupNotFoundException; -import org.asamk.signal.manager.NotAGroupMemberException; import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; -import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; import java.nio.charset.Charset; @@ -21,13 +16,7 @@ import java.util.ArrayList; import java.util.List; import static org.asamk.signal.util.ErrorUtils.handleAssertionError; -import static org.asamk.signal.util.ErrorUtils.handleDBusExecutionException; -import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; -import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; -import static org.asamk.signal.util.ErrorUtils.handleIOException; -import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException; -import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class SendCommand implements DbusCommand { @@ -65,20 +54,11 @@ public class SendCommand implements DbusCommand { try { signal.sendEndSessionMessage(ns.getList("recipient")); return 0; - } catch (IOException e) { - handleIOException(e); - return 3; - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - return 3; } catch (AssertionError e) { handleAssertionError(e); return 1; } catch (DBusExecutionException e) { - handleDBusExecutionException(e); - return 1; - } catch (InvalidNumberException e) { - handleInvalidNumberException(e); + System.err.println("Failed to send message: " + e.getMessage()); return 1; } } @@ -94,47 +74,42 @@ public class SendCommand implements DbusCommand { } } + List attachments = ns.getList("attachment"); + if (attachments == null) { + attachments = new ArrayList<>(); + } + try { - List attachments = ns.getList("attachment"); - if (attachments == null) { - attachments = new ArrayList<>(); - } - long timestamp; if (ns.getString("group") != null) { - byte[] groupId = Util.decodeGroupId(ns.getString("group")); - timestamp = signal.sendGroupMessage(messageText, attachments, groupId); - } else { - timestamp = signal.sendMessage(messageText, attachments, ns.getList("recipient")); + byte[] groupId; + try { + groupId = Util.decodeGroupId(ns.getString("group")); + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + return 1; + } + + long timestamp = signal.sendGroupMessage(messageText, attachments, groupId); + System.out.println(timestamp); + return 0; } - System.out.println(timestamp); - return 0; - } catch (IOException e) { - handleIOException(e); - return 3; - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - return 3; } catch (AssertionError e) { handleAssertionError(e); return 1; - } catch (GroupNotFoundException e) { - handleGroupNotFoundException(e); + } catch (DBusExecutionException e) { + System.err.println("Failed to send message: " + e.getMessage()); return 1; - } catch (NotAGroupMemberException e) { - handleNotAGroupMemberException(e); - return 1; - } catch (AttachmentInvalidException e) { - System.err.println("Failed to add attachment: " + e.getMessage()); - System.err.println("Aborting sending."); + } + + try { + long timestamp = signal.sendMessage(messageText, attachments, ns.getList("recipient")); + System.out.println(timestamp); + return 0; + } catch (AssertionError e) { + handleAssertionError(e); return 1; } catch (DBusExecutionException e) { - handleDBusExecutionException(e); - return 1; - } catch (GroupIdFormatException e) { - handleGroupIdFormatException(e); - return 1; - } catch (InvalidNumberException e) { - handleInvalidNumberException(e); + System.err.println("Failed to send message: " + e.getMessage()); return 1; } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 7251a185..925b8c90 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -4,25 +4,16 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.manager.AttachmentInvalidException; -import org.asamk.signal.manager.GroupNotFoundException; -import org.asamk.signal.manager.NotAGroupMemberException; import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; -import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; -import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.util.Base64; -import java.io.IOException; import java.util.ArrayList; import java.util.List; -import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; -import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; -import static org.asamk.signal.util.ErrorUtils.handleIOException; -import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException; -import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class UpdateGroupCommand implements DbusCommand { @@ -46,52 +37,48 @@ public class UpdateGroupCommand implements DbusCommand { return 1; } - try { - byte[] groupId = null; - if (ns.getString("group") != null) { + byte[] groupId = null; + if (ns.getString("group") != null) { + try { groupId = Util.decodeGroupId(ns.getString("group")); + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + return 1; } - if (groupId == null) { - groupId = new byte[0]; - } - String groupName = ns.getString("name"); - if (groupName == null) { - groupName = ""; - } - List groupMembers = ns.getList("member"); - if (groupMembers == null) { - groupMembers = new ArrayList<>(); - } - String groupAvatar = ns.getString("avatar"); - if (groupAvatar == null) { - groupAvatar = ""; - } + } + if (groupId == null) { + groupId = new byte[0]; + } + + String groupName = ns.getString("name"); + if (groupName == null) { + groupName = ""; + } + + List groupMembers = ns.getList("member"); + if (groupMembers == null) { + groupMembers = new ArrayList<>(); + } + + String groupAvatar = ns.getString("avatar"); + if (groupAvatar == null) { + groupAvatar = ""; + } + + try { byte[] newGroupId = signal.updateGroup(groupId, groupName, groupMembers, groupAvatar); if (groupId.length != newGroupId.length) { System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …"); } return 0; - } catch (IOException e) { - handleIOException(e); - return 3; - } catch (AttachmentInvalidException e) { + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } catch (Signal.Error.AttachmentInvalid e) { System.err.println("Failed to add avatar attachment for group\": " + e.getMessage()); - System.err.println("Aborting sending."); return 1; - } catch (GroupNotFoundException e) { - handleGroupNotFoundException(e); - return 1; - } catch (NotAGroupMemberException e) { - handleNotAGroupMemberException(e); - return 1; - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - return 3; - } catch (GroupIdFormatException e) { - handleGroupIdFormatException(e); - return 1; - } catch (InvalidNumberException e) { - handleInvalidNumberException(e); + } catch (DBusExecutionException e) { + System.err.println("Failed to send message: " + e.getMessage()); return 1; } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java new file mode 100644 index 00000000..17cc2caa --- /dev/null +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -0,0 +1,205 @@ +package org.asamk.signal.dbus; + +import org.asamk.Signal; +import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.GroupNotFoundException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.storage.groups.GroupInfo; +import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.util.InvalidNumberException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class DbusSignalImpl implements Signal { + + private final Manager m; + + public DbusSignalImpl(final Manager m) { + this.m = m; + } + + @Override + public boolean isRemote() { + return false; + } + + @Override + public String getObjectPath() { + return null; + } + + @Override + public long sendMessage(final String message, final List attachments, final String recipient) { + List recipients = new ArrayList<>(1); + recipients.add(recipient); + return sendMessage(message, attachments, recipients); + } + + private static DBusExecutionException convertEncapsulatedExceptions(EncapsulatedExceptions e) { + if (e.getNetworkExceptions().size() + e.getUnregisteredUserExceptions().size() + e.getUntrustedIdentityExceptions().size() == 1) { + if (e.getNetworkExceptions().size() == 1) { + NetworkFailureException n = e.getNetworkExceptions().get(0); + return new Error.Failure("Network failure for \"" + n.getE164number() + "\": " + n.getMessage()); + } else if (e.getUnregisteredUserExceptions().size() == 1) { + UnregisteredUserException n = e.getUnregisteredUserExceptions().get(0); + return new Error.UnregisteredUser("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage()); + } else if (e.getUntrustedIdentityExceptions().size() == 1) { + UntrustedIdentityException n = e.getUntrustedIdentityExceptions().get(0); + return new Error.UntrustedIdentity("Untrusted Identity for \"" + n.getIdentifier() + "\": " + n.getMessage()); + } + } + + StringBuilder message = new StringBuilder(); + message.append("Failed to send (some) messages:").append('\n'); + for (NetworkFailureException n : e.getNetworkExceptions()) { + message.append("Network failure for \"").append(n.getE164number()).append("\": ").append(n.getMessage()).append('\n'); + } + for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) { + message.append("Unregistered user \"").append(n.getE164Number()).append("\": ").append(n.getMessage()).append('\n'); + } + for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) { + message.append("Untrusted Identity for \"").append(n.getIdentifier()).append("\": ").append(n.getMessage()).append('\n'); + } + + return new Error.Failure(message.toString()); + } + + @Override + public long sendMessage(final String message, final List attachments, final List recipients) { + try { + return m.sendMessage(message, attachments, recipients); + } catch (EncapsulatedExceptions e) { + throw convertEncapsulatedExceptions(e); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + + @Override + public void sendEndSessionMessage(final List recipients) { + try { + m.sendEndSessionMessage(recipients); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (EncapsulatedExceptions e) { + throw convertEncapsulatedExceptions(e); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } + } + + @Override + public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) { + try { + return m.sendGroupMessage(message, attachments, groupId); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (EncapsulatedExceptions e) { + throw convertEncapsulatedExceptions(e); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } + } + + @Override + public String getContactName(final String number) { + try { + return m.getContactName(number); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } + } + + @Override + public void setContactName(final String number, final String name) { + try { + m.setContactName(number, name); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } + } + + @Override + public void setContactBlocked(final String number, final boolean blocked) { + try { + m.setContactBlocked(number, blocked); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } + } + + @Override + public void setGroupBlocked(final byte[] groupId, final boolean blocked) { + try { + m.setGroupBlocked(groupId, blocked); + } catch (GroupNotFoundException e) { + throw new Error.GroupNotFound(e.getMessage()); + } + } + + @Override + public List getGroupIds() { + List groups = m.getGroups(); + List ids = new ArrayList<>(groups.size()); + for (GroupInfo group : groups) { + ids.add(group.groupId); + } + return ids; + } + + @Override + public String getGroupName(final byte[] groupId) { + GroupInfo group = m.getGroup(groupId); + if (group == null) { + return ""; + } else { + return group.name; + } + } + + @Override + public List getGroupMembers(final byte[] groupId) { + GroupInfo group = m.getGroup(groupId); + if (group == null) { + return Collections.emptyList(); + } else { + return new ArrayList<>(group.getMembersE164()); + } + } + + @Override + public byte[] updateGroup(final byte[] groupId, final String name, final List members, final String avatar) { + try { + return m.updateGroup(groupId, name, members, avatar); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (EncapsulatedExceptions e) { + throw convertEncapsulatedExceptions(e); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } + } + + @Override + public boolean isRegistered() { + return true; + } +} diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 9d79c468..01275cec 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -18,7 +18,6 @@ package org.asamk.signal.manager; import com.fasterxml.jackson.databind.ObjectMapper; -import org.asamk.Signal; import org.asamk.signal.storage.SignalAccount; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; @@ -142,7 +141,7 @@ import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -public class Manager implements Signal, Closeable { +public class Manager implements Closeable { private final SleepTimer timer = new UptimeSleepTimer(); private final SignalServiceConfiguration serviceConfiguration; @@ -478,7 +477,6 @@ public class Manager implements Signal, Closeable { return account.getGroupStore().getGroups(); } - @Override public long sendGroupMessage(String messageText, List attachments, byte[] groupId) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { @@ -641,15 +639,6 @@ public class Manager implements Signal, Closeable { getMessageSender().sendReceipt(remoteAddress, getAccessFor(remoteAddress), receiptMessage); } - @Override - public long sendMessage(String message, List attachments, String recipient) - throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException { - List recipients = new ArrayList<>(1); - recipients.add(recipient); - return sendMessage(message, attachments, recipients); - } - - @Override public long sendMessage(String messageText, List attachments, List recipients) throws IOException, EncapsulatedExceptions, AttachmentInvalidException, InvalidNumberException { @@ -682,7 +671,6 @@ public class Manager implements Signal, Closeable { sendMessageLegacy(messageBuilder, getSignalServiceAddresses(recipients)); } - @Override public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, InvalidNumberException { SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asEndSessionMessage(); @@ -699,7 +687,6 @@ public class Manager implements Signal, Closeable { } } - @Override public String getContactName(String number) throws InvalidNumberException { ContactInfo contact = account.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number)); if (contact == null) { @@ -709,7 +696,6 @@ public class Manager implements Signal, Closeable { } } - @Override public void setContactName(String number, String name) throws InvalidNumberException { final SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number); ContactInfo contact = account.getContactStore().getContact(address); @@ -724,7 +710,6 @@ public class Manager implements Signal, Closeable { account.save(); } - @Override public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException { setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked); } @@ -742,7 +727,6 @@ public class Manager implements Signal, Closeable { account.save(); } - @Override public void setGroupBlocked(final byte[] groupId, final boolean blocked) throws GroupNotFoundException { GroupInfo group = getGroup(groupId); if (group == null) { @@ -755,37 +739,6 @@ public class Manager implements Signal, Closeable { } } - @Override - public List getGroupIds() { - List groups = getGroups(); - List ids = new ArrayList<>(groups.size()); - for (GroupInfo group : groups) { - ids.add(group.groupId); - } - return ids; - } - - @Override - public String getGroupName(byte[] groupId) { - GroupInfo group = getGroup(groupId); - if (group == null) { - return ""; - } else { - return group.name; - } - } - - @Override - public List getGroupMembers(byte[] groupId) { - GroupInfo group = getGroup(groupId); - if (group == null) { - return Collections.emptyList(); - } else { - return new ArrayList<>(group.getMembersE164()); - } - } - - @Override public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { if (groupId.length == 0) { groupId = null; @@ -1764,16 +1717,6 @@ public class Manager implements Signal, Closeable { return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); } - @Override - public boolean isRemote() { - return false; - } - - @Override - public String getObjectPath() { - return null; - } - private void sendGroups() throws IOException, UntrustedIdentityException { File groupsFile = IOUtils.createTempFile(); diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 53a39205..a09be3d0 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -2,7 +2,6 @@ package org.asamk.signal.util; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.NotAGroupMemberException; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; @@ -50,11 +49,6 @@ public class ErrorUtils { System.err.println("Aborting sending."); } - public static void handleDBusExecutionException(DBusExecutionException e) { - System.err.println("Cannot connect to dbus: " + e.getMessage()); - System.err.println("Aborting."); - } - public static void handleGroupIdFormatException(GroupIdFormatException e) { System.err.println(e.getMessage()); System.err.println("Aborting sending."); @@ -62,7 +56,6 @@ public class ErrorUtils { public static void handleInvalidNumberException(InvalidNumberException e) { System.err.println("Failed to parse recipient: " + e.getMessage()); - System.err.println(e.getMessage()); System.err.println("Aborting sending."); } } From b382a4260b367311945344715842588f333af0a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=BA=C5=A1=20Ko=C5=A1=C3=BAt?= Date: Sat, 16 May 2020 22:55:53 +0200 Subject: [PATCH 063/103] Contact config - message expiration time (#308) Co-authored-by: Matus Kosut --- man/signal-cli.1.adoc | 4 ++++ .../signal/commands/UpdateContactCommand.java | 14 +++++++++++++ .../org/asamk/signal/manager/Manager.java | 21 +++++++++++++++---- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index ecd7ac9f..aeea729a 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -252,6 +252,10 @@ Specify the contact phone number. *-n*, *--name*:: Specify the new name for this contact. +*-e*, *--expiration*:: +Set expiration time of messages (seconds). +To disable expiration set expiration time to 0. + === block Block the given contacts or groups (no messages will be received). diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index 77f38e5e..ae84893d 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -3,6 +3,8 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import java.io.IOException; + import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -15,6 +17,10 @@ public class UpdateContactCommand implements LocalCommand { subparser.addArgument("-n", "--name") .required(true) .help("New contact name"); + subparser.addArgument("-e", "--expiration") + .required(false) + .type(int.class) + .help("Set expiration time of messages (seconds)"); subparser.help("Update the details of a given contact"); } @@ -30,8 +36,16 @@ public class UpdateContactCommand implements LocalCommand { try { m.setContactName(number, name); + + Integer expiration = ns.getInt("expiration"); + if (expiration != null) { + m.setExpirationTimer(number, expiration); + } } catch (InvalidNumberException e) { System.out.println("Invalid contact number: " + e.getMessage()); + } catch (IOException e) { + System.err.println("Update contact error: " + e.getMessage()); + return 3; } return 0; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 01275cec..b88e1a9f 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -758,10 +758,23 @@ public class Manager implements Closeable { /** * Change the expiration timer for a contact */ - public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) { - ContactInfo c = account.getContactStore().getContact(address); - c.messageExpirationTime = messageExpirationTimer; - account.getContactStore().updateContact(c); + public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) throws IOException { + final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder(); + ContactInfo contact = account.getContactStore().getContact(address); + contact.messageExpirationTime = messageExpirationTimer; + account.getContactStore().updateContact(contact); + account.save(); + messageBuilder.withExpiration(messageExpirationTimer); + messageBuilder.asExpirationUpdate(); + sendMessage(messageBuilder, Collections.singleton(address)); + } + + /** + * Change the expiration timer for a contact + */ + public void setExpirationTimer(String number, int messageExpirationTimer) throws IOException, InvalidNumberException { + SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number); + setExpirationTimer(address, messageExpirationTimer); } /** From e048b1886d4858050125c4a341df9a8987c2dcb8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 22 May 2020 12:30:36 +0200 Subject: [PATCH 064/103] Store number/uuid in recipient store after receiving a message Necessary to find the correct session if the sender send the next message with only the uuid. Fixes #309 --- src/main/java/org/asamk/signal/manager/Manager.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index b88e1a9f..81f870cd 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1411,6 +1411,11 @@ public class Manager implements Closeable { System.err.println("Ignoring error: " + e.getMessage()); continue; } + if (envelope.hasSource()) { + // Store uuid if we don't have it already + SignalServiceAddress source = envelope.getSourceAddress(); + resolveSignalServiceAddress(source); + } if (!envelope.isReceipt()) { try { content = decryptMessage(envelope); @@ -1472,6 +1477,9 @@ public class Manager implements Closeable { } else { sender = content.getSender(); } + // Store uuid if we don't have it already + resolveSignalServiceAddress(sender); + if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); From 1aba9f370e7a98a4c662ac4efef39702cd40f868 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 22 May 2020 12:48:39 +0200 Subject: [PATCH 065/103] Extract sending of expiration timer update to separate method --- src/main/java/org/asamk/signal/manager/Manager.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 81f870cd..f2a26480 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -759,13 +759,16 @@ public class Manager implements Closeable { * Change the expiration timer for a contact */ public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) throws IOException { - final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder(); ContactInfo contact = account.getContactStore().getContact(address); contact.messageExpirationTime = messageExpirationTimer; account.getContactStore().updateContact(contact); + sendExpirationTimerUpdate(address); account.save(); - messageBuilder.withExpiration(messageExpirationTimer); - messageBuilder.asExpirationUpdate(); + } + + private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException { + final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .asExpirationUpdate(); sendMessage(messageBuilder, Collections.singleton(address)); } From bfb12b0872f89e073de3f1790ae8c5d8503a602a Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 22 May 2020 12:54:22 +0200 Subject: [PATCH 066/103] Refactor sendMessage and extract sendSelfMessage method --- .../org/asamk/signal/manager/Manager.java | 66 +++++++++++-------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index f2a26480..324c4045 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1110,11 +1110,10 @@ public class Manager implements Closeable { } SignalServiceDataMessage message = null; try { - SignalServiceMessageSender messageSender = getMessageSender(); - message = messageBuilder.build(); if (message.getGroupContext().isPresent()) { try { + SignalServiceMessageSender messageSender = getMessageSender(); final boolean isRecipientUpdate = false; List result = messageSender.sendMessage(new ArrayList<>(recipients), getAccessFor(recipients), isRecipientUpdate, message); for (SendMessageResult r : result) { @@ -1127,25 +1126,6 @@ public class Manager implements Closeable { account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); return Collections.emptyList(); } - } else if (recipients.size() == 1 && recipients.contains(account.getSelfAddress())) { - SignalServiceAddress recipient = account.getSelfAddress(); - final Optional unidentifiedAccess = getAccessFor(recipient); - SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient), - message.getTimestamp(), - message, - message.getExpiresInSeconds(), - Collections.singletonMap(recipient, unidentifiedAccess.isPresent()), - false); - SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); - - List results = new ArrayList<>(recipients.size()); - try { - messageSender.sendMessage(syncMessage, unidentifiedAccess); - } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); - results.add(SendMessageResult.identityFailure(recipient, e.getIdentityKey())); - } - return results; } else { // Send to all individually, so sync messages are sent correctly List results = new ArrayList<>(recipients.size()); @@ -1159,12 +1139,10 @@ public class Manager implements Closeable { messageBuilder.withProfileKey(null); } message = messageBuilder.build(); - try { - SendMessageResult result = messageSender.sendMessage(address, getAccessFor(address), message); - results.add(result); - } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); - results.add(SendMessageResult.identityFailure(address, e.getIdentityKey())); + if (address.matches(account.getSelfAddress())) { + results.add(sendSelfMessage(message)); + } else { + results.add(sendMessage(address, message)); } } return results; @@ -1179,6 +1157,40 @@ public class Manager implements Closeable { } } + private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException { + SignalServiceMessageSender messageSender = getMessageSender(); + + SignalServiceAddress recipient = account.getSelfAddress(); + + final Optional unidentifiedAccess = getAccessFor(recipient); + SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient), + message.getTimestamp(), + message, + message.getExpiresInSeconds(), + Collections.singletonMap(recipient, unidentifiedAccess.isPresent()), + false); + SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); + + try { + messageSender.sendMessage(syncMessage, unidentifiedAccess); + return SendMessageResult.success(recipient, unidentifiedAccess.isPresent(), false); + } catch (UntrustedIdentityException e) { + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); + return SendMessageResult.identityFailure(recipient, e.getIdentityKey()); + } + } + + private SendMessageResult sendMessage(SignalServiceAddress address, SignalServiceDataMessage message) throws IOException { + SignalServiceMessageSender messageSender = getMessageSender(); + + try { + return messageSender.sendMessage(address, getAccessFor(address), message); + } catch (UntrustedIdentityException e) { + account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); + return SendMessageResult.identityFailure(address, e.getIdentityKey()); + } + } + private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, org.whispersystems.libsignal.UntrustedIdentityException { SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator()); try { From cf1cd60b9ffcab8d9e95edb82e35c127c439a495 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 22 May 2020 15:03:35 +0200 Subject: [PATCH 067/103] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 19882c3d..6fb89015 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_1_8 mainClassName = 'org.asamk.signal.Main' -version = '0.6.7' +version = '0.6.8' compileJava.options.encoding = 'UTF-8' From f324a4329843a18fee99c59da32c71d04f8083e2 Mon Sep 17 00:00:00 2001 From: exquo <62397152+exquo@users.noreply.github.com> Date: Tue, 2 Jun 2020 11:01:43 +0000 Subject: [PATCH 068/103] Remove references to the native unix-java library Since version 0.6.8 signal-cli uses hypfvieh dbus-java, so installing the packages libunixsocket-java (Debian), libmatthew-java (Fedora) or libmatthew-unix-java (ArchLinux) is not necessary. --- man/signal-cli.1.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index aeea729a..7781b1c5 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -313,8 +313,6 @@ The path of the manifest.json or a zip file containing the sticker pack you wish === daemon signal-cli can run in daemon mode and provides an experimental dbus interface. -For dbus support you need jni/unix-java.so installed on your system (Debian: -libunixsocket-java ArchLinux: libmatthew-unix-java (AUR)). *--system*:: Use DBus system bus instead of user bus. From 22336460cc4a1f65b6b8cce96831862c7679e19a Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 4 Jun 2020 17:07:03 +0200 Subject: [PATCH 069/103] Update dependencies --- build.gradle | 2 +- .../java/org/asamk/signal/manager/ServiceConfig.java | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 6fb89015..5de0b6c9 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } dependencies { - implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_10' + implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_11' implementation 'org.bouncycastle:bcprov-jdk15on:1.65' implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1' implementation 'com.github.hypfvieh:dbus-java:3.2.1' diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java index f1323087..f8394ada 100644 --- a/src/main/java/org/asamk/signal/manager/ServiceConfig.java +++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java @@ -9,7 +9,9 @@ import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupSe import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; +import org.whispersystems.util.Base64; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -34,7 +36,7 @@ public class ServiceConfig { private final static Optional dns = Optional.absent(); - private final static byte[] zkGroupServerPublicParams = new byte[]{}; + private final static String zkGroupServerPublicParamsHex = "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0="; static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false, false); @@ -46,6 +48,13 @@ public class ServiceConfig { final List interceptors = Collections.singletonList(userAgentInterceptor); + final byte[] zkGroupServerPublicParams; + try { + zkGroupServerPublicParams = Base64.decode(zkGroupServerPublicParamsHex); + } catch (IOException e) { + throw new AssertionError(e); + } + return new SignalServiceConfiguration( new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}), From 5829e3f117b4297328be3f58e156b45198621a6e Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 4 Jun 2020 17:59:05 +0200 Subject: [PATCH 070/103] Create service config only once --- src/main/java/org/asamk/signal/Main.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 562267c4..b95b0093 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -43,6 +43,7 @@ import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import java.io.File; import java.io.IOException; @@ -102,14 +103,16 @@ public class Main { dataPath = getDefaultDataPath(); } + final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT); + if (username == null) { - ProvisioningManager pm = new ProvisioningManager(dataPath, ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT), BaseConfig.USER_AGENT); + ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT); return handleCommands(ns, pm); } Manager manager; try { - manager = Manager.init(username, dataPath, ServiceConfig.createDefaultServiceConfiguration(BaseConfig.USER_AGENT), BaseConfig.USER_AGENT); + manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT); } catch (Throwable e) { System.err.println("Error loading state file: " + e.getMessage()); return 2; From d14b8ac71ffd3f47d68f8b4a6cb707ffd1d0e154 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 4 Jun 2020 18:44:13 +0200 Subject: [PATCH 071/103] Detect when receive has caught up with old messages --- src/main/java/org/asamk/signal/manager/Manager.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 324c4045..243d8857 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1402,13 +1402,15 @@ public class Manager implements Closeable { messagePipe = messageReceiver.createMessagePipe(); } + boolean hasCaughtUpWithOldMessages = false; + while (true) { SignalServiceEnvelope envelope; SignalServiceContent content = null; Exception exception = null; final long now = new Date().getTime(); try { - envelope = messagePipe.read(timeout, unit, envelope1 -> { + Optional result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> { // store message on disk, before acknowledging receipt to the server try { String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : ""; @@ -1418,6 +1420,15 @@ public class Manager implements Closeable { System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage()); } }); + if (result.isPresent()) { + envelope = result.get(); + } else { + // Received indicator that server queue is empty + hasCaughtUpWithOldMessages = true; + + // Continue to wait another timeout for new messages + continue; + } } catch (TimeoutException e) { if (returnOnTimeout) return; From 34caba2a7abe89e60e631550efde8e2db80b72b5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 5 Jun 2020 12:13:17 +0200 Subject: [PATCH 072/103] Delay auto responses to messages until caught up with old messages To prevent responding with old state, if the last receive call was a long time ago. --- .../asamk/signal/manager/HandleAction.java | 156 ++++++++++++++++++ .../org/asamk/signal/manager/Manager.java | 103 +++++++----- 2 files changed, 214 insertions(+), 45 deletions(-) create mode 100644 src/main/java/org/asamk/signal/manager/HandleAction.java diff --git a/src/main/java/org/asamk/signal/manager/HandleAction.java b/src/main/java/org/asamk/signal/manager/HandleAction.java new file mode 100644 index 00000000..2ef99062 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/HandleAction.java @@ -0,0 +1,156 @@ +package org.asamk.signal.manager; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.util.Arrays; +import java.util.Objects; + +interface HandleAction { + + void execute(Manager m) throws Throwable; +} + +class SendReceiptAction implements HandleAction { + + private final SignalServiceAddress address; + private final long timestamp; + + public SendReceiptAction(final SignalServiceAddress address, final long timestamp) { + this.address = address; + this.timestamp = timestamp; + } + + @Override + public void execute(Manager m) throws Throwable { + m.sendReceipt(address, timestamp); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final SendReceiptAction that = (SendReceiptAction) o; + return timestamp == that.timestamp && + address.equals(that.address); + } + + @Override + public int hashCode() { + return Objects.hash(address, timestamp); + } +} + +class SendSyncContactsAction implements HandleAction { + + private static final SendSyncContactsAction INSTANCE = new SendSyncContactsAction(); + + private SendSyncContactsAction() { + } + + public static SendSyncContactsAction create() { + return INSTANCE; + } + + @Override + public void execute(Manager m) throws Throwable { + m.sendContacts(); + } +} + +class SendSyncGroupsAction implements HandleAction { + + private static final SendSyncGroupsAction INSTANCE = new SendSyncGroupsAction(); + + private SendSyncGroupsAction() { + } + + public static SendSyncGroupsAction create() { + return INSTANCE; + } + + @Override + public void execute(Manager m) throws Throwable { + m.sendGroups(); + } +} + +class SendSyncBlockedListAction implements HandleAction { + + private static final SendSyncBlockedListAction INSTANCE = new SendSyncBlockedListAction(); + + private SendSyncBlockedListAction() { + } + + public static SendSyncBlockedListAction create() { + return INSTANCE; + } + + @Override + public void execute(Manager m) throws Throwable { + m.sendBlockedList(); + } +} + +class SendGroupInfoRequestAction implements HandleAction { + + private final SignalServiceAddress address; + private final byte[] groupId; + + public SendGroupInfoRequestAction(final SignalServiceAddress address, final byte[] groupId) { + this.address = address; + this.groupId = groupId; + } + + @Override + public void execute(Manager m) throws Throwable { + m.sendGroupInfoRequest(groupId, address); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final SendGroupInfoRequestAction that = (SendGroupInfoRequestAction) o; + return address.equals(that.address) && + Arrays.equals(groupId, that.groupId); + } + + @Override + public int hashCode() { + int result = Objects.hash(address); + result = 31 * result + Arrays.hashCode(groupId); + return result; + } +} + +class SendGroupUpdateAction implements HandleAction { + + private final SignalServiceAddress address; + private final byte[] groupId; + + public SendGroupUpdateAction(final SignalServiceAddress address, final byte[] groupId) { + this.address = address; + this.groupId = groupId; + } + + @Override + public void execute(Manager m) throws Throwable { + m.sendUpdateGroupMessage(groupId, address); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final SendGroupUpdateAction that = (SendGroupUpdateAction) o; + return address.equals(that.address) && + Arrays.equals(groupId, that.groupId); + } + + @Override + public int hashCode() { + int result = Objects.hash(address); + result = 31 * result + Arrays.hashCode(groupId); + return result; + } +} diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 243d8857..e57bd531 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -580,7 +580,7 @@ public class Manager implements Closeable { return g.groupId; } - private void sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { + void sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { if (groupId == null) { return; } @@ -616,7 +616,7 @@ public class Manager implements Closeable { .withExpiration(g.messageExpirationTime); } - private void sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions { + void sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions { if (groupId == null) { return; } @@ -631,7 +631,7 @@ public class Manager implements Closeable { sendMessageLegacy(messageBuilder, Collections.singleton(recipient)); } - private void sendReceipt(SignalServiceAddress remoteAddress, long messageId) throws IOException, UntrustedIdentityException { + void sendReceipt(SignalServiceAddress remoteAddress, long messageId) throws IOException, UntrustedIdentityException { SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, Collections.singletonList(messageId), System.currentTimeMillis()); @@ -1209,7 +1209,8 @@ public class Manager implements Closeable { account.getSignalProtocolStore().deleteAllSessions(source); } - private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) { + private List handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) { + List actions = new ArrayList<>(); if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) { SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId()); @@ -1244,12 +1245,8 @@ public class Manager implements Closeable { account.getGroupStore().updateGroup(group); break; case DELIVER: - if (group == null) { - try { - sendGroupInfoRequest(groupInfo.getGroupId(), source); - } catch (IOException | EncapsulatedExceptions e) { - e.printStackTrace(); - } + if (group == null && !isSync) { + actions.add(new SendGroupInfoRequestAction(source, groupInfo.getGroupId())); } break; case QUIT: @@ -1259,14 +1256,8 @@ public class Manager implements Closeable { } break; case REQUEST_INFO: - if (group != null) { - try { - sendUpdateGroupMessage(groupInfo.getGroupId(), source); - } catch (IOException | EncapsulatedExceptions | AttachmentInvalidException e) { - e.printStackTrace(); - } catch (GroupNotFoundException | NotAGroupMemberException e) { - // We have left this group, so don't send a group update message - } + if (group != null && !isSync) { + actions.add(new SendGroupUpdateAction(source, group.groupId)); } break; } @@ -1341,6 +1332,7 @@ public class Manager implements Closeable { } } } + return actions; } private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { @@ -1383,7 +1375,14 @@ public class Manager implements Closeable { } catch (Exception e) { return; } - handleMessage(envelope, content, ignoreAttachments); + List actions = handleMessage(envelope, content, ignoreAttachments); + for (HandleAction action : actions) { + try { + action.execute(this); + } catch (Throwable e) { + e.printStackTrace(); + } + } } account.save(); handler.handleMessage(envelope, content, null); @@ -1398,6 +1397,8 @@ public class Manager implements Closeable { retryFailedReceivedMessages(handler, ignoreAttachments); final SignalServiceMessageReceiver messageReceiver = getMessageReceiver(); + Set queuedActions = null; + if (messagePipe == null) { messagePipe = messageReceiver.createMessagePipe(); } @@ -1426,6 +1427,18 @@ public class Manager implements Closeable { // Received indicator that server queue is empty hasCaughtUpWithOldMessages = true; + if (queuedActions != null) { + for (HandleAction action : queuedActions) { + try { + action.execute(this); + } catch (Throwable e) { + e.printStackTrace(); + } + } + queuedActions.clear(); + queuedActions = null; + } + // Continue to wait another timeout for new messages continue; } @@ -1448,7 +1461,21 @@ public class Manager implements Closeable { } catch (Exception e) { exception = e; } - handleMessage(envelope, content, ignoreAttachments); + List actions = handleMessage(envelope, content, ignoreAttachments); + if (hasCaughtUpWithOldMessages) { + for (HandleAction action : actions) { + try { + action.execute(this); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } else { + if (queuedActions == null) { + queuedActions = new HashSet<>(); + } + queuedActions.addAll(actions); + } } account.save(); if (!isMessageBlocked(envelope, content)) { @@ -1495,7 +1522,8 @@ public class Manager implements Closeable { return false; } - private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) { + private List handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) { + List actions = new ArrayList<>(); if (content != null) { SignalServiceAddress sender; if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { @@ -1510,44 +1538,28 @@ public class Manager implements Closeable { SignalServiceDataMessage message = content.getDataMessage().get(); if (content.isNeedsReceipt()) { - try { - sendReceipt(sender, message.getTimestamp()); - } catch (IOException | UntrustedIdentityException | IllegalArgumentException e) { - e.printStackTrace(); - } + actions.add(new SendReceiptAction(sender, message.getTimestamp())); } - handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments); + actions.addAll(handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments)); } if (content.getSyncMessage().isPresent()) { account.setMultiDevice(true); SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) { SentTranscriptMessage message = syncMessage.getSent().get(); - handleSignalServiceDataMessage(message.getMessage(), true, sender, message.getDestination().orNull(), ignoreAttachments); + actions.addAll(handleSignalServiceDataMessage(message.getMessage(), true, sender, message.getDestination().orNull(), ignoreAttachments)); } if (syncMessage.getRequest().isPresent()) { RequestMessage rm = syncMessage.getRequest().get(); if (rm.isContactsRequest()) { - try { - sendContacts(); - } catch (UntrustedIdentityException | IOException | IllegalArgumentException e) { - e.printStackTrace(); - } + actions.add(SendSyncContactsAction.create()); } if (rm.isGroupsRequest()) { - try { - sendGroups(); - } catch (UntrustedIdentityException | IOException | IllegalArgumentException e) { - e.printStackTrace(); - } + actions.add(SendSyncGroupsAction.create()); } if (rm.isBlockedListRequest()) { - try { - sendBlockedList(); - } catch (UntrustedIdentityException | IOException | IllegalArgumentException e) { - e.printStackTrace(); - } + actions.add(SendSyncBlockedListAction.create()); } // TODO Handle rm.isConfigurationRequest(); } @@ -1681,6 +1693,7 @@ public class Manager implements Closeable { } } } + return actions; } private File getContactAvatarFile(String number) { @@ -1764,7 +1777,7 @@ public class Manager implements Closeable { return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); } - private void sendGroups() throws IOException, UntrustedIdentityException { + void sendGroups() throws IOException, UntrustedIdentityException { File groupsFile = IOUtils.createTempFile(); try { @@ -1853,7 +1866,7 @@ public class Manager implements Closeable { } } - private void sendBlockedList() throws IOException, UntrustedIdentityException { + void sendBlockedList() throws IOException, UntrustedIdentityException { List addresses = new ArrayList<>(); for (ContactInfo record : account.getContactStore().getContacts()) { if (record.blocked) { From a83924238fb212280c6199fc765ed7ae7144bfdf Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 5 Jun 2020 12:13:38 +0200 Subject: [PATCH 073/103] Print more information for sync requests --- .../java/org/asamk/signal/ReceiveMessageHandler.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 898f382c..a9ecb627 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -114,6 +114,15 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (syncMessage.getRequest().get().isGroupsRequest()) { System.out.println(" - groups request"); } + if (syncMessage.getRequest().get().isBlockedListRequest()) { + System.out.println(" - blocked list request"); + } + if (syncMessage.getRequest().get().isConfigurationRequest()) { + System.out.println(" - configuration request"); + } + if (syncMessage.getRequest().get().isKeysRequest()) { + System.out.println(" - keys request"); + } } if (syncMessage.getSent().isPresent()) { System.out.println("Received sync sent message"); From 4177deccf1e91483f54c5fcfacffce0ce525ad39 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 5 Jun 2020 12:14:04 +0200 Subject: [PATCH 074/103] Ignore closed channel exception when closing the channel lock --- src/main/java/org/asamk/signal/storage/SignalAccount.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 94935441..d0638e41 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -38,6 +38,7 @@ import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.channels.Channels; +import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.util.Collection; @@ -429,7 +430,10 @@ public class SignalAccount implements Closeable { @Override public void close() throws IOException { synchronized (fileChannel) { - lock.close(); + try { + lock.close(); + } catch (ClosedChannelException ignored) { + } fileChannel.close(); } } From 23282fc7e857a7628577cc9b8afb843c7d013007 Mon Sep 17 00:00:00 2001 From: Elburz Sorkhabi Date: Sun, 21 Jun 2020 16:56:54 -0400 Subject: [PATCH 075/103] add further clarification to addDevice docs I added a short note mentioning to add quotation marks around the full URI, otherwise the user receives "Invalid device URI" errors from Java. --- man/signal-cli.1.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 7781b1c5..38e432d5 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -103,7 +103,7 @@ Link another device to this device. Only works, if this is the master device. *--uri* URI:: -Specify the uri contained in the QR code shown by the new device. +Specify the uri contained in the QR code shown by the new device. You will need the full uri enclosed in quotation marks, such as "tsdevice:/?uuid=....." === listDevices From 2f9873c35ec08f35e24a2b5f35e23f6c704c8773 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 2 Jul 2020 21:34:52 +0200 Subject: [PATCH 076/103] Use isEmpty() --- src/main/java/org/asamk/signal/manager/Manager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index e57bd531..9515a2fd 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -746,7 +746,7 @@ public class Manager implements Closeable { if (name.isEmpty()) { name = null; } - if (members.size() == 0) { + if (members.isEmpty()) { members = null; } if (avatar.isEmpty()) { From 1ba2626ae9c3b90b26cb103ebf35d41fd70a52b5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 4 Aug 2020 22:37:04 +0200 Subject: [PATCH 077/103] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..0815e8af --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,54 @@ +name: "CodeQL" + +on: + push: + branches: [master, ] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: '0 7 * * 4' + +jobs: + analyse: + name: Analyse + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + # Override language selection by uncommenting this and choosing your languages + # with: + # languages: go, javascript, csharp, python, cpp, java + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 3aade456432576a805128df8b6a1a69c760344b2 Mon Sep 17 00:00:00 2001 From: Mateusz Piotrowski <0mp@FreeBSD.org> Date: Wed, 5 Aug 2020 11:08:03 +0200 Subject: [PATCH 078/103] Fix a typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 31a12c94..588016d3 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ You can find further instructions on the Wiki: ## Usage -Important: The USERNAME (your phone number) must include the country calling code, i.e. the number must start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list of all country codes. +Important: The USERNAME (your phone number) must include the country calling code, i.e. the number must start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list of all country codes.) * Register a number (with SMS verification) From f96770df3c6602ba45cb7e1ff10faa81602988c7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 4 Jul 2020 08:18:04 +0200 Subject: [PATCH 079/103] Move decrypting recipient profile to separate method --- src/main/java/org/asamk/signal/manager/Manager.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 9515a2fd..2b561304 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -425,7 +425,7 @@ public class Manager implements Closeable { account.getDeviceId(), account.getSignalProtocolStore(), userAgent, account.isMultiDevice(), attachmentsV3, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations); } - private SignalServiceProfile getRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess) throws IOException { + private SignalServiceProfile getEncryptedRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess) throws IOException { SignalServiceMessagePipe pipe = unidentifiedMessagePipe != null && unidentifiedAccess.isPresent() ? unidentifiedMessagePipe : messagePipe; @@ -444,6 +444,10 @@ public class Manager implements Closeable { } } + private SignalProfile getRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess, ProfileKey profileKey) throws IOException { + return decryptProfile(getEncryptedRecipientProfile(address, unidentifiedAccess), profileKey); + } + private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { File file = getGroupAvatarFile(groupId); if (!file.exists()) { @@ -980,7 +984,7 @@ public class Manager implements Closeable { } SignalProfile targetProfile; try { - targetProfile = decryptProfile(getRecipientProfile(recipient, Optional.absent()), theirProfileKey); + targetProfile = getRecipientProfile(recipient, Optional.absent(), theirProfileKey); } catch (IOException e) { System.err.println("Failed to get recipient profile: " + e); return null; From 6b02b1076cab8c902de8cca8c81611c80abc6c2a Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 6 Aug 2020 22:34:50 +0200 Subject: [PATCH 080/103] Update dependencies --- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.jar | Bin 58694 -> 58910 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 ++ gradlew.bat | 1 + 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 5de0b6c9..609f8bfb 100644 --- a/build.gradle +++ b/build.gradle @@ -18,9 +18,9 @@ repositories { dependencies { implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_11' - implementation 'org.bouncycastle:bcprov-jdk15on:1.65' + implementation 'org.bouncycastle:bcprov-jdk15on:1.66' implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1' - implementation 'com.github.hypfvieh:dbus-java:3.2.1' + implementation 'com.github.hypfvieh:dbus-java:3.2.2' implementation 'org.slf4j:slf4j-nop:1.7.30' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 490fda8577df6c95960ba7077c43220e5bb2c0d9..62d4c053550b91381bbd28b1afc82d634bf73a8a 100644 GIT binary patch delta 6447 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 F{|nTN-B$nr diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4b44297..bb8b2fc2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 2fe81a7d..fbd7c515 100755 --- a/gradlew +++ b/gradlew @@ -82,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -129,6 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath diff --git a/gradlew.bat b/gradlew.bat index 9109989e..a9f778a7 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -84,6 +84,7 @@ set CMD_LINE_ARGS=%* set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% From e6c1e03e212580989534b987deda27a2a7422549 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 9 Aug 2020 13:00:29 +0200 Subject: [PATCH 081/103] Update dependencies updateProfile can now only update both name and avatar at the same time, as the upstream API has changed. --- build.gradle | 4 +- man/signal-cli.1.adoc | 2 +- .../asamk/signal/ReceiveMessageHandler.java | 4 +- .../signal/commands/UpdateProfileCommand.java | 41 +++++-------------- .../org/asamk/signal/manager/Manager.java | 32 ++++++--------- .../java/org/asamk/signal/manager/Utils.java | 23 +++++++---- 6 files changed, 42 insertions(+), 64 deletions(-) diff --git a/build.gradle b/build.gradle index 609f8bfb..23e8b01d 100644 --- a/build.gradle +++ b/build.gradle @@ -17,10 +17,10 @@ repositories { } dependencies { - implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_11' + implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_12' implementation 'org.bouncycastle:bcprov-jdk15on:1.66' implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1' - implementation 'com.github.hypfvieh:dbus-java:3.2.2' + implementation 'com.github.hypfvieh:dbus-java:3.2.3' implementation 'org.slf4j:slf4j-nop:1.7.30' } diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 38e432d5..98a5da2a 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -227,7 +227,7 @@ Specify the safety number of the key, only use this option if you have verified === updateProfile -Update the name and/or avatar image visible by message recipients for the current users. +Update the name and avatar image visible by message recipients for the current users. The profile is stored encrypted on the Signal servers. The decryption key is sent with every outgoing messages (excluding group messages). diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index a9ecb627..9a75aa1c 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -200,7 +200,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { SignalServiceCallMessage callMessage = content.getCallMessage().get(); if (callMessage.getAnswerMessage().isPresent()) { AnswerMessage answerMessage = callMessage.getAnswerMessage().get(); - System.out.println("Answer message: " + answerMessage.getId() + ": " + answerMessage.getDescription()); + System.out.println("Answer message: " + answerMessage.getId() + ": " + answerMessage.getSdp()); } if (callMessage.getBusyMessage().isPresent()) { BusyMessage busyMessage = callMessage.getBusyMessage().get(); @@ -218,7 +218,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } if (callMessage.getOfferMessage().isPresent()) { OfferMessage offerMessage = callMessage.getOfferMessage().get(); - System.out.println("Offer message: " + offerMessage.getId() + ": " + offerMessage.getDescription()); + System.out.println("Offer message: " + offerMessage.getId() + ": " + offerMessage.getSdp()); } } if (content.getReceiptMessage().isPresent()) { diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java index a7b02937..218c8b77 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -14,16 +14,18 @@ public class UpdateProfileCommand implements LocalCommand { @Override public void attachToSubparser(final Subparser subparser) { - final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup(); + final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup() + .required(true); avatarOptions.addArgument("--avatar") .help("Path to new profile avatar"); avatarOptions.addArgument("--remove-avatar") .action(Arguments.storeTrue()); subparser.addArgument("--name") + .required(true) .help("New profile name"); - subparser.help("Set a name and/or avatar image for the user profile"); + subparser.help("Set a name and avatar image for the user profile"); } @Override @@ -34,38 +36,15 @@ public class UpdateProfileCommand implements LocalCommand { } String name = ns.getString("name"); - - if (name != null) { - try { - m.setProfileName(name); - } catch (IOException e) { - System.err.println("UpdateAccount error: " + e.getMessage()); - return 3; - } - } - String avatarPath = ns.getString("avatar"); - - if (avatarPath != null) { - File avatarFile = new File(avatarPath); - - try { - m.setProfileAvatar(avatarFile); - } catch (IOException e) { - System.err.println("UpdateAccount error: " + e.getMessage()); - return 3; - } - } - boolean removeAvatar = ns.getBoolean("remove_avatar"); - if (removeAvatar) { - try { - m.removeProfileAvatar(); - } catch (IOException e) { - System.err.println("UpdateAccount error: " + e.getMessage()); - return 3; - } + try { + File avatarFile = removeAvatar ? null : new File(avatarPath); + m.setProfile(name, avatarFile); + } catch (IOException e) { + System.err.println("UpdateAccount error: " + e.getMessage()); + return 3; } return 0; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 2b561304..d62bd296 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -38,7 +38,6 @@ import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.SelfSendException; import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.signal.zkgroup.InvalidInputException; -import org.signal.zkgroup.VerificationFailedException; import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.IdentityKey; @@ -135,6 +134,8 @@ import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; @@ -288,18 +289,10 @@ public class Manager implements Closeable { accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities); } - public void setProfileName(String name) throws IOException { - accountManager.setProfileName(account.getProfileKey(), name); - } - - public void setProfileAvatar(File avatar) throws IOException { - final StreamDetails streamDetails = Utils.createStreamDetailsFromFile(avatar); - accountManager.setProfileAvatar(account.getProfileKey(), streamDetails); - streamDetails.getStream().close(); - } - - public void removeProfileAvatar() throws IOException { - accountManager.setProfileAvatar(account.getProfileKey(), null); + public void setProfile(String name, File avatar) throws IOException { + try (final StreamDetails streamDetails = avatar == null ? null : Utils.createStreamDetailsFromFile(avatar)) { + accountManager.setVersionedProfile(account.getUuid(), account.getProfileKey(), name, streamDetails); + } } public void unregister() throws IOException { @@ -421,8 +414,9 @@ public class Manager implements Closeable { // TODO implement ZkGroup support final ClientZkProfileOperations clientZkProfileOperations = null; final boolean attachmentsV3 = false; + final ExecutorService executor = null; return new SignalServiceMessageSender(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), - account.getDeviceId(), account.getSignalProtocolStore(), userAgent, account.isMultiDevice(), attachmentsV3, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations); + account.getDeviceId(), account.getSignalProtocolStore(), userAgent, account.isMultiDevice(), attachmentsV3, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations, executor); } private SignalServiceProfile getEncryptedRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess) throws IOException { @@ -431,16 +425,16 @@ public class Manager implements Closeable { if (pipe != null) { try { - return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).getProfile(); - } catch (IOException ignored) { + return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile(); + } catch (IOException | InterruptedException | ExecutionException | TimeoutException ignored) { } } SignalServiceMessageReceiver receiver = getMessageReceiver(); try { - return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).getProfile(); - } catch (VerificationFailedException e) { - throw new AssertionError(e); + return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile(); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new IOException("Failed to retrieve profile", e); } } diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index 894bd58d..0244d409 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -75,13 +75,13 @@ class Utils { InputStream attachmentStream = new FileInputStream(attachmentFile); final long attachmentSize = attachmentFile.length(); final String mime = getFileMimeType(attachmentFile); - // TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option + // TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option final long uploadTimestamp = System.currentTimeMillis(); Optional preview = Optional.absent(); Optional caption = Optional.absent(); Optional blurHash = Optional.absent(); final Optional resumableUploadSpec = Optional.absent(); - return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, uploadTimestamp, caption, blurHash, null, null, resumableUploadSpec); + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, false, preview, 0, 0, uploadTimestamp, caption, blurHash, null, null, resumableUploadSpec); } static StreamDetails createStreamDetailsFromFile(File file) throws IOException { @@ -152,7 +152,7 @@ class Utils { try (FileInputStream f = new FileInputStream(file)) { DataInputStream in = new DataInputStream(f); int version = in.readInt(); - if (version > 3) { + if (version > 4) { return null; } int type = in.readInt(); @@ -179,26 +179,30 @@ class Utils { legacyMessage = new byte[legacyMessageLen]; in.readFully(legacyMessage); } - long serverTimestamp = 0; + long serverReceivedTimestamp = 0; String uuid = null; - if (version == 2) { - serverTimestamp = in.readLong(); + if (version >= 2) { + serverReceivedTimestamp = in.readLong(); uuid = in.readUTF(); if ("".equals(uuid)) { uuid = null; } } + long serverDeliveredTimestamp = 0; + if (version >= 4) { + serverDeliveredTimestamp = in.readLong(); + } Optional addressOptional = sourceUuid == null && source.isEmpty() ? Optional.absent() : Optional.of(new SignalServiceAddress(sourceUuid, source)); - return new SignalServiceEnvelope(type, addressOptional, sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid); + return new SignalServiceEnvelope(type, addressOptional, sourceDevice, timestamp, legacyMessage, content, serverReceivedTimestamp, serverDeliveredTimestamp, uuid); } } static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { try (FileOutputStream f = new FileOutputStream(file)) { try (DataOutputStream out = new DataOutputStream(f)) { - out.writeInt(3); // version + out.writeInt(4); // version out.writeInt(envelope.getType()); out.writeUTF(envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : ""); out.writeUTF(envelope.getSourceUuid().isPresent() ? envelope.getSourceUuid().get() : ""); @@ -216,9 +220,10 @@ class Utils { } else { out.writeInt(0); } - out.writeLong(envelope.getServerTimestamp()); + out.writeLong(envelope.getServerReceivedTimestamp()); String uuid = envelope.getUuid(); out.writeUTF(uuid == null ? "" : uuid); + out.writeLong(envelope.getServerDeliveredTimestamp()); } } } From b1e9f4e1587461b8ebeaa8d198bcc62744890e2d Mon Sep 17 00:00:00 2001 From: Mateusz Piotrowski <0mp@FreeBSD.org> Date: Fri, 14 Aug 2020 13:26:53 +0200 Subject: [PATCH 082/103] Mention the availability of signal-cli on FreeBSD --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 588016d3..a703dd56 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ signal-cli is primarily intended to be used on servers to notify admins of impor ## Installation -You can [build signal-cli](#building) yourself, or use the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/). You need to have at least JRE 7 installed, to run signal-cli. +You can [build signal-cli](#building) yourself, or use the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/) and there is a [FreeBSD port](https://www.freshports.org/net-im/signal-cli) available as well. You need to have at least JRE 7 installed, to run signal-cli. ### Install system-wide on Linux See [latest version](https://github.com/AsamK/signal-cli/releases). From 2c3d222e84aac308a6b43a96b5e0936a3446f5cf Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 9 Aug 2020 15:09:09 +0200 Subject: [PATCH 083/103] Remove unnecessary stderr output Fixes #334 --- .../java/org/asamk/signal/manager/Manager.java | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index d62bd296..7d13eddb 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -699,9 +699,6 @@ public class Manager implements Closeable { ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { contact = new ContactInfo(address); - System.err.println("Add contact " + contact.number + " named " + name); - } else { - System.err.println("Updating contact " + contact.number + " name " + contact.name + " -> " + name); } contact.name = name; account.getContactStore().updateContact(contact); @@ -716,9 +713,6 @@ public class Manager implements Closeable { ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { contact = new ContactInfo(address); - System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + address.getNumber().orNull()); - } else { - System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + address.getNumber().orNull()); } contact.blocked = blocked; account.getContactStore().updateContact(contact); @@ -729,12 +723,11 @@ public class Manager implements Closeable { GroupInfo group = getGroup(groupId); if (group == null) { throw new GroupNotFoundException(groupId); - } else { - System.err.println((blocked ? "Blocking" : "Unblocking") + " group " + Base64.encodeBytes(groupId)); - group.blocked = blocked; - account.getGroupStore().updateGroup(group); - account.save(); } + + group.blocked = blocked; + account.getGroupStore().updateGroup(group); + account.save(); } public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { From 2c5a70cc47301bf0f049eb2633976460d3ced1b7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 9 Aug 2020 15:11:09 +0200 Subject: [PATCH 084/103] Improve stderr output --- .../org/asamk/signal/commands/ListIdentitiesCommand.java | 2 +- .../org/asamk/signal/commands/UpdateContactCommand.java | 7 ++++--- src/main/java/org/asamk/signal/manager/Manager.java | 4 +--- .../org/asamk/signal/storage/protocol/JsonPreKeyStore.java | 2 +- .../asamk/signal/storage/protocol/JsonSessionStore.java | 2 +- .../signal/storage/protocol/JsonSignedPreKeyStore.java | 2 +- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index 529c7c30..edb67c76 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -43,7 +43,7 @@ public class ListIdentitiesCommand implements LocalCommand { printIdentityFingerprint(m, id); } } catch (InvalidNumberException e) { - System.out.println("Invalid number: " + e.getMessage()); + System.err.println("Invalid number: " + e.getMessage()); } } return 0; diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index ae84893d..d7fa3893 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -3,11 +3,11 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import java.io.IOException; - import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.util.InvalidNumberException; +import java.io.IOException; + public class UpdateContactCommand implements LocalCommand { @Override @@ -42,7 +42,8 @@ public class UpdateContactCommand implements LocalCommand { m.setExpirationTimer(number, expiration); } } catch (InvalidNumberException e) { - System.out.println("Invalid contact number: " + e.getMessage()); + System.err.println("Invalid contact number: " + e.getMessage()); + return 1; } catch (IOException e) { System.err.println("Update contact error: " + e.getMessage()); return 3; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 7d13eddb..851b7820 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -556,9 +556,7 @@ public class Manager implements Closeable { for (ContactTokenDetails contact : contacts) { newE164Members.remove(contact.getNumber()); } - System.err.println("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal"); - System.err.println("Aborting…"); - System.exit(1); + throw new IOException("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal"); } g.addMembers(members); diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java index 16248c02..d09b5d02 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java @@ -70,7 +70,7 @@ class JsonPreKeyStore implements PreKeyStore { try { preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText())); } catch (IOException e) { - System.out.println(String.format("Error while decoding prekey for: %s", preKeyId)); + System.err.println(String.format("Error while decoding prekey for: %s", preKeyId)); } } } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index 5ce99742..1505f1b0 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -153,7 +153,7 @@ class JsonSessionStore implements SessionStore { SessionInfo sessionInfo = new SessionInfo(serviceAddress, deviceId, Base64.decode(record)); sessionStore.sessions.add(sessionInfo); } catch (IOException e) { - System.out.println(String.format("Error while decoding session for: %s", sessionName)); + System.err.println(String.format("Error while decoding session for: %s", sessionName)); } } } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java index edad8e7e..3927b98d 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java @@ -87,7 +87,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore { try { preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText())); } catch (IOException e) { - System.out.println(String.format("Error while decoding prekey for: %s", preKeyId)); + System.err.println(String.format("Error while decoding prekey for: %s", preKeyId)); } } } From 2e1660f0667dca8166cc37ba6d21503945cf8d15 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 14 Aug 2020 20:52:21 +0200 Subject: [PATCH 085/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a703dd56..be611049 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ signal-cli is primarily intended to be used on servers to notify admins of impor ## Installation -You can [build signal-cli](#building) yourself, or use the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/) and there is a [FreeBSD port](https://www.freshports.org/net-im/signal-cli) available as well. You need to have at least JRE 7 installed, to run signal-cli. +You can [build signal-cli](#building) yourself, or use the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/) and there is a [FreeBSD port](https://www.freshports.org/net-im/signal-cli) available as well. You need to have at least JRE 8 installed, to run signal-cli. ### Install system-wide on Linux See [latest version](https://github.com/AsamK/signal-cli/releases). From 1be13392dabe6b4c3f79659f57990834a6e2c6d0 Mon Sep 17 00:00:00 2001 From: exquo <62397152+exquo@users.noreply.github.com> Date: Mon, 17 Aug 2020 07:26:52 +0000 Subject: [PATCH 086/103] Adjust link to a renamed wiki page --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index be611049..fb0d9d88 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ ``` You can find further instructions on the Wiki: -- [Install on Ubuntu](https://github.com/AsamK/signal-cli/wiki/HowToUbuntu) +- [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart) - [DBus Service](https://github.com/AsamK/signal-cli/wiki/DBus-service) ## Usage From 057168c421c79e0bffeecf2164d793d31e6185f2 Mon Sep 17 00:00:00 2001 From: "Lars K.W. Gohlke" Date: Tue, 25 Aug 2020 08:47:21 +0200 Subject: [PATCH 087/103] set to jdk11 --- .github/workflows/ci.yml | 2 +- build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c1f00b0..a017816d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '1.8', '12.0.2' ] + java: [ '11', '12.0.2' ] steps: - uses: actions/checkout@v1 diff --git a/build.gradle b/build.gradle index 23e8b01d..b747db40 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ apply plugin: 'java' apply plugin: 'application' apply plugin: 'eclipse' -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 +sourceCompatibility = JavaVersion.VERSION_11 +targetCompatibility = JavaVersion.VERSION_11 mainClassName = 'org.asamk.signal.Main' From 7e1764200b73072fef36c71697cd6bd7c2692539 Mon Sep 17 00:00:00 2001 From: "Lars K.W. Gohlke" Date: Wed, 26 Aug 2020 21:05:26 +0200 Subject: [PATCH 088/103] Update codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0815e8af..edf8a5a2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -15,6 +15,12 @@ jobs: runs-on: ubuntu-latest steps: + + - name: Setup Java JDK + uses: actions/setup-java@v1.3.0 + with: + java-version: 11 + - name: Checkout repository uses: actions/checkout@v2 with: From a747ddbae4342d4d844f8432d9a14799c2c015da Mon Sep 17 00:00:00 2001 From: "Lars K.W. Gohlke" Date: Mon, 31 Aug 2020 21:53:59 +0200 Subject: [PATCH 089/103] replaces strings to enable compile time checking --- .../java/org/asamk/signal/util/SecurityProvider.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/asamk/signal/util/SecurityProvider.java b/src/main/java/org/asamk/signal/util/SecurityProvider.java index 9177a781..043a7894 100644 --- a/src/main/java/org/asamk/signal/util/SecurityProvider.java +++ b/src/main/java/org/asamk/signal/util/SecurityProvider.java @@ -15,11 +15,11 @@ public class SecurityProvider extends Provider { put("SecureRandom.DEFAULT", DefaultRandom.class.getName()); // Workaround for BKS truststore - put("KeyStore.BKS", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$Std"); - put("KeyStore.BKS-V1", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$Version1"); - put("KeyStore.BouncyCastle", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$BouncyCastleStore"); - put("KeyFactory.X.509", "org.bouncycastle.jcajce.provider.asymmetric.x509.KeyFactory"); - put("CertificateFactory.X.509", "org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory"); + put("KeyStore.BKS", org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.Std.class.getCanonicalName()); + put("KeyStore.BKS-V1", org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.Version1.class.getCanonicalName()); + put("KeyStore.BouncyCastle", org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi.BouncyCastleStore.class.getCanonicalName()); + put("KeyFactory.X.509", org.bouncycastle.jcajce.provider.asymmetric.x509.KeyFactory.class.getCanonicalName()); + put("CertificateFactory.X.509", org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory.class.getCanonicalName()); } public static class DefaultRandom extends SecureRandomSpi { From 0dfe4d189b26c0879b99790f68d0984486f80bcc Mon Sep 17 00:00:00 2001 From: "Lars K.W. Gohlke" Date: Mon, 31 Aug 2020 21:54:11 +0200 Subject: [PATCH 090/103] cleanup: removes deprecation --- src/main/java/org/asamk/signal/util/SecurityProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/util/SecurityProvider.java b/src/main/java/org/asamk/signal/util/SecurityProvider.java index 043a7894..8a0e53f8 100644 --- a/src/main/java/org/asamk/signal/util/SecurityProvider.java +++ b/src/main/java/org/asamk/signal/util/SecurityProvider.java @@ -11,7 +11,7 @@ public class SecurityProvider extends Provider { private static final String info = "Security Provider v1.0"; public SecurityProvider() { - super(PROVIDER_NAME, 1.0, info); + super(PROVIDER_NAME, "1.0", info); put("SecureRandom.DEFAULT", DefaultRandom.class.getName()); // Workaround for BKS truststore From 4d8b103570d622b3ca7cdc4a8ec5b2859c91502a Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 10 Sep 2020 10:16:24 +0200 Subject: [PATCH 091/103] Use java 14 for CI builds --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a017816d..1924d640 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '11', '12.0.2' ] + java: [ '11', '14' ] steps: - uses: actions/checkout@v1 From 6c4d272309c7f7f8e42ecf1070e561a321e6c5f0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 10 Sep 2020 11:06:33 +0200 Subject: [PATCH 092/103] Update libsignal-service --- build.gradle | 2 +- .../java/org/asamk/signal/manager/Manager.java | 15 +++++++++------ src/main/java/org/asamk/signal/manager/Utils.java | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index b747db40..0f7c9a84 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } dependencies { - implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_12' + implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_13' implementation 'org.bouncycastle:bcprov-jdk15on:1.66' implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1' implementation 'com.github.hypfvieh:dbus-java:3.2.3' diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 851b7820..73c2b5c7 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -153,6 +153,7 @@ public class Manager implements Closeable { private SignalServiceAccountManager accountManager; private SignalServiceMessagePipe messagePipe = null; private SignalServiceMessagePipe unidentifiedMessagePipe = null; + private boolean discoverableByPhoneNumber = true; public Manager(SignalAccount account, PathConfig pathConfig, SignalServiceConfiguration serviceConfiguration, String userAgent) { this.account = account; @@ -286,7 +287,7 @@ public class Manager implements Closeable { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities); + accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities, discoverableByPhoneNumber); } public void setProfile(String name, File avatar) throws IOException { @@ -371,7 +372,7 @@ public class Manager implements Closeable { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); // TODO make unrestricted unidentified access configurable - VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities); + VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities, discoverableByPhoneNumber); UUID uuid = UuidUtil.parseOrNull(response.getUuid()); // TODO response.isStorageCapable() @@ -836,7 +837,8 @@ public class Manager implements Closeable { throw new StickerPackInvalidException("Could not find find " + sticker.file); } - StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or("")); + String contentType = Utils.getFileMimeType(new File(sticker.file), null); + StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or(""), contentType); stickers.add(stickerInfo); } @@ -853,7 +855,8 @@ public class Manager implements Closeable { throw new StickerPackInvalidException("Could not find find " + pack.cover.file); } - cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or("")); + String contentType = Utils.getFileMimeType(new File(pack.cover.file), null); + cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or(""), contentType); } return new SignalServiceStickerManifestUpload( @@ -925,10 +928,10 @@ public class Manager implements Closeable { private byte[] getSenderCertificate() { // TODO support UUID capable sender certificates - // byte[] certificate = accountManager.getSenderCertificate(); + // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy(); byte[] certificate; try { - certificate = accountManager.getSenderCertificateLegacy(); + certificate = accountManager.getSenderCertificate(); } catch (IOException e) { System.err.println("Failed to get sender certificate: " + e); return null; diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index 0244d409..05fcfb5e 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -58,7 +58,7 @@ class Utils { return signalServiceAttachments; } - private static String getFileMimeType(File file) throws IOException { + static String getFileMimeType(File file, String defaultMimeType) throws IOException { String mime = Files.probeContentType(file.toPath()); if (mime == null) { try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) { @@ -66,7 +66,7 @@ class Utils { } } if (mime == null) { - mime = "application/octet-stream"; + return defaultMimeType; } return mime; } @@ -74,7 +74,7 @@ class Utils { static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException { InputStream attachmentStream = new FileInputStream(attachmentFile); final long attachmentSize = attachmentFile.length(); - final String mime = getFileMimeType(attachmentFile); + final String mime = getFileMimeType(attachmentFile, "application/octet-stream"); // TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option final long uploadTimestamp = System.currentTimeMillis(); Optional preview = Optional.absent(); From 0f3aa2251962f240818169bc7b8ab98c61f02020 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 10 Sep 2020 10:19:59 +0200 Subject: [PATCH 093/103] Retrieve avatar profile image --- .../org/asamk/signal/manager/Manager.java | 78 ++++++++++++------- .../asamk/signal/manager/ServiceConfig.java | 1 + .../asamk/signal/manager/SignalProfile.java | 24 ++++-- .../java/org/asamk/signal/util/IOUtils.java | 17 ++++ 4 files changed, 85 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 73c2b5c7..31f2d9d6 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -142,6 +142,8 @@ import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import static org.asamk.signal.manager.ServiceConfig.capabilities; + public class Manager implements Closeable { private final SleepTimer timer = new UptimeSleepTimer(); @@ -287,7 +289,7 @@ public class Manager implements Closeable { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities, discoverableByPhoneNumber); + accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber); } public void setProfile(String name, File avatar) throws IOException { @@ -372,7 +374,7 @@ public class Manager implements Closeable { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); // TODO make unrestricted unidentified access configurable - VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, ServiceConfig.capabilities, discoverableByPhoneNumber); + VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber); UUID uuid = UuidUtil.parseOrNull(response.getUuid()); // TODO response.isStorageCapable() @@ -440,7 +442,27 @@ public class Manager implements Closeable { } private SignalProfile getRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess, ProfileKey profileKey) throws IOException { - return decryptProfile(getEncryptedRecipientProfile(address, unidentifiedAccess), profileKey); + final SignalServiceProfile encryptedProfile = getEncryptedRecipientProfile(address, unidentifiedAccess); + + File avatarFile = null; + try { + avatarFile = encryptedProfile.getAvatar() == null ? null : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey); + } catch (AssertionError e) { + System.err.println("Failed to retrieve profile avatar: " + e.getMessage()); + } + + ProfileCipher profileCipher = new ProfileCipher(profileKey); + try { + return new SignalProfile( + encryptedProfile.getIdentityKey(), + encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))), + avatarFile, + encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(), + encryptedProfile.isUnrestrictedUnidentifiedAccess(), + encryptedProfile.getCapabilities()); + } catch (InvalidCiphertextException e) { + return null; + } } private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { @@ -944,21 +966,6 @@ public class Manager implements Closeable { return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey()); } - private static SignalProfile decryptProfile(SignalServiceProfile encryptedProfile, ProfileKey profileKey) throws IOException { - ProfileCipher profileCipher = new ProfileCipher(profileKey); - try { - return new SignalProfile( - encryptedProfile.getIdentityKey(), - encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))), - encryptedProfile.getAvatar(), - encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(), - encryptedProfile.isUnrestrictedUnidentifiedAccess() - ); - } catch (InvalidCiphertextException e) { - return null; - } - } - private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { ContactInfo contact = account.getContactStore().getContact(recipient); if (contact == null || contact.profileKey == null) { @@ -1718,6 +1725,29 @@ public class Manager implements Closeable { } } + private File getProfileAvatarFile(SignalServiceAddress address) { + return new File(pathConfig.getAvatarsPath(), "profile-" + address.getLegacyIdentifier()); + } + + private File retrieveProfileAvatar(SignalServiceAddress address, String avatarPath, ProfileKey profileKey) throws IOException { + IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); + SignalServiceMessageReceiver receiver = getMessageReceiver(); + File outputFile = getProfileAvatarFile(address); + + File tmpFile = IOUtils.createTempFile(); + try (InputStream input = receiver.retrieveProfileAvatar(avatarPath, tmpFile, profileKey, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { + // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ... + IOUtils.copyStreamToFile(input, outputFile, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); + } finally { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + System.err.println("Failed to delete received avatar temp file “" + tmpFile + "”: " + e.getMessage()); + } + } + return outputFile; + } + public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { return new File(pathConfig.getAttachmentsPath(), attachmentId.toString()); } @@ -1743,17 +1773,7 @@ public class Manager implements Closeable { File tmpFile = IOUtils.createTempFile(); try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE)) { - try (OutputStream output = new FileOutputStream(outputFile)) { - byte[] buffer = new byte[4096]; - int read; - - while ((read = input.read(buffer)) != -1) { - output.write(buffer, 0, read); - } - } catch (FileNotFoundException e) { - e.printStackTrace(); - return null; - } + IOUtils.copyStreamToFile(input, outputFile); } finally { try { Files.delete(tmpFile.toPath()); diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java index f8394ada..a8b0c5b6 100644 --- a/src/main/java/org/asamk/signal/manager/ServiceConfig.java +++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java @@ -26,6 +26,7 @@ public class ServiceConfig { final static int PREKEY_MINIMUM_COUNT = 20; final static int PREKEY_BATCH_SIZE = 100; final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; + final static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024; private final static String URL = "https://textsecure-service.whispersystems.org"; private final static String CDN_URL = "https://cdn.signal.org"; diff --git a/src/main/java/org/asamk/signal/manager/SignalProfile.java b/src/main/java/org/asamk/signal/manager/SignalProfile.java index 4f529c0d..1217793c 100644 --- a/src/main/java/org/asamk/signal/manager/SignalProfile.java +++ b/src/main/java/org/asamk/signal/manager/SignalProfile.java @@ -1,23 +1,30 @@ package org.asamk.signal.manager; +import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; + +import java.io.File; + public class SignalProfile { private final String identityKey; private final String name; - private final String avatar; + private final File avatarFile; private final String unidentifiedAccess; private final boolean unrestrictedUnidentifiedAccess; - public SignalProfile(final String identityKey, final String name, final String avatar, final String unidentifiedAccess, final boolean unrestrictedUnidentifiedAccess) { + private final SignalServiceProfile.Capabilities capabilities; + + public SignalProfile(final String identityKey, final String name, final File avatarFile, final String unidentifiedAccess, final boolean unrestrictedUnidentifiedAccess, final SignalServiceProfile.Capabilities capabilities) { this.identityKey = identityKey; this.name = name; - this.avatar = avatar; + this.avatarFile = avatarFile; this.unidentifiedAccess = unidentifiedAccess; this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; + this.capabilities = capabilities; } public String getIdentityKey() { @@ -28,8 +35,8 @@ public class SignalProfile { return name; } - public String getAvatar() { - return avatar; + public File getAvatarFile() { + return avatarFile; } public String getUnidentifiedAccess() { @@ -40,14 +47,19 @@ public class SignalProfile { return unrestrictedUnidentifiedAccess; } + public SignalServiceProfile.Capabilities getCapabilities() { + return capabilities; + } + @Override public String toString() { return "SignalProfile{" + "identityKey='" + identityKey + '\'' + ", name='" + name + '\'' + - ", avatar='" + avatar + '\'' + + ", avatarFile=" + avatarFile + ", unidentifiedAccess='" + unidentifiedAccess + '\'' + ", unrestrictedUnidentifiedAccess=" + unrestrictedUnidentifiedAccess + + ", capabilities=" + capabilities + '}'; } } diff --git a/src/main/java/org/asamk/signal/util/IOUtils.java b/src/main/java/org/asamk/signal/util/IOUtils.java index f21c1572..1163d079 100644 --- a/src/main/java/org/asamk/signal/util/IOUtils.java +++ b/src/main/java/org/asamk/signal/util/IOUtils.java @@ -4,8 +4,10 @@ import org.whispersystems.signalservice.internal.util.Util; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.StringWriter; import java.nio.charset.Charset; import java.nio.file.Files; @@ -77,4 +79,19 @@ public class IOUtils { return System.getProperty("user.home") + "/.local/share"; } + + public static void copyStreamToFile(InputStream input, File outputFile) throws IOException { + copyStreamToFile(input, outputFile, 8192); + } + + public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException { + try (OutputStream output = new FileOutputStream(outputFile)) { + byte[] buffer = new byte[bufferSize]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } + } } From b94c1e50e62946a4d774a4c53ce70858145a4422 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 10 Sep 2020 14:20:16 +0200 Subject: [PATCH 094/103] Cache profiles for 24h before retrieving them again --- .../org/asamk/signal/manager/Manager.java | 14 +++ .../asamk/signal/storage/SignalAccount.java | 17 ++++ .../signal/storage/profiles/ProfileStore.java | 90 +++++++++++++++++++ .../profiles}/SignalProfile.java | 18 +++- .../storage/profiles/SignalProfileEntry.java | 30 +++++++ .../storage/protocol/RecipientStore.java | 3 - 6 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java rename src/main/java/org/asamk/signal/{manager => storage/profiles}/SignalProfile.java (69%) create mode 100644 src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 31f2d9d6..d16aabe6 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -22,6 +22,8 @@ import org.asamk.signal.storage.SignalAccount; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.JsonGroupStore; +import org.asamk.signal.storage.profiles.SignalProfile; +import org.asamk.signal.storage.profiles.SignalProfileEntry; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; @@ -442,6 +444,18 @@ public class Manager implements Closeable { } private SignalProfile getRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess, ProfileKey profileKey) throws IOException { + SignalProfileEntry profileEntry = account.getProfileStore().getProfile(address); + long now = new Date().getTime(); + // Profiles are cache for 24h before retrieving them again + if (profileEntry == null || profileEntry.getProfile() == null || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000) { + SignalProfile profile = retrieveRecipientProfile(address, unidentifiedAccess, profileKey); + profileEntry = new SignalProfileEntry(profileKey, now, profile); + account.getProfileStore().updateProfile(address, profileEntry); + } + return profileEntry.getProfile(); + } + + private SignalProfile retrieveRecipientProfile(SignalServiceAddress address, Optional unidentifiedAccess, ProfileKey profileKey) throws IOException { final SignalServiceProfile encryptedProfile = getEncryptedRecipientProfile(address, unidentifiedAccess); File avatarFile = null; diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index d0638e41..d9b37825 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -14,6 +14,7 @@ import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.contacts.JsonContactsStore; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.JsonGroupStore; +import org.asamk.signal.storage.profiles.ProfileStore; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; import org.asamk.signal.storage.protocol.RecipientStore; @@ -67,6 +68,7 @@ public class SignalAccount implements Closeable { private JsonGroupStore groupStore; private JsonContactsStore contactStore; private RecipientStore recipientStore; + private ProfileStore profileStore; private SignalAccount(final FileChannel fileChannel, final FileLock lock) { this.fileChannel = fileChannel; @@ -109,6 +111,7 @@ public class SignalAccount implements Closeable { account.groupStore = new JsonGroupStore(); account.contactStore = new JsonContactsStore(); account.recipientStore = new RecipientStore(); + account.profileStore = new ProfileStore(); account.registered = false; return account; @@ -134,6 +137,7 @@ public class SignalAccount implements Closeable { account.groupStore = new JsonGroupStore(); account.contactStore = new JsonContactsStore(); account.recipientStore = new RecipientStore(); + account.profileStore = new ProfileStore(); account.registered = true; account.isMultiDevice = true; @@ -245,6 +249,14 @@ public class SignalAccount implements Closeable { } } + JsonNode profileStoreNode = rootNode.get("profileStore"); + if (profileStoreNode != null) { + profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class); + } + if (profileStore == null) { + profileStore = new ProfileStore(); + } + JsonNode threadStoreNode = rootNode.get("threadStore"); if (threadStoreNode != null) { LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class); @@ -291,6 +303,7 @@ public class SignalAccount implements Closeable { .putPOJO("groupStore", groupStore) .putPOJO("contactStore", contactStore) .putPOJO("recipientStore", recipientStore) + .putPOJO("profileStore", profileStore) ; try { synchronized (fileChannel) { @@ -347,6 +360,10 @@ public class SignalAccount implements Closeable { return recipientStore; } + public ProfileStore getProfileStore() { + return profileStore; + } + public String getUsername() { return username; } diff --git a/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java b/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java new file mode 100644 index 00000000..691ac0ec --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java @@ -0,0 +1,90 @@ +package org.asamk.signal.storage.profiles; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.util.Base64; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class ProfileStore { + + private static final ObjectMapper jsonProcessor = new ObjectMapper(); + + @JsonProperty("profiles") + @JsonDeserialize(using = ProfileStoreDeserializer.class) + @JsonSerialize(using = ProfileStoreSerializer.class) + private final Map profiles = new HashMap<>(); + + public SignalProfileEntry getProfile(SignalServiceAddress serviceAddress) { + return profiles.get(serviceAddress); + } + + public SignalProfileEntry updateProfile(SignalServiceAddress serviceAddress, SignalProfileEntry profile) { + return profiles.put(serviceAddress, profile); + } + + public static class ProfileStoreDeserializer extends JsonDeserializer> { + + @Override + public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + Map addresses = new HashMap<>(); + + if (node.isArray()) { + for (JsonNode recipient : node) { + String recipientName = recipient.get("name").asText(); + UUID uuid = UuidUtil.parseOrThrow(recipient.get("uuid").asText()); + final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, recipientName); + ProfileKey profileKey = null; + try { + profileKey = new ProfileKey(Base64.decode(recipient.get("profileKey").asText())); + } catch (InvalidInputException ignored) { + } + long lastUpdateTimestamp = recipient.get("lastUpdateTimestamp").asLong(); + SignalProfile profile = jsonProcessor.treeToValue(recipient.get("profile"), SignalProfile.class); + addresses.put(serviceAddress, new SignalProfileEntry(profileKey, lastUpdateTimestamp, profile)); + } + } + + return addresses; + } + } + + public static class ProfileStoreSerializer extends JsonSerializer> { + + @Override + public void serialize(Map profiles, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { + json.writeStartArray(); + for (Map.Entry entry : profiles.entrySet()) { + final SignalServiceAddress address = entry.getKey(); + final SignalProfileEntry profileEntry = entry.getValue(); + json.writeStartObject(); + json.writeStringField("name", address.getNumber().get()); + json.writeStringField("uuid", address.getUuid().get().toString()); + json.writeStringField("profileKey", Base64.encodeBytes(profileEntry.getProfileKey().serialize())); + json.writeNumberField("lastUpdateTimestamp", profileEntry.getLastUpdateTimestamp()); + json.writeObjectField("profile", profileEntry.getProfile()); + json.writeEndObject(); + } + json.writeEndArray(); + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/SignalProfile.java b/src/main/java/org/asamk/signal/storage/profiles/SignalProfile.java similarity index 69% rename from src/main/java/org/asamk/signal/manager/SignalProfile.java rename to src/main/java/org/asamk/signal/storage/profiles/SignalProfile.java index 1217793c..71ab60e6 100644 --- a/src/main/java/org/asamk/signal/manager/SignalProfile.java +++ b/src/main/java/org/asamk/signal/storage/profiles/SignalProfile.java @@ -1,4 +1,6 @@ -package org.asamk.signal.manager; +package org.asamk.signal.storage.profiles; + +import com.fasterxml.jackson.annotation.JsonProperty; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; @@ -6,16 +8,21 @@ import java.io.File; public class SignalProfile { + @JsonProperty private final String identityKey; + @JsonProperty private final String name; private final File avatarFile; + @JsonProperty private final String unidentifiedAccess; + @JsonProperty private final boolean unrestrictedUnidentifiedAccess; + @JsonProperty private final SignalServiceProfile.Capabilities capabilities; public SignalProfile(final String identityKey, final String name, final File avatarFile, final String unidentifiedAccess, final boolean unrestrictedUnidentifiedAccess, final SignalServiceProfile.Capabilities capabilities) { @@ -27,6 +34,15 @@ public class SignalProfile { this.capabilities = capabilities; } + public SignalProfile(@JsonProperty("identityKey") final String identityKey, @JsonProperty("name") final String name, @JsonProperty("unidentifiedAccess") final String unidentifiedAccess, @JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess, @JsonProperty("capabilities") final SignalServiceProfile.Capabilities capabilities) { + this.identityKey = identityKey; + this.name = name; + this.avatarFile = null; + this.unidentifiedAccess = unidentifiedAccess; + this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; + this.capabilities = capabilities; + } + public String getIdentityKey() { return identityKey; } diff --git a/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java b/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java new file mode 100644 index 00000000..0423e12e --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java @@ -0,0 +1,30 @@ +package org.asamk.signal.storage.profiles; + +import org.signal.zkgroup.profiles.ProfileKey; + +public class SignalProfileEntry { + + private ProfileKey profileKey; + + private long lastUpdateTimestamp; + + private SignalProfile profile; + + public SignalProfileEntry(final ProfileKey profileKey, final long lastUpdateTimestamp, final SignalProfile profile) { + this.profileKey = profileKey; + this.lastUpdateTimestamp = lastUpdateTimestamp; + this.profile = profile; + } + + public ProfileKey getProfileKey() { + return profileKey; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public SignalProfile getProfile() { + return profile; + } +} diff --git a/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java b/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java index 4943bd36..47f06c46 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java @@ -26,9 +26,6 @@ public class RecipientStore { @JsonSerialize(using = RecipientStoreSerializer.class) private final Set addresses = new HashSet<>(); - public RecipientStore() { - } - public SignalServiceAddress resolveServiceAddress(SignalServiceAddress serviceAddress) { if (addresses.contains(serviceAddress)) { // If the Set already contains the exact address with UUID and Number, From 20e253372b0b976bc57c2b6cae2bced4bfa0333c Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 10 Sep 2020 14:40:52 +0200 Subject: [PATCH 095/103] Remove deprecated calls --- src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java | 2 -- src/main/java/org/asamk/signal/storage/SignalAccount.java | 1 - 2 files changed, 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java index 5aa57f44..dfe51fe7 100644 --- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -5,7 +5,6 @@ import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; import org.asamk.signal.json.JsonError; @@ -25,7 +24,6 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler this.m = m; this.jsonProcessor = new ObjectMapper(); jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect - jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index d9b37825..f3acb47c 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -75,7 +75,6 @@ public class SignalAccount implements Closeable { this.lock = lock; jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. - jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); From 4d80117e877b51083b3c797555273b69e914e170 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 10 Sep 2020 16:02:41 +0200 Subject: [PATCH 096/103] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 58910 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- gradlew.bat | 21 +++------------------ 4 files changed, 5 insertions(+), 20 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c053550b91381bbd28b1afc82d634bf73a8a..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 6656 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 Yte!%o?r4GTrVtxD61Ym!|5fq-1K$0e!T1w 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bb8b2fc2..12d38de6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index fbd7c515..4f906e0c 100755 --- a/gradlew +++ b/gradlew @@ -130,7 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath diff --git a/gradlew.bat b/gradlew.bat index a9f778a7..ac1b06f9 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +64,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell From 87c0282af50f4dc9741572018955bc6c8a1d3f90 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 10 Sep 2020 16:02:46 +0200 Subject: [PATCH 097/103] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0f7c9a84..4b466b6f 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_11 mainClassName = 'org.asamk.signal.Main' -version = '0.6.8' +version = '0.6.9' compileJava.options.encoding = 'UTF-8' From 7334c7845076aacc59eea674703f7541133bc87e Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 11 Sep 2020 09:14:41 +0200 Subject: [PATCH 098/103] Prevent corrupting account file, when serialization fails --- .../org/asamk/signal/storage/SignalAccount.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index f3acb47c..6043d803 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -34,6 +34,8 @@ import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.util.Base64; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.IOException; @@ -305,11 +307,16 @@ public class SignalAccount implements Closeable { .putPOJO("profileStore", profileStore) ; try { - synchronized (fileChannel) { - fileChannel.position(0); - jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode); - fileChannel.truncate(fileChannel.position()); - fileChannel.force(false); + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + // Write to memory first to prevent corrupting the file in case of serialization errors + jsonProcessor.writeValue(output, rootNode); + ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); + synchronized (fileChannel) { + fileChannel.position(0); + input.transferTo(Channels.newOutputStream(fileChannel)); + fileChannel.truncate(fileChannel.position()); + fileChannel.force(false); + } } } catch (Exception e) { System.err.println(String.format("Error saving file: %s", e.getMessage())); From 2ccff8f51deff3f16d3af75ea230a095c0d46b0f Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 11 Sep 2020 09:16:31 +0200 Subject: [PATCH 099/103] Support saving profiles for users without uuids Fixes #347 --- .../signal/storage/profiles/ProfileStore.java | 30 ++++++++++++------- .../protocol/JsonIdentityKeyStore.java | 2 +- .../storage/protocol/JsonSessionStore.java | 2 +- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java b/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java index 691ac0ec..43a39671 100644 --- a/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java +++ b/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java @@ -36,8 +36,8 @@ public class ProfileStore { return profiles.get(serviceAddress); } - public SignalProfileEntry updateProfile(SignalServiceAddress serviceAddress, SignalProfileEntry profile) { - return profiles.put(serviceAddress, profile); + public void updateProfile(SignalServiceAddress serviceAddress, SignalProfileEntry profile) { + profiles.put(serviceAddress, profile); } public static class ProfileStoreDeserializer extends JsonDeserializer> { @@ -49,17 +49,21 @@ public class ProfileStore { Map addresses = new HashMap<>(); if (node.isArray()) { - for (JsonNode recipient : node) { - String recipientName = recipient.get("name").asText(); - UUID uuid = UuidUtil.parseOrThrow(recipient.get("uuid").asText()); - final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, recipientName); + for (JsonNode entry : node) { + String name = entry.hasNonNull("name") + ? entry.get("name").asText() + : null; + UUID uuid = entry.hasNonNull("uuid") + ? UuidUtil.parseOrNull(entry.get("uuid").asText()) + : null; + final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, name); ProfileKey profileKey = null; try { - profileKey = new ProfileKey(Base64.decode(recipient.get("profileKey").asText())); + profileKey = new ProfileKey(Base64.decode(entry.get("profileKey").asText())); } catch (InvalidInputException ignored) { } - long lastUpdateTimestamp = recipient.get("lastUpdateTimestamp").asLong(); - SignalProfile profile = jsonProcessor.treeToValue(recipient.get("profile"), SignalProfile.class); + long lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong(); + SignalProfile profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class); addresses.put(serviceAddress, new SignalProfileEntry(profileKey, lastUpdateTimestamp, profile)); } } @@ -77,8 +81,12 @@ public class ProfileStore { final SignalServiceAddress address = entry.getKey(); final SignalProfileEntry profileEntry = entry.getValue(); json.writeStartObject(); - json.writeStringField("name", address.getNumber().get()); - json.writeStringField("uuid", address.getUuid().get().toString()); + if (address.getNumber().isPresent()) { + json.writeStringField("name", address.getNumber().get()); + } + if (address.getUuid().isPresent()) { + json.writeStringField("uuid", address.getUuid().get().toString()); + } json.writeStringField("profileKey", Base64.encodeBytes(profileEntry.getProfileKey().serialize())); json.writeNumberField("lastUpdateTimestamp", profileEntry.getLastUpdateTimestamp()); json.writeObjectField("profile", profileEntry.getProfile()); diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index fcb71c5e..45c4024c 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -190,7 +190,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { JsonNode trustedKeysNode = node.get("trustedKeys"); if (trustedKeysNode.isArray()) { for (JsonNode trustedKey : trustedKeysNode) { - String trustedKeyName = trustedKey.has("name") + String trustedKeyName = trustedKey.hasNonNull("name") ? trustedKey.get("name").asText() : null; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index 1505f1b0..b1ca622a 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -133,7 +133,7 @@ class JsonSessionStore implements SessionStore { if (node.isArray()) { for (JsonNode session : node) { - String sessionName = session.has("name") + String sessionName = session.hasNonNull("name") ? session.get("name").asText() : null; if (UuidUtil.isUuid(sessionName)) { From bb59d1c9c9f32981261a722ea7b1f9af3fab7624 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 11 Sep 2020 09:23:30 +0200 Subject: [PATCH 100/103] Ignore error if downloading profile avatar fails --- src/main/java/org/asamk/signal/manager/Manager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index d16aabe6..d96a1f21 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -461,8 +461,8 @@ public class Manager implements Closeable { File avatarFile = null; try { avatarFile = encryptedProfile.getAvatar() == null ? null : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey); - } catch (AssertionError e) { - System.err.println("Failed to retrieve profile avatar: " + e.getMessage()); + } catch (Throwable e) { + System.err.println("Failed to retrieve profile avatar, ignoring: " + e.getMessage()); } ProfileCipher profileCipher = new ProfileCipher(profileKey); From bb24a2aa31b872e2a94bff105e38be70edaac700 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 11 Sep 2020 15:35:44 +0200 Subject: [PATCH 101/103] Add workaround to fix crashes if native libzkgroup is not available Fixes #350 Fixes #349 Fixes #344 --- build.gradle | 2 +- .../java/org/asamk/signal/manager/Manager.java | 15 ++++++++++++++- .../asamk/signal/manager/ProvisioningManager.java | 15 ++++++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 4b466b6f..48a3ba48 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } dependencies { - implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_13' + implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_14' implementation 'org.bouncycastle:bcprov-jdk15on:1.66' implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1' implementation 'com.github.hypfvieh:dbus-java:3.2.3' diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index d96a1f21..c4969fa4 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -66,6 +66,8 @@ import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -107,6 +109,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; +import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.util.Base64; @@ -178,7 +181,17 @@ public class Manager implements Closeable { } private SignalServiceAccountManager createSignalServiceAccountManager() { - return new SignalServiceAccountManager(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), userAgent, timer); + GroupsV2Operations groupsV2Operations; + try { + groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceConfiguration)); + } catch (Throwable ignored) { + groupsV2Operations = null; + } + return new SignalServiceAccountManager(serviceConfiguration, + new DynamicCredentialsProvider(account.getUuid(), account.getUsername(), account.getPassword(), null, account.getDeviceId()), + userAgent, + groupsV2Operations, + timer); } private IdentityKeyPair getIdentityKeyPair() { diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index db6f4238..4f1aca18 100644 --- a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -23,10 +23,13 @@ import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.KeyHelper; import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import java.io.IOException; import java.util.concurrent.TimeoutException; @@ -51,7 +54,17 @@ public class ProvisioningManager { registrationId = KeyHelper.generateRegistrationId(false); password = KeyUtils.createPassword(); final SleepTimer timer = new UptimeSleepTimer(); - accountManager = new SignalServiceAccountManager(serviceConfiguration, null, null, password, SignalServiceAddress.DEFAULT_DEVICE_ID, userAgent, timer); + GroupsV2Operations groupsV2Operations; + try { + groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceConfiguration)); + } catch (Throwable ignored) { + groupsV2Operations = null; + } + accountManager = new SignalServiceAccountManager(serviceConfiguration, + new DynamicCredentialsProvider(null, null, password, null, SignalServiceAddress.DEFAULT_DEVICE_ID), + userAgent, + groupsV2Operations, + timer); } public String getDeviceLinkUri() throws TimeoutException, IOException { From ad509e8097733e91837338f6470441567f6970b2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 11 Sep 2020 16:22:30 +0200 Subject: [PATCH 102/103] Refactor ProfileStore to handle name/uuid addresses correctly --- .../org/asamk/signal/manager/Manager.java | 4 +- .../signal/storage/profiles/ProfileStore.java | 41 ++++++++++++------- .../storage/profiles/SignalProfileEntry.java | 16 ++++++-- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index c4969fa4..2ce59cdc 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -462,8 +462,8 @@ public class Manager implements Closeable { // Profiles are cache for 24h before retrieving them again if (profileEntry == null || profileEntry.getProfile() == null || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000) { SignalProfile profile = retrieveRecipientProfile(address, unidentifiedAccess, profileKey); - profileEntry = new SignalProfileEntry(profileKey, now, profile); - account.getProfileStore().updateProfile(address, profileEntry); + account.getProfileStore().updateProfile(address, profileKey, now, profile); + return profile; } return profileEntry.getProfile(); } diff --git a/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java b/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java index 43a39671..24b08968 100644 --- a/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java +++ b/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java @@ -19,8 +19,9 @@ import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.util.Base64; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; -import java.util.Map; +import java.util.List; import java.util.UUID; public class ProfileStore { @@ -30,23 +31,36 @@ public class ProfileStore { @JsonProperty("profiles") @JsonDeserialize(using = ProfileStoreDeserializer.class) @JsonSerialize(using = ProfileStoreSerializer.class) - private final Map profiles = new HashMap<>(); + private final List profiles = new ArrayList<>(); public SignalProfileEntry getProfile(SignalServiceAddress serviceAddress) { - return profiles.get(serviceAddress); + for (SignalProfileEntry entry : profiles) { + if (entry.getServiceAddress().matches(serviceAddress)) { + return entry; + } + } + return null; } - public void updateProfile(SignalServiceAddress serviceAddress, SignalProfileEntry profile) { - profiles.put(serviceAddress, profile); + public void updateProfile(SignalServiceAddress serviceAddress, ProfileKey profileKey, long now, SignalProfile profile) { + SignalProfileEntry newEntry = new SignalProfileEntry(serviceAddress, profileKey, now, profile); + for (int i = 0; i < profiles.size(); i++) { + if (profiles.get(i).getServiceAddress().matches(serviceAddress)) { + profiles.set(i, newEntry); + return; + } + } + + profiles.add(newEntry); } - public static class ProfileStoreDeserializer extends JsonDeserializer> { + public static class ProfileStoreDeserializer extends JsonDeserializer> { @Override - public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + public List deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - Map addresses = new HashMap<>(); + List addresses = new ArrayList<>(); if (node.isArray()) { for (JsonNode entry : node) { @@ -64,7 +78,7 @@ public class ProfileStore { } long lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong(); SignalProfile profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class); - addresses.put(serviceAddress, new SignalProfileEntry(profileKey, lastUpdateTimestamp, profile)); + addresses.add(new SignalProfileEntry(serviceAddress, profileKey, lastUpdateTimestamp, profile)); } } @@ -72,14 +86,13 @@ public class ProfileStore { } } - public static class ProfileStoreSerializer extends JsonSerializer> { + public static class ProfileStoreSerializer extends JsonSerializer> { @Override - public void serialize(Map profiles, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { + public void serialize(List profiles, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { json.writeStartArray(); - for (Map.Entry entry : profiles.entrySet()) { - final SignalServiceAddress address = entry.getKey(); - final SignalProfileEntry profileEntry = entry.getValue(); + for (SignalProfileEntry profileEntry : profiles) { + final SignalServiceAddress address = profileEntry.getServiceAddress(); json.writeStartObject(); if (address.getNumber().isPresent()) { json.writeStringField("name", address.getNumber().get()); diff --git a/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java b/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java index 0423e12e..ed1f7127 100644 --- a/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java +++ b/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java @@ -1,21 +1,29 @@ package org.asamk.signal.storage.profiles; import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; public class SignalProfileEntry { - private ProfileKey profileKey; + private final SignalServiceAddress serviceAddress; - private long lastUpdateTimestamp; + private final ProfileKey profileKey; - private SignalProfile profile; + private final long lastUpdateTimestamp; - public SignalProfileEntry(final ProfileKey profileKey, final long lastUpdateTimestamp, final SignalProfile profile) { + private final SignalProfile profile; + + public SignalProfileEntry(final SignalServiceAddress serviceAddress, final ProfileKey profileKey, final long lastUpdateTimestamp, final SignalProfile profile) { + this.serviceAddress = serviceAddress; this.profileKey = profileKey; this.lastUpdateTimestamp = lastUpdateTimestamp; this.profile = profile; } + public SignalServiceAddress getServiceAddress() { + return serviceAddress; + } + public ProfileKey getProfileKey() { return profileKey; } From e713fd83a9c812307fec2685198fc9c2dfec3389 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 11 Sep 2020 16:25:12 +0200 Subject: [PATCH 103/103] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 48a3ba48..0607c12b 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_11 mainClassName = 'org.asamk.signal.Main' -version = '0.6.9' +version = '0.6.10' compileJava.options.encoding = 'UTF-8'