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/build.gradle b/build.gradle index 5c8984da..e6cad8c8 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' @@ -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_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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f3d88b1c..490fda85 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a2bf1313..a4b44297 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.2.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists 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 diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 835b9c34..f8b353c7 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -13,11 +13,11 @@ 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; @@ -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/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 0b3da9a2..3ba65f7d 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -47,14 +47,15 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { System.out.println(message.getBody().get()); if (!message.isEndSession() && - !(message.getGroupInfo().isPresent() && - message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) { + !(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.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() : "", JsonDbusReceiveMessageHandler.getAttachments(message, m))); } catch (DBusException e) { @@ -66,7 +67,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { if (sync_message.getSent().isPresent()) { SentTranscriptMessage transcript = sync_message.getSent().get(); - if (!envelope.isUnidentifiedSender() && envelope.hasSource() && (transcript.getDestination().isPresent() || transcript.getMessage().getGroupInfo().isPresent())) { + if (!envelope.isUnidentifiedSender() && envelope.hasSource() && (transcript.getDestination().isPresent() || transcript.getMessage().getGroupContext().isPresent())) { SignalServiceDataMessage message = transcript.getMessage(); try { @@ -75,7 +76,8 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { transcript.getTimestamp(), envelope.getSourceAddress().getNumber().get(), transcript.getDestination().isPresent() ? transcript.getDestination().get().getNumber().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() : "", JsonDbusReceiveMessageHandler.getAttachments(message, m))); } catch (DBusException e) { 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/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/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/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index bf39b93f..fe3dd669 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() + ")"); } @@ -109,7 +103,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()) { @@ -129,6 +123,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"; } @@ -144,14 +145,14 @@ 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()) { 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()) { @@ -168,7 +169,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()) { @@ -253,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()) { @@ -322,7 +323,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 +331,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/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/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/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/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/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/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index dd3e5e46..529c7c30 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -7,18 +7,16 @@ 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, - theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toStringCondensed(theirId.getFingerprint()), digits)); + 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)); } @Override @@ -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/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 931ebb67..42ab7327 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())))); 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())); 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; 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/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/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 7b72caae..eb1327ac 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -9,8 +9,8 @@ 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; import java.io.IOException; @@ -19,6 +19,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 { @@ -61,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 { @@ -90,6 +91,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/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/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; 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/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java new file mode 100644 index 00000000..fe25966c --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java @@ -0,0 +1,34 @@ +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(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/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/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java index 364f1eab..fff8179c 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(32); + } + 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 f5bbe146..ccb2fbce 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; @@ -27,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; @@ -77,6 +80,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.SignalServiceReceiptMessage; +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; @@ -93,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; @@ -101,8 +106,10 @@ 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; import org.whispersystems.util.Base64; import java.io.File; @@ -113,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; @@ -125,16 +134,16 @@ 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; 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 { - private static final SignalServiceProfile.Capabilities capabilities = new SignalServiceProfile.Capabilities(false, false); - private final String settingsPath; private final String dataPath; private final String attachmentsPath; @@ -160,12 +169,12 @@ public class Manager implements Signal { return username; } - private SignalServiceAddress getSelfAddress() { - return new SignalServiceAddress(null, 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); + return new SignalServiceAccountManager(BaseConfig.serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer); } private IdentityKey getIdentity() { @@ -181,6 +190,10 @@ public class Manager implements Signal { } private String getMessageCachePath(String sender) { + if (sender == null || sender.isEmpty()) { + return getMessageCachePath(); + } + return getMessageCachePath() + "/" + sender.replace("/", "_"); } @@ -199,18 +212,20 @@ public class Manager implements Signal { return; } account = SignalAccount.load(dataPath, username); + account.setResolver(this::resolveSignalServiceAddress); migrateLegacyConfigs(); accountManager = getSignalServiceAccountManager(); - try { - if (account.isRegistered() && accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) { + if (account.isRegistered()) { + if (accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) { refreshPreKeys(); account.save(); } - } catch (AuthorizationFailedException e) { - System.err.println("Authorization failed, was the number registered elsewhere?"); - throw e; + if (account.getUuid() == null) { + account.setUuid(accountManager.getOwnUuid()); + account.save(); + } } } @@ -245,9 +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 { ProfileKey profileKey = KeyUtils.createProfileKey(); account = SignalAccount.create(dataPath, username, identityKey, registrationId, profileKey); + account.setResolver(this::resolveSignalServiceAddress); account.save(); } } @@ -261,6 +278,7 @@ public class Manager implements Signal { createNewIdentity(); } account.setPassword(KeyUtils.createPassword()); + account.setUuid(null); accountManager = getSignalServiceAccountManager(); if (voiceVerification) { @@ -274,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 { @@ -334,7 +352,8 @@ 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); + account.setResolver(this::resolveSignalServiceAddress); refreshPreKeys(); @@ -412,11 +431,13 @@ 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, 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(); @@ -442,11 +463,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()); } @@ -492,12 +513,10 @@ public class Manager implements Signal { if (g == null) { throw new GroupNotFoundException(groupId); } - for (String member : g.members) { - if (member.equals(this.username)) { - return g; - } + if (!g.isMember(account.getSelfAddress())) { + throw new NotAGroupMemberException(groupId, g.name); } - throw new NotAGroupMemberException(groupId, g.name); + return g; } public List getGroups() { @@ -518,26 +537,20 @@ 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); - // Don't send group message to ourself - final List membersSend = new ArrayList<>(g.members); - membersSend.remove(this.username); - sendMessageLegacy(messageBuilder, membersSend); + messageBuilder.withExpiration(g.messageExpirationTime); + + 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) - .withProfileKey(account.getProfileKey().serialize()); + .withReaction(reaction); if (groupId != null) { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) .withId(groupId) @@ -545,10 +558,7 @@ public class Manager implements Signal { 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); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { @@ -560,18 +570,18 @@ public class Manager implements Signal { .asGroupMessage(group); final GroupInfo g = getGroupForSending(groupId); - g.members.remove(this.username); + g.removeMember(account.getSelfAddress()); account.getGroupStore().updateGroup(g); - sendMessageLegacy(messageBuilder, g.members); + 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 { GroupInfo g; if (groupId == null) { // Create new group g = new GroupInfo(KeyUtils.createGroupId()); - g.members.add(username); + g.addMembers(Collections.singleton(account.getSelfAddress())); } else { g = getGroupForSending(groupId); } @@ -581,31 +591,26 @@ 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); - } - 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) { @@ -618,29 +623,24 @@ public class Manager implements Signal { SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); - // Don't send group message to ourself - final List membersSend = new ArrayList<>(g.members); - membersSend.remove(this.username); - sendMessageLegacy(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, g.getMembersWithout(account.getSelfAddress())); 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.isMember(recipient)) { 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) { @@ -658,18 +658,12 @@ 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, String recipient) throws IOException, EncapsulatedExceptions { + private void sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException, EncapsulatedExceptions { if (groupId == null) { return; } @@ -680,20 +674,21 @@ 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 - final List membersSend = new ArrayList<>(); - membersSend.add(recipient); - sendMessageLegacy(messageBuilder, membersSend); + 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 { + throws EncapsulatedExceptions, AttachmentInvalidException, IOException, InvalidNumberException { List recipients = new ArrayList<>(1); recipients.add(recipient); sendMessage(message, attachments, recipients); @@ -702,7 +697,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); @@ -720,32 +715,29 @@ 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, + public void sendMessageReaction(String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients) - 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) - .withProfileKey(account.getProfileKey().serialize()); - sendMessageLegacy(messageBuilder, recipients); + .withReaction(reaction); + 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); - ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber); + ContactInfo contact = account.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number)); if (contact == null) { return ""; } else { @@ -755,14 +747,13 @@ public class Manager implements Signal { @Override public void setContactName(String number, String name) throws InvalidNumberException { - String canonicalizedNumber = Utils.canonicalizeNumber(number, username); - ContactInfo contact = account.getContactStore().getContact(canonicalizedNumber); + final SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number); + ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { - contact = new ContactInfo(); - contact.number = canonicalizedNumber; - System.err.println("Add contact " + canonicalizedNumber + " named " + name); + contact = new ContactInfo(address); + 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); @@ -771,14 +762,16 @@ public class Manager implements Signal { @Override public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException { - number = Utils.canonicalizeNumber(number, username); - ContactInfo contact = account.getContactStore().getContact(number); + setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked); + } + + private void setContactBlocked(SignalServiceAddress address, boolean blocked) { + ContactInfo contact = account.getContactStore().getContact(address); if (contact == null) { - contact = new ContactInfo(); - contact.number = number; - System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + number); + contact = new ContactInfo(address); + 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); @@ -822,14 +815,14 @@ 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()); } } @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; } @@ -842,19 +835,130 @@ 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); } /** - * Change the expiration timer for a thread (number of groupId) - * - * @param numberOrGroupId - * @param messageExpirationTimer + * 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); + } + + /** + * 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 { + 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."); + } + + if (pack.stickers.isEmpty()) { + throw new StickerPackInvalidException("Must include stickers."); + } + + List stickers = new ArrayList<>(pack.stickers.size()); + for (JsonStickerPack.JsonSticker sticker : pack.stickers) { + if (sticker.file == null) { + throw new StickerPackInvalidException("Must set a 'file' field on each sticker."); + } + + 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(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or("")); + stickers.add(stickerInfo); + } + + StickerInfo cover = null; + if (pack.cover != null) { + if (pack.cover.file == null) { + throw new StickerPackInvalidException("Must set a 'file' field on the cover."); + } + + Pair data; + try { + data = getInputStreamAndLength(rootPath, zip, pack.cover.file); + } catch (IOException ignored) { + throw new StickerPackInvalidException("Could not find find " + pack.cover.file); + } + + cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or("")); + } + + return new SignalServiceStickerManifestUpload( + pack.title, + pack.author, + cover, + stickers); + } + + private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException { + InputStream inputStream; + if (zip != null) { + inputStream = zip.getInputStream(zip.getEntry("manifest.json")); + } else { + inputStream = new FileInputStream((new File(rootPath, "manifest.json"))); + } + return new ObjectMapper().readValue(inputStream, JsonStickerPack.class); + } + + 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 { + final File file = new File(rootPath, subfile); + return new Pair<>(new FileInputStream(file), file.length()); + } } private void requestSyncGroups() throws IOException { @@ -897,8 +1001,16 @@ public class Manager implements Signal { } } - private byte[] getSenderCertificate() throws IOException { - byte[] certificate = accountManager.getSenderCertificate(); + private byte[] getSenderCertificate() { + // TODO support UUID capable sender certificates + // byte[] certificate = accountManager.getSenderCertificate(); + 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; } @@ -922,18 +1034,24 @@ public class Manager implements Signal { } } - private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) throws IOException { - ContactInfo contact = account.getContactStore().getContact(recipient.getNumber().get()); + private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { + ContactInfo contact = account.getContactStore().getContact(recipient); if (contact == null || contact.profileKey == null) { return null; } 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; @@ -946,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(); @@ -964,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)); @@ -972,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(); @@ -991,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; } } @@ -1005,7 +1133,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); @@ -1015,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()) { @@ -1027,14 +1155,17 @@ 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()); + for (String number : numbers) { + signalServiceAddresses.add(canonicalizeAndResolveSignalServiceAddress(number)); + } + return signalServiceAddresses; + } + + private List sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) + throws IOException { if (messagePipe == null) { messagePipe = getMessageReceiver().createMessagePipe(); } @@ -1046,22 +1177,22 @@ 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<>(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); + 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 (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(), @@ -1071,30 +1202,32 @@ 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) { - 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; } else { // Send to all individually, so sync messages are sent correctly - List results = new ArrayList<>(recipientsTS.size()); - for (SignalServiceAddress address : recipientsTS) { - ThreadInfo thread = account.getThreadStore().getThread(address.getNumber().get()); - if (thread != null) { - messageBuilder.withExpiration(thread.messageExpirationTime); + List results = new ArrayList<>(recipients.size()); + for (SignalServiceAddress address : recipients) { + 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 { 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())); } } @@ -1102,34 +1235,35 @@ public class Manager implements Signal { } } finally { if (message != null && message.isEndSession()) { - for (SignalServiceAddress recipient : recipientsTS) { - handleEndSession(recipient.getNumber().get()); + for (SignalServiceAddress recipient : recipients) { + handleEndSession(recipient); } } account.save(); } } - 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()); + 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); } } - private void handleEndSession(String source) { + private void handleEndSession(SignalServiceAddress source) { account.getSignalProtocolStore().deleteAllSessions(source); } - private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, SignalServiceAddress destination, boolean ignoreAttachments) { - String threadId; - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); - threadId = Base64.encodeBytes(groupInfo.getGroupId()); + private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) { + 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: @@ -1175,7 +1309,7 @@ public class Manager implements Signal { e.printStackTrace(); } } else { - group.members.remove(source); + group.removeMember(source); account.getGroupStore().updateGroup(group); } break; @@ -1191,25 +1325,30 @@ public class Manager implements Signal { } break; } - } else { - if (isSync) { - threadId = destination.getNumber().get(); - } else { - threadId = source; - } } if (message.isEndSession()) { - handleEndSession(isSync ? destination.getNumber().get() : source); + handleEndSession(isSync ? destination : source); } 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.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()); + } + 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) { @@ -1224,19 +1363,24 @@ public class Manager implements Signal { } } if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { - if (source.equals(username)) { + 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(); - contact.number = source; - } - contact.profileKey = Base64.encodeBytes(message.getProfileKey().get()); - account.getContactStore().updateContact(contact); } if (message.getPreviews().isPresent()) { final List previews = message.getPreviews().get(); @@ -1260,6 +1404,7 @@ public class Manager implements Signal { } for (final File dir : Objects.requireNonNull(cachePath.listFiles())) { if (!dir.isDirectory()) { + retryFailedReceivedMessage(handler, ignoreAttachments, dir); continue; } @@ -1267,38 +1412,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(); @@ -1343,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()); @@ -1372,15 +1521,15 @@ 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; } 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; @@ -1400,14 +1549,23 @@ public class Manager implements Signal { } if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - handleSignalServiceDataMessage(message, false, sender.getNumber().get(), getSelfAddress(), ignoreAttachments); + + 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()) { 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(); @@ -1451,7 +1609,10 @@ public class Manager implements Signal { } syncGroup.addMembers(g.getMembers()); if (!g.isActive()) { - syncGroup.members.remove(username); + syncGroup.removeMember(account.getSelfAddress()); + } else { + // Add ourself to the member set as it's marked as active + syncGroup.addMembers(Collections.singleton(account.getSelfAddress())); } syncGroup.blocked = g.isBlocked(); if (g.getColor().isPresent()) { @@ -1481,13 +1642,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 { @@ -1512,10 +1667,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(); @@ -1528,16 +1682,10 @@ 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()) { - 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(); @@ -1563,7 +1711,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 @@ -1665,10 +1813,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.members.contains(username), 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)); } } @@ -1701,17 +1848,9 @@ 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)) { - 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; @@ -1722,7 +1861,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)); } @@ -1782,20 +1921,19 @@ public class Manager implements Signal { } public ContactInfo getContact(String number) { - return account.getContactStore().getContact(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, username); - return new Pair<>(canonicalizedNumber, account.getSignalProtocolStore().getIdentities(canonicalizedNumber)); + public List getIdentities(String number) throws InvalidNumberException { + return account.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number)); } /** @@ -1804,8 +1942,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; } @@ -1814,9 +1953,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(); } @@ -1832,19 +1971,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(); } @@ -1860,15 +2000,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(); } @@ -1878,8 +2019,26 @@ public class Manager implements Signal { return true; } - public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) { - return Utils.computeSafetyNumber(username, 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 eccc1656..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; @@ -35,12 +34,10 @@ 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 java.util.UUID; import static org.whispersystems.signalservice.internal.util.Util.isEmpty; @@ -149,38 +146,19 @@ 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); 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 @@ -208,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()) { @@ -256,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 fd4da41f..73f79a48 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -10,10 +10,14 @@ 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.protocol.SignalServiceAddressResolver; +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; @@ -32,6 +36,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 +44,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; @@ -53,7 +59,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 @@ -82,27 +87,26 @@ 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; 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; 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; @@ -138,6 +142,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(); @@ -189,10 +201,27 @@ 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()) { + if (thread.id == null || thread.id.isEmpty()) { + continue; + } + 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) { + } + } } } @@ -202,6 +231,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) @@ -214,7 +244,6 @@ public class SignalAccount { .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) .putPOJO("contactStore", contactStore) - .putPOJO("threadStore", threadStore) ; try { synchronized (fileChannel) { @@ -245,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); @@ -269,16 +302,20 @@ public class SignalAccount { return contactStore; } - public JsonThreadStore getThreadStore() { - return threadStore; - } - public String getUsername() { return username; } + public UUID getUuid() { + return uuid; + } + + public void setUuid(final UUID uuid) { + this.uuid = 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/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java index 2ab2a515..4d3a5e95 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,9 +15,15 @@ public class ContactInfo { @JsonProperty public String number; + @JsonProperty + public UUID uuid; + @JsonProperty public String color; + @JsonProperty(defaultValue = "0") + public int messageExpirationTime; + @JsonProperty public String profileKey; @@ -28,8 +36,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; - } - } } 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..e3a45420 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,9 +32,13 @@ 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 = "0") + public int messageExpirationTime; @JsonProperty(defaultValue = "false") public boolean blocked; @JsonProperty @@ -38,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); @@ -47,6 +65,7 @@ public class GroupInfo { this.blocked = blocked; this.inboxPosition = inboxPosition; this.archived = archived; + this.messageExpirationTime = messageExpirationTime; } @JsonIgnore @@ -56,16 +75,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; } } } 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..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 - * @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/storage/threads/JsonThreadStore.java b/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java similarity index 87% 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..6bea1bfe 100644 --- a/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java +++ b/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java @@ -18,23 +18,15 @@ 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<>(); - 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()); } 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."); + } } 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) { 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()) { 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 { 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); + } + } }