From c39b5450ff6905b407ec575a4237106b7584593d Mon Sep 17 00:00:00 2001 From: John Freed Date: Wed, 18 Aug 2021 10:37:58 +0200 Subject: [PATCH] implement uploadStickerPack for Dbus; fix isRegistered error Note: isRegistered (and related methods) for Dbus can throw an InvalidNumberException when the phone number is incorrectly formatted. Previously this led to uncaught exceptions. They are now handled. The problem is in SignalServiceAccountManager.java in the package org.whispersystems.signalservice.api, which ignores the first character of a proposed phone number and checks that the rest is a legitimate int64. updated documentation --- .../org/asamk/signal/manager/Manager.java | 53 +++++++++++++++---- man/signal-cli-dbus.5.adoc | 45 ++++++++++++---- src/main/java/org/asamk/Signal.java | 8 +-- .../signal/commands/GetUserStatusCommand.java | 5 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 26 ++++++++- 5 files changed, 111 insertions(+), 26 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 0086db65..c1d62e69 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -322,10 +322,16 @@ public class Manager implements Closeable { * @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 get the contacts to check if they're registered + * @throws InvalidNumberException if phone number is incorrectly formatted */ - public Map areUsersRegistered(Set numbers) throws IOException { + public Map areUsersRegistered(Set numbers) throws IOException, InvalidNumberException { // Note "contactDetails" has no optionals. It only gives us info on users who are registered - var contactDetails = getRegisteredUsers(numbers); + Map contactDetails = null; + try { + contactDetails = getRegisteredUsers(numbers); + } catch (InvalidNumberException e) { + throw new InvalidNumberException(e.getMessage()); + } var registeredUsers = contactDetails.keySet(); @@ -924,6 +930,7 @@ public class Manager implements Closeable { .map(this::resolveSignalServiceAddress) .collect(Collectors.toList()); final var newE164Members = new HashSet(); + Map registeredUsers = null; for (var member : newMemberAddresses) { if (!member.getNumber().isPresent()) { continue; @@ -931,7 +938,11 @@ public class Manager implements Closeable { newE164Members.add(member.getNumber().get()); } - final var registeredUsers = getRegisteredUsers(newE164Members); + try { + registeredUsers = getRegisteredUsers(newE164Members); + } catch (InvalidNumberException e) { + throw new IOException("Invalid number detected: " + e.getMessage()); + } if (registeredUsers.size() != newE164Members.size()) { // Some of the new members are not registered on Signal newE164Members.removeAll(registeredUsers.keySet()); @@ -1513,25 +1524,35 @@ public class Manager implements Closeable { return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet()); } - private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException { + private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException, InvalidNumberException { final var address = resolveSignalServiceAddress(recipientId); if (!address.getNumber().isPresent()) { return recipientId; } final var number = address.getNumber().get(); - final var uuidMap = getRegisteredUsers(Set.of(number)); + Map uuidMap; + try { + uuidMap = getRegisteredUsers(Set.of(number)); + } catch (InvalidNumberException e) { + logger.warn("Improperly formatted number: {}", e.getMessage()); + uuidMap = Map.of(); + } return resolveRecipientTrusted(new SignalServiceAddress(uuidMap.getOrDefault(number, null), number)); } - private Map getRegisteredUsers(final Set numbers) throws IOException { + private Map getRegisteredUsers(final Set numbers) throws IOException, InvalidNumberException { + Map registeredUsers = null; try { - return dependencies.getAccountManager() + registeredUsers = dependencies.getAccountManager() .getRegisteredUsers(ServiceConfig.getIasKeyStore(), numbers, serviceEnvironmentConfig.getCdsMrenclave()); } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { - throw new IOException(e); + throw new IOException(e.getMessage()); + } catch (NumberFormatException e) { + throw new InvalidNumberException(e.getMessage()); } + return registeredUsers; } public void sendTypingMessage( @@ -1681,7 +1702,13 @@ public class Manager implements Closeable { ContentHint.DEFAULT, message); } catch (UnregisteredUserException e) { - final var newRecipientId = refreshRegisteredUser(recipientId); + RecipientId newRecipientId = null; + try { + newRecipientId = refreshRegisteredUser(recipientId); + } catch (InvalidNumberException ine) { + logger.warn("Invalid number"); + return SendMessageResult.unregisteredFailure(resolveSignalServiceAddress(recipientId)); + } return messageSender.sendDataMessage(resolveSignalServiceAddress(newRecipientId), unidentifiedAccessHelper.getAccessFor(newRecipientId), ContentHint.DEFAULT, @@ -1700,7 +1727,13 @@ public class Manager implements Closeable { try { return messageSender.sendNullMessage(address, unidentifiedAccessHelper.getAccessFor(recipientId)); } catch (UnregisteredUserException e) { - final var newRecipientId = refreshRegisteredUser(recipientId); + RecipientId newRecipientId = null; + try { + newRecipientId = refreshRegisteredUser(recipientId); + } catch (InvalidNumberException ine) { + logger.warn("Invalid number"); + return SendMessageResult.unregisteredFailure(resolveSignalServiceAddress(recipientId)); + } final var newAddress = resolveSignalServiceAddress(newRecipientId); return messageSender.sendNullMessage(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId)); } diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 76b9f541..fc69c1f2 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -113,7 +113,7 @@ isMember(base64GroupId) -> active:: * base64GroupId : String representing the internal group identifier in Base64 format * active : Boolean representing whether you are a member of the group -Note that this method does not raise an Exception for a non-existing/unknown group but will simply return false +Exceptions: None; returns false for a non-existing/unknown group sendEndSessionMessage(recipients) -> <>:: * recipients : String array of phone numbers @@ -195,13 +195,13 @@ Depending on the type of the recipient(s) field this deletes a message with one Exceptions: Failure, InvalidNumber -sendContacts() -> <> +sendContacts() -> <>:: Sends a synchronization message with the local contacts list to all linked devices. Exceptions: Failure, UntrustedIdentity -sendSyncRequest() -> <> +sendSyncRequest() -> <>:: Sends a synchronization request to the primary device (for group, contacts, ...). Only works if a secondary device is running the daemon. @@ -213,7 +213,7 @@ trust(number, safetyNumber) -> <>:: Exceptions: Failure, InvalidNumber; -sendTyping(typingAction, base64GroupId, recipients) -> <> +sendTyping(typingAction, base64GroupId, recipients) -> <>:: * typingAction : true = start typing, false = stop typing * base64GroupId : String representing the internal group identifier in Base64 format * recipients : List of phone numbers @@ -226,14 +226,20 @@ getContactName(number) -> name:: * number : Phone number * name : Contact's name in local storage (from the primary device for a linked account, or the one set with setContactName); if not set, contact's profile name is used +Exception: InvalidNumber + setContactName(number,name<>) -> <>:: * number : Phone number * name : Name to be set in contacts (in local storage with signal-cli) +Exception: InvalidNumber + setExpirationTimer(number,expiration) -> <>:: * number : Phone number * expiration : Expiration time for messages sent to this number (in seconds). Set to 0 to disable. +Exception: InvalidNumber + getGroupIds() -> groupList:: getGroupIds(dummy) -> groupList:: dummy : any string (ignored by method; forces output to be identical with getBase64GroupIds) @@ -242,18 +248,22 @@ base64GroupList : Array of strings representing the internal group identifiers i All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked() +Exceptions: None + getBase64GroupIds() -> groupList:: groupList : Array of strings representing the internal group identifiers in Base64 format All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked() +Exceptions: None + getGroupName(groupId) -> groupName:: getGroupName(base64GroupId) -> groupName:: groupName : The display name of the group groupId : Byte array representing the internal group identifier base64GroupId : String representing the internal group identifier in Base64 format -Exceptions: None, if the group name is not found an empty string is returned +Exceptions: None; if the group name is not found an empty string is returned getGroupMembers(groupId) -> members:: getGroupMembers(base64GroupId) -> members:: @@ -261,24 +271,28 @@ members : String array with the phone numbers of all active members of a g groupId : Byte array representing the internal group identifier base64GroupId : String representing the internal group identifier in Base64 format -Exceptions: None, if the group name is not found an empty array is returned +Exceptions: None; if the group name is not found an empty array is returned listNumbers() -> numbers:: numbers : String array of all known numbers This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages) +Exceptions: None + getContactNumber(name) -> numbers:: * numbers : Array of phone numbers * name : Contact or profile name ("firstname lastname") Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set. +Exception: Failure + isContactBlocked(number) -> state:: * number : Phone number * state : true=blocked, false=not blocked -Exceptions: None, for unknown numbers false is returned +Exception: InvalidNumber for an incorrectly formatted phone number. For unknown numbers, false is returned, but no exception is raised. isGroupBlocked(groupId) -> state:: isGroupBlocked(base64GroupId) -> state:: @@ -286,11 +300,13 @@ isGroupBlocked(base64GroupId) -> state:: * base64GroupId : String representing the internal group identifier in Base64 format * state : true=blocked, false=not blocked -Exceptions: None, for unknown groups false is returned +Exceptions: None; for unknown groups false is returned version() -> version:: * version : Version string of signal-cli +Exceptions: None + isRegistered(number) -> result:: isRegistered(numbers) -> results:: * number : Phone number @@ -298,6 +314,8 @@ isRegistered(numbers) -> results:: * result : true=number is registered, false=number is not registered * results : Boolean array of results +Exception: InvalidNumber for an incorrectly formatted phone number. For unknown numbers, false is returned, but no exception is raised. + listIdentity(number) -> results:: * number : Phone number * results : Array of elements, each consisting of four strings: trust_level, date_added, fingerprint, safety_number @@ -309,7 +327,7 @@ listIdentity(number) -> results:: getObjectPath() -> objectPath:: * objectPath : The DBus object path associated with this connection -updateAccount() -> <> +updateAccount() -> <>:: Updates the account attributes on the Signal server. @@ -351,17 +369,22 @@ removePin() -> <>:: Removes registration PIN protection. -verify(number, verificationCode) -> <> +verify(number, verificationCode) -> <>:: * number : Phone number * verificationCode : Code received from Signal after successful registration request Command fails if PIN was set after previous registration; use verifyWithPin instead. -verifyWithPin(number, verificationCode, pin) -> <> +verifyWithPin(number, verificationCode, pin) -> <>:: * number : Phone number * verificationCode : Code received from Signal after successful registration request * pin : PIN you set with setPin command after verifying previous registration +uploadStickerPack(stickerPackPath) -> <>:: +* stickerPackPath : Path to the sticker pack + +Exception: Failure + == Signals SyncMessageReceived (timestamp, sender, destination, groupId, message, attachments):: diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 3f20648e..27115bd8 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -133,7 +133,7 @@ public interface Signal extends DBusInterface { void setGroupBlocked(String base64GroupId, boolean blocked) throws Error.GroupNotFound; - List getGroupMembers(String dummy); + List getGroupMembers(String base64GroupId); long sendGroupMessage( String message, List attachments, String base64GroupId @@ -147,9 +147,9 @@ public interface Signal extends DBusInterface { String base64GroupId, String name, List members, String avatar ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound; - boolean isRegistered(String number); + boolean isRegistered(String number) throws Error.Failure, Error.InvalidNumber; - List isRegistered(List numbers); + List isRegistered(List numbers) throws Error.Failure, Error.InvalidNumber; void updateProfile( String givenName, String familyName, String about, String aboutEmoji, String avatarPath, boolean removeAvatar @@ -209,6 +209,8 @@ public interface Signal extends DBusInterface { void joinGroup(final String groupLink) throws Error.Failure; + void uploadStickerPack(String stickerPackPath) throws Error.Failure; + class MessageReceived extends DBusSignal { private final long timestamp; diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java index 91e6e47c..01c64f28 100644 --- a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -11,6 +11,7 @@ import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.manager.Manager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; import java.util.HashSet; @@ -34,12 +35,14 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { @Override public void handleCommand(final Namespace ns, final Manager m) throws CommandException { // Get a map of registration statuses - Map registered; + Map registered = null; try { registered = m.areUsersRegistered(new HashSet<>(ns.getList("number"))); } catch (IOException e) { logger.debug("Failed to check registered users", e); throw new IOErrorException("Unable to check if users are registered"); + } catch (InvalidNumberException e) { + logger.debug("Invalid number format", e); } // Output diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 177ffcc6..99bd7ed2 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -19,6 +19,7 @@ import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.AvatarStore; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; +import org.asamk.signal.manager.StickerPackInvalidException; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.groups.GroupId; @@ -753,6 +754,9 @@ public class DbusSignalImpl implements Signal { @Override public boolean isRegistered(String number) { + if (number.isEmpty()) { + return false; + } try { Map registered; List numbers = new ArrayList(); @@ -761,14 +765,19 @@ public class DbusSignalImpl implements Signal { return registered.get(number); } catch (IOException e) { throw new Error.Failure(e.getMessage()); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); } } @Override public List isRegistered(List numbers) { + List results = new ArrayList (); + if (numbers.isEmpty()) { + return results; + } try { Map registered; - List results = new ArrayList (); registered = m.areUsersRegistered(new HashSet(numbers)); for (String number : numbers) { results.add(registered.get(number)); @@ -776,6 +785,8 @@ public class DbusSignalImpl implements Signal { return results; } catch (IOException e) { throw new Error.Failure(e.getMessage()); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); } } @@ -1014,4 +1025,17 @@ public class DbusSignalImpl implements Signal { return group.isMember(m.getSelfRecipientId()); } } + + @Override + public void uploadStickerPack(String stickerPackPath) { + File path = new File(stickerPackPath); + + try { + var url = m.uploadStickerPack(path); + } catch (IOException e) { + throw new Error.Failure("Upload error (maybe image size is too large):" + e.getMessage()); + } catch (StickerPackInvalidException e) { + throw new Error.Failure("Invalid sticker pack: " + e.getMessage()); + } + } }