From 9912da9546718fc2fe5139b277e0d5263a85ef67 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 21 Dec 2020 16:59:54 +0100 Subject: [PATCH 01/32] Show group invite link in group list --- .../signal/commands/ListGroupsCommand.java | 8 +- .../signal/manager/GroupInviteLinkUrl.java | 140 ++++++++++++++++++ .../signal/manager/GroupLinkPassword.java | 40 +++++ .../org/asamk/signal/manager/KeyUtils.java | 2 +- .../signal/storage/groups/GroupInfo.java | 4 + .../signal/storage/groups/GroupInfoV1.java | 6 + .../signal/storage/groups/GroupInfoV2.java | 15 ++ 7 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/asamk/signal/manager/GroupInviteLinkUrl.java create mode 100644 src/main/java/org/asamk/signal/manager/GroupLinkPassword.java diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index b9f54a6b..e7844fda 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -4,6 +4,7 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.GroupInviteLinkUrl; import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.groups.GroupInfo; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -35,15 +36,18 @@ public class ListGroupsCommand implements LocalCommand { .map(SignalServiceAddress::getLegacyIdentifier) .collect(Collectors.toSet()); + final GroupInviteLinkUrl groupInviteLink = group.getGroupInviteLink(); + System.out.println(String.format( - "Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s", + "Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s Link: %s", Base64.encodeBytes(group.groupId), group.getTitle(), group.isMember(m.getSelfAddress()), group.isBlocked(), members, pendingMembers, - requestingMembers)); + requestingMembers, + groupInviteLink == null ? '-' : groupInviteLink.getUrl())); } else { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", Base64.encodeBytes(group.groupId), diff --git a/src/main/java/org/asamk/signal/manager/GroupInviteLinkUrl.java b/src/main/java/org/asamk/signal/manager/GroupInviteLinkUrl.java new file mode 100644 index 00000000..67ce7892 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/GroupInviteLinkUrl.java @@ -0,0 +1,140 @@ +package org.asamk.signal.manager; + +import com.google.protobuf.ByteString; + +import org.signal.storageservice.protos.groups.GroupInviteLink; +import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; +import org.whispersystems.util.Base64UrlSafe; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +public final class GroupInviteLinkUrl { + + private static final String GROUP_URL_HOST = "signal.group"; + private static final String GROUP_URL_PREFIX = "https://" + GROUP_URL_HOST + "/#"; + + private final GroupMasterKey groupMasterKey; + private final GroupLinkPassword password; + private final String url; + + public static GroupInviteLinkUrl forGroup(GroupMasterKey groupMasterKey, DecryptedGroup group) { + return new GroupInviteLinkUrl(groupMasterKey, + GroupLinkPassword.fromBytes(group.getInviteLinkPassword().toByteArray())); + } + + public static boolean isGroupLink(String urlString) { + return getGroupUrl(urlString) != null; + } + + /** + * @return null iff not a group url. + * @throws InvalidGroupLinkException If group url, but cannot be parsed. + */ + public static GroupInviteLinkUrl fromUri(String urlString) throws InvalidGroupLinkException, UnknownGroupLinkVersionException { + URI uri = getGroupUrl(urlString); + + if (uri == null) { + return null; + } + + try { + if (!"/".equals(uri.getPath()) && uri.getPath().length() > 0) { + throw new InvalidGroupLinkException("No path was expected in uri"); + } + + String encoding = uri.getFragment(); + + if (encoding == null || encoding.length() == 0) { + throw new InvalidGroupLinkException("No reference was in the uri"); + } + + byte[] bytes = Base64UrlSafe.decodePaddingAgnostic(encoding); + GroupInviteLink groupInviteLink = GroupInviteLink.parseFrom(bytes); + + switch (groupInviteLink.getContentsCase()) { + case V1CONTENTS: { + GroupInviteLink.GroupInviteLinkContentsV1 groupInviteLinkContentsV1 = groupInviteLink.getV1Contents(); + GroupMasterKey groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.getGroupMasterKey() + .toByteArray()); + GroupLinkPassword password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.getInviteLinkPassword() + .toByteArray()); + + return new GroupInviteLinkUrl(groupMasterKey, password); + } + default: + throw new UnknownGroupLinkVersionException("Url contains no known group link content"); + } + } catch (InvalidInputException | IOException e) { + throw new InvalidGroupLinkException(e); + } + } + + /** + * @return {@link URI} if the host name matches. + */ + private static URI getGroupUrl(String urlString) { + try { + URI url = new URI(urlString); + + if (!"https".equalsIgnoreCase(url.getScheme()) && !"sgnl".equalsIgnoreCase(url.getScheme())) { + return null; + } + + return GROUP_URL_HOST.equalsIgnoreCase(url.getHost()) ? url : null; + } catch (URISyntaxException e) { + return null; + } + } + + private GroupInviteLinkUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) { + this.groupMasterKey = groupMasterKey; + this.password = password; + this.url = createUrl(groupMasterKey, password); + } + + protected static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) { + GroupInviteLink groupInviteLink = GroupInviteLink.newBuilder() + .setV1Contents(GroupInviteLink.GroupInviteLinkContentsV1.newBuilder() + .setGroupMasterKey(ByteString.copyFrom(groupMasterKey.serialize())) + .setInviteLinkPassword(ByteString.copyFrom(password.serialize()))) + .build(); + + String encoding = Base64UrlSafe.encodeBytesWithoutPadding(groupInviteLink.toByteArray()); + + return GROUP_URL_PREFIX + encoding; + } + + public String getUrl() { + return url; + } + + public GroupMasterKey getGroupMasterKey() { + return groupMasterKey; + } + + public GroupLinkPassword getPassword() { + return password; + } + + public final static class InvalidGroupLinkException extends Exception { + + public InvalidGroupLinkException(String message) { + super(message); + } + + public InvalidGroupLinkException(Throwable cause) { + super(cause); + } + } + + public final static class UnknownGroupLinkVersionException extends Exception { + + public UnknownGroupLinkVersionException(String message) { + super(message); + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/GroupLinkPassword.java b/src/main/java/org/asamk/signal/manager/GroupLinkPassword.java new file mode 100644 index 00000000..38e2aaf4 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/GroupLinkPassword.java @@ -0,0 +1,40 @@ +package org.asamk.signal.manager; + +import java.util.Arrays; + +public final class GroupLinkPassword { + + private static final int SIZE = 16; + + private final byte[] bytes; + + public static GroupLinkPassword createNew() { + return new GroupLinkPassword(KeyUtils.getSecretBytes(SIZE)); + } + + public static GroupLinkPassword fromBytes(byte[] bytes) { + return new GroupLinkPassword(bytes); + } + + private GroupLinkPassword(byte[] bytes) { + this.bytes = bytes; + } + + public byte[] serialize() { + return bytes.clone(); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof GroupLinkPassword)) { + return false; + } + + return Arrays.equals(bytes, ((GroupLinkPassword) other).bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } +} diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java index 1f12193c..d6f332c0 100644 --- a/src/main/java/org/asamk/signal/manager/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java @@ -39,7 +39,7 @@ class KeyUtils { return Base64.encodeBytes(secret); } - private static byte[] getSecretBytes(int size) { + static byte[] getSecretBytes(int size) { byte[] secret = new byte[size]; RandomUtils.getSecureRandom().nextBytes(secret); return secret; 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 4cd410b8..3571985b 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -3,6 +3,7 @@ package org.asamk.signal.storage.groups; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import org.asamk.signal.manager.GroupInviteLinkUrl; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Set; @@ -21,6 +22,9 @@ public abstract class GroupInfo { @JsonIgnore public abstract String getTitle(); + @JsonIgnore + public abstract GroupInviteLinkUrl getGroupInviteLink(); + @JsonIgnore public abstract Set getMembers(); diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java index 42c40e94..b06e2436 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.asamk.signal.manager.GroupInviteLinkUrl; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.io.IOException; @@ -55,6 +56,11 @@ public class GroupInfoV1 extends GroupInfo { return name; } + @Override + public GroupInviteLinkUrl getGroupInviteLink() { + return null; + } + public GroupInfoV1( @JsonProperty("groupId") byte[] groupId, @JsonProperty("expectedV2Id") byte[] expectedV2Id, diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java index a205d140..65f8c9a4 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java @@ -1,5 +1,7 @@ package org.asamk.signal.storage.groups; +import org.asamk.signal.manager.GroupInviteLinkUrl; +import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.zkgroup.groups.GroupMasterKey; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -41,6 +43,19 @@ public class GroupInfoV2 extends GroupInfo { return this.group.getTitle(); } + @Override + public GroupInviteLinkUrl getGroupInviteLink() { + if (this.group == null || this.group.getInviteLinkPassword() == null || ( + this.group.getAccessControl().getAddFromInviteLink() != AccessControl.AccessRequired.ANY + && this.group.getAccessControl().getAddFromInviteLink() + != AccessControl.AccessRequired.ADMINISTRATOR + )) { + return null; + } + + return GroupInviteLinkUrl.forGroup(masterKey, group); + } + @Override public Set getMembers() { if (this.group == null) { From 445e8592c491438e3ea863c2382ea42b1161fe84 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 21 Dec 2020 20:03:19 +0100 Subject: [PATCH 02/32] Implement join group via invitation link --- CHANGELOG.md | 1 + man/signal-cli.1.adoc | 7 + .../org/asamk/signal/commands/Commands.java | 1 + .../signal/commands/JoinGroupCommand.java | 84 ++++++++++++ .../signal/commands/SendReactionCommand.java | 3 +- .../org/asamk/signal/manager/GroupUtils.java | 4 +- .../org/asamk/signal/manager/Manager.java | 124 +++++++++++------- .../signal/manager/helper/GroupHelper.java | 67 +++++++++- .../org/asamk/signal/util/ErrorUtils.java | 4 +- 9 files changed, 239 insertions(+), 56 deletions(-) create mode 100644 src/main/java/org/asamk/signal/commands/JoinGroupCommand.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4972cbea..a5094ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - Accept group invitation with `updateGroup -g GROUP_ID` - Decline group invitation with `quitGroup -g GROUP_ID` +- Join group via invitation link `joinGroup --uri https://signal.group/#...` ### Fixed - Include group ids for v2 groups in json output diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index e9b52593..b5c22167 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -178,6 +178,13 @@ Don’t download attachments of received messages. *--json*:: Output received messages in json format, one object per line. +=== joinGroup + +Join a group via an invitation link. + +*--uri*:: +The invitation link URI (starts with `https://signal.group/#`) + === updateGroup Create or update a group. diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 183b40a0..85e7af32 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -16,6 +16,7 @@ public class Commands { addCommand("listDevices", new ListDevicesCommand()); addCommand("listGroups", new ListGroupsCommand()); addCommand("listIdentities", new ListIdentitiesCommand()); + addCommand("joinGroup", new JoinGroupCommand()); addCommand("quitGroup", new QuitGroupCommand()); addCommand("receive", new ReceiveCommand()); addCommand("register", new RegisterCommand()); diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java new file mode 100644 index 00000000..62b996cf --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -0,0 +1,84 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.Signal; +import org.asamk.signal.manager.GroupInviteLinkUrl; +import org.asamk.signal.manager.Manager; +import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; +import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException; +import org.whispersystems.util.Base64; + +import java.io.IOException; +import java.util.List; + +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; +import static org.asamk.signal.util.ErrorUtils.handleIOException; +import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults; + +public class JoinGroupCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("--uri").required(true).help("Specify the uri with the group invitation link."); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + final GroupInviteLinkUrl linkUrl; + String uri = ns.getString("uri"); + try { + linkUrl = GroupInviteLinkUrl.fromUri(uri); + } catch (GroupInviteLinkUrl.InvalidGroupLinkException e) { + System.err.println("Group link is invalid: " + e.getMessage()); + return 2; + } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) { + System.err.println("Group link was created with an incompatible version: " + e.getMessage()); + return 2; + } + + if (linkUrl == null) { + System.err.println("Link is not a signal group invitation link"); + return 2; + } + + try { + final Pair> results = m.joinGroup(linkUrl); + byte[] newGroupId = results.first(); + if (!m.getGroup(newGroupId).isMember(m.getSelfAddress())) { + System.out.println("Requested to join group \"" + Base64.encodeBytes(newGroupId) + "\""); + } else { + System.out.println("Joined group \"" + Base64.encodeBytes(newGroupId) + "\""); + } + return handleTimestampAndSendMessageResults(0, results.second()); + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } catch (GroupPatchNotAcceptedException e) { + System.err.println("Failed to join group, maybe already a member"); + return 1; + } catch (IOException e) { + e.printStackTrace(); + handleIOException(e); + return 1; + } catch (Signal.Error.AttachmentInvalid e) { + System.err.println("Failed to add avatar attachment for group\": " + e.getMessage()); + return 1; + } catch (DBusExecutionException e) { + System.err.println("Failed to send message: " + e.getMessage()); + return 1; + } catch (GroupLinkNotActiveException e) { + System.err.println("Group link is not valid: " + e.getMessage()); + return 2; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 6e5f24bb..000a1349 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -74,8 +74,7 @@ public class SendReactionCommand implements LocalCommand { targetTimestamp, ns.getList("recipient")); } - handleTimestampAndSendMessageResults(results.first(), results.second()); - return 0; + return handleTimestampAndSendMessageResults(results.first(), results.second()); } catch (IOException e) { handleIOException(e); return 3; diff --git a/src/main/java/org/asamk/signal/manager/GroupUtils.java b/src/main/java/org/asamk/signal/manager/GroupUtils.java index 0d192002..a0e95c7a 100644 --- a/src/main/java/org/asamk/signal/manager/GroupUtils.java +++ b/src/main/java/org/asamk/signal/manager/GroupUtils.java @@ -35,9 +35,9 @@ public class GroupUtils { return groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); } - public static GroupMasterKey deriveV2MigrationMasterKey(byte[] groupId) { + public static GroupMasterKey deriveV2MigrationMasterKey(byte[] groupIdV1) { try { - return new GroupMasterKey(new HKDFv3().deriveSecrets(groupId, + return new GroupMasterKey(new HKDFv3().deriveSecrets(groupIdV1, "GV2 Migration".getBytes(), GroupMasterKey.SIZE)); } catch (InvalidInputException e) { diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 6db40566..ab063d1b 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -45,6 +45,7 @@ import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.SelfSendException; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.VerificationFailedException; @@ -78,10 +79,10 @@ import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; -import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -849,6 +850,34 @@ public class Manager implements Closeable { return new Pair<>(g.groupId, result.second()); } + public Pair> joinGroup( + GroupInviteLinkUrl inviteLinkUrl + ) throws IOException, GroupLinkNotActiveException { + return sendJoinGroupMessage(inviteLinkUrl); + } + + private Pair> sendJoinGroupMessage( + GroupInviteLinkUrl inviteLinkUrl + ) throws IOException, GroupLinkNotActiveException { + final DecryptedGroupJoinInfo groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), + inviteLinkUrl.getPassword()); + final GroupChange groupChange = groupHelper.joinGroup(inviteLinkUrl.getGroupMasterKey(), + inviteLinkUrl.getPassword(), + groupJoinInfo); + final GroupInfoV2 group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(), + groupJoinInfo.getRevision() + 1, + groupChange.toByteArray()); + + if (group.getGroup() == null) { + // Only requested member, can't send update to group members + return new Pair<>(group.groupId, List.of()); + } + + final Pair> result = sendUpdateGroupMessage(group, group.getGroup(), groupChange); + + return new Pair<>(group.groupId, result.second()); + } + private Pair> sendUpdateGroupMessage( GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange ) throws IOException { @@ -1584,48 +1613,12 @@ public class Manager implements Closeable { final SignalServiceGroupV2 groupContext = message.getGroupContext().get().getGroupV2().get(); final GroupMasterKey groupMasterKey = groupContext.getMasterKey(); - final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - - byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); - GroupInfo groupInfo = account.getGroupStore().getGroupByV2Id(groupId); - if (groupInfo instanceof GroupInfoV1) { - // Received a v2 group message for a v2 group, we need to locally migrate the group - account.getGroupStore().deleteGroup(groupInfo.groupId); - GroupInfoV2 groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); - groupInfoV2.setGroup(getDecryptedGroup(groupSecretParams)); - account.getGroupStore().updateGroup(groupInfoV2); - System.err.println("Locally migrated group " - + Base64.encodeBytes(groupInfo.groupId) - + " to group v2, id: " - + Base64.encodeBytes(groupInfoV2.groupId) - + " !!!"); - } else if (groupInfo == null || groupInfo instanceof GroupInfoV2) { - GroupInfoV2 groupInfoV2 = groupInfo == null - ? new GroupInfoV2(groupId, groupMasterKey) - : (GroupInfoV2) groupInfo; - - if (groupInfoV2.getGroup() == null - || groupInfoV2.getGroup().getRevision() < groupContext.getRevision()) { - DecryptedGroup group = null; - if (groupContext.hasSignedGroupChange() - && groupInfoV2.getGroup() != null - && groupInfoV2.getGroup().getRevision() + 1 == groupContext.getRevision()) { - group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), - groupContext.getSignedGroupChange(), - groupMasterKey); - if (group != null) { - storeProfileKeysFromMembers(group); - } - } - if (group == null) { - group = getDecryptedGroup(groupSecretParams); - } - groupInfoV2.setGroup(group); - account.getGroupStore().updateGroup(groupInfoV2); - } - } + getOrMigrateGroup(groupMasterKey, + groupContext.getRevision(), + groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null); } } + final SignalServiceAddress conversationPartnerAddress = isSync ? destination : source; if (message.isEndSession()) { handleEndSession(conversationPartnerAddress); @@ -1708,16 +1701,47 @@ public class Manager implements Closeable { return actions; } - private DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) { - try { - final GroupsV2AuthorizationString groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); - DecryptedGroup group = groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString); - storeProfileKeysFromMembers(group); - return group; - } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { - System.err.println("Failed to retrieve Group V2 info, ignoring ..."); - return null; + private GroupInfoV2 getOrMigrateGroup( + final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange + ) { + final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + + byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); + GroupInfo groupInfo = account.getGroupStore().getGroupByV2Id(groupId); + final GroupInfoV2 groupInfoV2; + if (groupInfo instanceof GroupInfoV1) { + // Received a v2 group message for a v1 group, we need to locally migrate the group + account.getGroupStore().deleteGroup(groupInfo.groupId); + groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); + System.err.println("Locally migrated group " + + Base64.encodeBytes(groupInfo.groupId) + + " to group v2, id: " + + Base64.encodeBytes(groupInfoV2.groupId) + + " !!!"); + } else if (groupInfo instanceof GroupInfoV2) { + groupInfoV2 = (GroupInfoV2) groupInfo; + } else { + groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); } + + if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) { + DecryptedGroup group = null; + if (signedGroupChange != null + && groupInfoV2.getGroup() != null + && groupInfoV2.getGroup().getRevision() + 1 == revision) { + group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), signedGroupChange, groupMasterKey); + } + if (group == null) { + group = groupHelper.getDecryptedGroup(groupSecretParams); + } + if (group != null) { + storeProfileKeysFromMembers(group); + } + groupInfoV2.setGroup(group); + account.getGroupStore().updateGroup(groupInfoV2); + } + + return groupInfoV2; } private void storeProfileKeysFromMembers(final DecryptedGroup group) { diff --git a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 1f7e69e3..d66831bb 100644 --- a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -2,12 +2,15 @@ package org.asamk.signal.manager.helper; import com.google.protobuf.InvalidProtocolBufferException; +import org.asamk.signal.manager.GroupLinkPassword; import org.asamk.signal.storage.groups.GroupInfoV2; import org.asamk.signal.util.IOUtils; +import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; +import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.VerificationFailedException; @@ -19,6 +22,7 @@ import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.groupsv2.GroupCandidate; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; @@ -66,6 +70,27 @@ public class GroupHelper { this.groupAuthorizationProvider = groupAuthorizationProvider; } + public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) { + try { + final GroupsV2AuthorizationString groupsV2AuthorizationString = groupAuthorizationProvider.getAuthorizationForToday( + groupSecretParams); + return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString); + } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { + System.err.println("Failed to retrieve Group V2 info, ignoring ..."); + return null; + } + } + + public DecryptedGroupJoinInfo getDecryptedGroupJoinInfo( + GroupMasterKey groupMasterKey, GroupLinkPassword password + ) throws IOException, GroupLinkNotActiveException { + GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + + return groupsV2Api.getGroupJoinInfo(groupSecretParams, + Optional.fromNullable(password).transform(GroupLinkPassword::serialize), + groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams)); + } + public GroupInfoV2 createGroupV2( String name, Collection members, String avatarFile ) throws IOException { @@ -223,6 +248,32 @@ public class GroupHelper { } } + public GroupChange joinGroup( + GroupMasterKey groupMasterKey, + GroupLinkPassword groupLinkPassword, + DecryptedGroupJoinInfo decryptedGroupJoinInfo + ) throws IOException { + final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams); + + final SignalServiceAddress selfAddress = this.selfAddressProvider.getSelfAddress(); + final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential( + selfAddress); + if (profileKeyCredential == null) { + throw new IOException("Cannot join a V2 group as self does not have a versioned profile"); + } + + boolean requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink() + == AccessControl.AccessRequired.ADMINISTRATOR; + GroupChange.Actions.Builder change = requestToJoin + ? groupOperations.createGroupJoinRequest(profileKeyCredential) + : groupOperations.createGroupJoinDirect(profileKeyCredential); + + change.setSourceUuid(UuidUtil.toByteString(selfAddress.getUuid().get())); + + return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword); + } + public Pair acceptInvite(GroupInfoV2 groupInfoV2) throws IOException { final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); final GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams); @@ -284,13 +335,27 @@ public class GroupHelper { throw new IOException(e); } - GroupChange signedGroupChange = groupsV2Api.patchGroup(change.build(), + GroupChange signedGroupChange = groupsV2Api.patchGroup(changeActions, groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams), Optional.absent()); return new Pair<>(decryptedGroupState, signedGroupChange); } + private GroupChange commitChange( + GroupSecretParams groupSecretParams, + int currentRevision, + GroupChange.Actions.Builder change, + GroupLinkPassword password + ) throws IOException { + final int nextRevision = currentRevision + 1; + final GroupChange.Actions changeActions = change.setRevision(nextRevision).build(); + + return groupsV2Api.patchGroup(changeActions, + groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams), + Optional.fromNullable(password).transform(GroupLinkPassword::serialize)); + } + public DecryptedGroup getUpdatedDecryptedGroup( DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey ) { diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 8e27dd90..8e65d440 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -22,7 +22,9 @@ public class ErrorUtils { } public static int handleTimestampAndSendMessageResults(long timestamp, List results) { - System.out.println(timestamp); + if (timestamp != 0) { + System.out.println(timestamp); + } List errors = getErrorMessagesFromSendMessageResults(results); return handleSendMessageResultErrors(errors); } From 8957a08453edb6a68ddae1a9bd01485032c4e2d8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 21 Dec 2020 21:56:48 +0100 Subject: [PATCH 03/32] Print warning if libzkgroup is missing --- src/main/java/org/asamk/signal/Main.java | 5 +++++ src/main/java/org/asamk/signal/manager/ServiceConfig.java | 4 ++++ .../java/org/asamk/signal/storage/profiles/ProfileStore.java | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index a547e026..de827b1c 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -106,6 +106,11 @@ public class Main { final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration( BaseConfig.USER_AGENT); + if (!ServiceConfig.getCapabilities().isGv2()) { + System.err.println("WARNING: Support for new group V2 is disabled," + + " because the required native library dependency is missing: libzkgroup"); + } + if (username == null) { ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT); return handleCommands(ns, pm); diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java index 5721b166..353670ae 100644 --- a/src/main/java/org/asamk/signal/manager/ServiceConfig.java +++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java @@ -88,6 +88,10 @@ public class ServiceConfig { zkGroupServerPublicParams); } + public static AccountAttributes.Capabilities getCapabilities() { + return capabilities; + } + static KeyStore getIasKeyStore() { try { TrustStore contactTrustStore = IAS_TRUST_STORE; diff --git a/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java b/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java index 527ec15a..3b3d3f9f 100644 --- a/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java +++ b/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java @@ -112,7 +112,7 @@ public class ProfileStore { try { profileKeyCredential = new ProfileKeyCredential(Base64.decode(entry.get( "profileKeyCredential").asText())); - } catch (InvalidInputException ignored) { + } catch (Throwable ignored) { } } long lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong(); From 83d5d53d8a201cebbdbfb2ebe3f39d8d91a80091 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 21 Dec 2020 21:59:41 +0100 Subject: [PATCH 04/32] Bump version --- CHANGELOG.md | 2 ++ build.gradle | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5094ac2..38907d6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] + +## [0.7.1] - 2020-12-21 ### Added - Accept group invitation with `updateGroup -g GROUP_ID` - Decline group invitation with `quitGroup -g GROUP_ID` diff --git a/build.gradle b/build.gradle index 8b097612..48b57a4f 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_11 mainClassName = 'org.asamk.signal.Main' -version = '0.7.0' +version = '0.7.1' compileJava.options.encoding = 'UTF-8' From 548c313b4ca373559c14f3746d31c0b6cb9a721b Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 23 Dec 2020 00:18:21 +0100 Subject: [PATCH 05/32] Download quote attachment thumbnails and slightly improve the quote output --- .../org/asamk/signal/ReceiveMessageHandler.java | 6 +++--- .../java/org/asamk/signal/manager/Manager.java | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 5925b2b8..7ef89d19 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -452,9 +452,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (quote.getAttachments().size() > 0) { System.out.println(" Attachments: "); for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : quote.getAttachments()) { - System.out.println(" Filename: " + attachment.getFileName()); - System.out.println(" Type: " + attachment.getContentType()); - System.out.println(" Thumbnail:"); + System.out.println(" - Filename: " + attachment.getFileName()); + System.out.println(" Type: " + attachment.getContentType()); + System.out.println(" Thumbnail:"); if (attachment.getThumbnail() != null) { printAttachment(attachment.getThumbnail()); } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index ab063d1b..8a46846f 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1690,6 +1690,23 @@ public class Manager implements Closeable { } } } + if (message.getQuote().isPresent()) { + final SignalServiceDataMessage.Quote quote = message.getQuote().get(); + + for (SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment : quote.getAttachments()) { + final SignalServiceAttachment attachment = quotedAttachment.getThumbnail(); + if (attachment != null && attachment.isPointer()) { + try { + retrieveAttachment(attachment.asPointer()); + } catch (IOException | InvalidMessageException | MissingConfigurationException e) { + System.err.println("Failed to retrieve attachment (" + + attachment.asPointer().getRemoteId() + + "): " + + e.getMessage()); + } + } + } + } if (message.getSticker().isPresent()) { final SignalServiceDataMessage.Sticker messageSticker = message.getSticker().get(); Sticker sticker = account.getStickerStore().getSticker(messageSticker.getPackId()); From 58db3cbd53f3faec94ddfcd5e029865a380e6242 Mon Sep 17 00:00:00 2001 From: Atomic-Bean <75401809+Atomic-Bean@users.noreply.github.com> Date: Wed, 23 Dec 2020 20:53:40 +1030 Subject: [PATCH 06/32] Quotes, Mentions and Reactions in non-daemon JSON mode (#389) * Added support for quotes, mentions and reactions in non-daemon JSON output --- .../signal/JsonReceiveMessageHandler.java | 2 +- .../asamk/signal/ReceiveMessageHandler.java | 31 +++++++++----- .../asamk/signal/json/JsonDataMessage.java | 27 +++++++++++- .../org/asamk/signal/json/JsonMention.java | 22 ++++++++++ .../signal/json/JsonMessageEnvelope.java | 7 ++-- .../java/org/asamk/signal/json/JsonQuote.java | 42 +++++++++++++++++++ .../signal/json/JsonQuotedAttachment.java | 21 ++++++++++ .../org/asamk/signal/json/JsonReaction.java | 19 +++++++++ .../signal/json/JsonSyncDataMessage.java | 5 ++- .../asamk/signal/json/JsonSyncMessage.java | 5 ++- 10 files changed, 162 insertions(+), 19 deletions(-) create mode 100644 src/main/java/org/asamk/signal/json/JsonMention.java create mode 100644 src/main/java/org/asamk/signal/json/JsonQuote.java create mode 100644 src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java create mode 100644 src/main/java/org/asamk/signal/json/JsonReaction.java diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java index dfe51fe7..363fc304 100644 --- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -35,7 +35,7 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler result.putPOJO("error", new JsonError(exception)); } if (envelope != null) { - result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content)); + result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content, m)); } try { jsonProcessor.writeValue(System.out, result); diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 7ef89d19..f961fa1f 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -447,8 +447,14 @@ 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().getLegacyIdentifier()); + System.out.println(" Author: " + m.resolveSignalServiceAddress(quote.getAuthor()).getLegacyIdentifier()); System.out.println(" Text: " + quote.getText()); + if (quote.getMentions().size() > 0) { + System.out.println(" Mentions: "); + for (SignalServiceDataMessage.Mention mention : quote.getMentions()) { + printMention(mention, m); + } + } if (quote.getAttachments().size() > 0) { System.out.println(" Attachments: "); for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : quote.getAttachments()) { @@ -467,16 +473,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("Remote delete message: timestamp = " + remoteDelete.getTargetSentTimestamp()); } if (message.getMentions().isPresent()) { - final List mentions = message.getMentions().get(); System.out.println("Mentions: "); - for (SignalServiceDataMessage.Mention mention : mentions) { - System.out.println("- " - + mention.getUuid() - + ": " - + mention.getStart() - + " (length: " - + mention.getLength() - + ")"); + for (SignalServiceDataMessage.Mention mention : message.getMentions().get()) { + printMention(mention, m); } } @@ -488,6 +487,18 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } } + private void printMention(SignalServiceDataMessage.Mention mention, Manager m) { + System.out.println("- " + + m.resolveSignalServiceAddress( + new SignalServiceAddress(mention.getUuid(), null) + ).getLegacyIdentifier() + + ": " + + mention.getStart() + + " (length: " + + mention.getLength() + + ")"); + } + private void printAttachment(SignalServiceAttachment attachment) { System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + ( attachment.isStream() ? "Stream" : "" diff --git a/src/main/java/org/asamk/signal/json/JsonDataMessage.java b/src/main/java/org/asamk/signal/json/JsonDataMessage.java index 653a59e6..957e3a79 100644 --- a/src/main/java/org/asamk/signal/json/JsonDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonDataMessage.java @@ -1,6 +1,7 @@ package org.asamk.signal.json; import org.asamk.Signal; +import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; @@ -15,10 +16,14 @@ class JsonDataMessage { long timestamp; String message; int expiresInSeconds; + + JsonReaction reaction; + JsonQuote quote; + List mentions; List attachments; JsonGroupInfo groupInfo; - JsonDataMessage(SignalServiceDataMessage dataMessage) { + JsonDataMessage(SignalServiceDataMessage dataMessage, Manager m) { this.timestamp = dataMessage.getTimestamp(); if (dataMessage.getGroupContext().isPresent()) { if (dataMessage.getGroupContext().get().getGroupV1().isPresent()) { @@ -33,6 +38,20 @@ class JsonDataMessage { this.message = dataMessage.getBody().get(); } this.expiresInSeconds = dataMessage.getExpiresInSeconds(); + if (dataMessage.getReaction().isPresent()) { + this.reaction = new JsonReaction(dataMessage.getReaction().get(), m); + } + if (dataMessage.getQuote().isPresent()) { + this.quote = new JsonQuote(dataMessage.getQuote().get(), m); + } + if (dataMessage.getMentions().isPresent()) { + this.mentions = new ArrayList<>(dataMessage.getMentions().get().size()); + for (SignalServiceDataMessage.Mention mention : dataMessage.getMentions().get()) { + this.mentions.add(new JsonMention(mention, m)); + } + } else { + this.mentions = new ArrayList<>(); + } if (dataMessage.getAttachments().isPresent()) { this.attachments = new ArrayList<>(dataMessage.getAttachments().get().size()); for (SignalServiceAttachment attachment : dataMessage.getAttachments().get()) { @@ -47,6 +66,9 @@ class JsonDataMessage { timestamp = messageReceived.getTimestamp(); message = messageReceived.getMessage(); groupInfo = new JsonGroupInfo(messageReceived.getGroupId()); + reaction = null; // TODO Replace these 3 with the proper commands + quote = null; + mentions = null; attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList()); } @@ -54,6 +76,9 @@ class JsonDataMessage { timestamp = messageReceived.getTimestamp(); message = messageReceived.getMessage(); groupInfo = new JsonGroupInfo(messageReceived.getGroupId()); + reaction = null; // TODO Replace these 3 with the proper commands + quote = null; + mentions = null; attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList()); } } diff --git a/src/main/java/org/asamk/signal/json/JsonMention.java b/src/main/java/org/asamk/signal/json/JsonMention.java new file mode 100644 index 00000000..80683842 --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonMention.java @@ -0,0 +1,22 @@ +package org.asamk.signal.json; + +import org.asamk.signal.manager.Manager; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public class JsonMention { + + String name; + int start; + int length; + + JsonMention(SignalServiceDataMessage.Mention mention, Manager m) { + this.name = m.resolveSignalServiceAddress( + new SignalServiceAddress(mention.getUuid(), null) + ).getLegacyIdentifier(); + this.start = mention.getStart(); + this.length = mention.getLength(); + + } + +} diff --git a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index 5e5e6a33..787f62e2 100644 --- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -1,6 +1,7 @@ package org.asamk.signal.json; import org.asamk.Signal; +import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -18,7 +19,7 @@ public class JsonMessageEnvelope { JsonCallMessage callMessage; JsonReceiptMessage receiptMessage; - public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content) { + public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content, Manager m) { if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { SignalServiceAddress source = envelope.getSourceAddress(); this.source = source.getLegacyIdentifier(); @@ -35,10 +36,10 @@ public class JsonMessageEnvelope { this.sourceDevice = content.getSenderDevice(); } if (content.getDataMessage().isPresent()) { - this.dataMessage = new JsonDataMessage(content.getDataMessage().get()); + this.dataMessage = new JsonDataMessage(content.getDataMessage().get(), m); } if (content.getSyncMessage().isPresent()) { - this.syncMessage = new JsonSyncMessage(content.getSyncMessage().get()); + this.syncMessage = new JsonSyncMessage(content.getSyncMessage().get(), m); } if (content.getCallMessage().isPresent()) { this.callMessage = new JsonCallMessage(content.getCallMessage().get()); diff --git a/src/main/java/org/asamk/signal/json/JsonQuote.java b/src/main/java/org/asamk/signal/json/JsonQuote.java new file mode 100644 index 00000000..9a740582 --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonQuote.java @@ -0,0 +1,42 @@ +package org.asamk.signal.json; + +import org.asamk.signal.manager.Manager; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; + +import java.util.ArrayList; +import java.util.List; + +public class JsonQuote { + + long id; + String author; + String text; + + List mentions; + List attachments; + + JsonQuote(SignalServiceDataMessage.Quote quote, Manager m) { + this.id = quote.getId(); + this.author = m.resolveSignalServiceAddress(quote.getAuthor()).getLegacyIdentifier(); + this.text = quote.getText(); + + if (quote.getMentions().size() > 0) { + this.mentions = new ArrayList<>(quote.getMentions().size()); + + for (SignalServiceDataMessage.Mention quotedMention: quote.getMentions()){ + this.mentions.add(new JsonMention(quotedMention, m)); + } + } + + if (quote.getAttachments().size() > 0) { + this.attachments = new ArrayList<>(quote.getAttachments().size()); + + for (SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment : quote.getAttachments()) { + this.attachments.add(new JsonQuotedAttachment(quotedAttachment)); + } + } else { + this.attachments = new ArrayList<>(); + } + } + +} diff --git a/src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java b/src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java new file mode 100644 index 00000000..1aae3104 --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java @@ -0,0 +1,21 @@ +package org.asamk.signal.json; + +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; + +public class JsonQuotedAttachment { + + String contentType; + String filename; + JsonAttachment thumbnail; + + JsonQuotedAttachment(SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment) { + contentType = quotedAttachment.getContentType(); + filename = quotedAttachment.getFileName(); + if (quotedAttachment.getThumbnail() != null) { + thumbnail = new JsonAttachment(quotedAttachment.getThumbnail()); + } + else { + thumbnail = null; + } + } +} diff --git a/src/main/java/org/asamk/signal/json/JsonReaction.java b/src/main/java/org/asamk/signal/json/JsonReaction.java new file mode 100644 index 00000000..5e978fe0 --- /dev/null +++ b/src/main/java/org/asamk/signal/json/JsonReaction.java @@ -0,0 +1,19 @@ +package org.asamk.signal.json; + +import org.asamk.signal.manager.Manager; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Reaction; + +public class JsonReaction { + + String emoji; + String targetAuthor; + long targetSentTimestamp; + boolean isRemove; + + JsonReaction(Reaction reaction, Manager m) { + this.emoji = reaction.getEmoji(); + this.targetAuthor = m.resolveSignalServiceAddress(reaction.getTargetAuthor()).getLegacyIdentifier(); + this.targetSentTimestamp = reaction.getTargetSentTimestamp(); + this.isRemove = reaction.isRemove(); + } +} diff --git a/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java index c6571a93..7ea75bbd 100644 --- a/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java @@ -1,14 +1,15 @@ package org.asamk.signal.json; import org.asamk.Signal; +import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; class JsonSyncDataMessage extends JsonDataMessage { String destination; - JsonSyncDataMessage(SentTranscriptMessage transcriptMessage) { - super(transcriptMessage.getMessage()); + JsonSyncDataMessage(SentTranscriptMessage transcriptMessage, Manager m) { + super(transcriptMessage.getMessage(), m); if (transcriptMessage.getDestination().isPresent()) { this.destination = transcriptMessage.getDestination().get().getLegacyIdentifier(); } diff --git a/src/main/java/org/asamk/signal/json/JsonSyncMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncMessage.java index 31c39a3f..f29bc02e 100644 --- a/src/main/java/org/asamk/signal/json/JsonSyncMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncMessage.java @@ -1,6 +1,7 @@ package org.asamk.signal.json; import org.asamk.Signal; +import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -21,9 +22,9 @@ class JsonSyncMessage { List readMessages; JsonSyncMessageType type; - JsonSyncMessage(SignalServiceSyncMessage syncMessage) { + JsonSyncMessage(SignalServiceSyncMessage syncMessage, Manager m) { if (syncMessage.getSent().isPresent()) { - this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get()); + this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get(), m); } if (syncMessage.getBlockedList().isPresent()) { this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size()); From 67f62947c6d4cc5f4b9d4334bfe55b753bc6a12c Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 23 Dec 2020 11:24:07 +0100 Subject: [PATCH 07/32] Add null check and change some formatting --- .../asamk/signal/ReceiveMessageHandler.java | 13 +++------- .../asamk/signal/json/JsonDataMessage.java | 24 +++++++++---------- .../org/asamk/signal/json/JsonMention.java | 7 ++---- .../java/org/asamk/signal/json/JsonQuote.java | 22 ++++++++--------- .../signal/json/JsonQuotedAttachment.java | 3 +-- 5 files changed, 28 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index f961fa1f..711f503d 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -449,7 +449,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println("Quote: (" + quote.getId() + ")"); System.out.println(" Author: " + m.resolveSignalServiceAddress(quote.getAuthor()).getLegacyIdentifier()); System.out.println(" Text: " + quote.getText()); - if (quote.getMentions().size() > 0) { + if (quote.getMentions() != null && quote.getMentions().size() > 0) { System.out.println(" Mentions: "); for (SignalServiceDataMessage.Mention mention : quote.getMentions()) { printMention(mention, m); @@ -488,15 +488,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } private void printMention(SignalServiceDataMessage.Mention mention, Manager m) { - System.out.println("- " - + m.resolveSignalServiceAddress( - new SignalServiceAddress(mention.getUuid(), null) - ).getLegacyIdentifier() - + ": " - + mention.getStart() - + " (length: " - + mention.getLength() - + ")"); + System.out.println("- " + m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null)) + .getLegacyIdentifier() + ": " + mention.getStart() + " (length: " + mention.getLength() + ")"); } private void printAttachment(SignalServiceAttachment attachment) { diff --git a/src/main/java/org/asamk/signal/json/JsonDataMessage.java b/src/main/java/org/asamk/signal/json/JsonDataMessage.java index 957e3a79..57201eda 100644 --- a/src/main/java/org/asamk/signal/json/JsonDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonDataMessage.java @@ -2,12 +2,10 @@ package org.asamk.signal.json; import org.asamk.Signal; import org.asamk.signal.manager.Manager; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -45,20 +43,22 @@ class JsonDataMessage { this.quote = new JsonQuote(dataMessage.getQuote().get(), m); } if (dataMessage.getMentions().isPresent()) { - this.mentions = new ArrayList<>(dataMessage.getMentions().get().size()); - for (SignalServiceDataMessage.Mention mention : dataMessage.getMentions().get()) { - this.mentions.add(new JsonMention(mention, m)); - } + this.mentions = dataMessage.getMentions() + .get() + .stream() + .map(mention -> new JsonMention(mention, m)) + .collect(Collectors.toList()); } else { - this.mentions = new ArrayList<>(); + this.mentions = List.of(); } if (dataMessage.getAttachments().isPresent()) { - this.attachments = new ArrayList<>(dataMessage.getAttachments().get().size()); - for (SignalServiceAttachment attachment : dataMessage.getAttachments().get()) { - this.attachments.add(new JsonAttachment(attachment)); - } + this.attachments = dataMessage.getAttachments() + .get() + .stream() + .map(JsonAttachment::new) + .collect(Collectors.toList()); } else { - this.attachments = new ArrayList<>(); + this.attachments = List.of(); } } diff --git a/src/main/java/org/asamk/signal/json/JsonMention.java b/src/main/java/org/asamk/signal/json/JsonMention.java index 80683842..302128ed 100644 --- a/src/main/java/org/asamk/signal/json/JsonMention.java +++ b/src/main/java/org/asamk/signal/json/JsonMention.java @@ -11,12 +11,9 @@ public class JsonMention { int length; JsonMention(SignalServiceDataMessage.Mention mention, Manager m) { - this.name = m.resolveSignalServiceAddress( - new SignalServiceAddress(mention.getUuid(), null) - ).getLegacyIdentifier(); + this.name = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null)) + .getLegacyIdentifier(); this.start = mention.getStart(); this.length = mention.getLength(); - } - } diff --git a/src/main/java/org/asamk/signal/json/JsonQuote.java b/src/main/java/org/asamk/signal/json/JsonQuote.java index 9a740582..10cd0bf4 100644 --- a/src/main/java/org/asamk/signal/json/JsonQuote.java +++ b/src/main/java/org/asamk/signal/json/JsonQuote.java @@ -5,6 +5,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; public class JsonQuote { @@ -20,23 +21,20 @@ public class JsonQuote { this.author = m.resolveSignalServiceAddress(quote.getAuthor()).getLegacyIdentifier(); this.text = quote.getText(); - if (quote.getMentions().size() > 0) { - this.mentions = new ArrayList<>(quote.getMentions().size()); - - for (SignalServiceDataMessage.Mention quotedMention: quote.getMentions()){ - this.mentions.add(new JsonMention(quotedMention, m)); - } + if (quote.getMentions() != null && quote.getMentions().size() > 0) { + this.mentions = quote.getMentions() + .stream() + .map(quotedMention -> new JsonMention(quotedMention, m)) + .collect(Collectors.toList()); } if (quote.getAttachments().size() > 0) { - this.attachments = new ArrayList<>(quote.getAttachments().size()); - - for (SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment : quote.getAttachments()) { - this.attachments.add(new JsonQuotedAttachment(quotedAttachment)); - } + this.attachments = quote.getAttachments() + .stream() + .map(JsonQuotedAttachment::new) + .collect(Collectors.toList()); } else { this.attachments = new ArrayList<>(); } } - } diff --git a/src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java b/src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java index 1aae3104..bcbbe2a5 100644 --- a/src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java +++ b/src/main/java/org/asamk/signal/json/JsonQuotedAttachment.java @@ -13,8 +13,7 @@ public class JsonQuotedAttachment { filename = quotedAttachment.getFileName(); if (quotedAttachment.getThumbnail() != null) { thumbnail = new JsonAttachment(quotedAttachment.getThumbnail()); - } - else { + } else { thumbnail = null; } } From 9942d967a4a83230aadd21c86344c6f1ac246611 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 24 Dec 2020 16:36:47 +0100 Subject: [PATCH 08/32] Refactor to use GroupId class to wrap the byte array Helps distinguish between group v1 and v2 ids --- .../signal/JsonDbusReceiveMessageHandler.java | 21 +--- .../asamk/signal/ReceiveMessageHandler.java | 14 ++- .../asamk/signal/commands/BlockCommand.java | 5 +- .../signal/commands/JoinGroupCommand.java | 10 +- .../signal/commands/ListGroupsCommand.java | 5 +- .../signal/commands/QuitGroupCommand.java | 5 +- .../asamk/signal/commands/SendCommand.java | 4 +- .../signal/commands/SendReactionCommand.java | 5 +- .../asamk/signal/commands/UnblockCommand.java | 5 +- .../signal/commands/UpdateGroupCommand.java | 4 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 19 +-- .../org/asamk/signal/json/JsonGroupInfo.java | 2 +- .../org/asamk/signal/manager/GroupId.java | 63 ++++++++++ .../manager/GroupIdFormatException.java | 8 ++ .../org/asamk/signal/manager/GroupIdV1.java | 14 +++ .../org/asamk/signal/manager/GroupIdV2.java | 14 +++ .../manager/GroupNotFoundException.java | 6 +- .../org/asamk/signal/manager/GroupUtils.java | 33 ++++- .../asamk/signal/manager/HandleAction.java | 27 ++-- .../org/asamk/signal/manager/KeyUtils.java | 4 - .../org/asamk/signal/manager/Manager.java | 116 +++++++++--------- .../manager/NotAGroupMemberException.java | 6 +- .../signal/manager/helper/GroupHelper.java | 4 +- .../asamk/signal/storage/SignalAccount.java | 3 +- .../signal/storage/groups/GroupInfo.java | 10 +- .../signal/storage/groups/GroupInfoV1.java | 61 ++++++--- .../signal/storage/groups/GroupInfoV2.java | 11 +- .../signal/storage/groups/JsonGroupStore.java | 84 ++++++------- .../org/asamk/signal/util/ErrorUtils.java | 1 + .../signal/util/GroupIdFormatException.java | 10 -- src/main/java/org/asamk/signal/util/Util.java | 12 +- 31 files changed, 358 insertions(+), 228 deletions(-) create mode 100644 src/main/java/org/asamk/signal/manager/GroupId.java create mode 100644 src/main/java/org/asamk/signal/manager/GroupIdFormatException.java create mode 100644 src/main/java/org/asamk/signal/manager/GroupIdV1.java create mode 100644 src/main/java/org/asamk/signal/manager/GroupIdV2.java delete mode 100644 src/main/java/org/asamk/signal/util/GroupIdFormatException.java diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 41b91a48..50eb9f9b 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -65,7 +65,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { } else if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - byte[] groupId = getGroupId(m, message); + byte[] groupId = getGroupId(message); if (!message.isEndSession() && ( groupId == null || message.getGroupContext().get().getGroupV1Type() == null @@ -91,7 +91,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { .getGroupContext() .isPresent()) { SignalServiceDataMessage message = transcript.getMessage(); - byte[] groupId = getGroupId(m, message); + byte[] groupId = getGroupId(message); try { conn.sendMessage(new Signal.SyncMessageReceived(objectPath, @@ -112,20 +112,9 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { } } - private static byte[] getGroupId(final Manager m, final SignalServiceDataMessage message) { - byte[] groupId; - if (message.getGroupContext().isPresent()) { - if (message.getGroupContext().get().getGroupV1().isPresent()) { - groupId = message.getGroupContext().get().getGroupV1().get().getGroupId(); - } else if (message.getGroupContext().get().getGroupV2().isPresent()) { - groupId = GroupUtils.getGroupId(message.getGroupContext().get().getGroupV2().get().getMasterKey()); - } else { - groupId = null; - } - } else { - groupId = null; - } - return groupId; + private static byte[] getGroupId(final SignalServiceDataMessage message) { + return message.getGroupContext().isPresent() ? GroupUtils.getGroupId(message.getGroupContext().get()) + .serialize() : null; } static private List getAttachments(SignalServiceDataMessage message, Manager m) { diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 711f503d..99010e13 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -1,5 +1,6 @@ package org.asamk.signal; +import org.asamk.signal.manager.GroupId; import org.asamk.signal.manager.GroupUtils; import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.contacts.ContactInfo; @@ -328,8 +329,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { System.out.println(" - Timestamp: " + DateUtils.formatTimestamp(typingMessage.getTimestamp())); if (typingMessage.getGroupId().isPresent()) { System.out.println(" - Group Info:"); - System.out.println(" Id: " + Base64.encodeBytes(typingMessage.getGroupId().get())); - GroupInfo group = m.getGroup(typingMessage.getGroupId().get()); + final GroupId groupId = GroupId.unknownVersion(typingMessage.getGroupId().get()); + System.out.println(" Id: " + groupId.toBase64()); + GroupInfo group = m.getGroup(groupId); if (group != null) { System.out.println(" Name: " + group.getTitle()); } else { @@ -356,13 +358,14 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { if (message.getGroupContext().isPresent()) { System.out.println("Group info:"); final SignalServiceGroupContext groupContext = message.getGroupContext().get(); + final GroupId groupId = GroupUtils.getGroupId(groupContext); if (groupContext.getGroupV1().isPresent()) { SignalServiceGroup groupInfo = groupContext.getGroupV1().get(); - System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); + System.out.println(" Id: " + groupId.toBase64()); if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) { System.out.println(" Name: " + groupInfo.getName().get()); } else { - GroupInfo group = m.getGroup(groupInfo.getGroupId()); + GroupInfo group = m.getGroup(groupId); if (group != null) { System.out.println(" Name: " + group.getTitle()); } else { @@ -381,8 +384,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } } else if (groupContext.getGroupV2().isPresent()) { final SignalServiceGroupV2 groupInfo = groupContext.getGroupV2().get(); - byte[] groupId = GroupUtils.getGroupId(groupInfo.getMasterKey()); - System.out.println(" Id: " + Base64.encodeBytes(groupId)); + System.out.println(" Id: " + groupId.toBase64()); GroupInfo group = m.getGroup(groupId); if (group != null) { System.out.println(" Name: " + group.getTitle()); diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 95c3738c..2a9bc4e9 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -3,9 +3,10 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -36,7 +37,7 @@ public class BlockCommand implements LocalCommand { if (ns.getList("group") != null) { for (String groupIdString : ns.getList("group")) { try { - byte[] groupId = Util.decodeGroupId(groupIdString); + GroupId groupId = Util.decodeGroupId(groupIdString); m.setGroupBlocked(groupId, true); } catch (GroupIdFormatException | GroupNotFoundException e) { System.err.println(e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index 62b996cf..8438e1fa 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -4,6 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; +import org.asamk.signal.manager.GroupId; import org.asamk.signal.manager.GroupInviteLinkUrl; import org.asamk.signal.manager.Manager; import org.freedesktop.dbus.exceptions.DBusExecutionException; @@ -11,7 +12,6 @@ import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException; -import org.whispersystems.util.Base64; import java.io.IOException; import java.util.List; @@ -52,12 +52,12 @@ public class JoinGroupCommand implements LocalCommand { } try { - final Pair> results = m.joinGroup(linkUrl); - byte[] newGroupId = results.first(); + final Pair> results = m.joinGroup(linkUrl); + GroupId newGroupId = results.first(); if (!m.getGroup(newGroupId).isMember(m.getSelfAddress())) { - System.out.println("Requested to join group \"" + Base64.encodeBytes(newGroupId) + "\""); + System.out.println("Requested to join group \"" + newGroupId.toBase64() + "\""); } else { - System.out.println("Joined group \"" + Base64.encodeBytes(newGroupId) + "\""); + System.out.println("Joined group \"" + newGroupId.toBase64() + "\""); } return handleTimestampAndSendMessageResults(0, results.second()); } catch (AssertionError e) { diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index e7844fda..4d1032a2 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -8,7 +8,6 @@ import org.asamk.signal.manager.GroupInviteLinkUrl; 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; import java.util.Set; @@ -40,7 +39,7 @@ public class ListGroupsCommand implements LocalCommand { System.out.println(String.format( "Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s Link: %s", - Base64.encodeBytes(group.groupId), + group.getGroupId().toBase64(), group.getTitle(), group.isMember(m.getSelfAddress()), group.isBlocked(), @@ -50,7 +49,7 @@ public class ListGroupsCommand implements LocalCommand { groupInviteLink == null ? '-' : groupInviteLink.getUrl())); } else { System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b", - Base64.encodeBytes(group.groupId), + group.getGroupId().toBase64(), group.getTitle(), group.isMember(m.getSelfAddress()), group.isBlocked())); diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 20d06eba..efc63f8f 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -3,10 +3,11 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotAGroupMemberException; -import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -36,7 +37,7 @@ public class QuitGroupCommand implements LocalCommand { } try { - final byte[] groupId = Util.decodeGroupId(ns.getString("group")); + final GroupId groupId = Util.decodeGroupId(ns.getString("group")); final Pair> results = m.sendQuitGroupMessage(groupId); return handleTimestampAndSendMessageResults(results.first(), results.second()); } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 551cf938..04b06434 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -5,7 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.util.GroupIdFormatException; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; @@ -79,7 +79,7 @@ public class SendCommand implements DbusCommand { if (ns.getString("group") != null) { byte[] groupId; try { - groupId = Util.decodeGroupId(ns.getString("group")); + groupId = Util.decodeGroupId(ns.getString("group")).serialize(); } catch (GroupIdFormatException e) { handleGroupIdFormatException(e); return 1; diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 000a1349..345c9180 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -4,10 +4,11 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotAGroupMemberException; -import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -65,7 +66,7 @@ public class SendReactionCommand implements LocalCommand { try { final Pair> results; if (ns.getString("group") != null) { - byte[] groupId = Util.decodeGroupId(ns.getString("group")); + GroupId groupId = Util.decodeGroupId(ns.getString("group")); results = m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId); } else { results = m.sendMessageReaction(emoji, diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index b4f6cc3b..73e578ac 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -3,9 +3,10 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.util.GroupIdFormatException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -36,7 +37,7 @@ public class UnblockCommand implements LocalCommand { if (ns.getList("group") != null) { for (String groupIdString : ns.getList("group")) { try { - byte[] groupId = Util.decodeGroupId(groupIdString); + GroupId groupId = Util.decodeGroupId(groupIdString); m.setGroupBlocked(groupId, false); } catch (GroupIdFormatException | GroupNotFoundException e) { System.err.println(e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 4216fd9b..dae06b86 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.util.GroupIdFormatException; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.util.Base64; @@ -35,7 +35,7 @@ public class UpdateGroupCommand implements DbusCommand { byte[] groupId = null; if (ns.getString("group") != null) { try { - groupId = Util.decodeGroupId(ns.getString("group")); + groupId = Util.decodeGroupId(ns.getString("group")).serialize(); } catch (GroupIdFormatException e) { handleGroupIdFormatException(e); return 1; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 396063f2..cbb72835 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -2,6 +2,7 @@ package org.asamk.signal.dbus; import org.asamk.Signal; import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.GroupId; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotAGroupMemberException; @@ -92,7 +93,9 @@ public class DbusSignalImpl implements Signal { @Override public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) { try { - Pair> results = m.sendGroupMessage(message, attachments, groupId); + Pair> results = m.sendGroupMessage(message, + attachments, + GroupId.unknownVersion(groupId)); checkSendMessageResults(results.first(), results.second()); return results.first(); } catch (IOException e) { @@ -134,7 +137,7 @@ public class DbusSignalImpl implements Signal { @Override public void setGroupBlocked(final byte[] groupId, final boolean blocked) { try { - m.setGroupBlocked(groupId, blocked); + m.setGroupBlocked(GroupId.unknownVersion(groupId), blocked); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } @@ -145,14 +148,14 @@ public class DbusSignalImpl implements Signal { List groups = m.getGroups(); List ids = new ArrayList<>(groups.size()); for (GroupInfo group : groups) { - ids.add(group.groupId); + ids.add(group.getGroupId().serialize()); } return ids; } @Override public String getGroupName(final byte[] groupId) { - GroupInfo group = m.getGroup(groupId); + GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId)); if (group == null) { return ""; } else { @@ -162,7 +165,7 @@ public class DbusSignalImpl implements Signal { @Override public List getGroupMembers(final byte[] groupId) { - GroupInfo group = m.getGroup(groupId); + GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId)); if (group == null) { return Collections.emptyList(); } else { @@ -189,9 +192,11 @@ public class DbusSignalImpl implements Signal { if (avatar.isEmpty()) { avatar = null; } - final Pair> results = m.updateGroup(groupId, name, members, avatar); + final Pair> results = m.updateGroup(groupId == null + ? null + : GroupId.unknownVersion(groupId), name, members, avatar); checkSendMessageResults(0, results.second()); - return results.first(); + return results.first().serialize(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { diff --git a/src/main/java/org/asamk/signal/json/JsonGroupInfo.java b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java index 970cde52..9709be20 100644 --- a/src/main/java/org/asamk/signal/json/JsonGroupInfo.java +++ b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java @@ -31,7 +31,7 @@ class JsonGroupInfo { } JsonGroupInfo(SignalServiceGroupV2 groupInfo) { - this.groupId = Base64.encodeBytes(GroupUtils.getGroupId(groupInfo.getMasterKey())); + this.groupId = GroupUtils.getGroupIdV2(groupInfo.getMasterKey()).toBase64(); this.type = groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER"; } diff --git a/src/main/java/org/asamk/signal/manager/GroupId.java b/src/main/java/org/asamk/signal/manager/GroupId.java new file mode 100644 index 00000000..34e18e8e --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/GroupId.java @@ -0,0 +1,63 @@ +package org.asamk.signal.manager; + +import org.whispersystems.util.Base64; + +import java.util.Arrays; + +public abstract class GroupId { + + private final byte[] id; + + public static GroupIdV1 v1(byte[] id) { + return new GroupIdV1(id); + } + + public static GroupIdV2 v2(byte[] id) { + return new GroupIdV2(id); + } + + public static GroupId unknownVersion(byte[] id) { + if (id.length == 16) { + return new GroupIdV1(id); + } else if (id.length == 32) { + return new GroupIdV2(id); + } + + throw new AssertionError("Invalid group id of size " + id.length); + } + + public static GroupId fromBase64(String id) throws GroupIdFormatException { + try { + return unknownVersion(java.util.Base64.getDecoder().decode(id)); + } catch (Throwable e) { + throw new GroupIdFormatException(id, e); + } + } + + public GroupId(final byte[] id) { + this.id = id; + } + + public byte[] serialize() { + return id; + } + + public String toBase64() { + return Base64.encodeBytes(id); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final GroupId groupId = (GroupId) o; + + return Arrays.equals(id, groupId.id); + } + + @Override + public int hashCode() { + return Arrays.hashCode(id); + } +} diff --git a/src/main/java/org/asamk/signal/manager/GroupIdFormatException.java b/src/main/java/org/asamk/signal/manager/GroupIdFormatException.java new file mode 100644 index 00000000..83afd15b --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/GroupIdFormatException.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager; + +public class GroupIdFormatException extends Exception { + + public GroupIdFormatException(String groupId, Throwable e) { + super("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage(), e); + } +} diff --git a/src/main/java/org/asamk/signal/manager/GroupIdV1.java b/src/main/java/org/asamk/signal/manager/GroupIdV1.java new file mode 100644 index 00000000..40862f07 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/GroupIdV1.java @@ -0,0 +1,14 @@ +package org.asamk.signal.manager; + +import static org.asamk.signal.manager.KeyUtils.getSecretBytes; + +public class GroupIdV1 extends GroupId { + + public static GroupIdV1 createRandom() { + return new GroupIdV1(getSecretBytes(16)); + } + + public GroupIdV1(final byte[] id) { + super(id); + } +} diff --git a/src/main/java/org/asamk/signal/manager/GroupIdV2.java b/src/main/java/org/asamk/signal/manager/GroupIdV2.java new file mode 100644 index 00000000..b329be1d --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/GroupIdV2.java @@ -0,0 +1,14 @@ +package org.asamk.signal.manager; + +import java.util.Base64; + +public class GroupIdV2 extends GroupId { + + public static GroupIdV2 fromBase64(String groupId) { + return new GroupIdV2(Base64.getDecoder().decode(groupId)); + } + + public GroupIdV2(final byte[] id) { + super(id); + } +} diff --git a/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java index 0c0d6d2d..d7efa923 100644 --- a/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java +++ b/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java @@ -1,10 +1,8 @@ package org.asamk.signal.manager; -import org.whispersystems.util.Base64; - public class GroupNotFoundException extends Exception { - public GroupNotFoundException(byte[] groupId) { - super("Group not found: " + Base64.encodeBytes(groupId)); + public GroupNotFoundException(GroupId groupId) { + super("Group not found: " + groupId.toBase64()); } } diff --git a/src/main/java/org/asamk/signal/manager/GroupUtils.java b/src/main/java/org/asamk/signal/manager/GroupUtils.java index a0e95c7a..d86dfbe9 100644 --- a/src/main/java/org/asamk/signal/manager/GroupUtils.java +++ b/src/main/java/org/asamk/signal/manager/GroupUtils.java @@ -9,6 +9,7 @@ import org.signal.zkgroup.groups.GroupSecretParams; import org.whispersystems.libsignal.kdf.HKDFv3; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; public class GroupUtils { @@ -18,7 +19,7 @@ public class GroupUtils { ) { if (groupInfo instanceof GroupInfoV1) { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) - .withId(groupInfo.groupId) + .withId(groupInfo.getGroupId().serialize()) .build(); messageBuilder.asGroupMessage(group); } else { @@ -30,14 +31,34 @@ public class GroupUtils { } } - public static byte[] getGroupId(GroupMasterKey groupMasterKey) { - final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - return groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); + public static GroupId getGroupId(SignalServiceGroupContext context) { + if (context.getGroupV1().isPresent()) { + return GroupId.v1(context.getGroupV1().get().getGroupId()); + } else if (context.getGroupV2().isPresent()) { + return getGroupIdV2(context.getGroupV2().get().getMasterKey()); + } else { + return null; + } } - public static GroupMasterKey deriveV2MigrationMasterKey(byte[] groupIdV1) { + public static GroupIdV2 getGroupIdV2(GroupSecretParams groupSecretParams) { + return GroupId.v2(groupSecretParams.getPublicParams().getGroupIdentifier().serialize()); + } + + public static GroupIdV2 getGroupIdV2(GroupMasterKey groupMasterKey) { + final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + return getGroupIdV2(groupSecretParams); + } + + public static GroupIdV2 getGroupIdV2(GroupIdV1 groupIdV1) { + final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(deriveV2MigrationMasterKey( + groupIdV1)); + return getGroupIdV2(groupSecretParams); + } + + private static GroupMasterKey deriveV2MigrationMasterKey(GroupIdV1 groupIdV1) { try { - return new GroupMasterKey(new HKDFv3().deriveSecrets(groupIdV1, + return new GroupMasterKey(new HKDFv3().deriveSecrets(groupIdV1.serialize(), "GV2 Migration".getBytes(), GroupMasterKey.SIZE)); } catch (InvalidInputException e) { diff --git a/src/main/java/org/asamk/signal/manager/HandleAction.java b/src/main/java/org/asamk/signal/manager/HandleAction.java index 9bdd3885..aa25d8c5 100644 --- a/src/main/java/org/asamk/signal/manager/HandleAction.java +++ b/src/main/java/org/asamk/signal/manager/HandleAction.java @@ -2,7 +2,6 @@ package org.asamk.signal.manager; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import java.util.Arrays; import java.util.Objects; interface HandleAction { @@ -93,9 +92,9 @@ class SendSyncBlockedListAction implements HandleAction { class SendGroupInfoRequestAction implements HandleAction { private final SignalServiceAddress address; - private final byte[] groupId; + private final GroupIdV1 groupId; - public SendGroupInfoRequestAction(final SignalServiceAddress address, final byte[] groupId) { + public SendGroupInfoRequestAction(final SignalServiceAddress address, final GroupIdV1 groupId) { this.address = address; this.groupId = groupId; } @@ -109,14 +108,17 @@ class SendGroupInfoRequestAction implements HandleAction { public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; + final SendGroupInfoRequestAction that = (SendGroupInfoRequestAction) o; - return address.equals(that.address) && Arrays.equals(groupId, that.groupId); + + if (!address.equals(that.address)) return false; + return groupId.equals(that.groupId); } @Override public int hashCode() { - int result = Objects.hash(address); - result = 31 * result + Arrays.hashCode(groupId); + int result = address.hashCode(); + result = 31 * result + groupId.hashCode(); return result; } } @@ -124,9 +126,9 @@ class SendGroupInfoRequestAction implements HandleAction { class SendGroupUpdateAction implements HandleAction { private final SignalServiceAddress address; - private final byte[] groupId; + private final GroupIdV1 groupId; - public SendGroupUpdateAction(final SignalServiceAddress address, final byte[] groupId) { + public SendGroupUpdateAction(final SignalServiceAddress address, final GroupIdV1 groupId) { this.address = address; this.groupId = groupId; } @@ -140,14 +142,17 @@ class SendGroupUpdateAction implements HandleAction { public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; + final SendGroupUpdateAction that = (SendGroupUpdateAction) o; - return address.equals(that.address) && Arrays.equals(groupId, that.groupId); + + if (!address.equals(that.address)) return false; + return groupId.equals(that.groupId); } @Override public int hashCode() { - int result = Objects.hash(address); - result = 31 * result + Arrays.hashCode(groupId); + int result = address.hashCode(); + result = 31 * result + groupId.hashCode(); return result; } } diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java index d6f332c0..21f6037f 100644 --- a/src/main/java/org/asamk/signal/manager/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java @@ -26,10 +26,6 @@ class KeyUtils { return getSecret(18); } - static byte[] createGroupId() { - return getSecretBytes(16); - } - static byte[] createStickerUploadKey() { return getSecretBytes(32); } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 8a46846f..b5d425d8 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -679,7 +679,7 @@ public class Manager implements Closeable { } } - private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { + private Optional createGroupAvatarAttachment(GroupId groupId) throws IOException { File file = getGroupAvatarFile(groupId); if (!file.exists()) { return Optional.absent(); @@ -697,7 +697,7 @@ public class Manager implements Closeable { return Optional.of(Utils.createAttachment(file)); } - private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException { + private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { GroupInfo g = account.getGroupStore().getGroup(groupId); if (g == null) { throw new GroupNotFoundException(groupId); @@ -708,7 +708,7 @@ public class Manager implements Closeable { return g; } - private GroupInfo getGroupForUpdating(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException { + private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { GroupInfo g = account.getGroupStore().getGroup(groupId); if (g == null) { throw new GroupNotFoundException(groupId); @@ -724,7 +724,7 @@ public class Manager implements Closeable { } public Pair> sendGroupMessage( - SignalServiceDataMessage.Builder messageBuilder, byte[] groupId + SignalServiceDataMessage.Builder messageBuilder, GroupId groupId ) throws IOException, GroupNotFoundException, NotAGroupMemberException { final GroupInfo g = getGroupForSending(groupId); @@ -735,7 +735,7 @@ public class Manager implements Closeable { } public Pair> sendGroupMessage( - String messageText, List attachments, byte[] groupId + String messageText, List attachments, GroupId groupId ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withBody(messageText); @@ -747,7 +747,7 @@ public class Manager implements Closeable { } public Pair> sendGroupMessageReaction( - String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, byte[] groupId + String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, GroupId groupId ) throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException { SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, @@ -759,7 +759,7 @@ public class Manager implements Closeable { return sendGroupMessage(messageBuilder, groupId); } - public Pair> sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { + public Pair> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { SignalServiceDataMessage.Builder messageBuilder; @@ -767,7 +767,7 @@ public class Manager implements Closeable { if (g instanceof GroupInfoV1) { GroupInfoV1 groupInfoV1 = (GroupInfoV1) g; SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) - .withId(groupId) + .withId(groupId.serialize()) .build(); messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); groupInfoV1.removeMember(account.getSelfAddress()); @@ -783,8 +783,8 @@ public class Manager implements Closeable { return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress())); } - private Pair> sendUpdateGroupMessage( - byte[] groupId, String name, Collection members, String avatarFile + private Pair> sendUpdateGroupMessage( + GroupId groupId, String name, Collection members, String avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { GroupInfo g; SignalServiceDataMessage.Builder messageBuilder; @@ -792,7 +792,7 @@ public class Manager implements Closeable { // Create new group GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile); if (gv2 == null) { - GroupInfoV1 gv1 = new GroupInfoV1(KeyUtils.createGroupId()); + GroupInfoV1 gv1 = new GroupInfoV1(GroupIdV1.createRandom()); gv1.addMembers(Collections.singleton(account.getSelfAddress())); updateGroupV1(gv1, name, members, avatarFile); messageBuilder = getGroupUpdateMessageBuilder(gv1); @@ -834,7 +834,7 @@ public class Manager implements Closeable { groupGroupChangePair.second()); } - return new Pair<>(group.groupId, result.second()); + return new Pair<>(group.getGroupId(), result.second()); } else { GroupInfoV1 gv1 = (GroupInfoV1) group; updateGroupV1(gv1, name, members, avatarFile); @@ -847,16 +847,16 @@ public class Manager implements Closeable { final Pair> result = sendMessage(messageBuilder, g.getMembersIncludingPendingWithout(account.getSelfAddress())); - return new Pair<>(g.groupId, result.second()); + return new Pair<>(g.getGroupId(), result.second()); } - public Pair> joinGroup( + public Pair> joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { return sendJoinGroupMessage(inviteLinkUrl); } - private Pair> sendJoinGroupMessage( + private Pair> sendJoinGroupMessage( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { final DecryptedGroupJoinInfo groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), @@ -870,12 +870,12 @@ public class Manager implements Closeable { if (group.getGroup() == null) { // Only requested member, can't send update to group members - return new Pair<>(group.groupId, List.of()); + return new Pair<>(group.getGroupId(), List.of()); } final Pair> result = sendUpdateGroupMessage(group, group.getGroup(), groupChange); - return new Pair<>(group.groupId, result.second()); + return new Pair<>(group.getGroupId(), result.second()); } private Pair> sendUpdateGroupMessage( @@ -923,13 +923,13 @@ public class Manager implements Closeable { if (avatarFile != null) { IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); - File aFile = getGroupAvatarFile(g.groupId); + File aFile = getGroupAvatarFile(g.getGroupId()); Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } } Pair> sendUpdateGroupMessage( - byte[] groupId, SignalServiceAddress recipient + GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { GroupInfoV1 g; GroupInfo group = getGroupForSending(groupId); @@ -950,11 +950,11 @@ public class Manager implements Closeable { private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) - .withId(g.groupId) + .withId(g.getGroupId().serialize()) .withName(g.name) .withMembers(new ArrayList<>(g.getMembers())); - File aFile = getGroupAvatarFile(g.groupId); + File aFile = getGroupAvatarFile(g.getGroupId()); if (aFile.exists()) { try { group.withAvatar(Utils.createAttachment(aFile)); @@ -978,10 +978,10 @@ public class Manager implements Closeable { } Pair> sendGroupInfoRequest( - byte[] groupId, SignalServiceAddress recipient + GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException { SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO) - .withId(groupId); + .withId(groupId.serialize()); SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()); @@ -1087,7 +1087,7 @@ public class Manager implements Closeable { account.save(); } - public void setGroupBlocked(final byte[] groupId, final boolean blocked) throws GroupNotFoundException { + public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException { GroupInfo group = getGroup(groupId); if (group == null) { throw new GroupNotFoundException(groupId); @@ -1098,8 +1098,8 @@ public class Manager implements Closeable { account.save(); } - public Pair> updateGroup( - byte[] groupId, String name, List members, String avatar + public Pair> updateGroup( + GroupId groupId, String name, List members, String avatar ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { return sendUpdateGroupMessage(groupId, name, @@ -1137,7 +1137,7 @@ public class Manager implements Closeable { /** * Change the expiration timer for a group */ - public void setExpirationTimer(byte[] groupId, int messageExpirationTimer) { + public void setExpirationTimer(GroupId groupId, int messageExpirationTimer) { GroupInfo g = account.getGroupStore().getGroup(groupId); if (g instanceof GroupInfoV1) { GroupInfoV1 groupInfoV1 = (GroupInfoV1) g; @@ -1551,20 +1551,21 @@ public class Manager implements Closeable { if (message.getGroupContext().isPresent()) { if (message.getGroupContext().get().getGroupV1().isPresent()) { SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); - GroupInfo group = account.getGroupStore().getGroupByV1Id(groupInfo.getGroupId()); + GroupIdV1 groupId = GroupId.v1(groupInfo.getGroupId()); + GroupInfo group = account.getGroupStore().getGroup(groupId); if (group == null || group instanceof GroupInfoV1) { GroupInfoV1 groupV1 = (GroupInfoV1) group; switch (groupInfo.getType()) { case UPDATE: { if (groupV1 == null) { - groupV1 = new GroupInfoV1(groupInfo.getGroupId()); + groupV1 = new GroupInfoV1(groupId); } if (groupInfo.getAvatar().isPresent()) { SignalServiceAttachment avatar = groupInfo.getAvatar().get(); if (avatar.isPointer()) { try { - retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.groupId); + retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.getGroupId()); } catch (IOException | InvalidMessageException | MissingConfigurationException e) { System.err.println("Failed to retrieve group avatar (" + avatar.asPointer() .getRemoteId() + "): " + e.getMessage()); @@ -1589,7 +1590,7 @@ public class Manager implements Closeable { } case DELIVER: if (groupV1 == null && !isSync) { - actions.add(new SendGroupInfoRequestAction(source, groupInfo.getGroupId())); + actions.add(new SendGroupInfoRequestAction(source, groupV1.getGroupId())); } break; case QUIT: { @@ -1601,7 +1602,7 @@ public class Manager implements Closeable { } case REQUEST_INFO: if (groupV1 != null && !isSync) { - actions.add(new SendGroupUpdateAction(source, groupV1.groupId)); + actions.add(new SendGroupUpdateAction(source, groupV1.getGroupId())); } break; } @@ -1627,7 +1628,7 @@ public class Manager implements Closeable { if (message.getGroupContext().isPresent()) { if (message.getGroupContext().get().getGroupV1().isPresent()) { SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); - GroupInfoV1 group = account.getGroupStore().getOrCreateGroupV1(groupInfo.getGroupId()); + GroupInfoV1 group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId())); if (group != null) { if (group.messageExpirationTime != message.getExpiresInSeconds()) { group.messageExpirationTime = message.getExpiresInSeconds(); @@ -1723,17 +1724,17 @@ public class Manager implements Closeable { ) { final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); - GroupInfo groupInfo = account.getGroupStore().getGroupByV2Id(groupId); + GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams); + GroupInfo groupInfo = account.getGroupStore().getGroup(groupId); final GroupInfoV2 groupInfoV2; if (groupInfo instanceof GroupInfoV1) { // Received a v2 group message for a v1 group, we need to locally migrate the group - account.getGroupStore().deleteGroup(groupInfo.groupId); + account.getGroupStore().deleteGroup(groupInfo.getGroupId()); groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); System.err.println("Locally migrated group " - + Base64.encodeBytes(groupInfo.groupId) + + groupInfo.getGroupId().toBase64() + " to group v2, id: " - + Base64.encodeBytes(groupInfoV2.groupId) + + groupInfoV2.getGroupId().toBase64() + " !!!"); } else if (groupInfo instanceof GroupInfoV2) { groupInfoV2 = (GroupInfoV2) groupInfo; @@ -1970,19 +1971,14 @@ public class Manager implements Closeable { if (content != null && content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); if (message.getGroupContext().isPresent()) { - GroupInfo group = null; if (message.getGroupContext().get().getGroupV1().isPresent()) { SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); - if (groupInfo.getType() == SignalServiceGroup.Type.DELIVER) { - group = getGroup(groupInfo.getGroupId()); + if (groupInfo.getType() != SignalServiceGroup.Type.DELIVER) { + return false; } } - if (message.getGroupContext().get().getGroupV2().isPresent()) { - SignalServiceGroupV2 groupContext = message.getGroupContext().get().getGroupV2().get(); - final GroupMasterKey groupMasterKey = groupContext.getMasterKey(); - byte[] groupId = GroupUtils.getGroupId(groupMasterKey); - group = account.getGroupStore().getGroupByV2Id(groupId); - } + GroupId groupId = GroupUtils.getGroupId(message.getGroupContext().get()); + GroupInfo group = account.getGroupStore().getGroup(groupId); if (group != null && group.isBlocked()) { return true; } @@ -2055,7 +2051,8 @@ public class Manager implements Closeable { DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream); DeviceGroup g; while ((g = s.read()) != null) { - GroupInfoV1 syncGroup = account.getGroupStore().getOrCreateGroupV1(g.getId()); + GroupInfoV1 syncGroup = account.getGroupStore() + .getOrCreateGroupV1(GroupId.v1(g.getId())); if (syncGroup != null) { if (g.getName().isPresent()) { syncGroup.name = g.getName().get(); @@ -2076,7 +2073,7 @@ public class Manager implements Closeable { } if (g.getAvatar().isPresent()) { - retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); + retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.getGroupId()); } syncGroup.inboxPosition = g.getInboxPosition().orNull(); syncGroup.archived = g.isArchived(); @@ -2104,12 +2101,15 @@ public class Manager implements Closeable { for (SignalServiceAddress address : blockedListMessage.getAddresses()) { setContactBlocked(resolveSignalServiceAddress(address), true); } - for (byte[] groupId : blockedListMessage.getGroupIds()) { + for (GroupId groupId : blockedListMessage.getGroupIds() + .stream() + .map(GroupId::unknownVersion) + .collect(Collectors.toSet())) { try { setGroupBlocked(groupId, true); } catch (GroupNotFoundException e) { System.err.println("BlockedListMessage contained groupID that was not found in GroupStore: " - + Base64.encodeBytes(groupId)); + + groupId.toBase64()); } } } @@ -2229,12 +2229,12 @@ public class Manager implements Closeable { } } - private File getGroupAvatarFile(byte[] groupId) { - return new File(pathConfig.getAvatarsPath(), "group-" + Base64.encodeBytes(groupId).replace("/", "_")); + private File getGroupAvatarFile(GroupId groupId) { + return new File(pathConfig.getAvatarsPath(), "group-" + groupId.toBase64().replace("/", "_")); } private File retrieveGroupAvatarAttachment( - SignalServiceAttachment attachment, byte[] groupId + SignalServiceAttachment attachment, GroupId groupId ) throws IOException, InvalidMessageException, MissingConfigurationException { IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); if (attachment.isPointer()) { @@ -2333,10 +2333,10 @@ public class Manager implements Closeable { for (GroupInfo record : account.getGroupStore().getGroups()) { if (record instanceof GroupInfoV1) { GroupInfoV1 groupInfo = (GroupInfoV1) record; - out.write(new DeviceGroup(groupInfo.groupId, + out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), Optional.fromNullable(groupInfo.name), new ArrayList<>(groupInfo.getMembers()), - createGroupAvatarAttachment(groupInfo.groupId), + createGroupAvatarAttachment(groupInfo.getGroupId()), groupInfo.isMember(account.getSelfAddress()), Optional.of(groupInfo.messageExpirationTime), Optional.fromNullable(groupInfo.color), @@ -2442,7 +2442,7 @@ public class Manager implements Closeable { List groupIds = new ArrayList<>(); for (GroupInfo record : account.getGroupStore().getGroups()) { if (record.isBlocked()) { - groupIds.add(record.groupId); + groupIds.add(record.getGroupId().serialize()); } } sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); @@ -2466,7 +2466,7 @@ public class Manager implements Closeable { return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number)); } - public GroupInfo getGroup(byte[] groupId) { + public GroupInfo getGroup(GroupId groupId) { return account.getGroupStore().getGroup(groupId); } diff --git a/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java index 8c0e9be0..2c9b3f33 100644 --- a/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java +++ b/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java @@ -1,10 +1,8 @@ package org.asamk.signal.manager; -import org.whispersystems.util.Base64; - public class NotAGroupMemberException extends Exception { - public NotAGroupMemberException(byte[] groupId, String groupName) { - super("User is not a member in group: " + groupName + " (" + Base64.encodeBytes(groupId) + ")"); + public NotAGroupMemberException(GroupId groupId, String groupName) { + super("User is not a member in group: " + groupName + " (" + groupId.toBase64() + ")"); } } diff --git a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index d66831bb..6fd35003 100644 --- a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -2,7 +2,9 @@ package org.asamk.signal.manager.helper; import com.google.protobuf.InvalidProtocolBufferException; +import org.asamk.signal.manager.GroupIdV2; import org.asamk.signal.manager.GroupLinkPassword; +import org.asamk.signal.manager.GroupUtils; import org.asamk.signal.storage.groups.GroupInfoV2; import org.asamk.signal.util.IOUtils; import org.signal.storageservice.protos.groups.AccessControl; @@ -117,7 +119,7 @@ public class GroupHelper { return null; } - final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize(); + final GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams); final GroupMasterKey masterKey = groupSecretParams.getMasterKey(); GroupInfoV2 g = new GroupInfoV2(groupId, masterKey); g.setGroup(decryptedGroup); diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 4f9d8628..d3145ecd 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.asamk.signal.manager.GroupId; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.contacts.JsonContactsStore; import org.asamk.signal.storage.groups.GroupInfo; @@ -309,7 +310,7 @@ public class SignalAccount implements Closeable { contactInfo.messageExpirationTime = thread.messageExpirationTime; contactStore.updateContact(contactInfo); } else { - GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id)); + GroupInfo groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id)); if (groupInfo instanceof GroupInfoV1) { ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime; groupStore.updateGroup(groupInfo); 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 3571985b..40b8c884 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -1,8 +1,8 @@ package org.asamk.signal.storage.groups; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.asamk.signal.manager.GroupId; import org.asamk.signal.manager.GroupInviteLinkUrl; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -12,12 +12,8 @@ import java.util.stream.Stream; public abstract class GroupInfo { - @JsonProperty - public final byte[] groupId; - - public GroupInfo(byte[] groupId) { - this.groupId = groupId; - } + @JsonIgnore + public abstract GroupId getGroupId(); @JsonIgnore public abstract String getTitle(); diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java index b06e2436..90b26b81 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java @@ -13,7 +13,11 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdV1; +import org.asamk.signal.manager.GroupIdV2; import org.asamk.signal.manager.GroupInviteLinkUrl; +import org.asamk.signal.manager.GroupUtils; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.io.IOException; @@ -26,8 +30,9 @@ public class GroupInfoV1 extends GroupInfo { private static final ObjectMapper jsonProcessor = new ObjectMapper(); - @JsonProperty - public byte[] expectedV2Id; + private final GroupIdV1 groupId; + + private GroupIdV2 expectedV2Id; @JsonProperty public String name; @@ -47,18 +52,8 @@ public class GroupInfoV1 extends GroupInfo { @JsonProperty(defaultValue = "false") public boolean archived; - public GroupInfoV1(byte[] groupId) { - super(groupId); - } - - @Override - public String getTitle() { - return name; - } - - @Override - public GroupInviteLinkUrl getGroupInviteLink() { - return null; + public GroupInfoV1(GroupIdV1 groupId) { + this.groupId = groupId; } public GroupInfoV1( @@ -74,8 +69,8 @@ public class GroupInfoV1 extends GroupInfo { @JsonProperty("messageExpirationTime") int messageExpirationTime, @JsonProperty("active") boolean _ignored_active ) { - super(groupId); - this.expectedV2Id = expectedV2Id; + this.groupId = GroupId.v1(groupId); + this.expectedV2Id = GroupId.v2(expectedV2Id); this.name = name; this.members.addAll(members); this.color = color; @@ -85,6 +80,40 @@ public class GroupInfoV1 extends GroupInfo { this.messageExpirationTime = messageExpirationTime; } + @Override + @JsonIgnore + public GroupIdV1 getGroupId() { + return groupId; + } + + @JsonProperty("groupId") + private byte[] getGroupIdJackson() { + return groupId.serialize(); + } + + @JsonIgnore + public GroupIdV2 getExpectedV2Id() { + if (expectedV2Id == null) { + expectedV2Id = GroupUtils.getGroupIdV2(groupId); + } + return expectedV2Id; + } + + @JsonProperty("expectedV2Id") + private byte[] getExpectedV2IdJackson() { + return expectedV2Id.serialize(); + } + + @Override + public String getTitle() { + return name; + } + + @Override + public GroupInviteLinkUrl getGroupInviteLink() { + return null; + } + @JsonIgnore public Set getMembers() { return members; diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java index 65f8c9a4..1b00caaa 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java @@ -1,5 +1,6 @@ package org.asamk.signal.storage.groups; +import org.asamk.signal.manager.GroupIdV2; import org.asamk.signal.manager.GroupInviteLinkUrl; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.local.DecryptedGroup; @@ -13,16 +14,22 @@ import java.util.stream.Collectors; public class GroupInfoV2 extends GroupInfo { + private final GroupIdV2 groupId; private final GroupMasterKey masterKey; private boolean blocked; private DecryptedGroup group; // stored as a file with hexadecimal groupId as name - public GroupInfoV2(final byte[] groupId, final GroupMasterKey masterKey) { - super(groupId); + public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) { + this.groupId = groupId; this.masterKey = masterKey; } + @Override + public GroupIdV2 getGroupId() { + return groupId; + } + public GroupMasterKey getMasterKey() { return masterKey; } diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index 2175e293..d6a99f1c 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -12,6 +12,9 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.GroupIdV1; +import org.asamk.signal.manager.GroupIdV2; import org.asamk.signal.manager.GroupUtils; import org.asamk.signal.util.Hex; import org.asamk.signal.util.IOUtils; @@ -25,7 +28,6 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -39,7 +41,7 @@ public class JsonGroupStore { @JsonProperty("groups") @JsonSerialize(using = GroupsSerializer.class) @JsonDeserialize(using = GroupsDeserializer.class) - private final Map groups = new HashMap<>(); + private final Map groups = new HashMap<>(); private JsonGroupStore() { } @@ -49,11 +51,11 @@ public class JsonGroupStore { } public void updateGroup(GroupInfo group) { - groups.put(Base64.encodeBytes(group.groupId), group); + groups.put(group.getGroupId(), group); if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) { try { IOUtils.createPrivateDirectories(groupCachePath); - try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.groupId))) { + try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.getGroupId()))) { ((GroupInfoV2) group).getGroup().writeTo(stream); } } catch (IOException e) { @@ -62,57 +64,50 @@ public class JsonGroupStore { } } - public void deleteGroup(byte[] groupId) { - groups.remove(Base64.encodeBytes(groupId)); + public void deleteGroup(GroupId groupId) { + groups.remove(groupId); } - public GroupInfo getGroup(byte[] groupId) { - final GroupInfo group = groups.get(Base64.encodeBytes(groupId)); - if (group == null & groupId.length == 16) { - return getGroupByV1Id(groupId); - } - loadDecryptedGroup(group); - return group; - } - - public GroupInfo getGroupByV1Id(byte[] groupIdV1) { - GroupInfo group = groups.get(Base64.encodeBytes(groupIdV1)); + public GroupInfo getGroup(GroupId groupId) { + GroupInfo group = groups.get(groupId); if (group == null) { - group = groups.get(Base64.encodeBytes(GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(groupIdV1)))); - } - loadDecryptedGroup(group); - return group; - } - - public GroupInfo getGroupByV2Id(byte[] groupIdV2) { - GroupInfo group = groups.get(Base64.encodeBytes(groupIdV2)); - if (group == null) { - for (GroupInfo g : groups.values()) { - if (g instanceof GroupInfoV1 && Arrays.equals(groupIdV2, ((GroupInfoV1) g).expectedV2Id)) { - group = g; - break; - } + if (groupId instanceof GroupIdV1) { + group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId)); + } else if (groupId instanceof GroupIdV2) { + group = getGroupV1ByV2Id((GroupIdV2) groupId); } } loadDecryptedGroup(group); return group; } + private GroupInfoV1 getGroupV1ByV2Id(GroupIdV2 groupIdV2) { + for (GroupInfo g : groups.values()) { + if (g instanceof GroupInfoV1) { + final GroupInfoV1 gv1 = (GroupInfoV1) g; + if (groupIdV2.equals(gv1.getExpectedV2Id())) { + return gv1; + } + } + } + return null; + } + private void loadDecryptedGroup(final GroupInfo group) { if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) { - try (FileInputStream stream = new FileInputStream(getGroupFile(group.groupId))) { + try (FileInputStream stream = new FileInputStream(getGroupFile(group.getGroupId()))) { ((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream)); } catch (IOException ignored) { } } } - private File getGroupFile(final byte[] groupId) { - return new File(groupCachePath, Hex.toStringCondensed(groupId)); + private File getGroupFile(final GroupId groupId) { + return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize())); } - public GroupInfoV1 getOrCreateGroupV1(byte[] groupId) { - GroupInfo group = groups.get(Base64.encodeBytes(groupId)); + public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) { + GroupInfo group = getGroup(groupId); if (group instanceof GroupInfoV1) { return (GroupInfoV1) group; } @@ -146,7 +141,7 @@ public class JsonGroupStore { } else if (group instanceof GroupInfoV2) { final GroupInfoV2 groupV2 = (GroupInfoV2) group; jgen.writeStartObject(); - jgen.writeStringField("groupId", Base64.encodeBytes(groupV2.groupId)); + jgen.writeStringField("groupId", groupV2.getGroupId().toBase64()); jgen.writeStringField("masterKey", Base64.encodeBytes(groupV2.getMasterKey().serialize())); jgen.writeBooleanField("blocked", groupV2.isBlocked()); jgen.writeEndObject(); @@ -158,34 +153,31 @@ public class JsonGroupStore { } } - private static class GroupsDeserializer extends JsonDeserializer> { + private static class GroupsDeserializer extends JsonDeserializer> { @Override - public Map deserialize( + public Map deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) throws IOException { - Map groups = new HashMap<>(); + Map groups = new HashMap<>(); JsonNode node = jsonParser.getCodec().readTree(jsonParser); for (JsonNode n : node) { GroupInfo g; if (n.has("masterKey")) { // a v2 group - byte[] groupId = Base64.decode(n.get("groupId").asText()); + GroupIdV2 groupId = GroupIdV2.fromBase64(n.get("groupId").asText()); try { GroupMasterKey masterKey = new GroupMasterKey(Base64.decode(n.get("masterKey").asText())); g = new GroupInfoV2(groupId, masterKey); } catch (InvalidInputException e) { - throw new AssertionError("Invalid master key for group " + Base64.encodeBytes(groupId)); + throw new AssertionError("Invalid master key for group " + groupId.toBase64()); } g.setBlocked(n.get("blocked").asBoolean(false)); } else { GroupInfoV1 gv1 = jsonProcessor.treeToValue(n, GroupInfoV1.class); - if (gv1.expectedV2Id == null) { - gv1.expectedV2Id = GroupUtils.getGroupId(GroupUtils.deriveV2MigrationMasterKey(gv1.groupId)); - } g = gv1; } - groups.put(Base64.encodeBytes(g.groupId), g); + groups.put(g.getGroupId(), g); } return groups; diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 8e65d440..44d505be 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -1,5 +1,6 @@ package org.asamk.signal.util; +import org.asamk.signal.manager.GroupIdFormatException; import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.NotAGroupMemberException; import org.whispersystems.signalservice.api.messages.SendMessageResult; diff --git a/src/main/java/org/asamk/signal/util/GroupIdFormatException.java b/src/main/java/org/asamk/signal/util/GroupIdFormatException.java deleted file mode 100644 index 5a5c4570..00000000 --- a/src/main/java/org/asamk/signal/util/GroupIdFormatException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.asamk.signal.util; - -import java.io.IOException; - -public class GroupIdFormatException extends Exception { - - public GroupIdFormatException(String groupId, IOException e) { - super("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage()); - } -} diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index bc2d3377..3cd5619a 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -2,13 +2,13 @@ package org.asamk.signal.util; import com.fasterxml.jackson.databind.JsonNode; +import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.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; import java.io.InvalidObjectException; public class Util { @@ -48,12 +48,8 @@ public class Util { return node; } - public static byte[] decodeGroupId(String groupId) throws GroupIdFormatException { - try { - return Base64.decode(groupId); - } catch (IOException e) { - throw new GroupIdFormatException(groupId, e); - } + public static GroupId decodeGroupId(String groupId) throws GroupIdFormatException { + return GroupId.fromBase64(groupId); } public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { From e11e02088648458f039f9b2b69a5e9a9dfe5789f Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 24 Dec 2020 17:53:23 +0100 Subject: [PATCH 09/32] Retrieve group v2 avatars Fixes #392 --- .../org/asamk/signal/manager/Manager.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index b5d425d8..4c10a529 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1754,6 +1754,11 @@ public class Manager implements Closeable { } if (group != null) { storeProfileKeysFromMembers(group); + try { + retrieveGroupAvatar(groupId, groupSecretParams, group.getAvatar()); + } catch (IOException e) { + System.err.println("Failed to download group avatar, ignoring ..."); + } } groupInfoV2.setGroup(group); account.getGroupStore().updateGroup(groupInfoV2); @@ -2246,6 +2251,35 @@ public class Manager implements Closeable { } } + private File retrieveGroupAvatar( + GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey + ) throws IOException { + IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); + SignalServiceMessageReceiver receiver = getOrCreateMessageReceiver(); + File outputFile = getGroupAvatarFile(groupId); + GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams); + + File tmpFile = IOUtils.createTempFile(); + tmpFile.deleteOnExit(); + try (InputStream input = receiver.retrieveGroupsV2ProfileAvatar(cdnKey, + tmpFile, + ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { + byte[] encryptedData = IOUtils.readFully(input); + + byte[] decryptedData = groupOperations.decryptAvatar(encryptedData); + try (OutputStream output = new FileOutputStream(outputFile)) { + output.write(decryptedData); + } + } finally { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + System.err.println("Failed to delete received avatar temp file “" + tmpFile + "”: " + e.getMessage()); + } + } + return outputFile; + } + private File getProfileAvatarFile(SignalServiceAddress address) { return new File(pathConfig.getAvatarsPath(), "profile-" + address.getLegacyIdentifier()); } From ff998fce578ebc035439f8eaad8e1b48e482a3c5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 24 Dec 2020 18:05:12 +0100 Subject: [PATCH 10/32] Fix handling data messages of sync messages --- .../java/org/asamk/signal/manager/Manager.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 4c10a529..e02106b9 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1621,7 +1621,7 @@ public class Manager implements Closeable { } final SignalServiceAddress conversationPartnerAddress = isSync ? destination : source; - if (message.isEndSession()) { + if (conversationPartnerAddress != null && message.isEndSession()) { handleEndSession(conversationPartnerAddress); } if (message.isExpirationUpdate() || message.getBody().isPresent()) { @@ -1638,7 +1638,7 @@ public class Manager implements Closeable { } else if (message.getGroupContext().get().getGroupV2().isPresent()) { // disappearing message timer already stored in the DecryptedGroup } - } else { + } else if (conversationPartnerAddress != null) { ContactInfo contact = account.getContactStore().getContact(conversationPartnerAddress); if (contact == null) { contact = new ContactInfo(conversationPartnerAddress); @@ -2025,13 +2025,11 @@ public class Manager implements Closeable { if (syncMessage.getSent().isPresent()) { SentTranscriptMessage message = syncMessage.getSent().get(); final SignalServiceAddress destination = message.getDestination().orNull(); - if (destination != null) { - actions.addAll(handleSignalServiceDataMessage(message.getMessage(), - true, - sender, - destination, - ignoreAttachments)); - } + actions.addAll(handleSignalServiceDataMessage(message.getMessage(), + true, + sender, + destination, + ignoreAttachments)); } if (syncMessage.getRequest().isPresent()) { RequestMessage rm = syncMessage.getRequest().get(); From caabde4acfc1c6bccec981ff763d6dca6f6ed383 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 25 Dec 2020 13:42:51 +0100 Subject: [PATCH 11/32] Fix prevention of adding group members a second time --- src/main/java/org/asamk/signal/manager/Manager.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index e02106b9..2553a416 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -816,7 +816,10 @@ public class Manager implements Closeable { if (members != null) { final Set newMembers = new HashSet<>(members); - newMembers.removeAll(group.getMembers()); + newMembers.removeAll(group.getMembers() + .stream() + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toSet())); if (newMembers.size() > 0) { Pair groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers); From 6a82029ab481eb657847651b4f5200acf57b2553 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 25 Dec 2020 13:46:35 +0100 Subject: [PATCH 12/32] Use base64 group id for protobuf group file to match avatar files base64 with '/' replaced by '_' --- .../signal/storage/groups/JsonGroupStore.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index d6a99f1c..6f0f3c04 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -58,6 +58,10 @@ public class JsonGroupStore { try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.getGroupId()))) { ((GroupInfoV2) group).getGroup().writeTo(stream); } + final File groupFileLegacy = getGroupFileLegacy(group.getGroupId()); + if (groupFileLegacy.exists()) { + groupFileLegacy.delete(); + } } catch (IOException e) { System.err.println("Failed to cache group, ignoring ..."); } @@ -95,17 +99,28 @@ public class JsonGroupStore { private void loadDecryptedGroup(final GroupInfo group) { if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) { - try (FileInputStream stream = new FileInputStream(getGroupFile(group.getGroupId()))) { + File groupFile = getGroupFile(group.getGroupId()); + if (!groupFile.exists()) { + groupFile = getGroupFileLegacy(group.getGroupId()); + } + if (!groupFile.exists()) { + return; + } + try (FileInputStream stream = new FileInputStream(groupFile)) { ((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream)); } catch (IOException ignored) { } } } - private File getGroupFile(final GroupId groupId) { + private File getGroupFileLegacy(final GroupId groupId) { return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize())); } + private File getGroupFile(final GroupId groupId) { + return new File(groupCachePath, groupId.toBase64().replace("/", "_")); + } + public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) { GroupInfo group = getGroup(groupId); if (group instanceof GroupInfoV1) { From 5c754b6f5d5bd3273b3c0722cf3eabbcd02c20b9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 25 Dec 2020 22:34:30 +0100 Subject: [PATCH 13/32] Use slf4j simple logger --- build.gradle | 2 +- src/main/java/org/asamk/signal/Main.java | 199 +++++++++--------- .../org/asamk/signal/manager/Manager.java | 114 +++++----- .../org/asamk/signal/manager/PathConfig.java | 22 +- .../signal/manager/ProvisioningManager.java | 3 +- .../signal/manager/helper/GroupHelper.java | 38 ++-- .../asamk/signal/storage/SignalAccount.java | 10 +- .../signal/storage/groups/JsonGroupStore.java | 6 +- .../protocol/JsonIdentityKeyStore.java | 6 +- .../storage/protocol/JsonPreKeyStore.java | 6 +- .../storage/protocol/JsonSessionStore.java | 8 +- .../protocol/JsonSignedPreKeyStore.java | 6 +- 12 files changed, 237 insertions(+), 183 deletions(-) diff --git a/build.gradle b/build.gradle index 48b57a4f..1fbf5948 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ dependencies { implementation 'org.bouncycastle:bcprov-jdk15on:1.67' implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1' implementation 'com.github.hypfvieh:dbus-java:3.2.4' - implementation 'org.slf4j:slf4j-nop:1.7.30' + implementation 'org.slf4j:slf4j-simple:1.7.30' } jar { diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index de827b1c..97e3f9b2 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -41,6 +41,8 @@ import org.asamk.signal.util.SecurityProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; @@ -50,10 +52,10 @@ import java.io.IOException; import java.security.Security; import java.util.Map; -import static org.whispersystems.signalservice.internal.util.Util.isEmpty; - public class Main { + final static Logger logger = LoggerFactory.getLogger(Main.class); + public static void main(String[] args) { installSecurityProviderWorkaround(); @@ -62,7 +64,7 @@ public class Main { System.exit(1); } - int res = handleCommands(ns); + int res = init(ns); System.exit(res); } @@ -72,77 +74,81 @@ public class Main { Security.addProvider(new BouncyCastleProvider()); } - private static int handleCommands(Namespace ns) { + public static int init(Namespace ns) { + if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { + return initDbusClient(ns, ns.getBoolean("dbus_system")); + } + final String username = ns.getString("username"); - if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { - try { - DBusConnection.DBusBusType busType; - if (ns.getBoolean("dbus_system")) { - busType = DBusConnection.DBusBusType.SYSTEM; - } else { - busType = DBusConnection.DBusBusType.SESSION; - } - try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) { - Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME, - DbusConfig.SIGNAL_OBJECTPATH, - Signal.class); - - return handleCommands(ns, ts, dBusConn); - } - } catch (UnsatisfiedLinkError e) { - System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); - return 1; - } catch (DBusException | IOException e) { - e.printStackTrace(); - return 3; - } + final File dataPath; + String config = ns.getString("config"); + if (config != null) { + dataPath = new File(config); } else { - String dataPath = ns.getString("config"); - if (isEmpty(dataPath)) { - dataPath = getDefaultDataPath(); - } + dataPath = getDefaultDataPath(); + } - final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration( - BaseConfig.USER_AGENT); + final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration( + BaseConfig.USER_AGENT); - if (!ServiceConfig.getCapabilities().isGv2()) { - System.err.println("WARNING: Support for new group V2 is disabled," - + " because the required native library dependency is missing: libzkgroup"); - } + if (!ServiceConfig.getCapabilities().isGv2()) { + logger.warn("WARNING: Support for new group V2 is disabled," + + " because the required native library dependency is missing: libzkgroup"); + } - if (username == null) { - ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT); - return handleCommands(ns, pm); - } + if (username == null) { + ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT); + return handleCommands(ns, pm); + } - Manager manager; + Manager manager; + try { + manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT); + } catch (Throwable e) { + logger.error("Error loading state file: {}", e.getMessage()); + return 2; + } + + try (Manager m = manager) { try { - manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT); - } catch (Throwable e) { - System.err.println("Error loading state file: " + e.getMessage()); + m.checkAccountState(); + } catch (AuthorizationFailedException e) { + if (!"register".equals(ns.getString("command"))) { + // Register command should still be possible, if current authorization fails + System.err.println("Authorization failed, was the number registered elsewhere?"); + return 2; + } + } catch (IOException e) { + logger.error("Error while checking account: {}", e.getMessage()); return 2; } - try (Manager m = manager) { - try { - m.checkAccountState(); - } catch (AuthorizationFailedException e) { - if (!"register".equals(ns.getString("command"))) { - // Register command should still be possible, if current authorization fails - System.err.println("Authorization failed, was the number registered elsewhere?"); - return 2; - } - } catch (IOException e) { - System.err.println("Error while checking account: " + e.getMessage()); - return 2; - } + return handleCommands(ns, m); + } catch (IOException e) { + logger.error("Cleanup failed", e); + return 3; + } + } - return handleCommands(ns, m); - } catch (IOException e) { - e.printStackTrace(); - return 3; + private static int initDbusClient(final Namespace ns, final boolean systemBus) { + try { + DBusConnection.DBusBusType busType; + if (systemBus) { + busType = DBusConnection.DBusBusType.SYSTEM; + } else { + busType = DBusConnection.DBusBusType.SESSION; } + try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) { + Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME, + DbusConfig.SIGNAL_OBJECTPATH, + Signal.class); + + return handleCommands(ns, ts, dBusConn); + } + } catch (DBusException | IOException e) { + logger.error("Dbus client failed", e); + return 3; } } @@ -205,19 +211,19 @@ public class Main { * * @return the data directory to be used by signal-cli. */ - private static String getDefaultDataPath() { - String dataPath = IOUtils.getDataHomeDir() + "/signal-cli"; - if (new File(dataPath).exists()) { + private static File getDefaultDataPath() { + File dataPath = new File(IOUtils.getDataHomeDir(), "/signal-cli"); + if (dataPath.exists()) { return dataPath; } - String legacySettingsPath = System.getProperty("user.home") + "/.config/signal"; - if (new File(legacySettingsPath).exists()) { + File legacySettingsPath = new File(System.getProperty("user.home"), "/.config/signal"); + if (legacySettingsPath.exists()) { return legacySettingsPath; } - legacySettingsPath = System.getProperty("user.home") + "/.config/textsecure"; - if (new File(legacySettingsPath).exists()) { + legacySettingsPath = new File(System.getProperty("user.home"), "/.config/textsecure"); + if (legacySettingsPath.exists()) { return legacySettingsPath; } @@ -225,32 +231,7 @@ public class Main { } private static Namespace parseArgs(String[] args) { - ArgumentParser parser = ArgumentParsers.newFor("signal-cli") - .build() - .defaultHelp(true) - .description("Commandline interface for Signal.") - .version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION); - - parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version()); - parser.addArgument("--config") - .help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli)."); - - MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup(); - mut.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification."); - mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue()); - mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue()); - - Subparsers subparsers = parser.addSubparsers() - .title("subcommands") - .dest("command") - .description("valid subcommands") - .help("additional help"); - - final Map commands = Commands.getCommands(); - for (Map.Entry entry : commands.entrySet()) { - Subparser subparser = subparsers.addParser(entry.getKey()); - entry.getValue().attachToSubparser(subparser); - } + ArgumentParser parser = buildArgumentParser(); Namespace ns; try { @@ -283,4 +264,34 @@ public class Main { } return ns; } + + private static ArgumentParser buildArgumentParser() { + ArgumentParser parser = ArgumentParsers.newFor("signal-cli") + .build() + .defaultHelp(true) + .description("Commandline interface for Signal.") + .version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION); + + parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version()); + parser.addArgument("--config") + .help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli)."); + + MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup(); + mut.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification."); + mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue()); + mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue()); + + Subparsers subparsers = parser.addSubparsers() + .title("subcommands") + .dest("command") + .description("valid subcommands") + .help("additional help"); + + final Map commands = Commands.getCommands(); + for (Map.Entry entry : commands.entrySet()) { + Subparser subparser = subparsers.addParser(entry.getKey()); + entry.getValue().attachToSubparser(subparser); + } + return parser; + } } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 2553a416..c679449d 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -55,6 +55,8 @@ import org.signal.zkgroup.groups.GroupSecretParams; import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; @@ -173,6 +175,8 @@ import static org.asamk.signal.manager.ServiceConfig.getIasKeyStore; public class Manager implements Closeable { + final static Logger logger = LoggerFactory.getLogger(Manager.class); + private final SleepTimer timer = new UptimeSleepTimer(); private final SignalServiceConfiguration serviceConfiguration; @@ -274,7 +278,7 @@ public class Manager implements Closeable { } public static Manager init( - String username, String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent + String username, File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent ) throws IOException { PathConfig pathConfig = PathConfig.createDefault(settingsPath); @@ -590,7 +594,7 @@ public class Manager implements Closeable { try { profile = retrieveRecipientProfile(address, profileKey); } catch (IOException e) { - System.err.println("Failed to retrieve profile, ignoring: " + e.getMessage()); + logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage()); profileEntry.setRequestPending(false); return null; } @@ -613,7 +617,7 @@ public class Manager implements Closeable { profileAndCredential = profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL); } catch (IOException e) { - System.err.println("Failed to retrieve profile key credential, ignoring: " + e.getMessage()); + logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage()); return null; } @@ -646,7 +650,7 @@ public class Manager implements Closeable { ? null : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey); } catch (Throwable e) { - System.err.println("Failed to retrieve profile avatar, ignoring: " + e.getMessage()); + logger.warn("Failed to retrieve profile avatar, ignoring: {}", e.getMessage()); } ProfileCipher profileCipher = new ProfileCipher(profileKey); @@ -1327,7 +1331,7 @@ public class Manager implements Closeable { try { certificate = accountManager.getSenderCertificate(); } catch (IOException e) { - System.err.println("Failed to get sender certificate: " + e); + logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage()); return null; } // TODO cache for a day @@ -1366,7 +1370,7 @@ public class Manager implements Closeable { missingUuids.stream().map(a -> a.getNumber().get()).collect(Collectors.toSet()), CDS_MRENCLAVE); } catch (IOException | Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException e) { - System.err.println("Failed to resolve uuids from server: " + e.getMessage()); + logger.warn("Failed to resolve uuids from server, ignoring: {}", e.getMessage()); registeredUsers = new HashMap<>(); } @@ -1570,8 +1574,9 @@ public class Manager implements Closeable { try { retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.getGroupId()); } catch (IOException | InvalidMessageException | MissingConfigurationException e) { - System.err.println("Failed to retrieve group avatar (" + avatar.asPointer() - .getRemoteId() + "): " + e.getMessage()); + logger.warn("Failed to retrieve avatar for group {}, ignoring: {}", + groupId.toBase64(), + e.getMessage()); } } } @@ -1593,7 +1598,7 @@ public class Manager implements Closeable { } case DELIVER: if (groupV1 == null && !isSync) { - actions.add(new SendGroupInfoRequestAction(source, groupV1.getGroupId())); + actions.add(new SendGroupInfoRequestAction(source, groupId)); } break; case QUIT: { @@ -1658,10 +1663,9 @@ public class Manager implements Closeable { try { retrieveAttachment(attachment.asPointer()); } catch (IOException | InvalidMessageException | MissingConfigurationException e) { - System.err.println("Failed to retrieve attachment (" - + attachment.asPointer().getRemoteId() - + "): " - + e.getMessage()); + logger.warn("Failed to retrieve attachment ({}), ignoring: {}", + attachment.asPointer().getRemoteId(), + e.getMessage()); } } } @@ -1686,10 +1690,9 @@ public class Manager implements Closeable { try { retrieveAttachment(attachment); } catch (IOException | InvalidMessageException | MissingConfigurationException e) { - System.err.println("Failed to retrieve attachment (" - + attachment.getRemoteId() - + "): " - + e.getMessage()); + logger.warn("Failed to retrieve preview image ({}), ignoring: {}", + attachment.getRemoteId(), + e.getMessage()); } } } @@ -1703,10 +1706,9 @@ public class Manager implements Closeable { try { retrieveAttachment(attachment.asPointer()); } catch (IOException | InvalidMessageException | MissingConfigurationException e) { - System.err.println("Failed to retrieve attachment (" - + attachment.asPointer().getRemoteId() - + "): " - + e.getMessage()); + logger.warn("Failed to retrieve quote attachment thumbnail ({}), ignoring: {}", + attachment.asPointer().getRemoteId(), + e.getMessage()); } } } @@ -1734,11 +1736,9 @@ public class Manager implements Closeable { // Received a v2 group message for a v1 group, we need to locally migrate the group account.getGroupStore().deleteGroup(groupInfo.getGroupId()); groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); - System.err.println("Locally migrated group " - + groupInfo.getGroupId().toBase64() - + " to group v2, id: " - + groupInfoV2.getGroupId().toBase64() - + " !!!"); + logger.info("Locally migrated group {} to group v2, id: {}", + groupInfo.getGroupId().toBase64(), + groupInfoV2.getGroupId().toBase64()); } else if (groupInfo instanceof GroupInfoV2) { groupInfoV2 = (GroupInfoV2) groupInfo; } else { @@ -1757,10 +1757,13 @@ public class Manager implements Closeable { } if (group != null) { storeProfileKeysFromMembers(group); - try { - retrieveGroupAvatar(groupId, groupSecretParams, group.getAvatar()); - } catch (IOException e) { - System.err.println("Failed to download group avatar, ignoring ..."); + final String avatar = group.getAvatar(); + if (avatar != null && !avatar.isEmpty()) { + try { + retrieveGroupAvatar(groupId, groupSecretParams, avatar); + } catch (IOException e) { + logger.warn("Failed to download group avatar, ignoring: {}", e.getMessage()); + } } } groupInfoV2.setGroup(group); @@ -1830,7 +1833,7 @@ public class Manager implements Closeable { try { Files.delete(fileEntry.toPath()); } catch (IOException e) { - System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); + logger.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry, e.getMessage()); } return; } @@ -1848,7 +1851,7 @@ public class Manager implements Closeable { try { Files.delete(fileEntry.toPath()); } catch (IOException e) { - System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); + logger.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry, e.getMessage()); } } @@ -1880,8 +1883,7 @@ public class Manager implements Closeable { File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp()); Utils.storeEnvelope(envelope1, cacheFile); } catch (IOException e) { - System.err.println("Failed to store encrypted message in disk cache, ignoring: " - + e.getMessage()); + logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage()); } }); if (result.isPresent()) { @@ -1910,7 +1912,7 @@ public class Manager implements Closeable { if (returnOnTimeout) return; continue; } catch (InvalidVersionException e) { - System.err.println("Ignoring error: " + e.getMessage()); + logger.warn("Error while receiving messages, ignoring: {}", e.getMessage()); continue; } @@ -1954,7 +1956,7 @@ public class Manager implements Closeable { // Try to delete directory if empty new File(getMessageCachePath()).delete(); } catch (IOException e) { - System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage()); + logger.warn("Failed to delete cached message file “{}”, ignoring: {}", cacheFile, e.getMessage()); } } } @@ -2088,16 +2090,18 @@ public class Manager implements Closeable { } } } catch (Exception e) { + logger.warn("Failed to handle received sync groups “{}”, ignoring: {}", + tmpFile, + e.getMessage()); e.printStackTrace(); } finally { if (tmpFile != null) { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete received groups temp file “" - + tmpFile - + "”: " - + e.getMessage()); + logger.warn("Failed to delete received groups temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); } } } @@ -2114,8 +2118,8 @@ public class Manager implements Closeable { try { setGroupBlocked(groupId, true); } catch (GroupNotFoundException e) { - System.err.println("BlockedListMessage contained groupID that was not found in GroupStore: " - + groupId.toBase64()); + logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}", + groupId.toBase64()); } } } @@ -2176,10 +2180,9 @@ public class Manager implements Closeable { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete received contacts temp file “" - + tmpFile - + "”: " - + e.getMessage()); + logger.warn("Failed to delete received contacts temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); } } } @@ -2275,7 +2278,9 @@ public class Manager implements Closeable { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete received avatar temp file “" + tmpFile + "”: " + e.getMessage()); + logger.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); } } return outputFile; @@ -2303,7 +2308,9 @@ public class Manager implements Closeable { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete received avatar temp file “" + tmpFile + "”: " + e.getMessage()); + logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); } } return outputFile; @@ -2343,10 +2350,9 @@ public class Manager implements Closeable { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete received attachment temp file “" - + tmpFile - + "”: " - + e.getMessage()); + logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); } } return outputFile; @@ -2397,7 +2403,7 @@ public class Manager implements Closeable { try { Files.delete(groupsFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete groups temp file “" + groupsFile + "”: " + e.getMessage()); + logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage()); } } } @@ -2462,7 +2468,7 @@ public class Manager implements Closeable { try { Files.delete(contactsFile.toPath()); } catch (IOException e) { - System.err.println("Failed to delete contacts temp file “" + contactsFile + "”: " + e.getMessage()); + logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/manager/PathConfig.java b/src/main/java/org/asamk/signal/manager/PathConfig.java index c0c9e1e7..ca750931 100644 --- a/src/main/java/org/asamk/signal/manager/PathConfig.java +++ b/src/main/java/org/asamk/signal/manager/PathConfig.java @@ -1,30 +1,34 @@ package org.asamk.signal.manager; +import java.io.File; + public class PathConfig { - private final String dataPath; - private final String attachmentsPath; - private final String avatarsPath; + private final File dataPath; + private final File attachmentsPath; + private final File avatarsPath; - public static PathConfig createDefault(final String settingsPath) { - return new PathConfig(settingsPath + "/data", settingsPath + "/attachments", settingsPath + "/avatars"); + public static PathConfig createDefault(final File settingsPath) { + return new PathConfig(new File(settingsPath, "/data"), + new File(settingsPath, "/attachments"), + new File(settingsPath, "/avatars")); } - private PathConfig(final String dataPath, final String attachmentsPath, final String avatarsPath) { + private PathConfig(final File dataPath, final File attachmentsPath, final File avatarsPath) { this.dataPath = dataPath; this.attachmentsPath = attachmentsPath; this.avatarsPath = avatarsPath; } public String getDataPath() { - return dataPath; + return dataPath.getPath(); } public String getAttachmentsPath() { - return attachmentsPath; + return attachmentsPath.getPath(); } public String getAvatarsPath() { - return avatarsPath; + return avatarsPath.getPath(); } } diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index eb70b351..f81cfa49 100644 --- a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -31,6 +31,7 @@ import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; +import java.io.File; import java.io.IOException; import java.util.concurrent.TimeoutException; @@ -45,7 +46,7 @@ public class ProvisioningManager { private final int registrationId; private final String password; - public ProvisioningManager(String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) { + public ProvisioningManager(File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) { this.pathConfig = PathConfig.createDefault(settingsPath); this.serviceConfiguration = serviceConfiguration; this.userAgent = userAgent; diff --git a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 6fd35003..5a88bc66 100644 --- a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -6,6 +6,7 @@ import org.asamk.signal.manager.GroupIdV2; import org.asamk.signal.manager.GroupLinkPassword; import org.asamk.signal.manager.GroupUtils; import org.asamk.signal.storage.groups.GroupInfoV2; +import org.asamk.signal.storage.profiles.SignalProfile; import org.asamk.signal.util.IOUtils; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.GroupChange; @@ -20,6 +21,8 @@ import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.groups.GroupSecretParams; import org.signal.zkgroup.groups.UuidCiphertext; import org.signal.zkgroup.profiles.ProfileKeyCredential; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; @@ -44,6 +47,8 @@ import java.util.stream.Collectors; public class GroupHelper { + final static Logger logger = LoggerFactory.getLogger(GroupHelper.class); + private final ProfileKeyCredentialProvider profileKeyCredentialProvider; private final ProfileProvider profileProvider; @@ -78,7 +83,7 @@ public class GroupHelper { groupSecretParams); return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString); } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { - System.err.println("Failed to retrieve Group V2 info, ignoring ..."); + logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); return null; } } @@ -111,11 +116,11 @@ public class GroupHelper { groupsV2Api.putNewGroup(newGroup, groupAuthForToday); decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday); } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { - System.err.println("Failed to create V2 group: " + e.getMessage()); + logger.warn("Failed to create V2 group: {}", e.getMessage()); return null; } if (decryptedGroup == null) { - System.err.println("Failed to create V2 group!"); + logger.warn("Failed to create V2 group, unknown error!"); return null; } @@ -141,7 +146,7 @@ public class GroupHelper { final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential( selfAddressProvider.getSelfAddress()); if (profileKeyCredential == null) { - System.err.println("Cannot create a V2 group as self does not have a versioned profile"); + logger.warn("Cannot create a V2 group as self does not have a versioned profile"); return null; } @@ -165,22 +170,23 @@ public class GroupHelper { } private boolean areMembersValid(final Collection members) { - final int noUuidCapability = members.stream() + final Set noUuidCapability = members.stream() .filter(address -> !address.getUuid().isPresent()) - .collect(Collectors.toUnmodifiableSet()) - .size(); - if (noUuidCapability > 0) { - System.err.println("Cannot create a V2 group as " + noUuidCapability + " members don't have a UUID."); + .map(SignalServiceAddress::getLegacyIdentifier) + .collect(Collectors.toSet()); + if (noUuidCapability.size() > 0) { + logger.warn("Cannot create a V2 group as some members don't have a UUID: {}", + String.join(", ", noUuidCapability)); return false; } - final int noGv2Capability = members.stream() + final Set noGv2Capability = members.stream() .map(profileProvider::getProfile) .filter(profile -> profile != null && !profile.getCapabilities().gv2) - .collect(Collectors.toUnmodifiableSet()) - .size(); - if (noGv2Capability > 0) { - System.err.println("Cannot create a V2 group as " + noGv2Capability + " members don't support Groups V2."); + .collect(Collectors.toSet()); + if (noGv2Capability.size() > 0) { + logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}", + noGv2Capability.stream().map(SignalProfile::getName).collect(Collectors.joining(", "))); return false; } @@ -219,7 +225,9 @@ public class GroupHelper { final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams); - if (!areMembersValid(newMembers)) return null; + if (!areMembersValid(newMembers)) { + throw new IOException("Failed to update group"); + } Set candidates = newMembers.stream() .map(member -> new GroupCandidate(member.getUuid().get(), diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index d3145ecd..e697efe3 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -29,6 +29,8 @@ import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; @@ -53,6 +55,8 @@ import java.util.stream.Collectors; public class SignalAccount implements Closeable { + final static Logger logger = LoggerFactory.getLogger(SignalAccount.class); + private final ObjectMapper jsonProcessor = new ObjectMapper(); private final FileChannel fileChannel; private final FileLock lock; @@ -357,7 +361,7 @@ public class SignalAccount implements Closeable { } } } catch (Exception e) { - System.err.println(String.format("Error saving file: %s", e.getMessage())); + logger.error("Error saving file: {}", e.getMessage()); } } @@ -365,9 +369,9 @@ public class SignalAccount implements Closeable { FileChannel fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel(); FileLock lock = fileChannel.tryLock(); if (lock == null) { - System.err.println("Config file is in use by another instance, waiting…"); + logger.info("Config file is in use by another instance, waiting…"); lock = fileChannel.lock(); - System.err.println("Config file lock acquired."); + logger.info("Config file lock acquired."); } return new Pair<>(fileChannel, lock); } diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index 6f0f3c04..18bf5ed0 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -21,6 +21,8 @@ import org.asamk.signal.util.IOUtils; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.util.Base64; import java.io.File; @@ -35,6 +37,8 @@ import java.util.Map; public class JsonGroupStore { + final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class); + private static final ObjectMapper jsonProcessor = new ObjectMapper(); public File groupCachePath; @@ -63,7 +67,7 @@ public class JsonGroupStore { groupFileLegacy.delete(); } } catch (IOException e) { - System.err.println("Failed to cache group, ignoring ..."); + logger.warn("Failed to cache group, ignoring: {}", e.getMessage()); } } } 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 0095e6d2..29160cf1 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -10,6 +10,8 @@ import com.fasterxml.jackson.databind.SerializerProvider; import org.asamk.signal.manager.TrustLevel; import org.asamk.signal.util.Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; @@ -27,6 +29,8 @@ import java.util.UUID; public class JsonIdentityKeyStore implements IdentityKeyStore { + final static Logger logger = LoggerFactory.getLogger(JsonIdentityKeyStore.class); + private final List identities = new ArrayList<>(); private final IdentityKeyPair identityKeyPair; @@ -219,7 +223,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { .asLong()) : new Date(); keyStore.saveIdentity(serviceAddress, id, trustLevel, added); } catch (InvalidKeyException | IOException e) { - System.out.println(String.format("Error while decoding key for: %s", trustedKeyName)); + logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java index dea1996d..523809c1 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java @@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.PreKeyStore; @@ -19,6 +21,8 @@ import java.util.Map; class JsonPreKeyStore implements PreKeyStore { + final static Logger logger = LoggerFactory.getLogger(JsonPreKeyStore.class); + private final Map store = new HashMap<>(); public JsonPreKeyStore() { @@ -72,7 +76,7 @@ class JsonPreKeyStore implements PreKeyStore { try { preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText())); } catch (IOException e) { - System.err.println(String.format("Error while decoding prekey for: %s", preKeyId)); + logger.warn("Error while decoding prekey for {}: {}", preKeyId, e.getMessage()); } } } 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 fae72bae..24e4594e 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -9,6 +9,8 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.asamk.signal.util.Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionStore; @@ -24,6 +26,8 @@ import java.util.UUID; class JsonSessionStore implements SessionStore { + final static Logger logger = LoggerFactory.getLogger(JsonSessionStore.class); + private final List sessions = new ArrayList<>(); private SignalServiceAddressResolver resolver; @@ -51,7 +55,7 @@ class JsonSessionStore implements SessionStore { try { return new SessionRecord(info.sessionRecord); } catch (IOException e) { - System.err.println("Failed to load session, resetting session: " + e); + logger.warn("Failed to load session, resetting session: {}", e.getMessage()); final SessionRecord sessionRecord = new SessionRecord(); info.sessionRecord = sessionRecord.serialize(); return sessionRecord; @@ -151,7 +155,7 @@ class JsonSessionStore implements SessionStore { SessionInfo sessionInfo = new SessionInfo(serviceAddress, deviceId, Base64.decode(record)); sessionStore.sessions.add(sessionInfo); } catch (IOException e) { - System.err.println(String.format("Error while decoding session for: %s", sessionName)); + logger.warn("Error while decoding session for {}: {}", sessionName, e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java index 255dd4e0..7accf5a1 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java @@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyStore; @@ -21,6 +23,8 @@ import java.util.Map; class JsonSignedPreKeyStore implements SignedPreKeyStore { + final static Logger logger = LoggerFactory.getLogger(JsonSignedPreKeyStore.class); + private final Map store = new HashMap<>(); public JsonSignedPreKeyStore() { @@ -89,7 +93,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore { try { preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText())); } catch (IOException e) { - System.err.println(String.format("Error while decoding prekey for: %s", preKeyId)); + logger.warn("Error while decoding prekey for {}: {}", preKeyId, e.getMessage()); } } } From 22f19c406779893d08675c2d06d2b7708cc3f2a8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 25 Dec 2020 23:07:36 +0100 Subject: [PATCH 14/32] Use File instead of String --- src/main/java/org/asamk/signal/Main.java | 8 ++-- .../commands/UploadStickerPackCommand.java | 3 +- .../org/asamk/signal/manager/Manager.java | 21 +++++----- .../org/asamk/signal/manager/PathConfig.java | 18 ++++---- .../signal/manager/UserAlreadyExists.java | 8 ++-- .../asamk/signal/storage/SignalAccount.java | 42 +++++++++++-------- .../java/org/asamk/signal/util/IOUtils.java | 15 +++---- 7 files changed, 61 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 97e3f9b2..6204778d 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -212,17 +212,19 @@ public class Main { * @return the data directory to be used by signal-cli. */ private static File getDefaultDataPath() { - File dataPath = new File(IOUtils.getDataHomeDir(), "/signal-cli"); + File dataPath = new File(IOUtils.getDataHomeDir(), "signal-cli"); if (dataPath.exists()) { return dataPath; } - File legacySettingsPath = new File(System.getProperty("user.home"), "/.config/signal"); + File configPath = new File(System.getProperty("user.home"), ".config"); + + File legacySettingsPath = new File(configPath, "signal"); if (legacySettingsPath.exists()) { return legacySettingsPath; } - legacySettingsPath = new File(System.getProperty("user.home"), "/.config/textsecure"); + legacySettingsPath = new File(configPath, "textsecure"); if (legacySettingsPath.exists()) { return legacySettingsPath; } diff --git a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java index 77df2b22..f9f5d95b 100644 --- a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java +++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java @@ -6,6 +6,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.StickerPackInvalidException; +import java.io.File; import java.io.IOException; public class UploadStickerPackCommand implements LocalCommand { @@ -19,7 +20,7 @@ public class UploadStickerPackCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { try { - String path = ns.getString("path"); + File path = new File(ns.getString("path")); String url = m.uploadStickerPack(path); System.out.println(url); return 0; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index c679449d..761905df 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -259,22 +259,22 @@ public class Manager implements Closeable { return account.getDeviceId(); } - private String getMessageCachePath() { - return pathConfig.getDataPath() + "/" + account.getUsername() + ".d/msg-cache"; + private File getMessageCachePath() { + return SignalAccount.getMessageCachePath(pathConfig.getDataPath(), account.getUsername()); } - private String getMessageCachePath(String sender) { + private File getMessageCachePath(String sender) { if (sender == null || sender.isEmpty()) { return getMessageCachePath(); } - return getMessageCachePath() + "/" + sender.replace("/", "_"); + return new File(getMessageCachePath(), sender.replace("/", "_")); } private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException { - String cachePath = getMessageCachePath(sender); + File cachePath = getMessageCachePath(sender); IOUtils.createPrivateDirectories(cachePath); - return new File(cachePath + "/" + now + "_" + timestamp); + return new File(cachePath, now + "_" + timestamp); } public static Manager init( @@ -1161,7 +1161,7 @@ public class Manager implements Closeable { * @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 { + public String uploadStickerPack(File path) throws IOException, StickerPackInvalidException { SignalServiceStickerManifestUpload manifest = getSignalServiceStickerManifestUpload(path); SignalServiceMessageSender messageSender = createMessageSender(); @@ -1186,12 +1186,11 @@ public class Manager implements Closeable { } private SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload( - final String path + final File file ) 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")) { @@ -1788,7 +1787,7 @@ public class Manager implements Closeable { private void retryFailedReceivedMessages( ReceiveMessageHandler handler, boolean ignoreAttachments ) { - final File cachePath = new File(getMessageCachePath()); + final File cachePath = getMessageCachePath(); if (!cachePath.exists()) { return; } @@ -1954,7 +1953,7 @@ public class Manager implements Closeable { cacheFile = getMessageCacheFile(source, now, envelope.getTimestamp()); Files.delete(cacheFile.toPath()); // Try to delete directory if empty - new File(getMessageCachePath()).delete(); + getMessageCachePath().delete(); } catch (IOException e) { logger.warn("Failed to delete cached message file “{}”, ignoring: {}", cacheFile, e.getMessage()); } diff --git a/src/main/java/org/asamk/signal/manager/PathConfig.java b/src/main/java/org/asamk/signal/manager/PathConfig.java index ca750931..d96034df 100644 --- a/src/main/java/org/asamk/signal/manager/PathConfig.java +++ b/src/main/java/org/asamk/signal/manager/PathConfig.java @@ -9,9 +9,9 @@ public class PathConfig { private final File avatarsPath; public static PathConfig createDefault(final File settingsPath) { - return new PathConfig(new File(settingsPath, "/data"), - new File(settingsPath, "/attachments"), - new File(settingsPath, "/avatars")); + return new PathConfig(new File(settingsPath, "data"), + new File(settingsPath, "attachments"), + new File(settingsPath, "avatars")); } private PathConfig(final File dataPath, final File attachmentsPath, final File avatarsPath) { @@ -20,15 +20,15 @@ public class PathConfig { this.avatarsPath = avatarsPath; } - public String getDataPath() { - return dataPath.getPath(); + public File getDataPath() { + return dataPath; } - public String getAttachmentsPath() { - return attachmentsPath.getPath(); + public File getAttachmentsPath() { + return attachmentsPath; } - public String getAvatarsPath() { - return avatarsPath.getPath(); + public File getAvatarsPath() { + return avatarsPath; } } diff --git a/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java b/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java index a07c455b..d506f0c6 100644 --- a/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java +++ b/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java @@ -1,11 +1,13 @@ package org.asamk.signal.manager; +import java.io.File; + public class UserAlreadyExists extends Exception { private final String username; - private final String fileName; + private final File fileName; - public UserAlreadyExists(String username, String fileName) { + public UserAlreadyExists(String username, File fileName) { this.username = username; this.fileName = fileName; } @@ -14,7 +16,7 @@ public class UserAlreadyExists extends Exception { return username; } - public String getFileName() { + public File getFileName() { return fileName; } } diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index e697efe3..3af52708 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -90,8 +90,8 @@ public class SignalAccount implements Closeable { jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } - public static SignalAccount load(String dataPath, String username) throws IOException { - final String fileName = getFileName(dataPath, username); + public static SignalAccount load(File dataPath, String username) throws IOException { + final File fileName = getFileName(dataPath, username); final Pair pair = openFileChannel(fileName); try { SignalAccount account = new SignalAccount(pair.first(), pair.second()); @@ -105,11 +105,11 @@ public class SignalAccount implements Closeable { } public static SignalAccount create( - String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey + File dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey ) throws IOException { IOUtils.createPrivateDirectories(dataPath); - String fileName = getFileName(dataPath, username); - if (!new File(fileName).exists()) { + File fileName = getFileName(dataPath, username); + if (!fileName.exists()) { IOUtils.createPrivateFile(fileName); } @@ -130,7 +130,7 @@ public class SignalAccount implements Closeable { } public static SignalAccount createLinkedAccount( - String dataPath, + File dataPath, String username, UUID uuid, String password, @@ -141,8 +141,8 @@ public class SignalAccount implements Closeable { ProfileKey profileKey ) throws IOException { IOUtils.createPrivateDirectories(dataPath); - String fileName = getFileName(dataPath, username); - if (!new File(fileName).exists()) { + File fileName = getFileName(dataPath, username); + if (!fileName.exists()) { IOUtils.createPrivateFile(fileName); } @@ -167,23 +167,31 @@ public class SignalAccount implements Closeable { return account; } - public static String getFileName(String dataPath, String username) { - return dataPath + "/" + username; + public static File getFileName(File dataPath, String username) { + return new File(dataPath, username); } - private static File getGroupCachePath(String dataPath, String username) { - return new File(new File(dataPath, username + ".d"), "group-cache"); + private static File getUserPath(final File dataPath, final String username) { + return new File(dataPath, username + ".d"); } - public static boolean userExists(String dataPath, String username) { + public static File getMessageCachePath(File dataPath, String username) { + return new File(getUserPath(dataPath, username), "msg-cache"); + } + + private static File getGroupCachePath(File dataPath, String username) { + return new File(getUserPath(dataPath, username), "group-cache"); + } + + public static boolean userExists(File dataPath, String username) { if (username == null) { return false; } - File f = new File(getFileName(dataPath, username)); + File f = getFileName(dataPath, username); return !(!f.exists() || f.isDirectory()); } - private void load(String dataPath) throws IOException { + private void load(File dataPath) throws IOException { JsonNode rootNode; synchronized (fileChannel) { fileChannel.position(0); @@ -365,8 +373,8 @@ public class SignalAccount implements Closeable { } } - private static Pair openFileChannel(String fileName) throws IOException { - FileChannel fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel(); + private static Pair openFileChannel(File fileName) throws IOException { + FileChannel fileChannel = new RandomAccessFile(fileName, "rw").getChannel(); FileLock lock = fileChannel.tryLock(); if (lock == null) { logger.info("Config file is in use by another instance, waiting…"); diff --git a/src/main/java/org/asamk/signal/util/IOUtils.java b/src/main/java/org/asamk/signal/util/IOUtils.java index 4d8adea6..59727a9a 100644 --- a/src/main/java/org/asamk/signal/util/IOUtils.java +++ b/src/main/java/org/asamk/signal/util/IOUtils.java @@ -46,11 +46,6 @@ public class IOUtils { return baos.toByteArray(); } - public static void createPrivateDirectories(String directoryPath) throws IOException { - final File file = new File(directoryPath); - createPrivateDirectories(file); - } - public static void createPrivateDirectories(File file) throws IOException { if (file.exists()) { return; @@ -65,8 +60,8 @@ public class IOUtils { } } - public static void createPrivateFile(String path) throws IOException { - final Path file = new File(path).toPath(); + public static void createPrivateFile(File path) throws IOException { + final Path file = path.toPath(); try { Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE); Files.createFile(file, PosixFilePermissions.asFileAttribute(perms)); @@ -75,13 +70,13 @@ public class IOUtils { } } - public static String getDataHomeDir() { + public static File getDataHomeDir() { String dataHome = System.getenv("XDG_DATA_HOME"); if (dataHome != null) { - return dataHome; + return new File(dataHome); } - return System.getProperty("user.home") + "/.local/share"; + return new File(new File(System.getProperty("user.home"), ".local"), "share"); } public static void copyStreamToFile(InputStream input, File outputFile) throws IOException { From 9e6a3534275d5bac8454792e05280f13fa5ef13c Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 29 Dec 2020 22:09:06 +0100 Subject: [PATCH 15/32] Move group classes to separate package --- .../asamk/signal/JsonDbusReceiveMessageHandler.java | 2 +- .../java/org/asamk/signal/ReceiveMessageHandler.java | 4 ++-- .../java/org/asamk/signal/commands/BlockCommand.java | 6 +++--- .../org/asamk/signal/commands/JoinGroupCommand.java | 4 ++-- .../org/asamk/signal/commands/ListGroupsCommand.java | 2 +- .../org/asamk/signal/commands/QuitGroupCommand.java | 8 ++++---- .../java/org/asamk/signal/commands/SendCommand.java | 2 +- .../org/asamk/signal/commands/SendReactionCommand.java | 8 ++++---- .../java/org/asamk/signal/commands/UnblockCommand.java | 6 +++--- .../org/asamk/signal/commands/UpdateGroupCommand.java | 2 +- .../java/org/asamk/signal/dbus/DbusSignalImpl.java | 6 +++--- src/main/java/org/asamk/signal/json/JsonGroupInfo.java | 2 +- .../java/org/asamk/signal/manager/HandleAction.java | 1 + src/main/java/org/asamk/signal/manager/KeyUtils.java | 4 ++-- src/main/java/org/asamk/signal/manager/Manager.java | 7 +++++++ .../org/asamk/signal/manager/{ => groups}/GroupId.java | 2 +- .../manager/{ => groups}/GroupIdFormatException.java | 2 +- .../asamk/signal/manager/{ => groups}/GroupIdV1.java | 2 +- .../asamk/signal/manager/{ => groups}/GroupIdV2.java | 2 +- .../manager/{ => groups}/GroupInviteLinkUrl.java | 2 +- .../signal/manager/{ => groups}/GroupLinkPassword.java | 4 +++- .../manager/{ => groups}/GroupNotFoundException.java | 2 +- .../asamk/signal/manager/{ => groups}/GroupUtils.java | 2 +- .../manager/{ => groups}/NotAGroupMemberException.java | 2 +- .../org/asamk/signal/manager/helper/GroupHelper.java | 6 +++--- .../java/org/asamk/signal/storage/SignalAccount.java | 2 +- .../org/asamk/signal/storage/groups/GroupInfo.java | 4 ++-- .../org/asamk/signal/storage/groups/GroupInfoV1.java | 10 +++++----- .../org/asamk/signal/storage/groups/GroupInfoV2.java | 4 ++-- .../asamk/signal/storage/groups/JsonGroupStore.java | 8 ++++---- src/main/java/org/asamk/signal/util/ErrorUtils.java | 6 +++--- src/main/java/org/asamk/signal/util/Util.java | 4 ++-- 32 files changed, 69 insertions(+), 59 deletions(-) rename src/main/java/org/asamk/signal/manager/{ => groups}/GroupId.java (97%) rename src/main/java/org/asamk/signal/manager/{ => groups}/GroupIdFormatException.java (85%) rename src/main/java/org/asamk/signal/manager/{ => groups}/GroupIdV1.java (87%) rename src/main/java/org/asamk/signal/manager/{ => groups}/GroupIdV2.java (86%) rename src/main/java/org/asamk/signal/manager/{ => groups}/GroupInviteLinkUrl.java (99%) rename src/main/java/org/asamk/signal/manager/{ => groups}/GroupLinkPassword.java (90%) rename src/main/java/org/asamk/signal/manager/{ => groups}/GroupNotFoundException.java (81%) rename src/main/java/org/asamk/signal/manager/{ => groups}/GroupUtils.java (98%) rename src/main/java/org/asamk/signal/manager/{ => groups}/NotAGroupMemberException.java (85%) diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 50eb9f9b..0cffd7b1 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -1,8 +1,8 @@ package org.asamk.signal; import org.asamk.Signal; -import org.asamk.signal.manager.GroupUtils; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.groups.GroupUtils; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 99010e13..8dc38e4f 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -1,8 +1,8 @@ package org.asamk.signal; -import org.asamk.signal.manager.GroupId; -import org.asamk.signal.manager.GroupUtils; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.util.DateUtils; diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 2a9bc4e9..627be5a9 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -3,10 +3,10 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.GroupId; -import org.asamk.signal.manager.GroupIdFormatException; -import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index 8438e1fa..305bf55b 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -4,9 +4,9 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.manager.GroupId; -import org.asamk.signal.manager.GroupInviteLinkUrl; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 4d1032a2..66ff3a00 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -4,8 +4,8 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.GroupInviteLinkUrl; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.storage.groups.GroupInfo; import org.whispersystems.signalservice.api.push.SignalServiceAddress; diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index efc63f8f..c1f6bfbf 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -3,11 +3,11 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.GroupId; -import org.asamk.signal.manager.GroupIdFormatException; -import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.Util; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.messages.SendMessageResult; diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 04b06434..ee51dd91 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -5,7 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.manager.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 345c9180..c680bfd7 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -4,11 +4,11 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.GroupId; -import org.asamk.signal.manager.GroupIdFormatException; -import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.Util; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.messages.SendMessageResult; diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index 73e578ac..14ea2996 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -3,10 +3,10 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.GroupId; -import org.asamk.signal.manager.GroupIdFormatException; -import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index dae06b86..a6f40ef2 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.Signal; -import org.asamk.signal.manager.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.util.Base64; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index cbb72835..df3f12f2 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -2,10 +2,10 @@ package org.asamk.signal.dbus; import org.asamk.Signal; import org.asamk.signal.manager.AttachmentInvalidException; -import org.asamk.signal.manager.GroupId; -import org.asamk.signal.manager.GroupNotFoundException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.util.ErrorUtils; import org.freedesktop.dbus.exceptions.DBusExecutionException; diff --git a/src/main/java/org/asamk/signal/json/JsonGroupInfo.java b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java index 9709be20..79967955 100644 --- a/src/main/java/org/asamk/signal/json/JsonGroupInfo.java +++ b/src/main/java/org/asamk/signal/json/JsonGroupInfo.java @@ -1,6 +1,6 @@ package org.asamk.signal.json; -import org.asamk.signal.manager.GroupUtils; +import org.asamk.signal.manager.groups.GroupUtils; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; import org.whispersystems.signalservice.api.push.SignalServiceAddress; diff --git a/src/main/java/org/asamk/signal/manager/HandleAction.java b/src/main/java/org/asamk/signal/manager/HandleAction.java index aa25d8c5..0dd151a9 100644 --- a/src/main/java/org/asamk/signal/manager/HandleAction.java +++ b/src/main/java/org/asamk/signal/manager/HandleAction.java @@ -1,5 +1,6 @@ package org.asamk.signal.manager; +import org.asamk.signal.manager.groups.GroupIdV1; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Objects; diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java index 21f6037f..6ac093db 100644 --- a/src/main/java/org/asamk/signal/manager/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java @@ -5,7 +5,7 @@ import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.util.Base64; -class KeyUtils { +public class KeyUtils { private KeyUtils() { } @@ -35,7 +35,7 @@ class KeyUtils { return Base64.encodeBytes(secret); } - static byte[] getSecretBytes(int size) { + public static byte[] getSecretBytes(int size) { byte[] secret = new byte[size]; RandomUtils.getSecureRandom().nextBytes(secret); return secret; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 761905df..c958e0a4 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -18,6 +18,13 @@ package org.asamk.signal.manager; import com.fasterxml.jackson.databind.ObjectMapper; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.groups.GroupIdV2; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; diff --git a/src/main/java/org/asamk/signal/manager/GroupId.java b/src/main/java/org/asamk/signal/manager/groups/GroupId.java similarity index 97% rename from src/main/java/org/asamk/signal/manager/GroupId.java rename to src/main/java/org/asamk/signal/manager/groups/GroupId.java index 34e18e8e..9a15de65 100644 --- a/src/main/java/org/asamk/signal/manager/GroupId.java +++ b/src/main/java/org/asamk/signal/manager/groups/GroupId.java @@ -1,4 +1,4 @@ -package org.asamk.signal.manager; +package org.asamk.signal.manager.groups; import org.whispersystems.util.Base64; diff --git a/src/main/java/org/asamk/signal/manager/GroupIdFormatException.java b/src/main/java/org/asamk/signal/manager/groups/GroupIdFormatException.java similarity index 85% rename from src/main/java/org/asamk/signal/manager/GroupIdFormatException.java rename to src/main/java/org/asamk/signal/manager/groups/GroupIdFormatException.java index 83afd15b..8050da22 100644 --- a/src/main/java/org/asamk/signal/manager/GroupIdFormatException.java +++ b/src/main/java/org/asamk/signal/manager/groups/GroupIdFormatException.java @@ -1,4 +1,4 @@ -package org.asamk.signal.manager; +package org.asamk.signal.manager.groups; public class GroupIdFormatException extends Exception { diff --git a/src/main/java/org/asamk/signal/manager/GroupIdV1.java b/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java similarity index 87% rename from src/main/java/org/asamk/signal/manager/GroupIdV1.java rename to src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java index 40862f07..d865356e 100644 --- a/src/main/java/org/asamk/signal/manager/GroupIdV1.java +++ b/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java @@ -1,4 +1,4 @@ -package org.asamk.signal.manager; +package org.asamk.signal.manager.groups; import static org.asamk.signal.manager.KeyUtils.getSecretBytes; diff --git a/src/main/java/org/asamk/signal/manager/GroupIdV2.java b/src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java similarity index 86% rename from src/main/java/org/asamk/signal/manager/GroupIdV2.java rename to src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java index b329be1d..913a9e93 100644 --- a/src/main/java/org/asamk/signal/manager/GroupIdV2.java +++ b/src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java @@ -1,4 +1,4 @@ -package org.asamk.signal.manager; +package org.asamk.signal.manager.groups; import java.util.Base64; diff --git a/src/main/java/org/asamk/signal/manager/GroupInviteLinkUrl.java b/src/main/java/org/asamk/signal/manager/groups/GroupInviteLinkUrl.java similarity index 99% rename from src/main/java/org/asamk/signal/manager/GroupInviteLinkUrl.java rename to src/main/java/org/asamk/signal/manager/groups/GroupInviteLinkUrl.java index 67ce7892..bf9e0e55 100644 --- a/src/main/java/org/asamk/signal/manager/GroupInviteLinkUrl.java +++ b/src/main/java/org/asamk/signal/manager/groups/GroupInviteLinkUrl.java @@ -1,4 +1,4 @@ -package org.asamk.signal.manager; +package org.asamk.signal.manager.groups; import com.google.protobuf.ByteString; diff --git a/src/main/java/org/asamk/signal/manager/GroupLinkPassword.java b/src/main/java/org/asamk/signal/manager/groups/GroupLinkPassword.java similarity index 90% rename from src/main/java/org/asamk/signal/manager/GroupLinkPassword.java rename to src/main/java/org/asamk/signal/manager/groups/GroupLinkPassword.java index 38e2aaf4..41be672a 100644 --- a/src/main/java/org/asamk/signal/manager/GroupLinkPassword.java +++ b/src/main/java/org/asamk/signal/manager/groups/GroupLinkPassword.java @@ -1,4 +1,6 @@ -package org.asamk.signal.manager; +package org.asamk.signal.manager.groups; + +import org.asamk.signal.manager.KeyUtils; import java.util.Arrays; diff --git a/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java b/src/main/java/org/asamk/signal/manager/groups/GroupNotFoundException.java similarity index 81% rename from src/main/java/org/asamk/signal/manager/GroupNotFoundException.java rename to src/main/java/org/asamk/signal/manager/groups/GroupNotFoundException.java index d7efa923..0fc0c444 100644 --- a/src/main/java/org/asamk/signal/manager/GroupNotFoundException.java +++ b/src/main/java/org/asamk/signal/manager/groups/GroupNotFoundException.java @@ -1,4 +1,4 @@ -package org.asamk.signal.manager; +package org.asamk.signal.manager.groups; public class GroupNotFoundException extends Exception { diff --git a/src/main/java/org/asamk/signal/manager/GroupUtils.java b/src/main/java/org/asamk/signal/manager/groups/GroupUtils.java similarity index 98% rename from src/main/java/org/asamk/signal/manager/GroupUtils.java rename to src/main/java/org/asamk/signal/manager/groups/GroupUtils.java index d86dfbe9..c5f727e1 100644 --- a/src/main/java/org/asamk/signal/manager/GroupUtils.java +++ b/src/main/java/org/asamk/signal/manager/groups/GroupUtils.java @@ -1,4 +1,4 @@ -package org.asamk.signal.manager; +package org.asamk.signal.manager.groups; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.GroupInfoV1; diff --git a/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/manager/groups/NotAGroupMemberException.java similarity index 85% rename from src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java rename to src/main/java/org/asamk/signal/manager/groups/NotAGroupMemberException.java index 2c9b3f33..08cbcacd 100644 --- a/src/main/java/org/asamk/signal/manager/NotAGroupMemberException.java +++ b/src/main/java/org/asamk/signal/manager/groups/NotAGroupMemberException.java @@ -1,4 +1,4 @@ -package org.asamk.signal.manager; +package org.asamk.signal.manager.groups; public class NotAGroupMemberException extends Exception { diff --git a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 5a88bc66..6a52e00e 100644 --- a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -2,9 +2,9 @@ package org.asamk.signal.manager.helper; import com.google.protobuf.InvalidProtocolBufferException; -import org.asamk.signal.manager.GroupIdV2; -import org.asamk.signal.manager.GroupLinkPassword; -import org.asamk.signal.manager.GroupUtils; +import org.asamk.signal.manager.groups.GroupIdV2; +import org.asamk.signal.manager.groups.GroupLinkPassword; +import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.storage.groups.GroupInfoV2; import org.asamk.signal.storage.profiles.SignalProfile; import org.asamk.signal.util.IOUtils; diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 3af52708..393d0449 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -10,7 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.asamk.signal.manager.GroupId; +import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.contacts.JsonContactsStore; import org.asamk.signal.storage.groups.GroupInfo; 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 40b8c884..fe725141 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -2,8 +2,8 @@ package org.asamk.signal.storage.groups; import com.fasterxml.jackson.annotation.JsonIgnore; -import org.asamk.signal.manager.GroupId; -import org.asamk.signal.manager.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Set; diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java index 90b26b81..e48fe297 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java @@ -13,11 +13,11 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.asamk.signal.manager.GroupId; -import org.asamk.signal.manager.GroupIdV1; -import org.asamk.signal.manager.GroupIdV2; -import org.asamk.signal.manager.GroupInviteLinkUrl; -import org.asamk.signal.manager.GroupUtils; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.groups.GroupIdV2; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupUtils; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.io.IOException; diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java index 1b00caaa..0139f879 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java @@ -1,7 +1,7 @@ package org.asamk.signal.storage.groups; -import org.asamk.signal.manager.GroupIdV2; -import org.asamk.signal.manager.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupIdV2; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.zkgroup.groups.GroupMasterKey; diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index 18bf5ed0..1aae49f7 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -12,10 +12,10 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.asamk.signal.manager.GroupId; -import org.asamk.signal.manager.GroupIdV1; -import org.asamk.signal.manager.GroupIdV2; -import org.asamk.signal.manager.GroupUtils; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.groups.GroupIdV2; +import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.util.Hex; import org.asamk.signal.util.IOUtils; import org.signal.storageservice.protos.groups.local.DecryptedGroup; diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 44d505be..e9553f98 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -1,8 +1,8 @@ package org.asamk.signal.util; -import org.asamk.signal.manager.GroupIdFormatException; -import org.asamk.signal.manager.GroupNotFoundException; -import org.asamk.signal.manager.NotAGroupMemberException; +import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.util.InvalidNumberException; diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 3cd5619a..79a6587b 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -2,8 +2,8 @@ package org.asamk.signal.util; import com.fasterxml.jackson.databind.JsonNode; -import org.asamk.signal.manager.GroupId; -import org.asamk.signal.manager.GroupIdFormatException; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdFormatException; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; From b738f5740c94fe7a5df9e322e1345a99ef0c5ce5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 29 Dec 2020 22:15:38 +0100 Subject: [PATCH 16/32] Move storage package to manager --- .../asamk/signal/ReceiveMessageHandler.java | 4 +- .../signal/commands/ListContactsCommand.java | 2 +- .../signal/commands/ListGroupsCommand.java | 2 +- .../commands/ListIdentitiesCommand.java | 10 +-- .../org/asamk/signal/dbus/DbusSignalImpl.java | 2 +- .../org/asamk/signal/manager/Manager.java | 37 +++++---- .../signal/manager/ProvisioningManager.java | 2 +- .../signal/manager/groups/GroupUtils.java | 6 +- .../signal/manager/helper/GroupHelper.java | 4 +- .../manager/helper/ProfileProvider.java | 2 +- .../helper/UnidentifiedAccessHelper.java | 2 +- .../{ => manager}/storage/SignalAccount.java | 32 ++++---- .../storage/contacts/ContactInfo.java | 2 +- .../storage/contacts/JsonContactsStore.java | 2 +- .../storage/groups/GroupInfo.java | 2 +- .../storage/groups/GroupInfoV1.java | 2 +- .../storage/groups/GroupInfoV2.java | 2 +- .../storage/groups/JsonGroupStore.java | 2 +- .../storage/profiles/ProfileStore.java | 2 +- .../storage/profiles/SignalProfile.java | 2 +- .../storage/profiles/SignalProfileEntry.java | 2 +- .../storage/protocol/IdentityInfo.java | 57 +++++++++++++ .../protocol/JsonIdentityKeyStore.java | 81 ++++--------------- .../storage/protocol/JsonPreKeyStore.java | 2 +- .../storage/protocol/JsonSessionStore.java | 2 +- .../protocol/JsonSignalProtocolStore.java | 8 +- .../protocol/JsonSignedPreKeyStore.java | 2 +- .../storage/protocol/RecipientStore.java | 2 +- .../storage/protocol/SessionInfo.java | 2 +- .../SignalServiceAddressResolver.java | 2 +- .../storage/stickers/Sticker.java | 2 +- .../storage/stickers/StickerStore.java | 2 +- .../threads/LegacyJsonThreadStore.java | 2 +- .../storage/threads/ThreadInfo.java | 2 +- 34 files changed, 148 insertions(+), 141 deletions(-) rename src/main/java/org/asamk/signal/{ => manager}/storage/SignalAccount.java (94%) rename src/main/java/org/asamk/signal/{ => manager}/storage/contacts/ContactInfo.java (95%) rename src/main/java/org/asamk/signal/{ => manager}/storage/contacts/JsonContactsStore.java (96%) rename src/main/java/org/asamk/signal/{ => manager}/storage/groups/GroupInfo.java (97%) rename src/main/java/org/asamk/signal/{ => manager}/storage/groups/GroupInfoV1.java (99%) rename src/main/java/org/asamk/signal/{ => manager}/storage/groups/GroupInfoV2.java (98%) rename src/main/java/org/asamk/signal/{ => manager}/storage/groups/JsonGroupStore.java (99%) rename src/main/java/org/asamk/signal/{ => manager}/storage/profiles/ProfileStore.java (99%) rename src/main/java/org/asamk/signal/{ => manager}/storage/profiles/SignalProfile.java (98%) rename src/main/java/org/asamk/signal/{ => manager}/storage/profiles/SignalProfileEntry.java (96%) create mode 100644 src/main/java/org/asamk/signal/manager/storage/protocol/IdentityInfo.java rename src/main/java/org/asamk/signal/{ => manager}/storage/protocol/JsonIdentityKeyStore.java (80%) rename src/main/java/org/asamk/signal/{ => manager}/storage/protocol/JsonPreKeyStore.java (98%) rename src/main/java/org/asamk/signal/{ => manager}/storage/protocol/JsonSessionStore.java (99%) rename src/main/java/org/asamk/signal/{ => manager}/storage/protocol/JsonSignalProtocolStore.java (95%) rename src/main/java/org/asamk/signal/{ => manager}/storage/protocol/JsonSignedPreKeyStore.java (98%) rename src/main/java/org/asamk/signal/{ => manager}/storage/protocol/RecipientStore.java (98%) rename src/main/java/org/asamk/signal/{ => manager}/storage/protocol/SessionInfo.java (89%) rename src/main/java/org/asamk/signal/{ => manager}/storage/protocol/SignalServiceAddressResolver.java (88%) rename src/main/java/org/asamk/signal/{ => manager}/storage/stickers/Sticker.java (93%) rename src/main/java/org/asamk/signal/{ => manager}/storage/stickers/StickerStore.java (98%) rename src/main/java/org/asamk/signal/{ => manager}/storage/threads/LegacyJsonThreadStore.java (97%) rename src/main/java/org/asamk/signal/{ => manager}/storage/threads/ThreadInfo.java (78%) diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 8dc38e4f..db78f454 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -3,8 +3,8 @@ package org.asamk.signal; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupUtils; -import org.asamk.signal.storage.contacts.ContactInfo; -import org.asamk.signal.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.contacts.ContactInfo; +import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index 24d6898c..2c98ec6b 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; -import org.asamk.signal.storage.contacts.ContactInfo; +import org.asamk.signal.manager.storage.contacts.ContactInfo; import java.util.List; diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 66ff3a00..b4be4ad0 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -6,7 +6,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; -import org.asamk.signal.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.groups.GroupInfo; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.List; diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index a75e4328..3f422cbd 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -4,7 +4,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; -import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; +import org.asamk.signal.manager.storage.protocol.IdentityInfo; import org.asamk.signal.util.Hex; import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -13,7 +13,7 @@ import java.util.List; public class ListIdentitiesCommand implements LocalCommand { - private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity theirId) { + private static void printIdentityFingerprint(Manager m, IdentityInfo 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(), @@ -35,14 +35,14 @@ public class ListIdentitiesCommand implements LocalCommand { return 1; } if (ns.get("number") == null) { - for (JsonIdentityKeyStore.Identity identity : m.getIdentities()) { + for (IdentityInfo identity : m.getIdentities()) { printIdentityFingerprint(m, identity); } } else { String number = ns.getString("number"); try { - List identities = m.getIdentities(number); - for (JsonIdentityKeyStore.Identity id : identities) { + List identities = m.getIdentities(number); + for (IdentityInfo id : identities) { printIdentityFingerprint(m, id); } } catch (InvalidNumberException e) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index df3f12f2..d19116a4 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -6,7 +6,7 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.util.ErrorUtils; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.util.Pair; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index c958e0a4..e00f44ac 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -28,15 +28,15 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; -import org.asamk.signal.storage.SignalAccount; -import org.asamk.signal.storage.contacts.ContactInfo; -import org.asamk.signal.storage.groups.GroupInfo; -import org.asamk.signal.storage.groups.GroupInfoV1; -import org.asamk.signal.storage.groups.GroupInfoV2; -import org.asamk.signal.storage.profiles.SignalProfile; -import org.asamk.signal.storage.profiles.SignalProfileEntry; -import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; -import org.asamk.signal.storage.stickers.Sticker; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.contacts.ContactInfo; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; +import org.asamk.signal.manager.storage.profiles.SignalProfile; +import org.asamk.signal.manager.storage.profiles.SignalProfileEntry; +import org.asamk.signal.manager.storage.protocol.IdentityInfo; +import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.signal.libsignal.metadata.InvalidMetadataMessageException; @@ -2422,8 +2422,7 @@ public class Manager implements Closeable { DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); for (ContactInfo record : account.getContactStore().getContacts()) { VerifiedMessage verifiedMessage = null; - JsonIdentityKeyStore.Identity currentIdentity = account.getSignalProtocolStore() - .getIdentity(record.getAddress()); + IdentityInfo currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress()); if (currentIdentity != null) { verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), @@ -2517,11 +2516,11 @@ public class Manager implements Closeable { return account.getGroupStore().getGroup(groupId); } - public List getIdentities() { + public List getIdentities() { return account.getSignalProtocolStore().getIdentities(); } - public List getIdentities(String number) throws InvalidNumberException { + public List getIdentities(String number) throws InvalidNumberException { return account.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number)); } @@ -2533,11 +2532,11 @@ public class Manager implements Closeable { */ public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException { SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name); - List ids = account.getSignalProtocolStore().getIdentities(address); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } - for (JsonIdentityKeyStore.Identity id : ids) { + for (IdentityInfo id : ids) { if (!Arrays.equals(id.getIdentityKey().serialize(), fingerprint)) { continue; } @@ -2563,11 +2562,11 @@ public class Manager implements Closeable { */ public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException { SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name); - List ids = account.getSignalProtocolStore().getIdentities(address); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } - for (JsonIdentityKeyStore.Identity id : ids) { + for (IdentityInfo id : ids) { if (!safetyNumber.equals(computeSafetyNumber(address, id.getIdentityKey()))) { continue; } @@ -2592,11 +2591,11 @@ public class Manager implements Closeable { */ public boolean trustIdentityAllKeys(String name) { SignalServiceAddress address = resolveSignalServiceAddress(name); - List ids = account.getSignalProtocolStore().getIdentities(address); + List ids = account.getSignalProtocolStore().getIdentities(address); if (ids == null) { return false; } - for (JsonIdentityKeyStore.Identity id : ids) { + for (IdentityInfo id : ids) { if (id.getTrustLevel() == TrustLevel.UNTRUSTED) { account.getSignalProtocolStore() .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index f81cfa49..95e92c7a 100644 --- a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -16,7 +16,7 @@ */ package org.asamk.signal.manager; -import org.asamk.signal.storage.SignalAccount; +import org.asamk.signal.manager.storage.SignalAccount; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.IdentityKeyPair; diff --git a/src/main/java/org/asamk/signal/manager/groups/GroupUtils.java b/src/main/java/org/asamk/signal/manager/groups/GroupUtils.java index c5f727e1..f56639e3 100644 --- a/src/main/java/org/asamk/signal/manager/groups/GroupUtils.java +++ b/src/main/java/org/asamk/signal/manager/groups/GroupUtils.java @@ -1,8 +1,8 @@ package org.asamk.signal.manager.groups; -import org.asamk.signal.storage.groups.GroupInfo; -import org.asamk.signal.storage.groups.GroupInfoV1; -import org.asamk.signal.storage.groups.GroupInfoV2; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.groups.GroupSecretParams; diff --git a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 6a52e00e..394eba57 100644 --- a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -5,8 +5,8 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.asamk.signal.manager.groups.GroupIdV2; import org.asamk.signal.manager.groups.GroupLinkPassword; import org.asamk.signal.manager.groups.GroupUtils; -import org.asamk.signal.storage.groups.GroupInfoV2; -import org.asamk.signal.storage.profiles.SignalProfile; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; +import org.asamk.signal.manager.storage.profiles.SignalProfile; import org.asamk.signal.util.IOUtils; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.GroupChange; diff --git a/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java b/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java index 1ff4cb05..c16b5e0d 100644 --- a/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java +++ b/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java @@ -1,6 +1,6 @@ package org.asamk.signal.manager.helper; -import org.asamk.signal.storage.profiles.SignalProfile; +import org.asamk.signal.manager.storage.profiles.SignalProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; public interface ProfileProvider { diff --git a/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java b/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java index 97331cf3..a994c40a 100644 --- a/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java @@ -1,6 +1,6 @@ package org.asamk.signal.manager.helper; -import org.asamk.signal.storage.profiles.SignalProfile; +import org.asamk.signal.manager.storage.profiles.SignalProfile; import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.util.guava.Optional; diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java similarity index 94% rename from src/main/java/org/asamk/signal/storage/SignalAccount.java rename to src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 393d0449..c3573209 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage; +package org.asamk.signal.manager.storage; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; @@ -11,20 +11,20 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; import org.asamk.signal.manager.groups.GroupId; -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.GroupInfoV1; -import org.asamk.signal.storage.groups.JsonGroupStore; -import org.asamk.signal.storage.profiles.ProfileStore; -import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; -import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; -import org.asamk.signal.storage.protocol.RecipientStore; -import org.asamk.signal.storage.protocol.SessionInfo; -import org.asamk.signal.storage.protocol.SignalServiceAddressResolver; -import org.asamk.signal.storage.stickers.StickerStore; -import org.asamk.signal.storage.threads.LegacyJsonThreadStore; -import org.asamk.signal.storage.threads.ThreadInfo; +import org.asamk.signal.manager.storage.contacts.ContactInfo; +import org.asamk.signal.manager.storage.contacts.JsonContactsStore; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.groups.JsonGroupStore; +import org.asamk.signal.manager.storage.profiles.ProfileStore; +import org.asamk.signal.manager.storage.protocol.IdentityInfo; +import org.asamk.signal.manager.storage.protocol.JsonSignalProtocolStore; +import org.asamk.signal.manager.storage.protocol.RecipientStore; +import org.asamk.signal.manager.storage.protocol.SessionInfo; +import org.asamk.signal.manager.storage.protocol.SignalServiceAddressResolver; +import org.asamk.signal.manager.storage.stickers.StickerStore; +import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore; +import org.asamk.signal.manager.storage.threads.ThreadInfo; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.signal.zkgroup.InvalidInputException; @@ -286,7 +286,7 @@ public class SignalAccount implements Closeable { session.address = recipientStore.resolveServiceAddress(session.address); } - for (JsonIdentityKeyStore.Identity identity : signalProtocolStore.getIdentities()) { + for (IdentityInfo identity : signalProtocolStore.getIdentities()) { identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress())); } } diff --git a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/manager/storage/contacts/ContactInfo.java similarity index 95% rename from src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java rename to src/main/java/org/asamk/signal/manager/storage/contacts/ContactInfo.java index 3b155210..4dd132f7 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java +++ b/src/main/java/org/asamk/signal/manager/storage/contacts/ContactInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.contacts; +package org.asamk.signal.manager.storage.contacts; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java b/src/main/java/org/asamk/signal/manager/storage/contacts/JsonContactsStore.java similarity index 96% rename from src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java rename to src/main/java/org/asamk/signal/manager/storage/contacts/JsonContactsStore.java index bb81b0c9..d2859f3f 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/contacts/JsonContactsStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.contacts; +package org.asamk.signal.manager.storage.contacts; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java similarity index 97% rename from src/main/java/org/asamk/signal/storage/groups/GroupInfo.java rename to src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java index fe725141..a644b620 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.groups; +package org.asamk.signal.manager.storage.groups; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java similarity index 99% rename from src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java rename to src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java index e48fe297..39591647 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV1.java +++ b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.groups; +package org.asamk.signal.manager.storage.groups; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java similarity index 98% rename from src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java rename to src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index 0139f879..17c23925 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfoV2.java +++ b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.groups; +package org.asamk.signal.manager.storage.groups; import org.asamk.signal.manager.groups.GroupIdV2; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java similarity index 99% rename from src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java rename to src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java index 1aae49f7..2b4dbcf5 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.groups; +package org.asamk.signal.manager.storage.groups; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; diff --git a/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java b/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java similarity index 99% rename from src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java rename to src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java index 3b3d3f9f..bff2f17e 100644 --- a/src/main/java/org/asamk/signal/storage/profiles/ProfileStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.profiles; +package org.asamk.signal.manager.storage.profiles; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; diff --git a/src/main/java/org/asamk/signal/storage/profiles/SignalProfile.java b/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfile.java similarity index 98% rename from src/main/java/org/asamk/signal/storage/profiles/SignalProfile.java rename to src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfile.java index 023458ed..48a38578 100644 --- a/src/main/java/org/asamk/signal/storage/profiles/SignalProfile.java +++ b/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfile.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.profiles; +package org.asamk.signal.manager.storage.profiles; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java b/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfileEntry.java similarity index 96% rename from src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java rename to src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfileEntry.java index e6acf30d..a81fbcb5 100644 --- a/src/main/java/org/asamk/signal/storage/profiles/SignalProfileEntry.java +++ b/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfileEntry.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.profiles; +package org.asamk.signal.manager.storage.profiles; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; diff --git a/src/main/java/org/asamk/signal/manager/storage/protocol/IdentityInfo.java b/src/main/java/org/asamk/signal/manager/storage/protocol/IdentityInfo.java new file mode 100644 index 00000000..d4af11f2 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/IdentityInfo.java @@ -0,0 +1,57 @@ +package org.asamk.signal.manager.storage.protocol; + +import org.asamk.signal.manager.TrustLevel; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.util.Date; + +public class IdentityInfo { + + SignalServiceAddress address; + IdentityKey identityKey; + TrustLevel trustLevel; + Date added; + + public IdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) { + this.address = address; + this.identityKey = identityKey; + this.trustLevel = trustLevel; + this.added = new Date(); + } + + IdentityInfo(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; + } + + public IdentityKey getIdentityKey() { + return this.identityKey; + } + + public TrustLevel getTrustLevel() { + return this.trustLevel; + } + + public Date getDateAdded() { + return this.added; + } + + public byte[] getFingerprint() { + return identityKey.getPublicKey().serialize(); + } +} diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java similarity index 80% rename from src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java index 29160cf1..517b384e 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -31,7 +31,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { final static Logger logger = LoggerFactory.getLogger(JsonIdentityKeyStore.class); - private final List identities = new ArrayList<>(); + private final List identities = new ArrayList<>(); private final IdentityKeyPair identityKeyPair; private final int localRegistrationId; @@ -85,7 +85,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { public boolean saveIdentity( SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel, Date added ) { - for (Identity id : identities) { + for (IdentityInfo id : identities) { if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) { continue; } @@ -97,7 +97,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { return true; } - identities.add(new Identity(serviceAddress, identityKey, trustLevel, added != null ? added : new Date())); + identities.add(new IdentityInfo(serviceAddress, identityKey, trustLevel, added != null ? added : new Date())); return false; } @@ -111,7 +111,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { public void setIdentityTrustLevel( SignalServiceAddress serviceAddress, IdentityKey identityKey, TrustLevel trustLevel ) { - for (Identity id : identities) { + for (IdentityInfo id : identities) { if (!id.address.matches(serviceAddress) || !id.identityKey.equals(identityKey)) { continue; } @@ -123,7 +123,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { return; } - identities.add(new Identity(serviceAddress, identityKey, trustLevel, new Date())); + identities.add(new IdentityInfo(serviceAddress, identityKey, trustLevel, new Date())); } @Override @@ -132,7 +132,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); boolean trustOnFirstUse = true; - for (Identity id : identities) { + for (IdentityInfo id : identities) { if (!id.address.matches(serviceAddress)) { continue; } @@ -150,14 +150,14 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { @Override public IdentityKey getIdentity(SignalProtocolAddress address) { SignalServiceAddress serviceAddress = resolveSignalServiceAddress(address.getName()); - Identity identity = getIdentity(serviceAddress); + IdentityInfo identity = getIdentity(serviceAddress); return identity == null ? null : identity.getIdentityKey(); } - public Identity getIdentity(SignalServiceAddress serviceAddress) { + public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) { long maxDate = 0; - Identity maxIdentity = null; - for (Identity id : this.identities) { + IdentityInfo maxIdentity = null; + for (IdentityInfo id : this.identities) { if (!id.address.matches(serviceAddress)) { continue; } @@ -171,14 +171,14 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { return maxIdentity; } - public List getIdentities() { + public List getIdentities() { // TODO deep copy return identities; } - public List getIdentities(SignalServiceAddress serviceAddress) { - List identities = new ArrayList<>(); - for (Identity identity : this.identities) { + public List getIdentities(SignalServiceAddress serviceAddress) { + List identities = new ArrayList<>(); + for (IdentityInfo identity : this.identities) { if (identity.address.matches(serviceAddress)) { identities.add(identity); } @@ -246,7 +246,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize())); json.writeArrayFieldStart("trustedKeys"); - for (Identity trustedKey : jsonIdentityKeyStore.identities) { + for (IdentityInfo trustedKey : jsonIdentityKeyStore.identities) { json.writeStartObject(); if (trustedKey.getAddress().getNumber().isPresent()) { json.writeStringField("name", trustedKey.getAddress().getNumber().get()); @@ -264,53 +264,4 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { } } - public static class Identity { - - SignalServiceAddress address; - IdentityKey identityKey; - TrustLevel trustLevel; - Date added; - - public Identity(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel) { - this.address = address; - this.identityKey = identityKey; - this.trustLevel = trustLevel; - this.added = new Date(); - } - - 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; - } - - public IdentityKey getIdentityKey() { - return this.identityKey; - } - - public TrustLevel getTrustLevel() { - return this.trustLevel; - } - - public Date getDateAdded() { - return this.added; - } - - public byte[] getFingerprint() { - return identityKey.getPublicKey().serialize(); - } - } } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonPreKeyStore.java similarity index 98% rename from src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/JsonPreKeyStore.java index 523809c1..4d884c3e 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonPreKeyStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSessionStore.java similarity index 99% rename from src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/JsonSessionStore.java index 24e4594e..f55aff14 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSessionStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java similarity index 95% rename from src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java index 5939749d..41a63013 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignalProtocolStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -91,11 +91,11 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { identityKeyStore.setIdentityTrustLevel(serviceAddress, identityKey, trustLevel); } - public List getIdentities() { + public List getIdentities() { return identityKeyStore.getIdentities(); } - public List getIdentities(SignalServiceAddress serviceAddress) { + public List getIdentities(SignalServiceAddress serviceAddress) { return identityKeyStore.getIdentities(serviceAddress); } @@ -109,7 +109,7 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { return identityKeyStore.getIdentity(address); } - public JsonIdentityKeyStore.Identity getIdentity(SignalServiceAddress serviceAddress) { + public IdentityInfo getIdentity(SignalServiceAddress serviceAddress) { return identityKeyStore.getIdentity(serviceAddress); } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignedPreKeyStore.java similarity index 98% rename from src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignedPreKeyStore.java index 7accf5a1..5eae4500 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSignedPreKeyStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/RecipientStore.java similarity index 98% rename from src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/RecipientStore.java index 701eca34..60634ae5 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/RecipientStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/RecipientStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; diff --git a/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java b/src/main/java/org/asamk/signal/manager/storage/protocol/SessionInfo.java similarity index 89% rename from src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/SessionInfo.java index 00221233..802b896b 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/SessionInfo.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/SessionInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import org.whispersystems.signalservice.api.push.SignalServiceAddress; diff --git a/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java b/src/main/java/org/asamk/signal/manager/storage/protocol/SignalServiceAddressResolver.java similarity index 88% rename from src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java rename to src/main/java/org/asamk/signal/manager/storage/protocol/SignalServiceAddressResolver.java index b1c5fb38..86eea05e 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/SignalServiceAddressResolver.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/SignalServiceAddressResolver.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.protocol; +package org.asamk.signal.manager.storage.protocol; import org.whispersystems.signalservice.api.push.SignalServiceAddress; diff --git a/src/main/java/org/asamk/signal/storage/stickers/Sticker.java b/src/main/java/org/asamk/signal/manager/storage/stickers/Sticker.java similarity index 93% rename from src/main/java/org/asamk/signal/storage/stickers/Sticker.java rename to src/main/java/org/asamk/signal/manager/storage/stickers/Sticker.java index 386924c4..54e95d0a 100644 --- a/src/main/java/org/asamk/signal/storage/stickers/Sticker.java +++ b/src/main/java/org/asamk/signal/manager/storage/stickers/Sticker.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.stickers; +package org.asamk.signal.manager.storage.stickers; public class Sticker { diff --git a/src/main/java/org/asamk/signal/storage/stickers/StickerStore.java b/src/main/java/org/asamk/signal/manager/storage/stickers/StickerStore.java similarity index 98% rename from src/main/java/org/asamk/signal/storage/stickers/StickerStore.java rename to src/main/java/org/asamk/signal/manager/storage/stickers/StickerStore.java index e5d817d2..10cd2e99 100644 --- a/src/main/java/org/asamk/signal/storage/stickers/StickerStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/stickers/StickerStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.stickers; +package org.asamk.signal.manager.storage.stickers; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java b/src/main/java/org/asamk/signal/manager/storage/threads/LegacyJsonThreadStore.java similarity index 97% rename from src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java rename to src/main/java/org/asamk/signal/manager/storage/threads/LegacyJsonThreadStore.java index 24463933..f37360a2 100644 --- a/src/main/java/org/asamk/signal/storage/threads/LegacyJsonThreadStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/threads/LegacyJsonThreadStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.threads; +package org.asamk.signal.manager.storage.threads; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; diff --git a/src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java b/src/main/java/org/asamk/signal/manager/storage/threads/ThreadInfo.java similarity index 78% rename from src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java rename to src/main/java/org/asamk/signal/manager/storage/threads/ThreadInfo.java index 67e6b474..b81a0051 100644 --- a/src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java +++ b/src/main/java/org/asamk/signal/manager/storage/threads/ThreadInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.threads; +package org.asamk.signal.manager.storage.threads; import com.fasterxml.jackson.annotation.JsonProperty; From bbdd6a89102f200f284a01a41ac2809c0759ae50 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 29 Dec 2020 22:48:39 +0100 Subject: [PATCH 17/32] Cleanup utils --- .../asamk/signal/manager/DeviceLinkInfo.java | 60 ++++ .../org/asamk/signal/manager/Manager.java | 43 +-- .../signal/manager/ProvisioningManager.java | 4 +- .../asamk/signal/manager/ServiceConfig.java | 11 + .../java/org/asamk/signal/manager/Utils.java | 304 ------------------ .../signal/manager/groups/GroupIdV1.java | 2 +- .../manager/groups/GroupLinkPassword.java | 2 +- .../signal/manager/helper/GroupHelper.java | 2 +- .../signal/manager/storage/SignalAccount.java | 22 +- .../storage/groups/JsonGroupStore.java | 2 +- .../protocol/JsonIdentityKeyStore.java | 6 +- .../storage/protocol/JsonSessionStore.java | 6 +- .../signal/manager/util/AttachmentUtils.java | 79 +++++ .../asamk/signal/manager/util/IOUtils.java | 72 +++++ .../signal/manager/{ => util}/KeyUtils.java | 10 +- .../manager/util/MessageCacheUtils.java | 105 ++++++ .../org/asamk/signal/manager/util/Utils.java | 97 ++++++ .../java/org/asamk/signal/util/IOUtils.java | 64 ---- src/main/java/org/asamk/signal/util/Util.java | 42 --- 19 files changed, 477 insertions(+), 456 deletions(-) create mode 100644 src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java delete mode 100644 src/main/java/org/asamk/signal/manager/Utils.java create mode 100644 src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java create mode 100644 src/main/java/org/asamk/signal/manager/util/IOUtils.java rename src/main/java/org/asamk/signal/manager/{ => util}/KeyUtils.java (79%) create mode 100644 src/main/java/org/asamk/signal/manager/util/MessageCacheUtils.java create mode 100644 src/main/java/org/asamk/signal/manager/util/Utils.java diff --git a/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java b/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java new file mode 100644 index 00000000..5b9fbe28 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java @@ -0,0 +1,60 @@ +package org.asamk.signal.manager; + +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.util.Base64; + +import java.io.IOException; +import java.net.URI; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static org.whispersystems.signalservice.internal.util.Util.isEmpty; + +public class DeviceLinkInfo { + + final String deviceIdentifier; + final ECPublicKey deviceKey; + + public static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws IOException, InvalidKeyException { + Map query = getQueryMap(linkUri.getRawQuery()); + String deviceIdentifier = query.get("uuid"); + String publicKeyEncoded = query.get("pub_key"); + + if (isEmpty(deviceIdentifier) || isEmpty(publicKeyEncoded)) { + throw new RuntimeException("Invalid device link uri"); + } + + ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); + + return new DeviceLinkInfo(deviceIdentifier, deviceKey); + } + + private static Map getQueryMap(String query) { + String[] params = query.split("&"); + Map map = new HashMap<>(); + for (String param : params) { + final String[] paramParts = param.split("="); + String name = URLDecoder.decode(paramParts[0], StandardCharsets.UTF_8); + String value = URLDecoder.decode(paramParts[1], StandardCharsets.UTF_8); + map.put(name, value); + } + return map; + } + + public DeviceLinkInfo(final String deviceIdentifier, final ECPublicKey deviceKey) { + this.deviceIdentifier = deviceIdentifier; + this.deviceKey = deviceKey; + } + + public String createDeviceLinkUri() { + return "tsdevice:/?uuid=" + + URLEncoder.encode(deviceIdentifier, StandardCharsets.UTF_8) + + "&pub_key=" + + URLEncoder.encode(Base64.encodeBytesWithoutPadding(deviceKey.serialize()), StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index e00f44ac..5ed8fc4e 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -37,8 +37,11 @@ import org.asamk.signal.manager.storage.profiles.SignalProfile; import org.asamk.signal.manager.storage.profiles.SignalProfileEntry; import org.asamk.signal.manager.storage.protocol.IdentityInfo; import org.asamk.signal.manager.storage.stickers.Sticker; -import org.asamk.signal.util.IOUtils; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.util.AttachmentUtils; +import org.asamk.signal.manager.util.IOUtils; +import org.asamk.signal.manager.util.KeyUtils; +import org.asamk.signal.manager.util.MessageCacheUtils; +import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.metadata.InvalidMetadataMessageException; import org.signal.libsignal.metadata.InvalidMetadataVersionException; import org.signal.libsignal.metadata.ProtocolDuplicateMessageException; @@ -50,6 +53,7 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException; import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.SelfSendException; +import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; @@ -125,6 +129,7 @@ import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; @@ -185,6 +190,7 @@ public class Manager implements Closeable { final static Logger logger = LoggerFactory.getLogger(Manager.class); private final SleepTimer timer = new UptimeSleepTimer(); + private final CertificateValidator certificateValidator = new CertificateValidator(ServiceConfig.getUnidentifiedSenderTrustRoot()); private final SignalServiceConfiguration serviceConfiguration; private final String userAgent; @@ -419,7 +425,7 @@ public class Manager implements Closeable { } public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - Utils.DeviceLinkInfo info = Utils.parseDeviceLinkUri(linkUri); + DeviceLinkInfo info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); addDevice(info.deviceIdentifier, info.deviceKey); } @@ -696,7 +702,7 @@ public class Manager implements Closeable { return Optional.absent(); } - return Optional.of(Utils.createAttachment(file)); + return Optional.of(AttachmentUtils.createAttachment(file)); } private Optional createContactAvatarAttachment(String number) throws IOException { @@ -705,7 +711,7 @@ public class Manager implements Closeable { return Optional.absent(); } - return Optional.of(Utils.createAttachment(file)); + return Optional.of(AttachmentUtils.createAttachment(file)); } private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { @@ -751,7 +757,7 @@ public class Manager implements Closeable { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withBody(messageText); if (attachments != null) { - messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments)); + messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments)); } return sendGroupMessage(messageBuilder, groupId); @@ -928,7 +934,7 @@ public class Manager implements Closeable { newE164Members.remove(contact.getNumber()); } throw new IOException("Failed to add members " - + Util.join(", ", newE164Members) + + String.join(", ", newE164Members) + " to group: Not registered on Signal"); } @@ -971,7 +977,7 @@ public class Manager implements Closeable { File aFile = getGroupAvatarFile(g.getGroupId()); if (aFile.exists()) { try { - group.withAvatar(Utils.createAttachment(aFile)); + group.withAvatar(AttachmentUtils.createAttachment(aFile)); } catch (IOException e) { throw new AttachmentInvalidException(aFile.toString(), e); } @@ -1022,7 +1028,7 @@ public class Manager implements Closeable { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .withBody(messageText); if (attachments != null) { - List attachmentStreams = Utils.getSignalServiceAttachments(attachments); + List attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); // Upload attachments here, so we only upload once even for multiple recipients SignalServiceMessageSender messageSender = createMessageSender(); @@ -1510,7 +1516,7 @@ public class Manager implements Closeable { 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()); + certificateValidator); try { return cipher.decrypt(envelope); } catch (ProtocolUntrustedIdentityException e) { @@ -1820,7 +1826,7 @@ public class Manager implements Closeable { ) { SignalServiceEnvelope envelope; try { - envelope = Utils.loadEnvelope(fileEntry); + envelope = MessageCacheUtils.loadEnvelope(fileEntry); if (envelope == null) { return; } @@ -1887,7 +1893,7 @@ public class Manager implements Closeable { try { String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : ""; File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp()); - Utils.storeEnvelope(envelope1, cacheFile); + MessageCacheUtils.storeEnvelope(envelope1, cacheFile); } catch (IOException e) { logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage()); } @@ -2240,7 +2246,7 @@ public class Manager implements Closeable { return retrieveAttachment(pointer, getContactAvatarFile(number), false); } else { SignalServiceAttachmentStream stream = attachment.asStream(); - return Utils.retrieveAttachment(stream, getContactAvatarFile(number)); + return AttachmentUtils.retrieveAttachment(stream, getContactAvatarFile(number)); } } @@ -2257,7 +2263,7 @@ public class Manager implements Closeable { return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false); } else { SignalServiceAttachmentStream stream = attachment.asStream(); - return Utils.retrieveAttachment(stream, getGroupAvatarFile(groupId)); + return AttachmentUtils.retrieveAttachment(stream, getGroupAvatarFile(groupId)); } } @@ -2509,7 +2515,7 @@ public class Manager implements Closeable { } public ContactInfo getContact(String number) { - return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number)); + return account.getContactStore().getContact(Utils.getSignalServiceAddressFromIdentifier(number)); } public GroupInfo getGroup(GroupId groupId) { @@ -2613,7 +2619,8 @@ public class Manager implements Closeable { public String computeSafetyNumber( SignalServiceAddress theirAddress, IdentityKey theirIdentityKey ) { - return Utils.computeSafetyNumber(account.getSelfAddress(), + return Utils.computeSafetyNumber(ServiceConfig.capabilities.isUuid(), + account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey); @@ -2626,12 +2633,12 @@ public class Manager implements Closeable { public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException { String canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier - : Util.canonicalizeNumber(identifier, account.getUsername()); + : PhoneNumberFormatter.formatNumber(identifier, account.getUsername()); return resolveSignalServiceAddress(canonicalizedNumber); } public SignalServiceAddress resolveSignalServiceAddress(String identifier) { - SignalServiceAddress address = Util.getSignalServiceAddressFromIdentifier(identifier); + SignalServiceAddress address = Utils.getSignalServiceAddressFromIdentifier(identifier); return resolveSignalServiceAddress(address); } diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 95e92c7a..8b3f0eb4 100644 --- a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -17,6 +17,7 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.util.KeyUtils; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.IdentityKeyPair; @@ -71,8 +72,7 @@ public class ProvisioningManager { public String getDeviceLinkUri() throws TimeoutException, IOException { String deviceUuid = accountManager.getNewDeviceUuid(); - return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(deviceUuid, - identityKey.getPublicKey().getPublicKey())); + return new DeviceLinkInfo(deviceUuid, identityKey.getPublicKey().getPublicKey()).createDeviceLinkUri(); } public String finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java index 353670ae..939d5b5b 100644 --- a/src/main/java/org/asamk/signal/manager/ServiceConfig.java +++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java @@ -1,6 +1,9 @@ package org.asamk.signal.manager; import org.signal.zkgroup.ServerPublicParams; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.push.TrustStore; @@ -106,6 +109,14 @@ public class ServiceConfig { } } + static ECPublicKey getUnidentifiedSenderTrustRoot() { + try { + return Curve.decodePoint(Base64.decode(UNIDENTIFIED_SENDER_TRUST_ROOT), 0); + } catch (InvalidKeyException | IOException e) { + throw new AssertionError(e); + } + } + private static Map makeSignalCdnUrlMapFor( SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls ) { diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java deleted file mode 100644 index 0a815ea9..00000000 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ /dev/null @@ -1,304 +0,0 @@ -package org.asamk.signal.manager; - -import org.signal.libsignal.metadata.certificate.CertificateValidator; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.ecc.Curve; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.fingerprint.Fingerprint; -import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator; -import org.whispersystems.libsignal.util.guava.Optional; -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.StreamDetails; -import org.whispersystems.signalservice.api.util.UuidUtil; -import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; -import org.whispersystems.util.Base64; - -import java.io.BufferedInputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.net.URLConnection; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import static org.whispersystems.signalservice.internal.util.Util.isEmpty; - -class Utils { - - static List getSignalServiceAttachments(List attachments) throws AttachmentInvalidException { - List signalServiceAttachments = null; - if (attachments != null) { - signalServiceAttachments = new ArrayList<>(attachments.size()); - for (String attachment : attachments) { - try { - signalServiceAttachments.add(createAttachment(new File(attachment))); - } catch (IOException e) { - throw new AttachmentInvalidException(attachment, e); - } - } - } - return signalServiceAttachments; - } - - static String getFileMimeType(File file, String defaultMimeType) throws IOException { - String mime = Files.probeContentType(file.toPath()); - if (mime == null) { - try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) { - mime = URLConnection.guessContentTypeFromStream(bufferedStream); - } - } - if (mime == null) { - return defaultMimeType; - } - return mime; - } - - static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException { - InputStream attachmentStream = new FileInputStream(attachmentFile); - final long attachmentSize = attachmentFile.length(); - final String mime = getFileMimeType(attachmentFile, "application/octet-stream"); - // TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option - final long uploadTimestamp = System.currentTimeMillis(); - Optional preview = Optional.absent(); - Optional caption = Optional.absent(); - Optional blurHash = Optional.absent(); - final Optional resumableUploadSpec = Optional.absent(); - return new SignalServiceAttachmentStream(attachmentStream, - mime, - attachmentSize, - Optional.of(attachmentFile.getName()), - false, - false, - preview, - 0, - 0, - uploadTimestamp, - caption, - blurHash, - null, - null, - resumableUploadSpec); - } - - static StreamDetails createStreamDetailsFromFile(File file) throws IOException { - InputStream stream = new FileInputStream(file); - final long size = file.length(); - String mime = Files.probeContentType(file.toPath()); - if (mime == null) { - mime = "application/octet-stream"; - } - return new StreamDetails(stream, mime, size); - } - - static CertificateValidator getCertificateValidator() { - try { - ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(ServiceConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), - 0); - return new CertificateValidator(unidentifiedSenderTrustRoot); - } catch (InvalidKeyException | IOException e) { - throw new AssertionError(e); - } - } - - private static Map getQueryMap(String query) { - String[] params = query.split("&"); - Map map = new HashMap<>(); - for (String param : params) { - final String[] paramParts = param.split("="); - String name = URLDecoder.decode(paramParts[0], StandardCharsets.UTF_8); - String value = URLDecoder.decode(paramParts[1], StandardCharsets.UTF_8); - map.put(name, value); - } - return map; - } - - static String createDeviceLinkUri(DeviceLinkInfo info) { - return "tsdevice:/?uuid=" - + URLEncoder.encode(info.deviceIdentifier, StandardCharsets.UTF_8) - + "&pub_key=" - + URLEncoder.encode(Base64.encodeBytesWithoutPadding(info.deviceKey.serialize()), - StandardCharsets.UTF_8); - } - - static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws IOException, InvalidKeyException { - Map query = getQueryMap(linkUri.getRawQuery()); - String deviceIdentifier = query.get("uuid"); - String publicKeyEncoded = query.get("pub_key"); - - if (isEmpty(deviceIdentifier) || isEmpty(publicKeyEncoded)) { - throw new RuntimeException("Invalid device link uri"); - } - - ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); - - return new DeviceLinkInfo(deviceIdentifier, deviceKey); - } - - static SignalServiceEnvelope loadEnvelope(File file) throws IOException { - try (FileInputStream f = new FileInputStream(file)) { - DataInputStream in = new DataInputStream(f); - int version = in.readInt(); - if (version > 4) { - 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 - in.readUTF(); - } - long timestamp = in.readLong(); - byte[] content = null; - int contentLen = in.readInt(); - if (contentLen > 0) { - content = new byte[contentLen]; - in.readFully(content); - } - byte[] legacyMessage = null; - int legacyMessageLen = in.readInt(); - if (legacyMessageLen > 0) { - legacyMessage = new byte[legacyMessageLen]; - in.readFully(legacyMessage); - } - long serverReceivedTimestamp = 0; - String uuid = null; - if (version >= 2) { - serverReceivedTimestamp = in.readLong(); - uuid = in.readUTF(); - if ("".equals(uuid)) { - uuid = null; - } - } - long serverDeliveredTimestamp = 0; - if (version >= 4) { - serverDeliveredTimestamp = in.readLong(); - } - Optional addressOptional = sourceUuid == null && source.isEmpty() - ? Optional.absent() - : Optional.of(new SignalServiceAddress(sourceUuid, source)); - return new SignalServiceEnvelope(type, - addressOptional, - sourceDevice, - timestamp, - legacyMessage, - content, - serverReceivedTimestamp, - serverDeliveredTimestamp, - uuid); - } - } - - static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { - try (FileOutputStream f = new FileOutputStream(file)) { - try (DataOutputStream out = new DataOutputStream(f)) { - out.writeInt(4); // version - out.writeInt(envelope.getType()); - 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()) { - out.writeInt(envelope.getContent().length); - out.write(envelope.getContent()); - } else { - out.writeInt(0); - } - if (envelope.hasLegacyMessage()) { - out.writeInt(envelope.getLegacyMessage().length); - out.write(envelope.getLegacyMessage()); - } else { - out.writeInt(0); - } - out.writeLong(envelope.getServerReceivedTimestamp()); - String uuid = envelope.getUuid(); - out.writeUTF(uuid == null ? "" : uuid); - out.writeLong(envelope.getServerDeliveredTimestamp()); - } - } - } - - static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException { - InputStream input = stream.getInputStream(); - - try (OutputStream output = new FileOutputStream(outputFile)) { - byte[] buffer = new byte[4096]; - int read; - - while ((read = input.read(buffer)) != -1) { - output.write(buffer, 0, read); - } - } catch (FileNotFoundException e) { - e.printStackTrace(); - return null; - } - return outputFile; - } - - static String computeSafetyNumber( - SignalServiceAddress ownAddress, - IdentityKey ownIdentityKey, - SignalServiceAddress theirAddress, - IdentityKey theirIdentityKey - ) { - int version; - byte[] ownId; - byte[] theirId; - - if (ServiceConfig.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; - if (!ownAddress.getNumber().isPresent() || !theirAddress.getNumber().isPresent()) { - return "INVALID ID"; - } - 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(); - } - - static class DeviceLinkInfo { - - final String deviceIdentifier; - final ECPublicKey deviceKey; - - DeviceLinkInfo(final String deviceIdentifier, final ECPublicKey deviceKey) { - this.deviceIdentifier = deviceIdentifier; - this.deviceKey = deviceKey; - } - } -} diff --git a/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java b/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java index d865356e..237a34b6 100644 --- a/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java +++ b/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java @@ -1,6 +1,6 @@ package org.asamk.signal.manager.groups; -import static org.asamk.signal.manager.KeyUtils.getSecretBytes; +import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes; public class GroupIdV1 extends GroupId { diff --git a/src/main/java/org/asamk/signal/manager/groups/GroupLinkPassword.java b/src/main/java/org/asamk/signal/manager/groups/GroupLinkPassword.java index 41be672a..7edc7afb 100644 --- a/src/main/java/org/asamk/signal/manager/groups/GroupLinkPassword.java +++ b/src/main/java/org/asamk/signal/manager/groups/GroupLinkPassword.java @@ -1,6 +1,6 @@ package org.asamk.signal.manager.groups; -import org.asamk.signal.manager.KeyUtils; +import org.asamk.signal.manager.util.KeyUtils; import java.util.Arrays; diff --git a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 394eba57..8a2320e0 100644 --- a/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -7,7 +7,7 @@ import org.asamk.signal.manager.groups.GroupLinkPassword; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.profiles.SignalProfile; -import org.asamk.signal.util.IOUtils; +import org.asamk.signal.manager.util.IOUtils; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.Member; diff --git a/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index c3573209..c787471f 100644 --- a/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -25,8 +25,8 @@ import org.asamk.signal.manager.storage.protocol.SignalServiceAddressResolver; import org.asamk.signal.manager.storage.stickers.StickerStore; import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore; import org.asamk.signal.manager.storage.threads.ThreadInfo; -import org.asamk.signal.util.IOUtils; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.util.IOUtils; +import org.asamk.signal.manager.util.Utils; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; @@ -211,28 +211,28 @@ public class SignalAccount implements Closeable { deviceId = node.asInt(); } if (rootNode.has("isMultiDevice")) { - isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean(); + isMultiDevice = Utils.getNotNullNode(rootNode, "isMultiDevice").asBoolean(); } - username = Util.getNotNullNode(rootNode, "username").asText(); - password = Util.getNotNullNode(rootNode, "password").asText(); + username = Utils.getNotNullNode(rootNode, "username").asText(); + password = Utils.getNotNullNode(rootNode, "password").asText(); JsonNode pinNode = rootNode.get("registrationLockPin"); registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText(); if (rootNode.has("signalingKey")) { - signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText(); + signalingKey = Utils.getNotNullNode(rootNode, "signalingKey").asText(); } if (rootNode.has("preKeyIdOffset")) { - preKeyIdOffset = Util.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0); + preKeyIdOffset = Utils.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0); } else { preKeyIdOffset = 0; } if (rootNode.has("nextSignedPreKeyId")) { - nextSignedPreKeyId = Util.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt(); + nextSignedPreKeyId = Utils.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt(); } else { nextSignedPreKeyId = 0; } if (rootNode.has("profileKey")) { try { - profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText())); + profileKey = new ProfileKey(Base64.decode(Utils.getNotNullNode(rootNode, "profileKey").asText())); } catch (InvalidInputException e) { throw new IOException( "Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes", @@ -240,9 +240,9 @@ public class SignalAccount implements Closeable { } } - signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), + signalProtocolStore = jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); - registered = Util.getNotNullNode(rootNode, "registered").asBoolean(); + registered = Utils.getNotNullNode(rootNode, "registered").asBoolean(); JsonNode groupStoreNode = rootNode.get("groupStore"); if (groupStoreNode != null) { groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class); diff --git a/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java index 2b4dbcf5..fdcd28a3 100644 --- a/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java @@ -16,8 +16,8 @@ import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupIdV1; import org.asamk.signal.manager.groups.GroupIdV2; import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.util.Hex; -import org.asamk.signal.util.IOUtils; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; diff --git a/src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java index 517b384e..5bc1c11f 100644 --- a/src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonIdentityKeyStore.java @@ -9,7 +9,7 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.asamk.signal.manager.TrustLevel; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; @@ -51,7 +51,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { if (resolver != null) { return resolver.resolveSignalServiceAddress(identifier); } else { - return Util.getSignalServiceAddressFromIdentifier(identifier); + return Utils.getSignalServiceAddressFromIdentifier(identifier); } } @@ -213,7 +213,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { UUID uuid = trustedKey.hasNonNull("uuid") ? UuidUtil.parseOrNull(trustedKey.get("uuid") .asText()) : null; final SignalServiceAddress serviceAddress = uuid == null - ? Util.getSignalServiceAddressFromIdentifier(trustedKeyName) + ? Utils.getSignalServiceAddressFromIdentifier(trustedKeyName) : new SignalServiceAddress(uuid, trustedKeyName); try { IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0); diff --git a/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSessionStore.java index f55aff14..6e300214 100644 --- a/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/manager/storage/protocol/JsonSessionStore.java @@ -8,7 +8,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.SignalProtocolAddress; @@ -43,7 +43,7 @@ class JsonSessionStore implements SessionStore { if (resolver != null) { return resolver.resolveSignalServiceAddress(identifier); } else { - return Util.getSignalServiceAddressFromIdentifier(identifier); + return Utils.getSignalServiceAddressFromIdentifier(identifier); } } @@ -147,7 +147,7 @@ class JsonSessionStore implements SessionStore { UUID uuid = session.hasNonNull("uuid") ? UuidUtil.parseOrNull(session.get("uuid").asText()) : null; final SignalServiceAddress serviceAddress = uuid == null - ? Util.getSignalServiceAddressFromIdentifier(sessionName) + ? Utils.getSignalServiceAddressFromIdentifier(sessionName) : new SignalServiceAddress(uuid, sessionName); final int deviceId = session.get("deviceId").asInt(); final String record = session.get("record").asText(); diff --git a/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java b/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java new file mode 100644 index 00000000..b9a97073 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/util/AttachmentUtils.java @@ -0,0 +1,79 @@ +package org.asamk.signal.manager.util; + +import org.asamk.signal.manager.AttachmentInvalidException; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; +import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +public class AttachmentUtils { + + public static List getSignalServiceAttachments(List attachments) throws AttachmentInvalidException { + List signalServiceAttachments = null; + if (attachments != null) { + signalServiceAttachments = new ArrayList<>(attachments.size()); + for (String attachment : attachments) { + try { + signalServiceAttachments.add(createAttachment(new File(attachment))); + } catch (IOException e) { + throw new AttachmentInvalidException(attachment, e); + } + } + } + return signalServiceAttachments; + } + + public static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException { + InputStream attachmentStream = new FileInputStream(attachmentFile); + final long attachmentSize = attachmentFile.length(); + final String mime = Utils.getFileMimeType(attachmentFile, "application/octet-stream"); + // TODO mabybe add a parameter to set the voiceNote, borderless, preview, width, height and caption option + final long uploadTimestamp = System.currentTimeMillis(); + Optional preview = Optional.absent(); + Optional caption = Optional.absent(); + Optional blurHash = Optional.absent(); + final Optional resumableUploadSpec = Optional.absent(); + return new SignalServiceAttachmentStream(attachmentStream, + mime, + attachmentSize, + Optional.of(attachmentFile.getName()), + false, + false, + preview, + 0, + 0, + uploadTimestamp, + caption, + blurHash, + null, + null, + resumableUploadSpec); + } + + public static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException { + InputStream input = stream.getInputStream(); + + try (OutputStream output = new FileOutputStream(outputFile)) { + byte[] buffer = new byte[4096]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } + return outputFile; + } +} diff --git a/src/main/java/org/asamk/signal/manager/util/IOUtils.java b/src/main/java/org/asamk/signal/manager/util/IOUtils.java new file mode 100644 index 00000000..06f8aa22 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/util/IOUtils.java @@ -0,0 +1,72 @@ +package org.asamk.signal.manager.util; + +import org.whispersystems.signalservice.internal.util.Util; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; +import java.util.Set; + +import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; + +public class IOUtils { + + public static File createTempFile() throws IOException { + return File.createTempFile("signal_tmp_", ".tmp"); + } + + public static byte[] readFully(InputStream in) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Util.copy(in, baos); + return baos.toByteArray(); + } + + public static void createPrivateDirectories(File file) throws IOException { + if (file.exists()) { + return; + } + + final Path path = file.toPath(); + try { + Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE); + Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms)); + } catch (UnsupportedOperationException e) { + Files.createDirectories(path); + } + } + + public static void createPrivateFile(File path) throws IOException { + final Path file = path.toPath(); + try { + Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE); + Files.createFile(file, PosixFilePermissions.asFileAttribute(perms)); + } catch (UnsupportedOperationException e) { + Files.createFile(file); + } + } + + public static void copyStreamToFile(InputStream input, File outputFile) throws IOException { + copyStreamToFile(input, outputFile, 8192); + } + + public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException { + try (OutputStream output = new FileOutputStream(outputFile)) { + byte[] buffer = new byte[bufferSize]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/util/KeyUtils.java similarity index 79% rename from src/main/java/org/asamk/signal/manager/KeyUtils.java rename to src/main/java/org/asamk/signal/manager/util/KeyUtils.java index 6ac093db..2b4bc371 100644 --- a/src/main/java/org/asamk/signal/manager/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/util/KeyUtils.java @@ -1,4 +1,4 @@ -package org.asamk.signal.manager; +package org.asamk.signal.manager.util; import org.asamk.signal.util.RandomUtils; import org.signal.zkgroup.InvalidInputException; @@ -10,11 +10,11 @@ public class KeyUtils { private KeyUtils() { } - static String createSignalingKey() { + public static String createSignalingKey() { return getSecret(52); } - static ProfileKey createProfileKey() { + public static ProfileKey createProfileKey() { try { return new ProfileKey(getSecretBytes(32)); } catch (InvalidInputException e) { @@ -22,11 +22,11 @@ public class KeyUtils { } } - static String createPassword() { + public static String createPassword() { return getSecret(18); } - static byte[] createStickerUploadKey() { + public static byte[] createStickerUploadKey() { return getSecretBytes(32); } diff --git a/src/main/java/org/asamk/signal/manager/util/MessageCacheUtils.java b/src/main/java/org/asamk/signal/manager/util/MessageCacheUtils.java new file mode 100644 index 00000000..8661c10b --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/util/MessageCacheUtils.java @@ -0,0 +1,105 @@ +package org.asamk.signal.manager.util; + +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.UUID; + +public class MessageCacheUtils { + + public static SignalServiceEnvelope loadEnvelope(File file) throws IOException { + try (FileInputStream f = new FileInputStream(file)) { + DataInputStream in = new DataInputStream(f); + int version = in.readInt(); + if (version > 4) { + 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 + in.readUTF(); + } + long timestamp = in.readLong(); + byte[] content = null; + int contentLen = in.readInt(); + if (contentLen > 0) { + content = new byte[contentLen]; + in.readFully(content); + } + byte[] legacyMessage = null; + int legacyMessageLen = in.readInt(); + if (legacyMessageLen > 0) { + legacyMessage = new byte[legacyMessageLen]; + in.readFully(legacyMessage); + } + long serverReceivedTimestamp = 0; + String uuid = null; + if (version >= 2) { + serverReceivedTimestamp = in.readLong(); + uuid = in.readUTF(); + if ("".equals(uuid)) { + uuid = null; + } + } + long serverDeliveredTimestamp = 0; + if (version >= 4) { + serverDeliveredTimestamp = in.readLong(); + } + Optional addressOptional = sourceUuid == null && source.isEmpty() + ? Optional.absent() + : Optional.of(new SignalServiceAddress(sourceUuid, source)); + return new SignalServiceEnvelope(type, + addressOptional, + sourceDevice, + timestamp, + legacyMessage, + content, + serverReceivedTimestamp, + serverDeliveredTimestamp, + uuid); + } + } + + public static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { + try (FileOutputStream f = new FileOutputStream(file)) { + try (DataOutputStream out = new DataOutputStream(f)) { + out.writeInt(4); // version + out.writeInt(envelope.getType()); + 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()) { + out.writeInt(envelope.getContent().length); + out.write(envelope.getContent()); + } else { + out.writeInt(0); + } + if (envelope.hasLegacyMessage()) { + out.writeInt(envelope.getLegacyMessage().length); + out.write(envelope.getLegacyMessage()); + } else { + out.writeInt(0); + } + out.writeLong(envelope.getServerReceivedTimestamp()); + String uuid = envelope.getUuid(); + out.writeUTF(uuid == null ? "" : uuid); + out.writeLong(envelope.getServerDeliveredTimestamp()); + } + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/util/Utils.java b/src/main/java/org/asamk/signal/manager/util/Utils.java new file mode 100644 index 00000000..e68b5ce3 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -0,0 +1,97 @@ +package org.asamk.signal.manager.util; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.fingerprint.Fingerprint; +import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.StreamDetails; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidObjectException; +import java.net.URLConnection; +import java.nio.file.Files; + +public class Utils { + + public static String getFileMimeType(File file, String defaultMimeType) throws IOException { + String mime = Files.probeContentType(file.toPath()); + if (mime == null) { + try (InputStream bufferedStream = new BufferedInputStream(new FileInputStream(file))) { + mime = URLConnection.guessContentTypeFromStream(bufferedStream); + } + } + if (mime == null) { + return defaultMimeType; + } + return mime; + } + + public static StreamDetails createStreamDetailsFromFile(File file) throws IOException { + InputStream stream = new FileInputStream(file); + final long size = file.length(); + String mime = Files.probeContentType(file.toPath()); + if (mime == null) { + mime = "application/octet-stream"; + } + return new StreamDetails(stream, mime, size); + } + + public static String computeSafetyNumber( + boolean isUuidCapable, + SignalServiceAddress ownAddress, + IdentityKey ownIdentityKey, + SignalServiceAddress theirAddress, + IdentityKey theirIdentityKey + ) { + int version; + byte[] ownId; + byte[] theirId; + + if (isUuidCapable && 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; + if (!ownAddress.getNumber().isPresent() || !theirAddress.getNumber().isPresent()) { + return "INVALID ID"; + } + 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(); + } + + public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) { + if (UuidUtil.isUuid(identifier)) { + return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null); + } else { + return new SignalServiceAddress(null, identifier); + } + } + + public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException { + JsonNode node = parent.get(name); + if (node == null) { + throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", + name)); + } + + return node; + } +} diff --git a/src/main/java/org/asamk/signal/util/IOUtils.java b/src/main/java/org/asamk/signal/util/IOUtils.java index 59727a9a..766d1905 100644 --- a/src/main/java/org/asamk/signal/util/IOUtils.java +++ b/src/main/java/org/asamk/signal/util/IOUtils.java @@ -1,35 +1,16 @@ package org.asamk.signal.util; -import org.whispersystems.signalservice.internal.util.Util; - -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.io.StringWriter; import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.EnumSet; -import java.util.Set; - -import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; -import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; -import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; public class IOUtils { private IOUtils() { } - public static File createTempFile() throws IOException { - return File.createTempFile("signal_tmp_", ".tmp"); - } - public static String readAll(InputStream in, Charset charset) throws IOException { StringWriter output = new StringWriter(); byte[] buffer = new byte[4096]; @@ -40,36 +21,6 @@ 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(File file) throws IOException { - if (file.exists()) { - return; - } - - final Path path = file.toPath(); - try { - Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE); - Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms)); - } catch (UnsupportedOperationException e) { - Files.createDirectories(path); - } - } - - public static void createPrivateFile(File path) throws IOException { - final Path file = path.toPath(); - try { - Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE); - Files.createFile(file, PosixFilePermissions.asFileAttribute(perms)); - } catch (UnsupportedOperationException e) { - Files.createFile(file); - } - } - public static File getDataHomeDir() { String dataHome = System.getenv("XDG_DATA_HOME"); if (dataHome != null) { @@ -78,19 +29,4 @@ public class IOUtils { return new File(new File(System.getProperty("user.home"), ".local"), "share"); } - - public static void copyStreamToFile(InputStream input, File outputFile) throws IOException { - copyStreamToFile(input, outputFile, 8192); - } - - public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException { - try (OutputStream output = new FileOutputStream(outputFile)) { - byte[] buffer = new byte[bufferSize]; - int read; - - while ((read = input.read(buffer)) != -1) { - output.write(buffer, 0, read); - } - } - } } diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 79a6587b..92bfae7b 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -1,15 +1,7 @@ package org.asamk.signal.util; -import com.fasterxml.jackson.databind.JsonNode; - import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.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 java.io.InvalidObjectException; public class Util { @@ -26,41 +18,7 @@ public class Util { return f.toString(); } - public static String join(CharSequence separator, Iterable list) { - StringBuilder buf = new StringBuilder(); - for (CharSequence str : list) { - if (buf.length() > 0) { - buf.append(separator); - } - buf.append(str); - } - - return buf.toString(); - } - - public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException { - JsonNode node = parent.get(name); - if (node == null) { - throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", - name)); - } - - return node; - } - public static GroupId decodeGroupId(String groupId) throws GroupIdFormatException { return GroupId.fromBase64(groupId); } - - public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { - return PhoneNumberFormatter.formatNumber(number, localNumber); - } - - public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) { - if (UuidUtil.isUuid(identifier)) { - return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null); - } else { - return new SignalServiceAddress(null, identifier); - } - } } From a52f6a6657585fbad5afa4c57ce37752118317e9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 30 Dec 2020 11:59:24 +0100 Subject: [PATCH 18/32] Replace Collections with Set.of/Map.of/List.of --- .../org/asamk/signal/dbus/DbusSignalImpl.java | 3 +-- .../org/asamk/signal/manager/Manager.java | 19 +++++++++---------- .../asamk/signal/manager/ServiceConfig.java | 3 +-- .../manager/storage/groups/GroupInfoV2.java | 7 +++---- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index d19116a4..278fbbd4 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -16,7 +16,6 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -167,7 +166,7 @@ public class DbusSignalImpl implements Signal { public List getGroupMembers(final byte[] groupId) { GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId)); if (group == null) { - return Collections.emptyList(); + return List.of(); } else { return group.getMembers() .stream() diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 5ed8fc4e..25b2ec54 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -164,7 +164,6 @@ import java.security.SignatureException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -211,7 +210,7 @@ public class Manager implements Closeable { private final ProfileHelper profileHelper; private final GroupHelper groupHelper; - public Manager( + Manager( SignalAccount account, PathConfig pathConfig, SignalServiceConfiguration serviceConfiguration, @@ -810,7 +809,7 @@ public class Manager implements Closeable { GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile); if (gv2 == null) { GroupInfoV1 gv1 = new GroupInfoV1(GroupIdV1.createRandom()); - gv1.addMembers(Collections.singleton(account.getSelfAddress())); + gv1.addMembers(List.of(account.getSelfAddress())); updateGroupV1(gv1, name, members, avatarFile); messageBuilder = getGroupUpdateMessageBuilder(gv1); g = gv1; @@ -965,7 +964,7 @@ public class Manager implements Closeable { SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); // Send group message only to the recipient who requested it - return sendMessage(messageBuilder, Collections.singleton(recipient)); + return sendMessage(messageBuilder, List.of(recipient)); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { @@ -1007,14 +1006,14 @@ public class Manager implements Closeable { .asGroupMessage(group.build()); // Send group info request message to the recipient who sent us a message with this groupId - return sendMessage(messageBuilder, Collections.singleton(recipient)); + return sendMessage(messageBuilder, List.of(recipient)); } void sendReceipt( SignalServiceAddress remoteAddress, long messageId ) throws IOException, UntrustedIdentityException { SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, - Collections.singletonList(messageId), + List.of(messageId), System.currentTimeMillis()); createMessageSender().sendReceipt(remoteAddress, @@ -1141,7 +1140,7 @@ public class Manager implements Closeable { private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asExpirationUpdate(); - sendMessage(messageBuilder, Collections.singleton(address)); + sendMessage(messageBuilder, List.of(address)); } /** @@ -1434,7 +1433,7 @@ public class Manager implements Closeable { .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED); - return new Pair<>(timestamp, Collections.emptyList()); + return new Pair<>(timestamp, List.of()); } } else { // Send to all individually, so sync messages are sent correctly @@ -1477,7 +1476,7 @@ public class Manager implements Closeable { message.getTimestamp(), message, message.getExpiresInSeconds(), - Collections.singletonMap(recipient, unidentifiedAccess.isPresent()), + Map.of(recipient, unidentifiedAccess.isPresent()), false); SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); @@ -2085,7 +2084,7 @@ public class Manager implements Closeable { syncGroup.removeMember(account.getSelfAddress()); } else { // Add ourself to the member set as it's marked as active - syncGroup.addMembers(Collections.singleton(account.getSelfAddress())); + syncGroup.addMembers(List.of(account.getSelfAddress())); } syncGroup.blocked = g.isBlocked(); if (g.getColor().isPresent()) { diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java index 939d5b5b..0ccd826a 100644 --- a/src/main/java/org/asamk/signal/manager/ServiceConfig.java +++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java @@ -20,7 +20,6 @@ import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -77,7 +76,7 @@ public class ServiceConfig { .header("User-Agent", userAgent) .build()); - final List interceptors = Collections.singletonList(userAgentInterceptor); + final List interceptors = List.of(userAgentInterceptor); return new SignalServiceConfiguration(new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, makeSignalCdnUrlMapFor(new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, diff --git a/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index 17c23925..2092c03a 100644 --- a/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -8,7 +8,6 @@ import org.signal.zkgroup.groups.GroupMasterKey; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; -import java.util.Collections; import java.util.Set; import java.util.stream.Collectors; @@ -66,7 +65,7 @@ public class GroupInfoV2 extends GroupInfo { @Override public Set getMembers() { if (this.group == null) { - return Collections.emptySet(); + return Set.of(); } return group.getMembersList() .stream() @@ -77,7 +76,7 @@ public class GroupInfoV2 extends GroupInfo { @Override public Set getPendingMembers() { if (this.group == null) { - return Collections.emptySet(); + return Set.of(); } return group.getPendingMembersList() .stream() @@ -88,7 +87,7 @@ public class GroupInfoV2 extends GroupInfo { @Override public Set getRequestingMembers() { if (this.group == null) { - return Collections.emptySet(); + return Set.of(); } return group.getRequestingMembersList() .stream() From 425626ef9475cbc90ef8ada95dee172389baf521 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Mar 2020 16:02:31 +0100 Subject: [PATCH 19/32] Implement registration pin lock with KBS Fixes #323 Fixes #268 --- build.gradle | 2 +- .../signal/commands/RemovePinCommand.java | 3 +- .../asamk/signal/commands/SetPinCommand.java | 3 +- .../asamk/signal/commands/VerifyCommand.java | 8 ++ .../org/asamk/signal/manager/Manager.java | 112 ++++++++++++++---- .../asamk/signal/manager/ServiceConfig.java | 25 ++-- .../signal/manager/helper/PinHelper.java | 90 ++++++++++++++ .../signal/manager/storage/SignalAccount.java | 19 ++- .../asamk/signal/manager/util/KeyUtils.java | 5 + .../asamk/signal/manager/util/PinHashing.java | 31 +++++ 10 files changed, 254 insertions(+), 44 deletions(-) create mode 100644 src/main/java/org/asamk/signal/manager/helper/PinHelper.java create mode 100644 src/main/java/org/asamk/signal/manager/util/PinHashing.java diff --git a/build.gradle b/build.gradle index 1fbf5948..1c42834b 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { dependencies { implementation 'com.github.turasa:signal-service-java:2.15.3_unofficial_15' - implementation 'org.bouncycastle:bcprov-jdk15on:1.67' + implementation 'org.bouncycastle:bcprov-jdk15on:1.68' implementation 'net.sourceforge.argparse4j:argparse4j:0.8.1' implementation 'com.github.hypfvieh:dbus-java:3.2.4' implementation 'org.slf4j:slf4j-simple:1.7.30' diff --git a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java index b7de5402..95531249 100644 --- a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java @@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import java.io.IOException; @@ -23,7 +24,7 @@ public class RemovePinCommand implements LocalCommand { try { m.setRegistrationLockPin(Optional.absent()); return 0; - } catch (IOException e) { + } catch (IOException | UnauthenticatedResponseException e) { System.err.println("Remove pin error: " + e.getMessage()); return 3; } diff --git a/src/main/java/org/asamk/signal/commands/SetPinCommand.java b/src/main/java/org/asamk/signal/commands/SetPinCommand.java index 9351dad0..c68ea3a9 100644 --- a/src/main/java/org/asamk/signal/commands/SetPinCommand.java +++ b/src/main/java/org/asamk/signal/commands/SetPinCommand.java @@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import java.io.IOException; @@ -26,7 +27,7 @@ public class SetPinCommand implements LocalCommand { String registrationLockPin = ns.getString("registrationLockPin"); m.setRegistrationLockPin(Optional.of(registrationLockPin)); return 0; - } catch (IOException e) { + } catch (IOException | UnauthenticatedResponseException e) { System.err.println("Set pin error: " + e.getMessage()); return 3; } diff --git a/src/main/java/org/asamk/signal/commands/VerifyCommand.java b/src/main/java/org/asamk/signal/commands/VerifyCommand.java index b6ad100b..d4b0a7cb 100644 --- a/src/main/java/org/asamk/signal/commands/VerifyCommand.java +++ b/src/main/java/org/asamk/signal/commands/VerifyCommand.java @@ -4,6 +4,8 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; +import org.whispersystems.signalservice.api.KeyBackupServicePinException; +import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; import org.whispersystems.signalservice.internal.push.LockedException; import java.io.IOException; @@ -31,6 +33,12 @@ public class VerifyCommand implements LocalCommand { System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: " + (e.getTimeRemaining() / 1000 / 60 / 60)); System.err.println("Use '--pin PIN_CODE' to specify the registration lock PIN"); + return 1; + } catch (KeyBackupServicePinException e) { + System.err.println("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining()); + return 1; + } catch (KeyBackupSystemNoDataException e) { + System.err.println("Verification failed! No KBS data."); return 3; } catch (IOException e) { System.err.println("Verify error: " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 25b2ec54..7ad7b888 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -26,6 +26,7 @@ import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.helper.GroupHelper; +import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.storage.SignalAccount; @@ -82,6 +83,10 @@ import org.whispersystems.libsignal.util.KeyHelper; import org.whispersystems.libsignal.util.Medium; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.KbsPinData; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.KeyBackupServicePinException; +import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; @@ -96,6 +101,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; +import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -138,6 +144,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf import org.whispersystems.signalservice.internal.contacts.crypto.Quote; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; +import org.whispersystems.signalservice.internal.push.LockedException; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; @@ -160,6 +167,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.security.KeyStore; import java.security.SignatureException; import java.util.ArrayList; import java.util.Arrays; @@ -193,6 +201,8 @@ public class Manager implements Closeable { private final SignalServiceConfiguration serviceConfiguration; private final String userAgent; + + // TODO make configurable private final boolean discoverableByPhoneNumber = true; private final boolean unrestrictedUnidentifiedAccess = false; @@ -209,6 +219,7 @@ public class Manager implements Closeable { private final UnidentifiedAccessHelper unidentifiedAccessHelper; private final ProfileHelper profileHelper; private final GroupHelper groupHelper; + private PinHelper pinHelper; Manager( SignalAccount account, @@ -222,8 +233,7 @@ public class Manager implements Closeable { this.userAgent = userAgent; this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create( serviceConfiguration)) : null; - this.accountManager = createSignalServiceAccountManager(); - this.groupsV2Api = accountManager.getGroupsV2Api(); + createSignalServiceAccountManager(); this.account.setResolver(this::resolveSignalServiceAddress); @@ -251,8 +261,8 @@ public class Manager implements Closeable { return account.getSelfAddress(); } - private SignalServiceAccountManager createSignalServiceAccountManager() { - return new SignalServiceAccountManager(serviceConfiguration, + private void createSignalServiceAccountManager() { + this.accountManager = new SignalServiceAccountManager(serviceConfiguration, new DynamicCredentialsProvider(account.getUuid(), account.getUsername(), account.getPassword(), @@ -261,6 +271,18 @@ public class Manager implements Closeable { userAgent, groupsV2Operations, timer); + this.groupsV2Api = accountManager.getGroupsV2Api(); + this.pinHelper = new PinHelper(createKeyBackupService()); + } + + private KeyBackupService createKeyBackupService() { + KeyStore keyStore = ServiceConfig.getIasKeyStore(); + + return accountManager.getKeyBackupService(keyStore, + ServiceConfig.KEY_BACKUP_ENCLAVE_NAME, + ServiceConfig.KEY_BACKUP_SERVICE_ID, + ServiceConfig.KEY_BACKUP_MRENCLAVE, + 10); } private IdentityKeyPair getIdentityKeyPair() { @@ -366,8 +388,7 @@ public class Manager implements Closeable { // Resetting UUID, because registering doesn't work otherwise account.setUuid(null); - accountManager = createSignalServiceAccountManager(); - this.groupsV2Api = accountManager.getGroupsV2Api(); + createSignalServiceAccountManager(); if (voiceVerification) { accountManager.requestVoiceVerificationCode(Locale.getDefault(), @@ -385,8 +406,9 @@ public class Manager implements Closeable { accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, - account.getRegistrationLockPin(), - account.getRegistrationLock(), + // set legacy pin only if no KBS master key is set + account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null, + account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(), unrestrictedUnidentifiedAccess, capabilities, @@ -479,26 +501,39 @@ public class Manager implements Closeable { } } - public void verifyAccount(String verificationCode, String pin) throws IOException { + public void verifyAccount( + String verificationCode, + String pin + ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); - // TODO make unrestricted unidentified access configurable - VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, - account.getSignalingKey(), - account.getSignalProtocolStore().getLocalRegistrationId(), - true, - pin, - null, - unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(), - unrestrictedUnidentifiedAccess, - capabilities, - discoverableByPhoneNumber); + VerifyAccountResponse response; + try { + response = verifyAccountWithCode(verificationCode, pin, null); + } catch (LockedException e) { + if (pin == null) { + throw e; + } + + KbsPinData registrationLockData = pinHelper.getRegistrationLockData(pin, e); + if (registrationLockData == null) { + throw e; + } + + String registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock(); + try { + response = verifyAccountWithCode(verificationCode, null, registrationLock); + } catch (LockedException _e) { + throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!"); + } + account.setPinMasterKey(registrationLockData.getMasterKey()); + } - UUID uuid = UuidUtil.parseOrNull(response.getUuid()); // TODO response.isStorageCapable() //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); + account.setRegistered(true); - account.setUuid(uuid); + account.setUuid(UuidUtil.parseOrNull(response.getUuid())); account.setRegistrationLockPin(pin); account.getSignalProtocolStore() .saveIdentity(account.getSelfAddress(), @@ -509,13 +544,40 @@ public class Manager implements Closeable { account.save(); } - public void setRegistrationLockPin(Optional pin) throws IOException { + private VerifyAccountResponse verifyAccountWithCode( + final String verificationCode, final String legacyPin, final String registrationLock + ) throws IOException { + return accountManager.verifyAccountWithCode(verificationCode, + account.getSignalingKey(), + account.getSignalProtocolStore().getLocalRegistrationId(), + true, + legacyPin, + registrationLock, + unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(), + unrestrictedUnidentifiedAccess, + capabilities, + discoverableByPhoneNumber); + } + + public void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException { if (pin.isPresent()) { + final MasterKey masterKey = account.getPinMasterKey() != null + ? account.getPinMasterKey() + : KeyUtils.createMasterKey(); + + pinHelper.setRegistrationLockPin(pin.get(), masterKey); + account.setRegistrationLockPin(pin.get()); - throw new RuntimeException("Not implemented anymore, will be replaced with KBS"); + account.setPinMasterKey(masterKey); } else { - account.setRegistrationLockPin(null); + // Remove legacy registration lock accountManager.removeRegistrationLockV1(); + + // Remove KBS Pin + pinHelper.removeRegistrationLockPin(); + + account.setRegistrationLockPin(null); + account.setPinMasterKey(null); } account.save(); } diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java index 0ccd826a..55935ced 100644 --- a/src/main/java/org/asamk/signal/manager/ServiceConfig.java +++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java @@ -1,5 +1,6 @@ package org.asamk.signal.manager; +import org.bouncycastle.util.encoders.Hex; import org.signal.zkgroup.ServerPublicParams; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.ecc.Curve; @@ -13,13 +14,13 @@ import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupSe import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; -import org.whispersystems.util.Base64; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; +import java.util.Base64; import java.util.List; import java.util.Map; @@ -28,7 +29,8 @@ import okhttp3.Interceptor; public class ServiceConfig { - final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; + final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() + .decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"); final static int PREKEY_MINIMUM_COUNT = 20; final static int PREKEY_BATCH_SIZE = 100; final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; @@ -37,6 +39,11 @@ public class ServiceConfig { final static String CDS_MRENCLAVE = "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15"; + final static String KEY_BACKUP_ENCLAVE_NAME = "fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe"; + final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode( + "fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe"); + final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87"; + private final static String URL = "https://textsecure-service.whispersystems.org"; private final static String CDN_URL = "https://cdn.signal.org"; private final static String CDN2_URL = "https://cdn2.signal.org"; @@ -48,18 +55,12 @@ public class ServiceConfig { private final static Optional dns = Optional.absent(); - private final static String zkGroupServerPublicParamsHex = "AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0="; - private final static byte[] zkGroupServerPublicParams; + private final static byte[] zkGroupServerPublicParams = Base64.getDecoder() + .decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0="); static final AccountAttributes.Capabilities capabilities; static { - try { - zkGroupServerPublicParams = Base64.decode(zkGroupServerPublicParamsHex); - } catch (IOException e) { - throw new AssertionError(e); - } - boolean zkGroupAvailable; try { new ServerPublicParams(zkGroupServerPublicParams); @@ -110,8 +111,8 @@ public class ServiceConfig { static ECPublicKey getUnidentifiedSenderTrustRoot() { try { - return Curve.decodePoint(Base64.decode(UNIDENTIFIED_SENDER_TRUST_ROOT), 0); - } catch (InvalidKeyException | IOException e) { + return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); + } catch (InvalidKeyException e) { throw new AssertionError(e); } } diff --git a/src/main/java/org/asamk/signal/manager/helper/PinHelper.java b/src/main/java/org/asamk/signal/manager/helper/PinHelper.java new file mode 100644 index 00000000..47ee6b40 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/helper/PinHelper.java @@ -0,0 +1,90 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.util.PinHashing; +import org.whispersystems.signalservice.api.KbsPinData; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.KeyBackupServicePinException; +import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; +import org.whispersystems.signalservice.api.kbs.HashedPin; +import org.whispersystems.signalservice.api.kbs.MasterKey; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; +import org.whispersystems.signalservice.internal.push.LockedException; + +import java.io.IOException; + +public class PinHelper { + + private final KeyBackupService keyBackupService; + + public PinHelper(final KeyBackupService keyBackupService) { + this.keyBackupService = keyBackupService; + } + + public void setRegistrationLockPin( + String pin, MasterKey masterKey + ) throws IOException, UnauthenticatedResponseException { + final KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); + final HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession); + + pinChangeSession.setPin(hashedPin, masterKey); + pinChangeSession.enableRegistrationLock(masterKey); + } + + public void removeRegistrationLockPin() throws IOException, UnauthenticatedResponseException { + final KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); + pinChangeSession.removePin(); + } + + public KbsPinData getRegistrationLockData( + String pin, + LockedException e + ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { + String basicStorageCredentials = e.getBasicStorageCredentials(); + if (basicStorageCredentials == null) { + return null; + } + + return getRegistrationLockData(pin, basicStorageCredentials); + } + + private KbsPinData getRegistrationLockData( + String pin, + String basicStorageCredentials + ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { + TokenResponse tokenResponse = keyBackupService.getToken(basicStorageCredentials); + if (tokenResponse == null || tokenResponse.getTries() == 0) { + throw new IOException("KBS Account locked"); + } + + KbsPinData registrationLockData = restoreMasterKey(pin, basicStorageCredentials, tokenResponse); + if (registrationLockData == null) { + throw new AssertionError("Failed to restore master key"); + } + return registrationLockData; + } + + private KbsPinData restoreMasterKey( + String pin, String basicStorageCredentials, TokenResponse tokenResponse + ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { + if (pin == null) return null; + + if (basicStorageCredentials == null) { + throw new AssertionError("Cannot restore KBS key, no storage credentials supplied"); + } + + KeyBackupService.RestoreSession session = keyBackupService.newRegistrationSession(basicStorageCredentials, + tokenResponse); + + try { + HashedPin hashedPin = PinHashing.hashPin(pin, session); + KbsPinData kbsData = session.restorePin(hashedPin); + if (kbsData == null) { + throw new AssertionError("Null not expected"); + } + return kbsData; + } catch (UnauthenticatedResponseException e) { + throw new IOException(e); + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index c787471f..1c35b1fb 100644 --- a/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -36,6 +36,7 @@ import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Medium; import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.util.Base64; @@ -66,6 +67,7 @@ public class SignalAccount implements Closeable { private boolean isMultiDevice = false; private String password; private String registrationLockPin; + private MasterKey pinMasterKey; private String signalingKey; private ProfileKey profileKey; private int preKeyIdOffset; @@ -217,6 +219,10 @@ public class SignalAccount implements Closeable { password = Utils.getNotNullNode(rootNode, "password").asText(); JsonNode pinNode = rootNode.get("registrationLockPin"); registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText(); + JsonNode pinMasterKeyNode = rootNode.get("pinMasterKey"); + pinMasterKey = pinMasterKeyNode == null || pinMasterKeyNode.isNull() + ? null + : new MasterKey(Base64.decode(pinMasterKeyNode.asText())); if (rootNode.has("signalingKey")) { signalingKey = Utils.getNotNullNode(rootNode, "signalingKey").asText(); } @@ -345,6 +351,7 @@ public class SignalAccount implements Closeable { .put("isMultiDevice", isMultiDevice) .put("password", password) .put("registrationLockPin", registrationLockPin) + .put("pinMasterKey", pinMasterKey == null ? null : Base64.encodeBytes(pinMasterKey.serialize())) .put("signalingKey", signalingKey) .put("preKeyIdOffset", preKeyIdOffset) .put("nextSignedPreKeyId", nextSignedPreKeyId) @@ -456,14 +463,18 @@ public class SignalAccount implements Closeable { return registrationLockPin; } - public String getRegistrationLock() { - return null; // TODO implement KBS - } - public void setRegistrationLockPin(final String registrationLockPin) { this.registrationLockPin = registrationLockPin; } + public MasterKey getPinMasterKey() { + return pinMasterKey; + } + + public void setPinMasterKey(final MasterKey pinMasterKey) { + this.pinMasterKey = pinMasterKey; + } + public String getSignalingKey() { return signalingKey; } diff --git a/src/main/java/org/asamk/signal/manager/util/KeyUtils.java b/src/main/java/org/asamk/signal/manager/util/KeyUtils.java index 2b4bc371..3f9ec08f 100644 --- a/src/main/java/org/asamk/signal/manager/util/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/util/KeyUtils.java @@ -3,6 +3,7 @@ package org.asamk.signal.manager.util; import org.asamk.signal.util.RandomUtils; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.util.Base64; public class KeyUtils { @@ -30,6 +31,10 @@ public class KeyUtils { return getSecretBytes(32); } + public static MasterKey createMasterKey() { + return MasterKey.createNew(RandomUtils.getSecureRandom()); + } + private static String getSecret(int size) { byte[] secret = getSecretBytes(size); return Base64.encodeBytes(secret); diff --git a/src/main/java/org/asamk/signal/manager/util/PinHashing.java b/src/main/java/org/asamk/signal/manager/util/PinHashing.java new file mode 100644 index 00000000..2adf8148 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/util/PinHashing.java @@ -0,0 +1,31 @@ +package org.asamk.signal.manager.util; + +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.bouncycastle.crypto.params.Argon2Parameters; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.kbs.HashedPin; +import org.whispersystems.signalservice.internal.registrationpin.PinHasher; + +public final class PinHashing { + + private PinHashing() { + } + + public static HashedPin hashPin(String pin, KeyBackupService.HashSession hashSession) { + final Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id).withParallelism(1) + .withIterations(32) + .withVersion(13) + .withMemoryAsKB(16 * 1024) + .withSalt(hashSession.hashSalt()) + .build(); + + final Argon2BytesGenerator generator = new Argon2BytesGenerator(); + generator.init(params); + + return PinHasher.hashPin(PinHasher.normalize(pin), password -> { + byte[] output = new byte[64]; + generator.generateBytes(password, output); + return output; + }); + } +} From f1e5fc6c0ba85ac95055b760d31d8fe0a1b27ab6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 31 Dec 2020 16:14:30 +0100 Subject: [PATCH 20/32] Request profiles without uuid if libzkgroup is not available Fixes #397 --- .../signal/manager/helper/ProfileHelper.java | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index c81e2ff7..60c47d8b 100644 --- a/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -95,7 +95,17 @@ public final class ProfileHelper { ? unidentifiedPipe : messagePipeProvider.getMessagePipe(false); if (pipe != null) { - return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType); + try { + return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType); + } catch (NoClassDefFoundError e) { + // Native zkgroup lib not available for ProfileKey + if (!address.getNumber().isPresent()) { + throw new NotFoundException("Can't request profile without number"); + } + SignalServiceAddress addressWithoutUuid = new SignalServiceAddress(Optional.absent(), + address.getNumber()); + return pipe.getProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType); + } } throw new IOException("No pipe available!"); @@ -106,9 +116,18 @@ public final class ProfileHelper { Optional profileKey, Optional unidentifiedAccess, SignalServiceProfile.RequestType requestType - ) { + ) throws NotFoundException { SignalServiceMessageReceiver receiver = messageReceiverProvider.getMessageReceiver(); - return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType); + try { + return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType); + } catch (NoClassDefFoundError e) { + // Native zkgroup lib not available for ProfileKey + if (!address.getNumber().isPresent()) { + throw new NotFoundException("Can't request profile without number"); + } + SignalServiceAddress addressWithoutUuid = new SignalServiceAddress(Optional.absent(), address.getNumber()); + return receiver.retrieveProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType); + } } private Optional getUnidentifiedAccess(SignalServiceAddress recipient) { From a475bc50e92ef5b7f5b8d765d942379e877432e9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 31 Dec 2020 18:07:06 +0100 Subject: [PATCH 21/32] Bump version --- CHANGELOG.md | 9 +++++++++ build.gradle | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38907d6f..e6eb3c39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## [Unreleased] +## [0.7.2] - 2020-12-31 +### Added +- Implement new registration lock PIN with `setPin` and `removePin` (with KBS) +- Include quotes, mentions and reactions in json output (Thanks @Atomic-Bean) + +### Fixed +- Retrieve avatars for v2 groups +- Download attachment thumbnail for quoted attachments + ## [0.7.1] - 2020-12-21 ### Added - Accept group invitation with `updateGroup -g GROUP_ID` diff --git a/build.gradle b/build.gradle index 1c42834b..ca346dbd 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_11 mainClassName = 'org.asamk.signal.Main' -version = '0.7.1' +version = '0.7.2' compileJava.options.encoding = 'UTF-8' From 1c5de83370e1108271bf72836c887fdea9cb46db Mon Sep 17 00:00:00 2001 From: Atomic-Bean <75401809+Atomic-Bean@users.noreply.github.com> Date: Mon, 4 Jan 2021 06:32:34 +1030 Subject: [PATCH 22/32] Command to check if number is registered (#391) * Added the isRegistered command * Minor fixes * Corrected description * Added AsamK's suggestions Fixes #178 --- man/signal-cli.1.adoc | 9 ++ .../org/asamk/signal/commands/Commands.java | 1 + .../signal/commands/GetUserStatusCommand.java | 87 +++++++++++++++++++ .../org/asamk/signal/manager/Manager.java | 22 +++++ 4 files changed, 119 insertions(+) create mode 100644 src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index b5c22167..fa2db7c3 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -124,6 +124,15 @@ Only works, if this is the master device. Specify the device you want to remove. Use listDevices to see the deviceIds. +=== getUserStatus + +Uses a list of phone numbers to determine the statuses of those users. Shows if they are registered on the Signal Servers or not. + +[NUMBER [NUMBER ...]]:: +One or more numbers to check. +*--json*:: +Output the statuses as an array of json objects. + === send Send a message to another user or group. diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 85e7af32..1e081dff 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -11,6 +11,7 @@ public class Commands { addCommand("addDevice", new AddDeviceCommand()); addCommand("block", new BlockCommand()); addCommand("daemon", new DaemonCommand()); + addCommand("getUserStatus", new GetUserStatusCommand()); addCommand("link", new LinkCommand()); addCommand("listContacts", new ListContactsCommand()); addCommand("listDevices", new ListDevicesCommand()); diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java new file mode 100644 index 00000000..c4bdf3d9 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -0,0 +1,87 @@ +package org.asamk.signal.commands; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.manager.Manager; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.Map; +import java.util.List; + +public class GetUserStatusCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("number").help("Phone number").nargs("+"); + subparser.help("Check if the specified phone number/s have been registered"); + subparser.addArgument("--json") + .help("Output received messages in json format, one json object per line.") + .action(Arguments.storeTrue()); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + // Setup the json object mapper + ObjectMapper jsonProcessor = new ObjectMapper(); + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect + jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + + // Get a map of registration statuses + Map registered; + try { + registered = m.areUsersRegistered(new HashSet<>(ns.getList("number"))); + } catch (IOException e) { + System.err.println("Unable to check if users are registered"); + return 1; + } + + // Output + if (ns.getBoolean("json")) { + List objects = new ArrayList<>(); + for (Map.Entry entry : registered.entrySet()) { + objects.add(new JsonIsRegistered(entry.getKey(), entry.getValue())); + } + + try { + System.out.println(jsonProcessor.writeValueAsString(objects)); + } catch (IOException e) { + System.err.println(e.getMessage()); + } + + } else { + for (Map.Entry entry : registered.entrySet()) { + System.out.println(entry.getKey() + ": " + entry.getValue()); + } + } + + return 0; + } + + private class JsonIsRegistered { + String name; + boolean isRegistered; + + public JsonIsRegistered(String name, boolean isRegistered) { + this.name = name; + this.isRegistered = isRegistered; + } + } + +} diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 7ad7b888..e964d218 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -383,6 +383,28 @@ public class Manager implements Closeable { return account.isRegistered(); } + /** + * This is used for checking a set of phone numbers for registration on Signal + * + * @param numbers The set of phone number in question + * @return A map of numbers to booleans. True if registered, false otherwise. Should never be null + * @throws IOException if its unable to check if the users are registered + */ + public Map areUsersRegistered(Set numbers) throws IOException { + // Note "contactDetails" has no optionals. It only gives us info on users who are registered + List contactDetails = this.accountManager.getContacts(numbers); + + // Make the initial map with all numbers set to false for now + Map usersRegistered = numbers.stream().collect(Collectors.toMap(x -> x, x -> false)); + + // Override the contacts we did obtain + for (ContactTokenDetails contactDetail : contactDetails) { + usersRegistered.put(contactDetail.getNumber(), true); + } + + return usersRegistered; + } + public void register(boolean voiceVerification, String captcha) throws IOException { account.setPassword(KeyUtils.createPassword()); From 00339b1abede407251c4b4f63c2cb7fefcf9b5e2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Jan 2021 21:04:32 +0100 Subject: [PATCH 23/32] Improve user status command --- .../signal/JsonReceiveMessageHandler.java | 4 +-- .../signal/commands/GetUserStatusCommand.java | 33 ++++++++----------- .../asamk/signal/commands/ReceiveCommand.java | 2 +- .../org/asamk/signal/manager/Manager.java | 15 +++------ 4 files changed, 21 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java index 363fc304..eb135e13 100644 --- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -3,7 +3,6 @@ package org.asamk.signal; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -23,8 +22,7 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler public JsonReceiveMessageHandler(Manager m) { this.m = m; this.jsonProcessor = new ObjectMapper(); - jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect - jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java index c4bdf3d9..882f4a5c 100644 --- a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -1,9 +1,6 @@ package org.asamk.signal.commands; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import net.sourceforge.argparse4j.impl.Arguments; @@ -13,11 +10,10 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.manager.Manager; import java.io.IOException; -import java.util.ArrayList; import java.util.HashSet; -import java.util.Set; -import java.util.Map; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; public class GetUserStatusCommand implements LocalCommand { @@ -39,14 +35,12 @@ public class GetUserStatusCommand implements LocalCommand { // Setup the json object mapper ObjectMapper jsonProcessor = new ObjectMapper(); - jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect - jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); // Get a map of registration statuses Map registered; try { - registered = m.areUsersRegistered(new HashSet<>(ns.getList("number"))); + registered = m.areUsersRegistered(new HashSet<>(ns.getList("number"))); } catch (IOException e) { System.err.println("Unable to check if users are registered"); return 1; @@ -54,17 +48,17 @@ public class GetUserStatusCommand implements LocalCommand { // Output if (ns.getBoolean("json")) { - List objects = new ArrayList<>(); - for (Map.Entry entry : registered.entrySet()) { - objects.add(new JsonIsRegistered(entry.getKey(), entry.getValue())); - } + List objects = registered.entrySet() + .stream() + .map(entry -> new JsonIsRegistered(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); try { - System.out.println(jsonProcessor.writeValueAsString(objects)); + jsonProcessor.writeValue(System.out, objects); + System.out.println(); } catch (IOException e) { System.err.println(e.getMessage()); } - } else { for (Map.Entry entry : registered.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); @@ -74,14 +68,15 @@ public class GetUserStatusCommand implements LocalCommand { return 0; } - private class JsonIsRegistered { - String name; - boolean isRegistered; + private static final class JsonIsRegistered { + + public String name; + + public boolean isRegistered; public JsonIsRegistered(String name, boolean isRegistered) { this.name = name; this.isRegistered = isRegistered; } } - } diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index bc68565a..84a918fb 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -44,7 +44,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { final ObjectMapper jsonProcessor; if (ns.getBoolean("json")) { jsonProcessor = new ObjectMapper(); - jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } else { jsonProcessor = null; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index e964d218..9a6afe70 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -394,15 +394,11 @@ public class Manager implements Closeable { // Note "contactDetails" has no optionals. It only gives us info on users who are registered List contactDetails = this.accountManager.getContacts(numbers); - // Make the initial map with all numbers set to false for now - Map usersRegistered = numbers.stream().collect(Collectors.toMap(x -> x, x -> false)); + Set registeredUsers = contactDetails.stream() + .map(ContactTokenDetails::getNumber) + .collect(Collectors.toSet()); - // Override the contacts we did obtain - for (ContactTokenDetails contactDetail : contactDetails) { - usersRegistered.put(contactDetail.getNumber(), true); - } - - return usersRegistered; + return numbers.stream().collect(Collectors.toMap(x -> x, registeredUsers::contains)); } public void register(boolean voiceVerification, String captcha) throws IOException { @@ -524,8 +520,7 @@ public class Manager implements Closeable { } public void verifyAccount( - String verificationCode, - String pin + String verificationCode, String pin ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); From 88d81c7a634938ca58d003861f6272f5bf56ee9b Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 21 Dec 2020 10:25:36 +0100 Subject: [PATCH 24/32] Move IdentityKeyPair generation to KeyUtils in preparation for rust libsignal which doesn't provide the method --- src/main/java/org/asamk/signal/manager/Manager.java | 2 +- .../asamk/signal/manager/ProvisioningManager.java | 2 +- .../org/asamk/signal/manager/util/KeyUtils.java | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 9a6afe70..666c085e 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -317,7 +317,7 @@ public class Manager implements Closeable { PathConfig pathConfig = PathConfig.createDefault(settingsPath); if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { - IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair(); + IdentityKeyPair identityKey = KeyUtils.generateIdentityKeyPair(); int registrationId = KeyHelper.generateRegistrationId(false); ProfileKey profileKey = KeyUtils.createProfileKey(); diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 8b3f0eb4..4195e8af 100644 --- a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -52,7 +52,7 @@ public class ProvisioningManager { this.serviceConfiguration = serviceConfiguration; this.userAgent = userAgent; - identityKey = KeyHelper.generateIdentityKeyPair(); + identityKey = KeyUtils.generateIdentityKeyPair(); registrationId = KeyHelper.generateRegistrationId(false); password = KeyUtils.createPassword(); final SleepTimer timer = new UptimeSleepTimer(); diff --git a/src/main/java/org/asamk/signal/manager/util/KeyUtils.java b/src/main/java/org/asamk/signal/manager/util/KeyUtils.java index 3f9ec08f..d8861b1b 100644 --- a/src/main/java/org/asamk/signal/manager/util/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/util/KeyUtils.java @@ -3,6 +3,11 @@ package org.asamk.signal.manager.util; import org.asamk.signal.util.RandomUtils; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.ECKeyPair; +import org.whispersystems.libsignal.ecc.ECPrivateKey; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.util.Base64; @@ -11,6 +16,14 @@ public class KeyUtils { private KeyUtils() { } + public static IdentityKeyPair generateIdentityKeyPair() { + ECKeyPair djbKeyPair = Curve.generateKeyPair(); + IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey()); + ECPrivateKey djbPrivateKey = djbKeyPair.getPrivateKey(); + + return new IdentityKeyPair(djbIdentityKey, djbPrivateKey); + } + public static String createSignalingKey() { return getSecret(52); } From 0c7da68d985a3703a6843338d55ef1723f6447e5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 10 Jan 2021 15:26:45 +0100 Subject: [PATCH 25/32] Download group info if it's missing in the cache --- .../org/asamk/signal/manager/Manager.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 666c085e..576dbb5a 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -793,7 +793,7 @@ public class Manager implements Closeable { } private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { - GroupInfo g = account.getGroupStore().getGroup(groupId); + GroupInfo g = getGroup(groupId); if (g == null) { throw new GroupNotFoundException(groupId); } @@ -804,7 +804,7 @@ public class Manager implements Closeable { } private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { - GroupInfo g = account.getGroupStore().getGroup(groupId); + GroupInfo g = getGroup(groupId); if (g == null) { throw new GroupNotFoundException(groupId); } @@ -1236,7 +1236,7 @@ public class Manager implements Closeable { * Change the expiration timer for a group */ public void setExpirationTimer(GroupId groupId, int messageExpirationTimer) { - GroupInfo g = account.getGroupStore().getGroup(groupId); + GroupInfo g = getGroup(groupId); if (g instanceof GroupInfoV1) { GroupInfoV1 groupInfoV1 = (GroupInfoV1) g; groupInfoV1.messageExpirationTime = messageExpirationTimer; @@ -1649,7 +1649,7 @@ public class Manager implements Closeable { if (message.getGroupContext().get().getGroupV1().isPresent()) { SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); GroupIdV1 groupId = GroupId.v1(groupInfo.getGroupId()); - GroupInfo group = account.getGroupStore().getGroup(groupId); + GroupInfo group = getGroup(groupId); if (group == null || group instanceof GroupInfoV1) { GroupInfoV1 groupV1 = (GroupInfoV1) group; switch (groupInfo.getType()) { @@ -1820,7 +1820,7 @@ public class Manager implements Closeable { final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams); - GroupInfo groupInfo = account.getGroupStore().getGroup(groupId); + GroupInfo groupInfo = getGroup(groupId); final GroupInfoV2 groupInfoV2; if (groupInfo instanceof GroupInfoV1) { // Received a v2 group message for a v1 group, we need to locally migrate the group @@ -2078,7 +2078,7 @@ public class Manager implements Closeable { } } GroupId groupId = GroupUtils.getGroupId(message.getGroupContext().get()); - GroupInfo group = account.getGroupStore().getGroup(groupId); + GroupInfo group = getGroup(groupId); if (group != null && group.isBlocked()) { return true; } @@ -2461,7 +2461,7 @@ public class Manager implements Closeable { try { try (OutputStream fos = new FileOutputStream(groupsFile)) { DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos); - for (GroupInfo record : account.getGroupStore().getGroups()) { + for (GroupInfo record : getGroups()) { if (record instanceof GroupInfoV1) { GroupInfoV1 groupInfo = (GroupInfoV1) record; out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), @@ -2570,7 +2570,7 @@ public class Manager implements Closeable { } } List groupIds = new ArrayList<>(); - for (GroupInfo record : account.getGroupStore().getGroups()) { + for (GroupInfo record : getGroups()) { if (record.isBlocked()) { groupIds.add(record.getGroupId().serialize()); } @@ -2597,7 +2597,13 @@ public class Manager implements Closeable { } public GroupInfo getGroup(GroupId groupId) { - return account.getGroupStore().getGroup(groupId); + final GroupInfo group = account.getGroupStore().getGroup(groupId); + if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) { + final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); + ((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams)); + account.getGroupStore().updateGroup(group); + } + return group; } public List getIdentities() { From 9244d1e8a861ce505083e813cf069a96a6622822 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 10 Jan 2021 17:07:26 +0100 Subject: [PATCH 26/32] Disable registration lock before removing pin --- CHANGELOG.md | 3 +++ .../java/org/asamk/signal/manager/helper/PinHelper.java | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6eb3c39..df9aa47f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Fixed +- Disable registration lock before removing the PIN + ## [0.7.2] - 2020-12-31 ### Added - Implement new registration lock PIN with `setPin` and `removePin` (with KBS) diff --git a/src/main/java/org/asamk/signal/manager/helper/PinHelper.java b/src/main/java/org/asamk/signal/manager/helper/PinHelper.java index 47ee6b40..b4fa04c4 100644 --- a/src/main/java/org/asamk/signal/manager/helper/PinHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/PinHelper.java @@ -33,12 +33,12 @@ public class PinHelper { public void removeRegistrationLockPin() throws IOException, UnauthenticatedResponseException { final KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); + pinChangeSession.disableRegistrationLock(); pinChangeSession.removePin(); } public KbsPinData getRegistrationLockData( - String pin, - LockedException e + String pin, LockedException e ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { String basicStorageCredentials = e.getBasicStorageCredentials(); if (basicStorageCredentials == null) { @@ -49,8 +49,7 @@ public class PinHelper { } private KbsPinData getRegistrationLockData( - String pin, - String basicStorageCredentials + String pin, String basicStorageCredentials ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { TokenResponse tokenResponse = keyBackupService.getToken(basicStorageCredentials); if (tokenResponse == null || tokenResponse.getTries() == 0) { From 51db5495c0a2a1b0c67637f5c77846abb9158c1e Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 10 Jan 2021 18:14:33 +0100 Subject: [PATCH 27/32] Fix pin hash version to match android --- CHANGELOG.md | 2 ++ src/main/java/org/asamk/signal/manager/util/PinHashing.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df9aa47f..1af850b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Fixed - Disable registration lock before removing the PIN +- Fix PIN hash version to match the official clients. + If you had previously set a PIN you need to set it again to be able to unlock the registration lock later. ## [0.7.2] - 2020-12-31 ### Added diff --git a/src/main/java/org/asamk/signal/manager/util/PinHashing.java b/src/main/java/org/asamk/signal/manager/util/PinHashing.java index 2adf8148..2fd2d802 100644 --- a/src/main/java/org/asamk/signal/manager/util/PinHashing.java +++ b/src/main/java/org/asamk/signal/manager/util/PinHashing.java @@ -14,7 +14,7 @@ public final class PinHashing { public static HashedPin hashPin(String pin, KeyBackupService.HashSession hashSession) { final Argon2Parameters params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id).withParallelism(1) .withIterations(32) - .withVersion(13) + .withVersion(Argon2Parameters.ARGON2_VERSION_13) .withMemoryAsKB(16 * 1024) .withSalt(hashSession.hashSalt()) .build(); From 263fdceb94d8de0e355254446b26f9aff3e4c40a Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 10 Jan 2021 22:41:39 +0100 Subject: [PATCH 28/32] Fix expectedV2Id serialization --- .../org/asamk/signal/manager/storage/groups/GroupInfoV1.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java index 39591647..d1230b27 100644 --- a/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java +++ b/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java @@ -101,7 +101,7 @@ public class GroupInfoV1 extends GroupInfo { @JsonProperty("expectedV2Id") private byte[] getExpectedV2IdJackson() { - return expectedV2Id.serialize(); + return getExpectedV2Id().serialize(); } @Override From 6c8a1ff3d30b7973b9459ffa4da4c7345d14defd Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 10 Jan 2021 22:31:18 +0100 Subject: [PATCH 29/32] Check for null query when decoding device link uri --- src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java b/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java index 5b9fbe28..779642b6 100644 --- a/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java +++ b/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java @@ -21,7 +21,12 @@ public class DeviceLinkInfo { final ECPublicKey deviceKey; public static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws IOException, InvalidKeyException { - Map query = getQueryMap(linkUri.getRawQuery()); + final String rawQuery = linkUri.getRawQuery(); + if (isEmpty(rawQuery)) { + throw new RuntimeException("Invalid device link uri"); + } + + Map query = getQueryMap(rawQuery); String deviceIdentifier = query.get("uuid"); String publicKeyEncoded = query.get("pub_key"); From e74be0c345321888c1fbfa05616cb90cf3f07ffb Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 10 Jan 2021 17:07:06 +0100 Subject: [PATCH 30/32] Refactor register and verify --- src/main/java/org/asamk/signal/Main.java | 126 +++++---- .../signal/commands/AddDeviceCommand.java | 4 - .../asamk/signal/commands/BlockCommand.java | 5 - .../asamk/signal/commands/DaemonCommand.java | 4 - .../signal/commands/GetUserStatusCommand.java | 5 - .../signal/commands/JoinGroupCommand.java | 5 - .../signal/commands/ListContactsCommand.java | 4 - .../signal/commands/ListDevicesCommand.java | 4 - .../signal/commands/ListGroupsCommand.java | 5 - .../commands/ListIdentitiesCommand.java | 4 - .../signal/commands/QuitGroupCommand.java | 5 - .../asamk/signal/commands/ReceiveCommand.java | 4 - .../signal/commands/RegisterCommand.java | 6 +- .../signal/commands/RegistrationCommand.java | 10 + .../signal/commands/RemoveDeviceCommand.java | 4 - .../signal/commands/RemovePinCommand.java | 4 - .../signal/commands/SendContactsCommand.java | 4 - .../signal/commands/SendReactionCommand.java | 5 - .../asamk/signal/commands/SetPinCommand.java | 4 - .../asamk/signal/commands/TrustCommand.java | 4 - .../asamk/signal/commands/UnblockCommand.java | 5 - .../signal/commands/UnregisterCommand.java | 4 - .../signal/commands/UpdateAccountCommand.java | 4 - .../signal/commands/UpdateContactCommand.java | 5 - .../signal/commands/UpdateProfileCommand.java | 5 - .../asamk/signal/commands/VerifyCommand.java | 10 +- .../org/asamk/signal/manager/Manager.java | 250 ++++-------------- .../manager/NotRegisteredException.java | 8 + .../signal/manager/ProvisioningManager.java | 4 +- .../signal/manager/RegistrationManager.java | 194 ++++++++++++++ .../asamk/signal/manager/ServiceConfig.java | 12 + .../helper/UnidentifiedAccessHelper.java | 2 +- .../signal/manager/storage/SignalAccount.java | 48 ++++ 33 files changed, 405 insertions(+), 362 deletions(-) create mode 100644 src/main/java/org/asamk/signal/commands/RegistrationCommand.java create mode 100644 src/main/java/org/asamk/signal/manager/NotRegisteredException.java create mode 100644 src/main/java/org/asamk/signal/manager/RegistrationManager.java diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 6204778d..b9209631 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -32,9 +32,12 @@ import org.asamk.signal.commands.DbusCommand; import org.asamk.signal.commands.ExtendedDbusCommand; import org.asamk.signal.commands.LocalCommand; import org.asamk.signal.commands.ProvisioningCommand; +import org.asamk.signal.commands.RegistrationCommand; import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotRegisteredException; import org.asamk.signal.manager.ProvisioningManager; +import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.ServiceConfig; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.SecurityProvider; @@ -43,7 +46,6 @@ import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; @@ -75,8 +77,14 @@ public class Main { } public static int init(Namespace ns) { + Command command = getCommand(ns); + if (command == null) { + logger.error("Command not implemented!"); + return 3; + } + if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { - return initDbusClient(ns, ns.getBoolean("dbus_system")); + return initDbusClient(command, ns, ns.getBoolean("dbus_system")); } final String username = ns.getString("username"); @@ -99,12 +107,31 @@ public class Main { if (username == null) { ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT); - return handleCommands(ns, pm); + return handleCommand(command, ns, pm); + } + + if (command instanceof RegistrationCommand) { + final RegistrationManager manager; + try { + manager = RegistrationManager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT); + } catch (Throwable e) { + logger.error("Error loading or creating state file: {}", e.getMessage()); + return 2; + } + try (RegistrationManager m = manager) { + return handleCommand(command, ns, m); + } catch (Exception e) { + logger.error("Cleanup failed", e); + return 3; + } } Manager manager; try { manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT); + } catch (NotRegisteredException e) { + System.err.println("User is not registered."); + return 1; } catch (Throwable e) { logger.error("Error loading state file: {}", e.getMessage()); return 2; @@ -113,25 +140,19 @@ public class Main { try (Manager m = manager) { try { m.checkAccountState(); - } catch (AuthorizationFailedException e) { - if (!"register".equals(ns.getString("command"))) { - // Register command should still be possible, if current authorization fails - System.err.println("Authorization failed, was the number registered elsewhere?"); - return 2; - } } catch (IOException e) { logger.error("Error while checking account: {}", e.getMessage()); return 2; } - return handleCommands(ns, m); + return handleCommand(command, ns, m); } catch (IOException e) { logger.error("Cleanup failed", e); return 3; } } - private static int initDbusClient(final Namespace ns, final boolean systemBus) { + private static int initDbusClient(final Command command, final Namespace ns, final boolean systemBus) { try { DBusConnection.DBusBusType busType; if (systemBus) { @@ -144,7 +165,7 @@ public class Main { DbusConfig.SIGNAL_OBJECTPATH, Signal.class); - return handleCommands(ns, ts, dBusConn); + return handleCommand(command, ns, ts, dBusConn); } } catch (DBusException | IOException e) { logger.error("Dbus client failed", e); @@ -152,56 +173,51 @@ public class Main { } } - private static int handleCommands(Namespace ns, Signal ts, DBusConnection dBusConn) { + private static Command getCommand(Namespace ns) { String commandKey = ns.getString("command"); final Map commands = Commands.getCommands(); - if (commands.containsKey(commandKey)) { - Command command = commands.get(commandKey); - - if (command instanceof ExtendedDbusCommand) { - return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn); - } else if (command instanceof DbusCommand) { - return ((DbusCommand) command).handleCommand(ns, ts); - } else { - System.err.println(commandKey + " is not yet implemented via dbus"); - return 1; - } + if (!commands.containsKey(commandKey)) { + return null; } - return 0; + return commands.get(commandKey); } - private static int handleCommands(Namespace ns, ProvisioningManager pm) { - String commandKey = ns.getString("command"); - final Map commands = Commands.getCommands(); - if (commands.containsKey(commandKey)) { - Command command = commands.get(commandKey); - - if (command instanceof ProvisioningCommand) { - return ((ProvisioningCommand) command).handleCommand(ns, pm); - } else { - System.err.println(commandKey + " only works with a username"); - return 1; - } - } - return 0; - } - - private static int handleCommands(Namespace ns, Manager m) { - String commandKey = ns.getString("command"); - final Map commands = Commands.getCommands(); - if (commands.containsKey(commandKey)) { - Command command = commands.get(commandKey); - - if (command instanceof LocalCommand) { - return ((LocalCommand) command).handleCommand(ns, m); - } else if (command instanceof DbusCommand) { - return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m)); - } else if (command instanceof ExtendedDbusCommand) { - System.err.println(commandKey + " only works via dbus"); - } + private static int handleCommand(Command command, Namespace ns, Signal ts, DBusConnection dBusConn) { + if (command instanceof ExtendedDbusCommand) { + return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn); + } else if (command instanceof DbusCommand) { + return ((DbusCommand) command).handleCommand(ns, ts); + } else { + System.err.println("Command is not yet implemented via dbus"); + return 1; + } + } + + private static int handleCommand(Command command, Namespace ns, ProvisioningManager pm) { + if (command instanceof ProvisioningCommand) { + return ((ProvisioningCommand) command).handleCommand(ns, pm); + } else { + System.err.println("Command only works with a username"); + return 1; + } + } + + private static int handleCommand(Command command, Namespace ns, RegistrationManager m) { + if (command instanceof RegistrationCommand) { + return ((RegistrationCommand) command).handleCommand(ns, m); + } + return 1; + } + + private static int handleCommand(Command command, Namespace ns, Manager m) { + if (command instanceof LocalCommand) { + return ((LocalCommand) command).handleCommand(ns, m); + } else if (command instanceof DbusCommand) { + return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m)); + } else { + System.err.println("Command only works via dbus"); return 1; } - return 0; } /** diff --git a/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java b/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java index dab886d7..c5d18ab1 100644 --- a/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java @@ -23,10 +23,6 @@ public class AddDeviceCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { m.addDeviceLink(new URI(ns.getString("uri"))); return 0; diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 627be5a9..60009cfb 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -21,11 +21,6 @@ public class BlockCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - for (String contact_number : ns.getList("contact")) { try { m.setContactBlocked(contact_number, true); diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 3caaaa37..c5ee2edc 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -35,10 +35,6 @@ public class DaemonCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } DBusConnection conn = null; try { try { diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java index 882f4a5c..0a1ddc4c 100644 --- a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -28,11 +28,6 @@ public class GetUserStatusCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - // Setup the json object mapper ObjectMapper jsonProcessor = new ObjectMapper(); jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index 305bf55b..c5975b0c 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -29,11 +29,6 @@ public class JoinGroupCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - final GroupInviteLinkUrl linkUrl; String uri = ns.getString("uri"); try { diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index 2c98ec6b..1a14e8df 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -16,10 +16,6 @@ public class ListContactsCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } List contacts = m.getContacts(); for (ContactInfo c : contacts) { System.out.println(String.format("Number: %s Name: %s Blocked: %b", c.number, c.name, c.blocked)); diff --git a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java index 4b9dac5c..a03b078f 100644 --- a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java @@ -18,10 +18,6 @@ public class ListDevicesCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { List devices = m.getLinkedDevices(); for (DeviceInfo d : devices) { diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index b4be4ad0..97af502e 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -64,11 +64,6 @@ public class ListGroupsCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - List groups = m.getGroups(); boolean detailed = ns.getBoolean("detailed"); diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index 3f422cbd..4caeca29 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -30,10 +30,6 @@ public class ListIdentitiesCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } if (ns.get("number") == null) { for (IdentityInfo identity : m.getIdentities()) { printIdentityFingerprint(m, identity); diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index c1f6bfbf..f258ae24 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -31,11 +31,6 @@ public class QuitGroupCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - try { final GroupId groupId = Util.decodeGroupId(ns.getString("group")); final Pair> results = m.sendQuitGroupMessage(groupId); diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 84a918fb..7dc9dcaf 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -146,10 +146,6 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } double timeout = 5; if (ns.getDouble("timeout") != null) { timeout = ns.getDouble("timeout"); diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java index f69e0844..da652f7d 100644 --- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java @@ -4,12 +4,12 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.RegistrationManager; import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; import java.io.IOException; -public class RegisterCommand implements LocalCommand { +public class RegisterCommand implements RegistrationCommand { @Override public void attachToSubparser(final Subparser subparser) { @@ -21,7 +21,7 @@ public class RegisterCommand implements LocalCommand { } @Override - public int handleCommand(final Namespace ns, final Manager m) { + public int handleCommand(final Namespace ns, final RegistrationManager m) { try { final boolean voiceVerification = ns.getBoolean("voice"); final String captcha = ns.getString("captcha"); diff --git a/src/main/java/org/asamk/signal/commands/RegistrationCommand.java b/src/main/java/org/asamk/signal/commands/RegistrationCommand.java new file mode 100644 index 00000000..8683570f --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/RegistrationCommand.java @@ -0,0 +1,10 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; + +import org.asamk.signal.manager.RegistrationManager; + +public interface RegistrationCommand extends Command { + + int handleCommand(Namespace ns, RegistrationManager m); +} diff --git a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java index 1e2343e7..78d14bbd 100644 --- a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java @@ -19,10 +19,6 @@ public class RemoveDeviceCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { int deviceId = ns.getInt("deviceId"); m.removeLinkedDevices(deviceId); diff --git a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java index 95531249..ada9c446 100644 --- a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java @@ -17,10 +17,6 @@ public class RemovePinCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { m.setRegistrationLockPin(Optional.absent()); return 0; diff --git a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java index 20e81a60..aaca283a 100644 --- a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java @@ -17,10 +17,6 @@ public class SendContactsCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { m.sendContacts(); return 0; diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index c680bfd7..2a9afa74 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -47,11 +47,6 @@ public class SendReactionCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && ns.getString("group") == null) { System.err.println("No recipients given"); System.err.println("Aborting sending."); diff --git a/src/main/java/org/asamk/signal/commands/SetPinCommand.java b/src/main/java/org/asamk/signal/commands/SetPinCommand.java index c68ea3a9..ac601b3b 100644 --- a/src/main/java/org/asamk/signal/commands/SetPinCommand.java +++ b/src/main/java/org/asamk/signal/commands/SetPinCommand.java @@ -19,10 +19,6 @@ public class SetPinCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { String registrationLockPin = ns.getString("registrationLockPin"); m.setRegistrationLockPin(Optional.of(registrationLockPin)); diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 076a86db..58c7371f 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -27,10 +27,6 @@ public class TrustCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } String number = ns.getString("number"); if (ns.getBoolean("trust_all_known_keys")) { boolean res = m.trustIdentityAllKeys(number); diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index 14ea2996..d191ef22 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -21,11 +21,6 @@ public class UnblockCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - for (String contact_number : ns.getList("contact")) { try { m.setContactBlocked(contact_number, false); diff --git a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java index 7a7616bd..079070e6 100644 --- a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java @@ -16,10 +16,6 @@ public class UnregisterCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { m.unregister(); return 0; diff --git a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java index 79459fe6..8211e190 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java @@ -16,10 +16,6 @@ public class UpdateAccountCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } try { m.updateAccountAttributes(); return 0; diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index da090209..c4da94a2 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -23,11 +23,6 @@ public class UpdateContactCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - String number = ns.getString("number"); String name = ns.getString("name"); diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java index 1e332fb4..968a8733 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -25,11 +25,6 @@ public class UpdateProfileCommand implements LocalCommand { @Override public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - String name = ns.getString("name"); String avatarPath = ns.getString("avatar"); boolean removeAvatar = ns.getBoolean("remove_avatar"); diff --git a/src/main/java/org/asamk/signal/commands/VerifyCommand.java b/src/main/java/org/asamk/signal/commands/VerifyCommand.java index d4b0a7cb..7fa10b6a 100644 --- a/src/main/java/org/asamk/signal/commands/VerifyCommand.java +++ b/src/main/java/org/asamk/signal/commands/VerifyCommand.java @@ -3,14 +3,14 @@ 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.asamk.signal.manager.RegistrationManager; import org.whispersystems.signalservice.api.KeyBackupServicePinException; import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; import org.whispersystems.signalservice.internal.push.LockedException; import java.io.IOException; -public class VerifyCommand implements LocalCommand { +public class VerifyCommand implements RegistrationCommand { @Override public void attachToSubparser(final Subparser subparser) { @@ -19,11 +19,7 @@ public class VerifyCommand implements LocalCommand { } @Override - public int handleCommand(final Namespace ns, final Manager m) { - if (m.isRegistered()) { - System.err.println("User registration is already verified"); - return 1; - } + public int handleCommand(final Namespace ns, final RegistrationManager m) { try { String verificationCode = ns.getString("verificationCode"); String pin = ns.getString("pin"); diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 576dbb5a..9d090a58 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -79,14 +79,10 @@ import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.util.KeyHelper; import org.whispersystems.libsignal.util.Medium; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.KbsPinData; import org.whispersystems.signalservice.api.KeyBackupService; -import org.whispersystems.signalservice.api.KeyBackupServicePinException; -import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; @@ -144,10 +140,8 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf import org.whispersystems.signalservice.internal.contacts.crypto.Quote; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; -import org.whispersystems.signalservice.internal.push.LockedException; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; -import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.util.Base64; @@ -167,7 +161,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.security.KeyStore; import java.security.SignatureException; import java.util.ArrayList; import java.util.Arrays; @@ -176,7 +169,6 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -202,24 +194,21 @@ public class Manager implements Closeable { private final SignalServiceConfiguration serviceConfiguration; private final String userAgent; - // TODO make configurable - private final boolean discoverableByPhoneNumber = true; - private final boolean unrestrictedUnidentifiedAccess = false; - - private final SignalAccount account; + private SignalAccount account; private final PathConfig pathConfig; - private SignalServiceAccountManager accountManager; - private GroupsV2Api groupsV2Api; + private final SignalServiceAccountManager accountManager; + private final GroupsV2Api groupsV2Api; private final GroupsV2Operations groupsV2Operations; + private final SignalServiceMessageReceiver messageReceiver; + private final ClientZkProfileOperations clientZkProfileOperations; - private SignalServiceMessageReceiver messageReceiver = null; private SignalServiceMessagePipe messagePipe = null; private SignalServiceMessagePipe unidentifiedMessagePipe = null; private final UnidentifiedAccessHelper unidentifiedAccessHelper; private final ProfileHelper profileHelper; private final GroupHelper groupHelper; - private PinHelper pinHelper; + private final PinHelper pinHelper; Manager( SignalAccount account, @@ -233,7 +222,30 @@ public class Manager implements Closeable { this.userAgent = userAgent; this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create( serviceConfiguration)) : null; - createSignalServiceAccountManager(); + this.accountManager = new SignalServiceAccountManager(serviceConfiguration, + new DynamicCredentialsProvider(account.getUuid(), + account.getUsername(), + account.getPassword(), + account.getSignalingKey(), + account.getDeviceId()), + userAgent, + groupsV2Operations, + timer); + this.groupsV2Api = accountManager.getGroupsV2Api(); + final KeyBackupService keyBackupService = ServiceConfig.createKeyBackupService(accountManager); + this.pinHelper = new PinHelper(keyBackupService); + this.clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create(serviceConfiguration) + .getProfileOperations() : null; + this.messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, + account.getUuid(), + account.getUsername(), + account.getPassword(), + account.getDeviceId(), + account.getSignalingKey(), + userAgent, + null, + timer, + clientZkProfileOperations); this.account.setResolver(this::resolveSignalServiceAddress); @@ -244,7 +256,7 @@ public class Manager implements Closeable { this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey, unidentifiedAccessHelper::getAccessFor, unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(), - this::getOrCreateMessageReceiver); + () -> messageReceiver); this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential, this::getRecipientProfile, account::getSelfAddress, @@ -261,30 +273,6 @@ public class Manager implements Closeable { return account.getSelfAddress(); } - private void createSignalServiceAccountManager() { - this.accountManager = new SignalServiceAccountManager(serviceConfiguration, - new DynamicCredentialsProvider(account.getUuid(), - account.getUsername(), - account.getPassword(), - null, - account.getDeviceId()), - userAgent, - groupsV2Operations, - timer); - this.groupsV2Api = accountManager.getGroupsV2Api(); - this.pinHelper = new PinHelper(createKeyBackupService()); - } - - private KeyBackupService createKeyBackupService() { - KeyStore keyStore = ServiceConfig.getIasKeyStore(); - - return accountManager.getKeyBackupService(keyStore, - ServiceConfig.KEY_BACKUP_ENCLAVE_NAME, - ServiceConfig.KEY_BACKUP_SERVICE_ID, - ServiceConfig.KEY_BACKUP_MRENCLAVE, - 10); - } - private IdentityKeyPair getIdentityKeyPair() { return account.getSignalProtocolStore().getIdentityKeyPair(); } @@ -313,56 +301,20 @@ public class Manager implements Closeable { public static Manager init( String username, File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent - ) throws IOException { + ) throws IOException, NotRegisteredException { PathConfig pathConfig = PathConfig.createDefault(settingsPath); if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { - IdentityKeyPair identityKey = KeyUtils.generateIdentityKeyPair(); - int registrationId = KeyHelper.generateRegistrationId(false); - - ProfileKey profileKey = KeyUtils.createProfileKey(); - SignalAccount account = SignalAccount.create(pathConfig.getDataPath(), - username, - identityKey, - registrationId, - profileKey); - account.save(); - - return new Manager(account, pathConfig, serviceConfiguration, userAgent); + throw new NotRegisteredException(); } SignalAccount account = SignalAccount.load(pathConfig.getDataPath(), username); - Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent); - - m.migrateLegacyConfigs(); - - return m; - } - - private void migrateLegacyConfigs() { - if (account.getProfileKey() == null && isRegistered()) { - // Old config file, creating new profile key - account.setProfileKey(KeyUtils.createProfileKey()); - account.save(); + if (!account.isRegistered()) { + throw new NotRegisteredException(); } - // Store profile keys only in profile store - for (ContactInfo contact : account.getContactStore().getContacts()) { - String profileKeyString = contact.profileKey; - if (profileKeyString == null) { - continue; - } - final ProfileKey profileKey; - try { - profileKey = new ProfileKey(Base64.decode(profileKeyString)); - } catch (InvalidInputException | IOException e) { - continue; - } - contact.profileKey = null; - account.getProfileStore().storeProfileKey(contact.getAddress(), profileKey); - } - // Ensure our profile key is stored in profile store - account.getProfileStore().storeProfileKey(getSelfAddress(), account.getProfileKey()); + + return new Manager(account, pathConfig, serviceConfiguration, userAgent); } public void checkAccountState() throws IOException { @@ -401,25 +353,6 @@ public class Manager implements Closeable { return numbers.stream().collect(Collectors.toMap(x -> x, registeredUsers::contains)); } - public void register(boolean voiceVerification, String captcha) throws IOException { - account.setPassword(KeyUtils.createPassword()); - - // Resetting UUID, because registering doesn't work otherwise - account.setUuid(null); - createSignalServiceAccountManager(); - - if (voiceVerification) { - accountManager.requestVoiceVerificationCode(Locale.getDefault(), - Optional.fromNullable(captcha), - Optional.absent()); - } else { - accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent()); - } - - account.setRegistered(false); - account.save(); - } - public void updateAccountAttributes() throws IOException { accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), @@ -427,10 +360,10 @@ public class Manager implements Closeable { // set legacy pin only if no KBS master key is set account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null, account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), - unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(), - unrestrictedUnidentifiedAccess, + account.getSelfUnidentifiedAccessKey(), + account.isUnrestrictedUnidentifiedAccess(), capabilities, - discoverableByPhoneNumber); + account.isDiscoverableByPhoneNumber()); } public void setProfile(String name, File avatar) throws IOException { @@ -519,63 +452,6 @@ public class Manager implements Closeable { } } - public void verifyAccount( - String verificationCode, String pin - ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { - verificationCode = verificationCode.replace("-", ""); - account.setSignalingKey(KeyUtils.createSignalingKey()); - VerifyAccountResponse response; - try { - response = verifyAccountWithCode(verificationCode, pin, null); - } catch (LockedException e) { - if (pin == null) { - throw e; - } - - KbsPinData registrationLockData = pinHelper.getRegistrationLockData(pin, e); - if (registrationLockData == null) { - throw e; - } - - String registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock(); - try { - response = verifyAccountWithCode(verificationCode, null, registrationLock); - } catch (LockedException _e) { - throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!"); - } - account.setPinMasterKey(registrationLockData.getMasterKey()); - } - - // TODO response.isStorageCapable() - //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); - - account.setRegistered(true); - account.setUuid(UuidUtil.parseOrNull(response.getUuid())); - account.setRegistrationLockPin(pin); - account.getSignalProtocolStore() - .saveIdentity(account.getSelfAddress(), - getIdentityKeyPair().getPublicKey(), - TrustLevel.TRUSTED_VERIFIED); - - refreshPreKeys(); - account.save(); - } - - private VerifyAccountResponse verifyAccountWithCode( - final String verificationCode, final String legacyPin, final String registrationLock - ) throws IOException { - return accountManager.verifyAccountWithCode(verificationCode, - account.getSignalingKey(), - account.getSignalProtocolStore().getLocalRegistrationId(), - true, - legacyPin, - registrationLock, - unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(), - unrestrictedUnidentifiedAccess, - capabilities, - discoverableByPhoneNumber); - } - public void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException { if (pin.isPresent()) { final MasterKey masterKey = account.getPinMasterKey() != null @@ -607,45 +483,21 @@ public class Manager implements Closeable { accountManager.setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys); } - private SignalServiceMessageReceiver createMessageReceiver() { - final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create( - serviceConfiguration).getProfileOperations() : null; - return new SignalServiceMessageReceiver(serviceConfiguration, - account.getUuid(), - account.getUsername(), - account.getPassword(), - account.getDeviceId(), - account.getSignalingKey(), - userAgent, - null, - timer, - clientZkProfileOperations); - } - - private SignalServiceMessageReceiver getOrCreateMessageReceiver() { - if (messageReceiver == null) { - messageReceiver = createMessageReceiver(); - } - return messageReceiver; - } - private SignalServiceMessagePipe getOrCreateMessagePipe() { if (messagePipe == null) { - messagePipe = getOrCreateMessageReceiver().createMessagePipe(); + messagePipe = messageReceiver.createMessagePipe(); } return messagePipe; } private SignalServiceMessagePipe getOrCreateUnidentifiedMessagePipe() { if (unidentifiedMessagePipe == null) { - unidentifiedMessagePipe = getOrCreateMessageReceiver().createUnidentifiedMessagePipe(); + unidentifiedMessagePipe = messageReceiver.createUnidentifiedMessagePipe(); } return unidentifiedMessagePipe; } private SignalServiceMessageSender createMessageSender() { - final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create( - serviceConfiguration).getProfileOperations() : null; final ExecutorService executor = null; return new SignalServiceMessageSender(serviceConfiguration, account.getUuid(), @@ -2349,13 +2201,12 @@ public class Manager implements Closeable { GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey ) throws IOException { IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); - SignalServiceMessageReceiver receiver = getOrCreateMessageReceiver(); File outputFile = getGroupAvatarFile(groupId); GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams); File tmpFile = IOUtils.createTempFile(); tmpFile.deleteOnExit(); - try (InputStream input = receiver.retrieveGroupsV2ProfileAvatar(cdnKey, + try (InputStream input = messageReceiver.retrieveGroupsV2ProfileAvatar(cdnKey, tmpFile, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { byte[] encryptedData = IOUtils.readFully(input); @@ -2384,11 +2235,10 @@ public class Manager implements Closeable { SignalServiceAddress address, String avatarPath, ProfileKey profileKey ) throws IOException { IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath()); - SignalServiceMessageReceiver receiver = getOrCreateMessageReceiver(); File outputFile = getProfileAvatarFile(address); File tmpFile = IOUtils.createTempFile(); - try (InputStream input = receiver.retrieveProfileAvatar(avatarPath, + try (InputStream input = messageReceiver.retrieveProfileAvatar(avatarPath, tmpFile, profileKey, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { @@ -2429,8 +2279,6 @@ public class Manager implements Closeable { } } - final SignalServiceMessageReceiver messageReceiver = getOrCreateMessageReceiver(); - File tmpFile = IOUtils.createTempFile(); try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, @@ -2451,7 +2299,6 @@ public class Manager implements Closeable { private InputStream retrieveAttachmentAsStream( SignalServiceAttachmentPointer pointer, File tmpFile ) throws IOException, InvalidMessageException, MissingConfigurationException { - final SignalServiceMessageReceiver messageReceiver = getOrCreateMessageReceiver(); return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); } @@ -2737,6 +2584,10 @@ public class Manager implements Closeable { @Override public void close() throws IOException { + close(true); + } + + void close(boolean closeAccount) throws IOException { if (messagePipe != null) { messagePipe.shutdown(); messagePipe = null; @@ -2747,7 +2598,10 @@ public class Manager implements Closeable { unidentifiedMessagePipe = null; } - account.close(); + if (closeAccount && account != null) { + account.close(); + } + account = null; } public interface ReceiveMessageHandler { diff --git a/src/main/java/org/asamk/signal/manager/NotRegisteredException.java b/src/main/java/org/asamk/signal/manager/NotRegisteredException.java new file mode 100644 index 00000000..c1b35a1c --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/NotRegisteredException.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager; + +public class NotRegisteredException extends Exception { + + public NotRegisteredException() { + super("User is not registered."); + } +} diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 4195e8af..475c90b8 100644 --- a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -124,8 +124,10 @@ public class ProvisioningManager { m.requestSyncBlocked(); m.requestSyncConfiguration(); - m.saveAccount(); + m.close(false); } + + account.save(); } return username; diff --git a/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/src/main/java/org/asamk/signal/manager/RegistrationManager.java new file mode 100644 index 00000000..e740bb91 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -0,0 +1,194 @@ +/* + Copyright (C) 2015-2021 AsamK and contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package org.asamk.signal.manager; + +import org.asamk.signal.manager.helper.PinHelper; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.util.KeyUtils; +import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.util.KeyHelper; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.KbsPinData; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.KeyBackupServicePinException; +import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.SleepTimer; +import org.whispersystems.signalservice.api.util.UptimeSleepTimer; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.whispersystems.signalservice.internal.push.LockedException; +import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; +import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +public class RegistrationManager implements AutoCloseable { + + private SignalAccount account; + private final PathConfig pathConfig; + private final SignalServiceConfiguration serviceConfiguration; + private final String userAgent; + + private final SignalServiceAccountManager accountManager; + private final PinHelper pinHelper; + + public RegistrationManager( + SignalAccount account, + PathConfig pathConfig, + SignalServiceConfiguration serviceConfiguration, + String userAgent + ) { + this.account = account; + this.pathConfig = pathConfig; + this.serviceConfiguration = serviceConfiguration; + this.userAgent = userAgent; + + final SleepTimer timer = new UptimeSleepTimer(); + this.accountManager = new SignalServiceAccountManager(serviceConfiguration, new DynamicCredentialsProvider( + // Using empty UUID, because registering doesn't work otherwise + null, + account.getUsername(), + account.getPassword(), + account.getSignalingKey(), + SignalServiceAddress.DEFAULT_DEVICE_ID), userAgent, null, timer); + final KeyBackupService keyBackupService = ServiceConfig.createKeyBackupService(accountManager); + this.pinHelper = new PinHelper(keyBackupService); + } + + public static RegistrationManager init( + String username, File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent + ) throws IOException { + PathConfig pathConfig = PathConfig.createDefault(settingsPath); + + if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { + IdentityKeyPair identityKey = KeyUtils.generateIdentityKeyPair(); + int registrationId = KeyHelper.generateRegistrationId(false); + + ProfileKey profileKey = KeyUtils.createProfileKey(); + SignalAccount account = SignalAccount.create(pathConfig.getDataPath(), + username, + identityKey, + registrationId, + profileKey); + account.save(); + + return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent); + } + + SignalAccount account = SignalAccount.load(pathConfig.getDataPath(), username); + + return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent); + } + + public void register(boolean voiceVerification, String captcha) throws IOException { + if (account.getPassword() == null) { + account.setPassword(KeyUtils.createPassword()); + } + + if (voiceVerification) { + accountManager.requestVoiceVerificationCode(Locale.getDefault(), + Optional.fromNullable(captcha), + Optional.absent()); + } else { + accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent()); + } + + account.setRegistered(false); + account.save(); + } + + public void verifyAccount( + String verificationCode, String pin + ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException { + verificationCode = verificationCode.replace("-", ""); + if (account.getSignalingKey() == null) { + account.setSignalingKey(KeyUtils.createSignalingKey()); + } + VerifyAccountResponse response; + try { + response = verifyAccountWithCode(verificationCode, pin, null); + account.setPinMasterKey(null); + } catch (LockedException e) { + if (pin == null) { + throw e; + } + + KbsPinData registrationLockData = pinHelper.getRegistrationLockData(pin, e); + if (registrationLockData == null) { + throw e; + } + + String registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock(); + try { + response = verifyAccountWithCode(verificationCode, null, registrationLock); + } catch (LockedException _e) { + throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!"); + } + account.setPinMasterKey(registrationLockData.getMasterKey()); + } + + // TODO response.isStorageCapable() + //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); + + account.setDeviceId(SignalServiceAddress.DEFAULT_DEVICE_ID); + account.setMultiDevice(false); + account.setRegistered(true); + account.setUuid(UuidUtil.parseOrNull(response.getUuid())); + account.setRegistrationLockPin(pin); + account.getSignalProtocolStore() + .saveIdentity(account.getSelfAddress(), + account.getSignalProtocolStore().getIdentityKeyPair().getPublicKey(), + TrustLevel.TRUSTED_VERIFIED); + + try (Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent)) { + + m.refreshPreKeys(); + + m.close(false); + } + + account.save(); + } + + private VerifyAccountResponse verifyAccountWithCode( + final String verificationCode, final String legacyPin, final String registrationLock + ) throws IOException { + return accountManager.verifyAccountWithCode(verificationCode, + account.getSignalingKey(), + account.getSignalProtocolStore().getLocalRegistrationId(), + true, + legacyPin, + registrationLock, + account.getSelfUnidentifiedAccessKey(), + account.isUnrestrictedUnidentifiedAccess(), + ServiceConfig.capabilities, + account.isDiscoverableByPhoneNumber()); + } + + @Override + public void close() throws Exception { + if (account != null) { + account.close(); + account = null; + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/ServiceConfig.java b/src/main/java/org/asamk/signal/manager/ServiceConfig.java index 55935ced..b6d4f4fd 100644 --- a/src/main/java/org/asamk/signal/manager/ServiceConfig.java +++ b/src/main/java/org/asamk/signal/manager/ServiceConfig.java @@ -6,6 +6,8 @@ import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; @@ -109,6 +111,16 @@ public class ServiceConfig { } } + static KeyBackupService createKeyBackupService(SignalServiceAccountManager accountManager) { + KeyStore keyStore = ServiceConfig.getIasKeyStore(); + + return accountManager.getKeyBackupService(keyStore, + ServiceConfig.KEY_BACKUP_ENCLAVE_NAME, + ServiceConfig.KEY_BACKUP_SERVICE_ID, + ServiceConfig.KEY_BACKUP_MRENCLAVE, + 10); + } + static ECPublicKey getUnidentifiedSenderTrustRoot() { try { return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); diff --git a/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java b/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java index a994c40a..3930154c 100644 --- a/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java +++ b/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java @@ -36,7 +36,7 @@ public class UnidentifiedAccessHelper { this.senderCertificateProvider = senderCertificateProvider; } - public byte[] getSelfUnidentifiedAccessKey() { + private byte[] getSelfUnidentifiedAccessKey() { return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey()); } diff --git a/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 1c35b1fb..a030af3f 100644 --- a/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -26,6 +26,7 @@ import org.asamk.signal.manager.storage.stickers.StickerStore; import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore; import org.asamk.signal.manager.storage.threads.ThreadInfo; import org.asamk.signal.manager.util.IOUtils; +import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.Utils; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; @@ -36,6 +37,7 @@ import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Medium; import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.util.Base64; @@ -98,6 +100,8 @@ public class SignalAccount implements Closeable { try { SignalAccount account = new SignalAccount(pair.first(), pair.second()); account.load(dataPath); + account.migrateLegacyConfigs(); + return account; } catch (Throwable e) { pair.second().close(); @@ -169,6 +173,31 @@ public class SignalAccount implements Closeable { return account; } + public void migrateLegacyConfigs() { + if (getProfileKey() == null && isRegistered()) { + // Old config file, creating new profile key + setProfileKey(KeyUtils.createProfileKey()); + save(); + } + // Store profile keys only in profile store + for (ContactInfo contact : getContactStore().getContacts()) { + String profileKeyString = contact.profileKey; + if (profileKeyString == null) { + continue; + } + final ProfileKey profileKey; + try { + profileKey = new ProfileKey(Base64.decode(profileKeyString)); + } catch (InvalidInputException | IOException e) { + continue; + } + contact.profileKey = null; + getProfileStore().storeProfileKey(contact.getAddress(), profileKey); + } + // Ensure our profile key is stored in profile store + getProfileStore().storeProfileKey(getSelfAddress(), getProfileKey()); + } + public static File getFileName(File dataPath, String username) { return new File(dataPath, username); } @@ -451,6 +480,10 @@ public class SignalAccount implements Closeable { return deviceId; } + public void setDeviceId(final int deviceId) { + this.deviceId = deviceId; + } + public String getPassword() { return password; } @@ -491,6 +524,10 @@ public class SignalAccount implements Closeable { this.profileKey = profileKey; } + public byte[] getSelfUnidentifiedAccessKey() { + return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey()); + } + public int getPreKeyIdOffset() { return preKeyIdOffset; } @@ -515,8 +552,19 @@ public class SignalAccount implements Closeable { isMultiDevice = multiDevice; } + public boolean isUnrestrictedUnidentifiedAccess() { + // TODO make configurable + return false; + } + + public boolean isDiscoverableByPhoneNumber() { + // TODO make configurable + return true; + } + @Override public void close() throws IOException { + save(); synchronized (fileChannel) { try { lock.close(); From 38267fa2a121319c0c28cd5d50a4b939f59229d5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 11 Jan 2021 19:50:19 +0100 Subject: [PATCH 31/32] Update copyright notices --- src/main/java/org/asamk/signal/Main.java | 2 +- src/main/java/org/asamk/signal/manager/Manager.java | 2 +- src/main/java/org/asamk/signal/manager/ProvisioningManager.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index b9209631..1eb36b2a 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2015-2020 AsamK and contributors + Copyright (C) 2015-2021 AsamK and contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 9d090a58..097c4c0d 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2015-2020 AsamK and contributors + Copyright (C) 2015-2021 AsamK and contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 475c90b8..0648c0d3 100644 --- a/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2015-2020 AsamK and contributors + Copyright (C) 2015-2021 AsamK and contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by From bc47c0d5d6ade54db2997f7d6ed5f7154781fcc7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 11 Jan 2021 21:18:03 +0100 Subject: [PATCH 32/32] Refactor message cache --- .../org/asamk/signal/manager/Manager.java | 94 +++---------------- .../signal/manager/storage/SignalAccount.java | 15 +++ .../storage/messageCache/CachedMessage.java | 38 ++++++++ .../storage/messageCache/MessageCache.java | 79 ++++++++++++++++ 4 files changed, 147 insertions(+), 79 deletions(-) create mode 100644 src/main/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java create mode 100644 src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 097c4c0d..d0a9e277 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -34,6 +34,7 @@ import org.asamk.signal.manager.storage.contacts.ContactInfo; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.groups.GroupInfoV2; +import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.profiles.SignalProfile; import org.asamk.signal.manager.storage.profiles.SignalProfileEntry; import org.asamk.signal.manager.storage.protocol.IdentityInfo; @@ -41,7 +42,6 @@ import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.KeyUtils; -import org.asamk.signal.manager.util.MessageCacheUtils; import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.metadata.InvalidMetadataMessageException; import org.signal.libsignal.metadata.InvalidMetadataVersionException; @@ -170,7 +170,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; @@ -188,7 +187,6 @@ public class Manager implements Closeable { final static Logger logger = LoggerFactory.getLogger(Manager.class); - private final SleepTimer timer = new UptimeSleepTimer(); private final CertificateValidator certificateValidator = new CertificateValidator(ServiceConfig.getUnidentifiedSenderTrustRoot()); private final SignalServiceConfiguration serviceConfiguration; @@ -222,6 +220,7 @@ public class Manager implements Closeable { this.userAgent = userAgent; this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create( serviceConfiguration)) : null; + final SleepTimer timer = new UptimeSleepTimer(); this.accountManager = new SignalServiceAccountManager(serviceConfiguration, new DynamicCredentialsProvider(account.getUuid(), account.getUsername(), @@ -281,24 +280,6 @@ public class Manager implements Closeable { return account.getDeviceId(); } - private File getMessageCachePath() { - return SignalAccount.getMessageCachePath(pathConfig.getDataPath(), account.getUsername()); - } - - private File getMessageCachePath(String sender) { - if (sender == null || sender.isEmpty()) { - return getMessageCachePath(); - } - - return new File(getMessageCachePath(), sender.replace("/", "_")); - } - - private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException { - File cachePath = getMessageCachePath(sender); - IOUtils.createPrivateDirectories(cachePath); - return new File(cachePath, now + "_" + timestamp); - } - public static Manager init( String username, File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent ) throws IOException, NotRegisteredException { @@ -1727,41 +1708,17 @@ public class Manager implements Closeable { } } - private void retryFailedReceivedMessages( - ReceiveMessageHandler handler, boolean ignoreAttachments - ) { - final File cachePath = getMessageCachePath(); - if (!cachePath.exists()) { - return; - } - for (final File dir : Objects.requireNonNull(cachePath.listFiles())) { - if (!dir.isDirectory()) { - retryFailedReceivedMessage(handler, ignoreAttachments, dir); - continue; - } - - for (final File fileEntry : Objects.requireNonNull(dir.listFiles())) { - if (!fileEntry.isFile()) { - continue; - } - retryFailedReceivedMessage(handler, ignoreAttachments, fileEntry); - } - // Try to delete directory if empty - dir.delete(); + private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { + for (CachedMessage cachedMessage : account.getMessageCache().getCachedMessages()) { + retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage); } } private void retryFailedReceivedMessage( - final ReceiveMessageHandler handler, final boolean ignoreAttachments, final File fileEntry + final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage ) { - SignalServiceEnvelope envelope; - try { - envelope = MessageCacheUtils.loadEnvelope(fileEntry); - if (envelope == null) { - return; - } - } catch (IOException e) { - e.printStackTrace(); + SignalServiceEnvelope envelope = cachedMessage.loadEnvelope(); + if (envelope == null) { return; } SignalServiceContent content = null; @@ -1772,11 +1729,7 @@ public class Manager implements Closeable { return; } catch (Exception er) { // All other errors are not recoverable, so delete the cached message - try { - Files.delete(fileEntry.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry, e.getMessage()); - } + cachedMessage.delete(); return; } List actions = handleMessage(envelope, content, ignoreAttachments); @@ -1790,11 +1743,7 @@ public class Manager implements Closeable { } account.save(); handler.handleMessage(envelope, content, null); - try { - Files.delete(fileEntry.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry, e.getMessage()); - } + cachedMessage.delete(); } public void receiveMessages( @@ -1808,7 +1757,7 @@ public class Manager implements Closeable { Set queuedActions = null; - getOrCreateMessagePipe(); + final SignalServiceMessagePipe messagePipe = getOrCreateMessagePipe(); boolean hasCaughtUpWithOldMessages = false; @@ -1816,17 +1765,11 @@ public class Manager implements Closeable { SignalServiceEnvelope envelope; SignalServiceContent content = null; Exception exception = null; - final long now = new Date().getTime(); + final CachedMessage[] cachedMessage = {null}; try { Optional result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> { // store message on disk, before acknowledging receipt to the server - try { - String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : ""; - File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp()); - MessageCacheUtils.storeEnvelope(envelope1, cacheFile); - } catch (IOException e) { - logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage()); - } + cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1); }); if (result.isPresent()) { envelope = result.get(); @@ -1890,15 +1833,8 @@ public class Manager implements Closeable { handler.handleMessage(envelope, content, exception); } if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) { - File cacheFile = null; - try { - String source = envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : ""; - cacheFile = getMessageCacheFile(source, now, envelope.getTimestamp()); - Files.delete(cacheFile.toPath()); - // Try to delete directory if empty - getMessageCachePath().delete(); - } catch (IOException e) { - logger.warn("Failed to delete cached message file “{}”, ignoring: {}", cacheFile, e.getMessage()); + if (cachedMessage[0] != null) { + cachedMessage[0].delete(); } } } diff --git a/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index a030af3f..6d592573 100644 --- a/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -16,6 +16,7 @@ import org.asamk.signal.manager.storage.contacts.JsonContactsStore; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.groups.JsonGroupStore; +import org.asamk.signal.manager.storage.messageCache.MessageCache; import org.asamk.signal.manager.storage.profiles.ProfileStore; import org.asamk.signal.manager.storage.protocol.IdentityInfo; import org.asamk.signal.manager.storage.protocol.JsonSignalProtocolStore; @@ -84,6 +85,8 @@ public class SignalAccount implements Closeable { private ProfileStore profileStore; private StickerStore stickerStore; + private MessageCache messageCache; + private SignalAccount(final FileChannel fileChannel, final FileLock lock) { this.fileChannel = fileChannel; this.lock = lock; @@ -130,6 +133,9 @@ public class SignalAccount implements Closeable { account.recipientStore = new RecipientStore(); account.profileStore = new ProfileStore(); account.stickerStore = new StickerStore(); + + account.messageCache = new MessageCache(getMessageCachePath(dataPath, username)); + account.registered = false; return account; @@ -167,6 +173,9 @@ public class SignalAccount implements Closeable { account.recipientStore = new RecipientStore(); account.profileStore = new ProfileStore(); account.stickerStore = new StickerStore(); + + account.messageCache = new MessageCache(getMessageCachePath(dataPath, username)); + account.registered = true; account.isMultiDevice = true; @@ -342,6 +351,8 @@ public class SignalAccount implements Closeable { stickerStore = new StickerStore(); } + messageCache = new MessageCache(getMessageCachePath(dataPath, username)); + JsonNode threadStoreNode = rootNode.get("threadStore"); if (threadStoreNode != null) { LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, @@ -460,6 +471,10 @@ public class SignalAccount implements Closeable { return stickerStore; } + public MessageCache getMessageCache() { + return messageCache; + } + public String getUsername() { return username; } diff --git a/src/main/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java b/src/main/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java new file mode 100644 index 00000000..6c20cf62 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java @@ -0,0 +1,38 @@ +package org.asamk.signal.manager.storage.messageCache; + +import org.asamk.signal.manager.util.MessageCacheUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public final class CachedMessage { + + final static Logger logger = LoggerFactory.getLogger(CachedMessage.class); + + private final File file; + + CachedMessage(final File file) { + this.file = file; + } + + public SignalServiceEnvelope loadEnvelope() { + try { + return MessageCacheUtils.loadEnvelope(file); + } catch (IOException e) { + logger.error("Failed to load cached message envelope “{}”: {}", file, e.getMessage()); + return null; + } + } + + public void delete() { + try { + Files.delete(file.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete cached message file “{}”, ignoring: {}", file, e.getMessage()); + } + } +} diff --git a/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java b/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java new file mode 100644 index 00000000..4e48ee76 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java @@ -0,0 +1,79 @@ +package org.asamk.signal.manager.storage.messageCache; + +import org.asamk.signal.manager.util.IOUtils; +import org.asamk.signal.manager.util.MessageCacheUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class MessageCache { + + final static Logger logger = LoggerFactory.getLogger(MessageCache.class); + + private final File messageCachePath; + + public MessageCache(final File messageCachePath) { + this.messageCachePath = messageCachePath; + } + + public Iterable getCachedMessages() { + if (!messageCachePath.exists()) { + return Collections.emptyList(); + } + + return Arrays.stream(Objects.requireNonNull(messageCachePath.listFiles())).flatMap(dir -> { + if (dir.isFile()) { + return Stream.of(dir); + } + + final File[] files = Objects.requireNonNull(dir.listFiles()); + if (files.length == 0) { + try { + Files.delete(dir.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete cache dir “{}”, ignoring: {}", dir, e.getMessage()); + } + return Stream.empty(); + } + return Arrays.stream(files).filter(File::isFile); + }).map(CachedMessage::new).collect(Collectors.toList()); + } + + public CachedMessage cacheMessage(SignalServiceEnvelope envelope) { + final long now = new Date().getTime(); + final String source = envelope.hasSource() ? envelope.getSourceAddress().getLegacyIdentifier() : ""; + + try { + File cacheFile = getMessageCacheFile(source, now, envelope.getTimestamp()); + MessageCacheUtils.storeEnvelope(envelope, cacheFile); + return new CachedMessage(cacheFile); + } catch (IOException e) { + logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage()); + return null; + } + } + + private File getMessageCachePath(String sender) { + if (sender == null || sender.isEmpty()) { + return messageCachePath; + } + + return new File(messageCachePath, sender.replace("/", "_")); + } + + private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException { + File cachePath = getMessageCachePath(sender); + IOUtils.createPrivateDirectories(cachePath); + return new File(cachePath, now + "_" + timestamp); + } +}