import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.ecc.ECPublicKey;
+import org.whispersystems.libsignal.fingerprint.Fingerprint;
+import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
+import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Pair;
* 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
+ * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null.
* @throws IOException if its unable to get the contacts to check if they're registered
*/
- public Map<String, Boolean> areUsersRegistered(Set<String> numbers) throws IOException {
+ public Map<String, Pair<String, UUID>> areUsersRegistered(Set<String> numbers) throws IOException {
+ Map<String, String> canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> {
+ try {
+ return canonicalizePhoneNumber(n);
+ } catch (InvalidNumberException e) {
+ return "";
+ }
+ }));
+
// Note "contactDetails" has no optionals. It only gives us info on users who are registered
- var contactDetails = getRegisteredUsers(numbers);
+ var contactDetails = getRegisteredUsers(canonicalizedNumbers.values()
+ .stream()
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toSet()));
- var registeredUsers = contactDetails.keySet();
+ // Store numbers as recipients so we have the number/uuid association
+ contactDetails.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number)));
- return numbers.stream().collect(Collectors.toMap(x -> x, registeredUsers::contains));
+ return numbers.stream().collect(Collectors.toMap(n -> n, n -> {
+ final var number = canonicalizedNumbers.get(n);
+ final var uuid = contactDetails.get(number);
+ return new Pair<>(number.isEmpty() ? null : number, uuid);
+ }));
}
public void updateAccountAttributes() throws IOException {
return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(resolveRecipient(recipient)));
}
- void sendReceipt(
- SignalServiceAddress remoteAddress, long messageId
+ public void sendReadReceipt(
+ String sender, List<Long> messageIds
+ ) throws IOException, UntrustedIdentityException, InvalidNumberException {
+ var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
+ messageIds,
+ System.currentTimeMillis());
+
+ sendHelper.sendReceiptMessage(receiptMessage, canonicalizeAndResolveRecipient(sender));
+ }
+
+ public void sendViewedReceipt(
+ String sender, List<Long> messageIds
+ ) throws IOException, UntrustedIdentityException, InvalidNumberException {
+ var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
+ messageIds,
+ System.currentTimeMillis());
+
+ sendHelper.sendReceiptMessage(receiptMessage, canonicalizeAndResolveRecipient(sender));
+ }
+
+ void sendDeliveryReceipt(
+ SignalServiceAddress remoteAddress, List<Long> messageIds
) throws IOException, UntrustedIdentityException {
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
- List.of(messageId),
+ messageIds,
System.currentTimeMillis());
sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(remoteAddress));
) throws IOException, InterruptedException {
retryFailedReceivedMessages(handler, ignoreAttachments);
- Set<HandleAction> queuedActions = null;
+ Set<HandleAction> queuedActions = new HashSet<>();
final var signalWebSocket = dependencies.getSignalWebSocket();
signalWebSocket.connect();
// Received indicator that server queue is empty
hasCaughtUpWithOldMessages = true;
- if (queuedActions != null) {
- for (var action : queuedActions) {
- try {
- action.execute(this);
- } catch (Throwable e) {
- if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) {
- Thread.currentThread().interrupt();
- }
- logger.warn("Message action failed.", e);
+ for (var action : queuedActions) {
+ try {
+ action.execute(this);
+ } catch (Throwable e) {
+ if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
}
+ logger.warn("Message action failed.", e);
}
- queuedActions.clear();
- queuedActions = null;
}
+ queuedActions.clear();
// Continue to wait another timeout for new messages
continue;
// address/uuid in envelope is sent by server
resolveRecipientTrusted(envelope.getSourceAddress());
}
- final var notAGroupMember = isNotAGroupMember(envelope, content);
if (!envelope.isReceipt()) {
try {
content = decryptMessage(envelope);
}
}
} else {
- if (queuedActions == null) {
- queuedActions = new HashSet<>();
- }
queuedActions.addAll(actions);
}
}
+ final var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content);
if (isMessageBlocked(envelope, content)) {
logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp());
- } else if (notAGroupMember) {
- logger.info("Ignoring a message from a non group member: {}", envelope.getTimestamp());
+ } else if (notAllowedToSendToGroup) {
+ logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}",
+ (envelope.hasSource() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(),
+ envelope.getTimestamp());
} else {
handler.handleMessage(envelope, content, exception);
}
return sourceContact != null && sourceContact.isBlocked();
}
- private boolean isNotAGroupMember(
+ private boolean isNotAllowedToSendToGroup(
SignalServiceEnvelope envelope, SignalServiceContent content
) {
SignalServiceAddress source;
return false;
}
- if (content != null && content.getDataMessage().isPresent()) {
- var message = content.getDataMessage().get();
- if (message.getGroupContext().isPresent()) {
- if (message.getGroupContext().get().getGroupV1().isPresent()) {
- var groupInfo = message.getGroupContext().get().getGroupV1().get();
- if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) {
- return false;
- }
- }
- var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
- var group = getGroup(groupId);
- if (group != null && !group.isMember(resolveRecipient(source))) {
- return true;
- }
+ if (content == null || !content.getDataMessage().isPresent()) {
+ return false;
+ }
+
+ var message = content.getDataMessage().get();
+ if (!message.getGroupContext().isPresent()) {
+ return false;
+ }
+
+ if (message.getGroupContext().get().getGroupV1().isPresent()) {
+ var groupInfo = message.getGroupContext().get().getGroupV1().get();
+ if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) {
+ return false;
}
}
- return false;
+
+ var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
+ var group = getGroup(groupId);
+ if (group == null) {
+ return false;
+ }
+
+ final var recipientId = resolveRecipient(source);
+ return !group.isMember(recipientId) || (
+ group.isAnnouncementGroup() && !group.isAdmin(recipientId)
+ );
}
private List<HandleAction> handleMessage(
TrustLevel.TRUSTED_VERIFIED);
}
+ /**
+ * Trust this the identity with this scannable safety number
+ *
+ * @param name username of the identity
+ * @param safetyNumber Scannable safety number
+ */
+ public boolean trustIdentityVerifiedSafetyNumber(String name, byte[] safetyNumber) throws InvalidNumberException {
+ var recipientId = canonicalizeAndResolveRecipient(name);
+ var address = account.getRecipientStore().resolveServiceAddress(recipientId);
+ return trustIdentity(recipientId, identityKey -> {
+ final var fingerprint = computeSafetyNumberFingerprint(address, identityKey);
+ try {
+ return fingerprint != null && fingerprint.getScannableFingerprint().compareTo(safetyNumber);
+ } catch (FingerprintVersionMismatchException | FingerprintParsingException e) {
+ return false;
+ }
+ }, TrustLevel.TRUSTED_VERIFIED);
+ }
+
/**
* Trust all keys of this identity without verification
*
}
public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
- final var fingerprint = Utils.computeSafetyNumber(capabilities.isUuid(),
- account.getSelfAddress(),
- getIdentityKeyPair().getPublicKey(),
- theirAddress,
- theirIdentityKey);
+ final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey);
return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText();
}
public byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
- final var fingerprint = Utils.computeSafetyNumber(capabilities.isUuid(),
+ final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey);
+ return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized();
+ }
+
+ private Fingerprint computeSafetyNumberFingerprint(
+ final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
+ ) {
+ return Utils.computeSafetyNumber(capabilities.isUuid(),
account.getSelfAddress(),
getIdentityKeyPair().getPublicKey(),
theirAddress,
theirIdentityKey);
- return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized();
}
@Deprecated
return account.getRecipientStore().resolveServiceAddress(recipientId);
}
- public RecipientId canonicalizeAndResolveRecipient(String identifier) throws InvalidNumberException {
- var canonicalizedNumber = UuidUtil.isUuid(identifier)
- ? identifier
- : PhoneNumberFormatter.formatNumber(identifier, account.getUsername());
+ private RecipientId canonicalizeAndResolveRecipient(String identifier) throws InvalidNumberException {
+ var canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier : canonicalizePhoneNumber(identifier);
return resolveRecipient(canonicalizedNumber);
}
+ private String canonicalizePhoneNumber(final String number) throws InvalidNumberException {
+ return PhoneNumberFormatter.formatNumber(number, account.getUsername());
+ }
+
private RecipientId resolveRecipient(final String identifier) {
var address = Utils.getSignalServiceAddressFromIdentifier(identifier);