From: AsamK Date: Fri, 30 Apr 2021 20:17:13 +0000 (+0200) Subject: Refactor contact and profile store X-Git-Tag: v0.8.2~38 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/224d8194cc4f6cd91dfdeb71703b75ef67e0b208 Refactor contact and profile store --- diff --git a/lib/src/main/java/org/asamk/signal/manager/HandleAction.java b/lib/src/main/java/org/asamk/signal/manager/HandleAction.java index ba426fa1..7fb80c34 100644 --- a/lib/src/main/java/org/asamk/signal/manager/HandleAction.java +++ b/lib/src/main/java/org/asamk/signal/manager/HandleAction.java @@ -1,6 +1,7 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.storage.recipients.RecipientId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Objects; @@ -160,15 +161,15 @@ class SendGroupInfoAction implements HandleAction { class RetrieveProfileAction implements HandleAction { - private final SignalServiceAddress address; + private final RecipientId recipientId; - public RetrieveProfileAction(final SignalServiceAddress address) { - this.address = address; + public RetrieveProfileAction(final RecipientId recipientId) { + this.recipientId = recipientId; } @Override public void execute(Manager m) throws Throwable { - m.getRecipientProfile(address, true); + m.getRecipientProfile(recipientId, true); } @Override @@ -178,11 +179,11 @@ class RetrieveProfileAction implements HandleAction { final RetrieveProfileAction that = (RetrieveProfileAction) o; - return address.equals(that.address); + return recipientId.equals(that.recipientId); } @Override public int hashCode() { - return address.hashCode(); + return recipientId.hashCode(); } } 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 4bced5e2..f62af7ad 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -30,13 +30,13 @@ 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; -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.identities.IdentityInfo; import org.asamk.signal.manager.storage.messageCache.CachedMessage; -import org.asamk.signal.manager.storage.profiles.SignalProfile; +import org.asamk.signal.manager.storage.recipients.Contact; +import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.util.AttachmentUtils; @@ -243,13 +243,15 @@ public class Manager implements Closeable { this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey, unidentifiedAccessHelper::getAccessFor, unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(), - () -> messageReceiver); + () -> messageReceiver, + this::resolveSignalServiceAddress); this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential, this::getRecipientProfile, - account::getSelfAddress, + account::getSelfRecipientId, groupsV2Operations, groupsV2Api, - this::getGroupAuthForToday); + this::getGroupAuthForToday, + this::resolveSignalServiceAddress); this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); } @@ -355,24 +357,26 @@ public class Manager implements Closeable { * if it's Optional.absent(), the avatar will be removed */ public void setProfile(String name, String about, String aboutEmoji, Optional avatar) throws IOException { - var profileEntry = account.getProfileStore().getProfileEntry(getSelfAddress()); - var profile = profileEntry == null ? null : profileEntry.getProfile(); - var newProfile = new SignalProfile(profile == null ? null : profile.getIdentityKey(), - name != null ? name : profile == null || profile.getName() == null ? "" : profile.getName(), - about != null ? about : profile == null || profile.getAbout() == null ? "" : profile.getAbout(), - aboutEmoji != null - ? aboutEmoji - : profile == null || profile.getAboutEmoji() == null ? "" : profile.getAboutEmoji(), - profile == null ? null : profile.getUnidentifiedAccess(), - account.isUnrestrictedUnidentifiedAccess(), - profile == null ? null : profile.getCapabilities()); + var profile = getRecipientProfile(account.getSelfRecipientId()); + var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); + if (name != null) { + builder.withGivenName(name); + builder.withFamilyName(null); + } + if (about != null) { + builder.withAbout(about); + } + if (aboutEmoji != null) { + builder.withAboutEmoji(aboutEmoji); + } + var newProfile = builder.build(); try (final var streamDetails = avatar == null ? avatarStore.retrieveProfileAvatar(getSelfAddress()) : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) { accountManager.setVersionedProfile(account.getUuid(), account.getProfileKey(), - newProfile.getName(), + newProfile.getInternalServiceName(), newProfile.getAbout(), newProfile.getAboutEmoji(), streamDetails); @@ -386,12 +390,7 @@ public class Manager implements Closeable { avatarStore.deleteProfileAvatar(getSelfAddress()); } } - account.getProfileStore() - .updateProfile(getSelfAddress(), - account.getProfileKey(), - System.currentTimeMillis(), - newProfile, - profileEntry == null ? null : profileEntry.getProfileKeyCredential()); + account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile); try { sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); @@ -527,99 +526,124 @@ public class Manager implements Closeable { ServiceConfig.AUTOMATIC_NETWORK_RETRY); } - public SignalProfile getRecipientProfile( + public Profile getRecipientProfile( SignalServiceAddress address ) { - return getRecipientProfile(address, false); + return getRecipientProfile(resolveRecipient(address), false); } - SignalProfile getRecipientProfile( - SignalServiceAddress address, boolean force + public Profile getRecipientProfile( + RecipientId recipientId + ) { + return getRecipientProfile(recipientId, false); + } + + private final Set pendingProfileRequest = new HashSet<>(); + + Profile getRecipientProfile( + RecipientId recipientId, boolean force ) { - var profileEntry = account.getProfileStore().getProfileEntry(address); - if (profileEntry == null) { - // retrieve profile to get identity key - retrieveEncryptedProfile(address); + var profileKey = account.getProfileStore().getProfileKey(recipientId); + if (profileKey == null) { + if (force) { + // retrieve profile to get identity key + retrieveEncryptedProfile(recipientId); + } return null; } + var profile = account.getProfileStore().getProfile(recipientId); + var now = new Date().getTime(); - // Profiles are cached for 24h before retrieving them again - if (!profileEntry.isRequestPending() && ( - force - || profileEntry.getProfile() == null - || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000 - )) { - profileEntry.setRequestPending(true); - final SignalServiceProfile encryptedProfile; - try { - encryptedProfile = retrieveEncryptedProfile(address); - } finally { - profileEntry.setRequestPending(false); + // Profiles are cached for 24h before retrieving them again, unless forced + if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) { + return profile; + } + + synchronized (pendingProfileRequest) { + if (pendingProfileRequest.contains(recipientId)) { + return profile; } - if (encryptedProfile == null) { - return null; + pendingProfileRequest.add(recipientId); + } + final SignalServiceProfile encryptedProfile; + try { + encryptedProfile = retrieveEncryptedProfile(recipientId); + } finally { + synchronized (pendingProfileRequest) { + pendingProfileRequest.remove(recipientId); } - - final var profileKey = profileEntry.getProfileKey(); - final var profile = decryptProfileAndDownloadAvatar(address, profileKey, encryptedProfile); - account.getProfileStore() - .updateProfile(address, profileKey, now, profile, profileEntry.getProfileKeyCredential()); - return profile; } - return profileEntry.getProfile(); + if (encryptedProfile == null) { + return null; + } + + profile = decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile); + account.getProfileStore().storeProfile(recipientId, profile); + + return profile; } - private SignalServiceProfile retrieveEncryptedProfile(SignalServiceAddress address) { + private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) { try { - final var profile = profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE) - .getProfile(); - try { - account.getIdentityKeyStore() - .saveIdentity(resolveRecipient(address), - new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())), - new Date()); - } catch (InvalidKeyException ignored) { - logger.warn("Got invalid identity key in profile for {}", address.getLegacyIdentifier()); - } - return profile; + return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile(); } catch (IOException e) { logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage()); return null; } } - private ProfileKeyCredential getRecipientProfileKeyCredential(SignalServiceAddress address) { - var profileEntry = account.getProfileStore().getProfileEntry(address); - if (profileEntry == null) { + private ProfileAndCredential retrieveProfileAndCredential( + final RecipientId recipientId, final SignalServiceProfile.RequestType requestType + ) throws IOException { + final var profileAndCredential = profileHelper.retrieveProfileSync(recipientId, requestType); + final var profile = profileAndCredential.getProfile(); + + try { + account.getIdentityKeyStore() + .saveIdentity(recipientId, + new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())), + new Date()); + } catch (InvalidKeyException ignored) { + logger.warn("Got invalid identity key in profile for {}", + resolveSignalServiceAddress(recipientId).getLegacyIdentifier()); + } + return profileAndCredential; + } + + private ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) { + var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId); + if (profileKeyCredential != null) { + return profileKeyCredential; + } + + ProfileAndCredential profileAndCredential; + try { + profileAndCredential = retrieveProfileAndCredential(recipientId, + SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL); + } catch (IOException e) { + logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage()); return null; } - if (profileEntry.getProfileKeyCredential() == null) { - ProfileAndCredential profileAndCredential; - try { - profileAndCredential = profileHelper.retrieveProfileSync(address, - SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL); - } catch (IOException e) { - logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage()); - return null; - } - var now = new Date().getTime(); - final var profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull(); - final var profile = decryptProfileAndDownloadAvatar(address, - profileEntry.getProfileKey(), + profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull(); + account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential); + + var profileKey = account.getProfileStore().getProfileKey(recipientId); + if (profileKey != null) { + final var profile = decryptProfileAndDownloadAvatar(recipientId, + profileKey, profileAndCredential.getProfile()); - account.getProfileStore() - .updateProfile(address, profileEntry.getProfileKey(), now, profile, profileKeyCredential); - return profileKeyCredential; + account.getProfileStore().storeProfile(recipientId, profile); } - return profileEntry.getProfileKeyCredential(); + + return profileKeyCredential; } - private SignalProfile decryptProfileAndDownloadAvatar( - final SignalServiceAddress address, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile + private Profile decryptProfileAndDownloadAvatar( + final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile ) { if (encryptedProfile.getAvatar() != null) { - downloadProfileAvatar(address, encryptedProfile.getAvatar(), profileKey); + downloadProfileAvatar(resolveSignalServiceAddress(recipientId), encryptedProfile.getAvatar(), profileKey); } return ProfileUtils.decryptProfile(profileKey, encryptedProfile); @@ -729,19 +753,23 @@ public class Manager implements Closeable { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { return sendUpdateGroupMessage(groupId, name, - members == null ? null : getSignalServiceAddresses(members), + members == null + ? null + : getSignalServiceAddresses(members).stream() + .map(this::resolveRecipient) + .collect(Collectors.toSet()), avatarFile); } private Pair> sendUpdateGroupMessage( - GroupId groupId, String name, Collection members, File avatarFile + GroupId groupId, String name, Set members, File avatarFile ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { GroupInfo g; SignalServiceDataMessage.Builder messageBuilder; if (groupId == null) { // Create new group var gv2 = groupHelper.createGroupV2(name == null ? "" : name, - members == null ? List.of() : members, + members == null ? Set.of() : members, avatarFile); if (gv2 == null) { var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); @@ -774,7 +802,7 @@ public class Manager implements Closeable { final var newMembers = new HashSet<>(members); newMembers.removeAll(group.getMembers() .stream() - .map(this::resolveSignalServiceAddress) + .map(this::resolveRecipient) .collect(Collectors.toSet())); if (newMembers.size() > 0) { var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers); @@ -810,18 +838,18 @@ public class Manager implements Closeable { } private void updateGroupV1( - final GroupInfoV1 g, - final String name, - final Collection members, - final File avatarFile + final GroupInfoV1 g, final String name, final Collection members, final File avatarFile ) throws IOException { if (name != null) { g.name = name; } if (members != null) { + final var memberAddresses = members.stream() + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toList()); final var newE164Members = new HashSet(); - for (var member : members) { + for (var member : memberAddresses) { if (g.isMember(member) || !member.getNumber().isPresent()) { continue; } @@ -837,7 +865,7 @@ public class Manager implements Closeable { + " to group: Not registered on Signal"); } - g.addMembers(members); + g.addMembers(memberAddresses); } if (avatarFile != null) { @@ -973,7 +1001,7 @@ public class Manager implements Closeable { System.currentTimeMillis()); createMessageSender().sendReceipt(remoteAddress, - unidentifiedAccessHelper.getAccessFor(remoteAddress), + unidentifiedAccessHelper.getAccessFor(resolveRecipient(remoteAddress)), receiptMessage); } @@ -1053,36 +1081,26 @@ public class Manager implements Closeable { } public String getContactName(String number) throws InvalidNumberException { - var contact = account.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number)); - if (contact == null) { - return ""; - } else { - return contact.name; - } + var contact = account.getContactStore().getContact(canonicalizeAndResolveRecipient(number)); + return contact == null || contact.getName() == null ? "" : contact.getName(); } public void setContactName(String number, String name) throws InvalidNumberException { - final var address = canonicalizeAndResolveSignalServiceAddress(number); - var contact = account.getContactStore().getContact(address); - if (contact == null) { - contact = new ContactInfo(address); - } - contact.name = name; - account.getContactStore().updateContact(contact); + final var recipientId = canonicalizeAndResolveRecipient(number); + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore().storeContact(recipientId, builder.withName(name).build()); account.save(); } public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException { - setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked); + setContactBlocked(canonicalizeAndResolveRecipient(number), blocked); } - private void setContactBlocked(SignalServiceAddress address, boolean blocked) { - var contact = account.getContactStore().getContact(address); - if (contact == null) { - contact = new ContactInfo(address); - } - contact.blocked = blocked; - account.getContactStore().updateContact(contact); + private void setContactBlocked(RecipientId recipientId, boolean blocked) { + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); account.save(); } @@ -1097,15 +1115,14 @@ public class Manager implements Closeable { account.save(); } - /** - * Change the expiration timer for a contact - */ - public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) throws IOException { - var contact = account.getContactStore().getContact(address); - contact.messageExpirationTime = messageExpirationTimer; - account.getContactStore().updateContact(contact); - sendExpirationTimerUpdate(address); - account.save(); + private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { + var contact = account.getContactStore().getContact(recipientId); + if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { + return; + } + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore() + .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); } private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException { @@ -1119,8 +1136,10 @@ public class Manager implements Closeable { public void setExpirationTimer( String number, int messageExpirationTimer ) throws IOException, InvalidNumberException { - var address = canonicalizeAndResolveSignalServiceAddress(number); - setExpirationTimer(address, messageExpirationTimer); + var recipientId = canonicalizeAndResolveRecipient(number); + setExpirationTimer(recipientId, messageExpirationTimer); + sendExpirationTimerUpdate(resolveSignalServiceAddress(recipientId)); + account.save(); } /** @@ -1298,6 +1317,7 @@ public class Manager implements Closeable { SignalServiceDataMessage.Builder messageBuilder, Collection recipients ) throws IOException { recipients = recipients.stream().map(this::resolveSignalServiceAddress).collect(Collectors.toSet()); + final var recipientIds = recipients.stream().map(this::resolveRecipient).collect(Collectors.toSet()); final var timestamp = System.currentTimeMillis(); messageBuilder.withTimestamp(timestamp); getOrCreateMessagePipe(); @@ -1310,7 +1330,7 @@ public class Manager implements Closeable { var messageSender = createMessageSender(); final var isRecipientUpdate = false; var result = messageSender.sendMessage(new ArrayList<>(recipients), - unidentifiedAccessHelper.getAccessFor(recipients), + unidentifiedAccessHelper.getAccessFor(recipientIds), isRecipientUpdate, message); @@ -1332,8 +1352,8 @@ public class Manager implements Closeable { messageBuilder.withProfileKey(account.getProfileKey().serialize()); var results = new ArrayList(recipients.size()); for (var address : recipients) { - final var contact = account.getContactStore().getContact(address); - final var expirationTime = contact != null ? contact.messageExpirationTime : 0; + final var contact = account.getContactStore().getContact(resolveRecipient(address)); + final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; messageBuilder.withExpiration(expirationTime); message = messageBuilder.build(); results.add(sendMessage(address, message)); @@ -1358,10 +1378,10 @@ public class Manager implements Closeable { getOrCreateMessagePipe(); getOrCreateUnidentifiedMessagePipe(); try { - final var address = getSelfAddress(); + final var recipientId = account.getSelfRecipientId(); - final var contact = account.getContactStore().getContact(address); - final var expirationTime = contact != null ? contact.messageExpirationTime : 0; + final var contact = account.getContactStore().getContact(recipientId); + final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; messageBuilder.withExpiration(expirationTime); var message = messageBuilder.build(); @@ -1377,7 +1397,7 @@ public class Manager implements Closeable { var recipient = account.getSelfAddress(); - final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(recipient); + final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(resolveRecipient(recipient)); var transcript = new SentTranscriptMessage(Optional.of(recipient), message.getTimestamp(), message, @@ -1404,7 +1424,9 @@ public class Manager implements Closeable { var messageSender = createMessageSender(); try { - return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message); + return messageSender.sendMessage(address, + unidentifiedAccessHelper.getAccessFor(resolveRecipient(address)), + message); } catch (UntrustedIdentityException e) { return SendMessageResult.identityFailure(address, e.getIdentityKey()); } @@ -1520,14 +1542,7 @@ public class Manager implements Closeable { // disappearing message timer already stored in the DecryptedGroup } } else if (conversationPartnerAddress != null) { - var contact = account.getContactStore().getContact(conversationPartnerAddress); - if (contact == null) { - contact = new ContactInfo(conversationPartnerAddress); - } - if (contact.messageExpirationTime != message.getExpiresInSeconds()) { - contact.messageExpirationTime = message.getExpiresInSeconds(); - account.getContactStore().updateContact(contact); - } + setExpirationTimer(resolveRecipient(conversationPartnerAddress), message.getExpiresInSeconds()); } } if (!ignoreAttachments) { @@ -1554,7 +1569,7 @@ public class Manager implements Closeable { if (source.matches(account.getSelfAddress())) { this.account.setProfileKey(profileKey); } - this.account.getProfileStore().storeProfileKey(source, profileKey); + this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey); } if (message.getPreviews().isPresent()) { final var previews = message.getPreviews().get(); @@ -1632,7 +1647,7 @@ public class Manager implements Closeable { private void storeProfileKeysFromMembers(final DecryptedGroup group) { for (var member : group.getMembersList()) { - final var address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(member.getUuid() + final var address = resolveRecipient(new SignalServiceAddress(UuidUtil.parseOrThrow(member.getUuid() .toByteArray()), null)); try { account.getProfileStore() @@ -1789,7 +1804,7 @@ public class Manager implements Closeable { if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) { final var recipientId = resolveRecipient(((org.whispersystems.libsignal.UntrustedIdentityException) exception) .getName()); - queuedActions.add(new RetrieveProfileAction(resolveSignalServiceAddress(recipientId))); + queuedActions.add(new RetrieveProfileAction(recipientId)); if (!envelope.hasSource()) { try { cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); @@ -1816,8 +1831,8 @@ public class Manager implements Closeable { } else { return false; } - var sourceContact = account.getContactStore().getContact(source); - if (sourceContact != null && sourceContact.blocked) { + final var recipientId = resolveRecipient(source); + if (isContactBlocked(recipientId)) { return true; } @@ -1834,6 +1849,16 @@ public class Manager implements Closeable { return false; } + public boolean isContactBlocked(final String identifier) throws InvalidNumberException { + final var recipientId = canonicalizeAndResolveRecipient(identifier); + return isContactBlocked(recipientId); + } + + private boolean isContactBlocked(final RecipientId recipientId) { + var sourceContact = account.getContactStore().getContact(recipientId); + return sourceContact != null && sourceContact.isBlocked(); + } + private boolean isNotAGroupMember( SignalServiceEnvelope envelope, SignalServiceContent content ) { @@ -1876,8 +1901,6 @@ public class Manager implements Closeable { } else { sender = content.getSender(); } - // Store uuid if we don't have it already - resolveSignalServiceAddress(sender); if (content.getDataMessage().isPresent()) { var message = content.getDataMessage().get(); @@ -1974,7 +1997,7 @@ public class Manager implements Closeable { if (syncMessage.getBlockedList().isPresent()) { final var blockedListMessage = syncMessage.getBlockedList().get(); for (var address : blockedListMessage.getAddresses()) { - setContactBlocked(resolveSignalServiceAddress(address), true); + setContactBlocked(resolveRecipient(address), true); } for (var groupId : blockedListMessage.getGroupIds() .stream() @@ -2001,19 +2024,19 @@ public class Manager implements Closeable { if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { account.setProfileKey(c.getProfileKey().get()); } - final var address = resolveSignalServiceAddress(c.getAddress()); - var contact = account.getContactStore().getContact(address); - if (contact == null) { - contact = new ContactInfo(address); - } + final var recipientId = resolveRecipientTrusted(c.getAddress()); + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null + ? Contact.newBuilder() + : Contact.newBuilder(contact); if (c.getName().isPresent()) { - contact.name = c.getName().get(); + builder.withName(c.getName().get()); } if (c.getColor().isPresent()) { - contact.color = c.getColor().get(); + builder.withColor(c.getColor().get()); } if (c.getProfileKey().isPresent()) { - account.getProfileStore().storeProfileKey(address, c.getProfileKey().get()); + account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get()); } if (c.getVerified().isPresent()) { final var verifiedMessage = c.getVerified().get(); @@ -2023,15 +2046,14 @@ public class Manager implements Closeable { TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (c.getExpirationTimer().isPresent()) { - contact.messageExpirationTime = c.getExpirationTimer().get(); + builder.withMessageExpirationTime(c.getExpirationTimer().get()); } - contact.blocked = c.isBlocked(); - contact.inboxPosition = c.getInboxPosition().orNull(); - contact.archived = c.isArchived(); - account.getContactStore().updateContact(contact); + builder.withBlocked(c.isBlocked()); + builder.withArchived(c.isArchived()); + account.getContactStore().storeContact(recipientId, builder.build()); if (c.getAvatar().isPresent()) { - downloadContactAvatar(c.getAvatar().get(), contact.getAddress()); + downloadContactAvatar(c.getAvatar().get(), c.getAddress()); } } } @@ -2079,7 +2101,7 @@ public class Manager implements Closeable { if (syncMessage.getFetchType().isPresent()) { switch (syncMessage.getFetchType().get()) { case LOCAL_PROFILE: - getRecipientProfile(getSelfAddress(), true); + getRecipientProfile(account.getSelfRecipientId(), true); case STORAGE_MANIFEST: // TODO } @@ -2294,28 +2316,31 @@ public class Manager implements Closeable { try { try (OutputStream fos = new FileOutputStream(contactsFile)) { var out = new DeviceContactsOutputStream(fos); - for (var record : account.getContactStore().getContacts()) { + for (var contactPair : account.getContactStore().getContacts()) { + final var recipientId = contactPair.first(); + final var contact = contactPair.second(); + final var address = resolveSignalServiceAddress(recipientId); + + var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId); VerifiedMessage verifiedMessage = null; - var currentIdentity = account.getIdentityKeyStore() - .getIdentity(resolveRecipientTrusted(record.getAddress())); if (currentIdentity != null) { - verifiedMessage = new VerifiedMessage(record.getAddress(), + verifiedMessage = new VerifiedMessage(address, currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime()); } - var profileKey = account.getProfileStore().getProfileKey(record.getAddress()); - out.write(new DeviceContact(record.getAddress(), - Optional.fromNullable(record.name), - createContactAvatarAttachment(record.getAddress()), - Optional.fromNullable(record.color), + var profileKey = account.getProfileStore().getProfileKey(recipientId); + out.write(new DeviceContact(address, + Optional.fromNullable(contact.getName()), + createContactAvatarAttachment(address), + Optional.fromNullable(contact.getColor()), Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), - record.blocked, - Optional.of(record.messageExpirationTime), - Optional.fromNullable(record.inboxPosition), - record.archived)); + contact.isBlocked(), + Optional.of(contact.getMessageExpirationTime()), + Optional.absent(), + contact.isArchived())); } if (account.getProfileKey() != null) { @@ -2356,8 +2381,8 @@ public class Manager implements Closeable { void sendBlockedList() throws IOException, UntrustedIdentityException { var addresses = new ArrayList(); for (var record : account.getContactStore().getContacts()) { - if (record.blocked) { - addresses.add(record.getAddress()); + if (record.second().isBlocked()) { + addresses.add(resolveSignalServiceAddress(record.first())); } } var groupIds = new ArrayList(); @@ -2379,22 +2404,25 @@ public class Manager implements Closeable { sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); } - public List getContacts() { + public List> getContacts() { return account.getContactStore().getContacts(); } - public String getContactOrProfileName(String number) { - final var address = Utils.getSignalServiceAddressFromIdentifier(number); + public String getContactOrProfileName(String number) throws InvalidNumberException { + final var recipientId = canonicalizeAndResolveRecipient(number); + final var recipient = account.getRecipientStore().getRecipient(recipientId); + if (recipient == null) { + return null; + } - final var contact = account.getContactStore().getContact(address); - if (contact != null && !Util.isEmpty(contact.name)) { - return contact.name; + if (recipient.getContact() != null && !Util.isEmpty(recipient.getContact().getName())) { + return recipient.getContact().getName(); } - final var profileEntry = account.getProfileStore().getProfileEntry(address); - if (profileEntry != null && profileEntry.getProfile() != null) { - return profileEntry.getProfile().getDisplayName(); + if (recipient.getProfile() != null && recipient.getProfile() != null) { + return recipient.getProfile().getDisplayName(); } + return null; } @@ -2530,11 +2558,11 @@ public class Manager implements Closeable { } public RecipientId resolveRecipient(SignalServiceAddress address) { - return account.getRecipientStore().resolveRecipientUntrusted(address); + return account.getRecipientStore().resolveRecipient(address); } private RecipientId resolveRecipientTrusted(SignalServiceAddress address) { - return account.getRecipientStore().resolveRecipient(address); + return account.getRecipientStore().resolveRecipientTrusted(address); } @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 9c1eecce..b3c524c6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -165,7 +165,7 @@ public class RegistrationManager implements Closeable { account.setUuid(UuidUtil.parseOrNull(response.getUuid())); account.setRegistrationLockPin(pin); account.getSessionStore().archiveAllSessions(); - final var recipientId = account.getRecipientStore().resolveRecipient(account.getSelfAddress()); + final var recipientId = account.getRecipientStore().resolveRecipientTrusted(account.getSelfAddress()); final var publicKey = account.getIdentityKeyPair().getPublicKey(); account.getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date()); account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index c76075be..849314da 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -5,7 +5,8 @@ import com.google.protobuf.InvalidProtocolBufferException; 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.manager.storage.recipients.Profile; +import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.util.IOUtils; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.GroupChange; @@ -38,7 +39,6 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.Collection; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -51,7 +51,7 @@ public class GroupHelper { private final ProfileProvider profileProvider; - private final SelfAddressProvider selfAddressProvider; + private final SelfRecipientIdProvider selfRecipientIdProvider; private final GroupsV2Operations groupsV2Operations; @@ -59,20 +59,24 @@ public class GroupHelper { private final GroupAuthorizationProvider groupAuthorizationProvider; + private final SignalServiceAddressResolver addressResolver; + public GroupHelper( final ProfileKeyCredentialProvider profileKeyCredentialProvider, final ProfileProvider profileProvider, - final SelfAddressProvider selfAddressProvider, + final SelfRecipientIdProvider selfRecipientIdProvider, final GroupsV2Operations groupsV2Operations, final GroupsV2Api groupsV2Api, - final GroupAuthorizationProvider groupAuthorizationProvider + final GroupAuthorizationProvider groupAuthorizationProvider, + final SignalServiceAddressResolver addressResolver ) { this.profileKeyCredentialProvider = profileKeyCredentialProvider; this.profileProvider = profileProvider; - this.selfAddressProvider = selfAddressProvider; + this.selfRecipientIdProvider = selfRecipientIdProvider; this.groupsV2Operations = groupsV2Operations; this.groupsV2Api = groupsV2Api; this.groupAuthorizationProvider = groupAuthorizationProvider; + this.addressResolver = addressResolver; } public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) { @@ -97,7 +101,7 @@ public class GroupHelper { } public GroupInfoV2 createGroupV2( - String name, Collection members, File avatarFile + String name, Set members, File avatarFile ) throws IOException { final var avatarBytes = readAvatarBytes(avatarFile); final var newGroup = buildNewGroupV2(name, members, avatarBytes); @@ -139,9 +143,9 @@ public class GroupHelper { } private GroupsV2Operations.NewGroup buildNewGroupV2( - String name, Collection members, byte[] avatar + String name, Set members, byte[] avatar ) { - final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfAddressProvider.getSelfAddress()); + final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientIdProvider.getSelfRecipientId()); if (profileKeyCredential == null) { logger.warn("Cannot create a V2 group as self does not have a versioned profile"); return null; @@ -149,10 +153,11 @@ public class GroupHelper { if (!areMembersValid(members)) return null; - var self = new GroupCandidate(selfAddressProvider.getSelfAddress().getUuid().orNull(), - Optional.fromNullable(profileKeyCredential)); + var self = new GroupCandidate(addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) + .getUuid() + .orNull(), Optional.fromNullable(profileKeyCredential)); var candidates = members.stream() - .map(member -> new GroupCandidate(member.getUuid().get(), + .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(), Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member)))) .collect(Collectors.toSet()); @@ -166,8 +171,9 @@ public class GroupHelper { 0); } - private boolean areMembersValid(final Collection members) { + private boolean areMembersValid(final Set members) { final var noUuidCapability = members.stream() + .map(addressResolver::resolveSignalServiceAddress) .filter(address -> !address.getUuid().isPresent()) .map(SignalServiceAddress::getLegacyIdentifier) .collect(Collectors.toSet()); @@ -179,11 +185,11 @@ public class GroupHelper { final var noGv2Capability = members.stream() .map(profileProvider::getProfile) - .filter(profile -> profile != null && !profile.getCapabilities().gv2) + .filter(profile -> profile != null && !profile.getCapabilities().contains(Profile.Capability.gv2)) .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::getDisplayName).collect(Collectors.joining(", "))); + noGv2Capability.stream().map(Profile::getDisplayName).collect(Collectors.joining(", "))); return false; } @@ -206,7 +212,8 @@ public class GroupHelper { change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey)); } - final var uuid = this.selfAddressProvider.getSelfAddress().getUuid(); + final var uuid = addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId()) + .getUuid(); if (uuid.isPresent()) { change.setSourceUuid(UuidUtil.toByteString(uuid.get())); } @@ -215,7 +222,7 @@ public class GroupHelper { } public Pair updateGroupV2( - GroupInfoV2 groupInfoV2, Set newMembers + GroupInfoV2 groupInfoV2, Set newMembers ) throws IOException { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); var groupOperations = groupsV2Operations.forGroup(groupSecretParams); @@ -225,24 +232,25 @@ public class GroupHelper { } var candidates = newMembers.stream() - .map(member -> new GroupCandidate(member.getUuid().get(), + .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(), Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member)))) .collect(Collectors.toSet()); - final var change = groupOperations.createModifyGroupMembershipChange(candidates, - selfAddressProvider.getSelfAddress().getUuid().get()); + final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) + .getUuid() + .get(); + final var change = groupOperations.createModifyGroupMembershipChange(candidates, uuid); - final var uuid = this.selfAddressProvider.getSelfAddress().getUuid(); - if (uuid.isPresent()) { - change.setSourceUuid(UuidUtil.toByteString(uuid.get())); - } + change.setSourceUuid(UuidUtil.toByteString(uuid)); return commitChange(groupInfoV2, change); } public Pair leaveGroup(GroupInfoV2 groupInfoV2) throws IOException { var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList(); - final var selfUuid = selfAddressProvider.getSelfAddress().getUuid().get(); + final var selfUuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) + .getUuid() + .get(); var selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfUuid); if (selfPendingMember.isPresent()) { @@ -260,8 +268,8 @@ public class GroupHelper { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); final var groupOperations = groupsV2Operations.forGroup(groupSecretParams); - final var selfAddress = this.selfAddressProvider.getSelfAddress(); - final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfAddress); + final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId(); + final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId); if (profileKeyCredential == null) { throw new IOException("Cannot join a V2 group as self does not have a versioned profile"); } @@ -271,7 +279,9 @@ public class GroupHelper { ? groupOperations.createGroupJoinRequest(profileKeyCredential) : groupOperations.createGroupJoinDirect(profileKeyCredential); - change.setSourceUuid(UuidUtil.toByteString(selfAddress.getUuid().get())); + change.setSourceUuid(UuidUtil.toByteString(addressResolver.resolveSignalServiceAddress(selfRecipientId) + .getUuid() + .get())); return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword); } @@ -280,15 +290,15 @@ public class GroupHelper { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); final var groupOperations = groupsV2Operations.forGroup(groupSecretParams); - final var selfAddress = this.selfAddressProvider.getSelfAddress(); - final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfAddress); + final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId(); + final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId); if (profileKeyCredential == null) { throw new IOException("Cannot join a V2 group as self does not have a versioned profile"); } final var change = groupOperations.createAcceptInviteChange(profileKeyCredential); - final var uuid = selfAddress.getUuid(); + final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientId).getUuid(); if (uuid.isPresent()) { change.setSourceUuid(UuidUtil.toByteString(uuid.get())); } @@ -330,7 +340,9 @@ public class GroupHelper { try { decryptedChange = groupOperations.decryptChange(changeActions, - selfAddressProvider.getSelfAddress().getUuid().get()); + addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) + .getUuid() + .get()); decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange); } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) { throw new IOException(e); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 5411bb06..2676135b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -1,5 +1,6 @@ package org.asamk.signal.manager.helper; +import org.asamk.signal.manager.storage.recipients.RecipientId; import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; @@ -27,23 +28,27 @@ public final class ProfileHelper { private final MessageReceiverProvider messageReceiverProvider; + private final SignalServiceAddressResolver addressResolver; + public ProfileHelper( final ProfileKeyProvider profileKeyProvider, final UnidentifiedAccessProvider unidentifiedAccessProvider, final MessagePipeProvider messagePipeProvider, - final MessageReceiverProvider messageReceiverProvider + final MessageReceiverProvider messageReceiverProvider, + final SignalServiceAddressResolver addressResolver ) { this.profileKeyProvider = profileKeyProvider; this.unidentifiedAccessProvider = unidentifiedAccessProvider; this.messagePipeProvider = messagePipeProvider; this.messageReceiverProvider = messageReceiverProvider; + this.addressResolver = addressResolver; } public ProfileAndCredential retrieveProfileSync( - SignalServiceAddress recipient, SignalServiceProfile.RequestType requestType + RecipientId recipientId, SignalServiceProfile.RequestType requestType ) throws IOException { try { - return retrieveProfile(recipient, requestType).get(10, TimeUnit.SECONDS); + return retrieveProfile(recipientId, requestType).get(10, TimeUnit.SECONDS); } catch (ExecutionException e) { if (e.getCause() instanceof PushNetworkException) { throw (PushNetworkException) e.getCause(); @@ -58,11 +63,12 @@ public final class ProfileHelper { } public ListenableFuture retrieveProfile( - SignalServiceAddress address, SignalServiceProfile.RequestType requestType + RecipientId recipientId, SignalServiceProfile.RequestType requestType ) { - var unidentifiedAccess = getUnidentifiedAccess(address); - var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(address)); + var unidentifiedAccess = getUnidentifiedAccess(recipientId); + var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId)); + final var address = addressResolver.resolveSignalServiceAddress(recipientId); if (unidentifiedAccess.isPresent()) { return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address, profileKey, @@ -126,8 +132,8 @@ public final class ProfileHelper { } } - private Optional getUnidentifiedAccess(SignalServiceAddress recipient) { - var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipient); + private Optional getUnidentifiedAccess(RecipientId recipientId) { + var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId); if (unidentifiedAccess.isPresent()) { return unidentifiedAccess.get().getTargetUnidentifiedAccess(); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyCredentialProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyCredentialProvider.java index ebb728c1..71d50046 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyCredentialProvider.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyCredentialProvider.java @@ -1,9 +1,9 @@ package org.asamk.signal.manager.helper; +import org.asamk.signal.manager.storage.recipients.RecipientId; import org.signal.zkgroup.profiles.ProfileKeyCredential; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; public interface ProfileKeyCredentialProvider { - ProfileKeyCredential getProfileKeyCredential(SignalServiceAddress address); + ProfileKeyCredential getProfileKeyCredential(RecipientId recipientId); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java index 9172710e..b98d674e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java @@ -1,9 +1,9 @@ package org.asamk.signal.manager.helper; +import org.asamk.signal.manager.storage.recipients.RecipientId; import org.signal.zkgroup.profiles.ProfileKey; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; public interface ProfileKeyProvider { - ProfileKey getProfileKey(SignalServiceAddress address); + ProfileKey getProfileKey(RecipientId address); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java index c16b5e0d..22915a95 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java @@ -1,9 +1,9 @@ package org.asamk.signal.manager.helper; -import org.asamk.signal.manager.storage.profiles.SignalProfile; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.asamk.signal.manager.storage.recipients.Profile; +import org.asamk.signal.manager.storage.recipients.RecipientId; public interface ProfileProvider { - SignalProfile getProfile(SignalServiceAddress address); + Profile getProfile(RecipientId address); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SelfAddressProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/SelfAddressProvider.java deleted file mode 100644 index 3591064f..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SelfAddressProvider.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.asamk.signal.manager.helper; - -import org.whispersystems.signalservice.api.push.SignalServiceAddress; - -public interface SelfAddressProvider { - - SignalServiceAddress getSelfAddress(); -} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SelfRecipientIdProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/SelfRecipientIdProvider.java new file mode 100644 index 00000000..83eb3b2b --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SelfRecipientIdProvider.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public interface SelfRecipientIdProvider { + + RecipientId getSelfRecipientId(); +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SignalServiceAddressResolver.java b/lib/src/main/java/org/asamk/signal/manager/helper/SignalServiceAddressResolver.java new file mode 100644 index 00000000..73efd323 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SignalServiceAddressResolver.java @@ -0,0 +1,9 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public interface SignalServiceAddressResolver { + + SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId); +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java index a3b8e3b5..0b82fb18 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java @@ -1,10 +1,10 @@ package org.asamk.signal.manager.helper; +import org.asamk.signal.manager.storage.recipients.RecipientId; import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Collection; import java.util.List; @@ -38,22 +38,25 @@ public class UnidentifiedAccessHelper { return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey()); } - public byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { - var theirProfileKey = profileKeyProvider.getProfileKey(recipient); - if (theirProfileKey == null) { - return null; - } - + public byte[] getTargetUnidentifiedAccessKey(RecipientId recipient) { var targetProfile = profileProvider.getProfile(recipient); - if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) { + if (targetProfile == null) { return null; } - if (targetProfile.isUnrestrictedUnidentifiedAccess()) { - return createUnrestrictedUnidentifiedAccess(); + switch (targetProfile.getUnidentifiedAccessMode()) { + case ENABLED: + var theirProfileKey = profileKeyProvider.getProfileKey(recipient); + if (theirProfileKey == null) { + return null; + } + + return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); + case UNRESTRICTED: + return createUnrestrictedUnidentifiedAccess(); + default: + return null; } - - return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); } public Optional getAccessForSync() { @@ -73,11 +76,11 @@ public class UnidentifiedAccessHelper { } } - public List> getAccessFor(Collection recipients) { + public List> getAccessFor(Collection recipients) { return recipients.stream().map(this::getAccessFor).collect(Collectors.toList()); } - public Optional getAccessFor(SignalServiceAddress recipient) { + public Optional getAccessFor(RecipientId recipient) { var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate(); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessProvider.java index a4b65a6f..cf2c4e4c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessProvider.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessProvider.java @@ -1,10 +1,10 @@ package org.asamk.signal.manager.helper; +import org.asamk.signal.manager.storage.recipients.RecipientId; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; public interface UnidentifiedAccessProvider { - Optional getAccessFor(SignalServiceAddress address); + Optional getAccessFor(RecipientId recipientId); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 9aa283a2..a8cb4591 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -10,18 +10,21 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.storage.contacts.ContactInfo; -import org.asamk.signal.manager.storage.contacts.JsonContactsStore; +import org.asamk.signal.manager.storage.contacts.ContactsStore; +import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore; import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.groups.JsonGroupStore; import org.asamk.signal.manager.storage.identities.IdentityKeyStore; import org.asamk.signal.manager.storage.messageCache.MessageCache; import org.asamk.signal.manager.storage.prekeys.PreKeyStore; import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore; +import org.asamk.signal.manager.storage.profiles.LegacyProfileStore; import org.asamk.signal.manager.storage.profiles.ProfileStore; import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore; import org.asamk.signal.manager.storage.protocol.SignalProtocolStore; +import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore; +import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientStore; import org.asamk.signal.manager.storage.sessions.SessionStore; @@ -45,6 +48,7 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.storage.StorageKey; +import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -57,9 +61,9 @@ import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.util.Base64; +import java.util.HashSet; import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; public class SignalAccount implements Closeable { @@ -88,9 +92,7 @@ public class SignalAccount implements Closeable { private SessionStore sessionStore; private IdentityKeyStore identityKeyStore; private JsonGroupStore groupStore; - private JsonContactsStore contactStore; private RecipientStore recipientStore; - private ProfileStore profileStore; private StickerStore stickerStore; private MessageCache messageCache; @@ -136,7 +138,6 @@ public class SignalAccount implements Closeable { account.username = username; account.profileKey = profileKey; account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username)); - account.contactStore = new JsonContactsStore(); account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), account::mergeRecipients); account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username)); @@ -151,7 +152,6 @@ public class SignalAccount implements Closeable { account.signedPreKeyStore, account.sessionStore, account.identityKeyStore); - account.profileStore = new ProfileStore(); account.stickerStore = new StickerStore(); account.messageCache = new MessageCache(getMessageCachePath(dataPath, username)); @@ -188,9 +188,9 @@ public class SignalAccount implements Closeable { account.profileKey = profileKey; account.deviceId = deviceId; account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username)); - account.contactStore = new JsonContactsStore(); account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), account::mergeRecipients); + account.recipientStore.resolveRecipientTrusted(account.getSelfAddress()); account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username)); account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username)); account.sessionStore = new SessionStore(getSessionsPath(dataPath, username), @@ -203,7 +203,6 @@ public class SignalAccount implements Closeable { account.signedPreKeyStore, account.sessionStore, account.identityKeyStore); - account.profileStore = new ProfileStore(); account.stickerStore = new StickerStore(); account.messageCache = new MessageCache(getMessageCachePath(dataPath, username)); @@ -222,23 +221,8 @@ public class SignalAccount implements Closeable { setProfileKey(KeyUtils.createProfileKey()); save(); } - // Store profile keys only in profile store - for (var contact : getContactStore().getContacts()) { - var profileKeyString = contact.profileKey; - if (profileKeyString == null) { - continue; - } - final ProfileKey profileKey; - try { - profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString)); - } catch (InvalidInputException ignored) { - continue; - } - contact.profileKey = null; - getProfileStore().storeProfileKey(contact.getAddress(), profileKey); - } // Ensure our profile key is stored in profile store - getProfileStore().storeProfileKey(getSelfAddress(), getProfileKey()); + getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey()); } private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { @@ -354,13 +338,15 @@ public class SignalAccount implements Closeable { } recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients); + var legacyRecipientStoreNode = rootNode.get("recipientStore"); if (legacyRecipientStoreNode != null) { logger.debug("Migrating legacy recipient store."); var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class); if (legacyRecipientStore != null) { - recipientStore.resolveRecipients(legacyRecipientStore.getAddresses()); + recipientStore.resolveRecipientsTrusted(legacyRecipientStore.getAddresses()); } + recipientStore.resolveRecipientTrusted(getSelfAddress()); } var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore") @@ -414,9 +400,9 @@ public class SignalAccount implements Closeable { identityKeyPair, registrationId); if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) { - logger.debug("Migrating identity session store."); + logger.debug("Migrating legacy identity session store."); for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) { - RecipientId recipientId = recipientStore.resolveRecipient(identity.getAddress()); + RecipientId recipientId = recipientStore.resolveRecipientTrusted(identity.getAddress()); identityKeyStore.saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded()); identityKeyStore.setIdentityTrustLevel(recipientId, identity.getIdentityKey(), @@ -436,20 +422,67 @@ public class SignalAccount implements Closeable { groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username)); } - var contactStoreNode = rootNode.get("contactStore"); - if (contactStoreNode != null) { - contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class); - } - if (contactStore == null) { - contactStore = new JsonContactsStore(); + if (rootNode.hasNonNull("contactStore")) { + logger.debug("Migrating legacy contact store."); + final var contactStoreNode = rootNode.get("contactStore"); + final var contactStore = jsonProcessor.convertValue(contactStoreNode, LegacyJsonContactsStore.class); + for (var contact : contactStore.getContacts()) { + final var recipientId = recipientStore.resolveRecipientTrusted(contact.getAddress()); + recipientStore.storeContact(recipientId, + new Contact(contact.name, + contact.color, + contact.messageExpirationTime, + contact.blocked, + contact.archived)); + + // Store profile keys only in profile store + var profileKeyString = contact.profileKey; + if (profileKeyString != null) { + final ProfileKey profileKey; + try { + profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString)); + getProfileStore().storeProfileKey(recipientId, profileKey); + } catch (InvalidInputException e) { + logger.warn("Failed to parse legacy contact profile key: {}", e.getMessage()); + } + } + } } - var profileStoreNode = rootNode.get("profileStore"); - if (profileStoreNode != null) { - profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class); - } - if (profileStore == null) { - profileStore = new ProfileStore(); + if (rootNode.hasNonNull("profileStore")) { + logger.debug("Migrating legacy profile store."); + var profileStoreNode = rootNode.get("profileStore"); + final var legacyProfileStore = jsonProcessor.convertValue(profileStoreNode, LegacyProfileStore.class); + for (var profileEntry : legacyProfileStore.getProfileEntries()) { + var recipientId = recipientStore.resolveRecipient(profileEntry.getServiceAddress()); + recipientStore.storeProfileKey(recipientId, profileEntry.getProfileKey()); + recipientStore.storeProfileKeyCredential(recipientId, profileEntry.getProfileKeyCredential()); + final var profile = profileEntry.getProfile(); + if (profile != null) { + final var capabilities = new HashSet(); + if (profile.getCapabilities().gv1Migration) { + capabilities.add(Profile.Capability.gv1Migration); + } + if (profile.getCapabilities().gv2) { + capabilities.add(Profile.Capability.gv2); + } + if (profile.getCapabilities().storage) { + capabilities.add(Profile.Capability.storage); + } + final var newProfile = new Profile(profileEntry.getLastUpdateTimestamp(), + profile.getGivenName(), + profile.getFamilyName(), + profile.getAbout(), + profile.getAboutEmoji(), + profile.isUnrestrictedUnidentifiedAccess() + ? Profile.UnidentifiedAccessMode.UNRESTRICTED + : profile.getUnidentifiedAccess() != null + ? Profile.UnidentifiedAccessMode.ENABLED + : Profile.UnidentifiedAccessMode.DISABLED, + capabilities); + recipientStore.storeProfile(recipientId, newProfile); + } + } } var stickerStoreNode = rootNode.get("stickerStore"); @@ -460,24 +493,6 @@ public class SignalAccount implements Closeable { stickerStore = new StickerStore(); } - if (recipientStore.isEmpty()) { - recipientStore.resolveRecipient(getSelfAddress()); - - recipientStore.resolveRecipients(contactStore.getContacts() - .stream() - .map(ContactInfo::getAddress) - .collect(Collectors.toList())); - - for (var group : groupStore.getGroups()) { - if (group instanceof GroupInfoV1) { - var groupInfoV1 = (GroupInfoV1) group; - groupInfoV1.members = groupInfoV1.members.stream() - .map(m -> recipientStore.resolveServiceAddress(m)) - .collect(Collectors.toSet()); - } - } - } - messageCache = new MessageCache(getMessageCachePath(dataPath, username)); var threadStoreNode = rootNode.get("threadStore"); @@ -489,10 +504,15 @@ public class SignalAccount implements Closeable { continue; } try { - var contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id)); - if (contactInfo != null) { - contactInfo.messageExpirationTime = thread.messageExpirationTime; - contactStore.updateContact(contactInfo); + if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) { + final var recipientId = recipientStore.resolveRecipient(thread.id); + var contact = recipientStore.getContact(recipientId); + if (contact != null) { + recipientStore.storeContact(recipientId, + Contact.newBuilder(contact) + .withMessageExpirationTime(thread.messageExpirationTime) + .build()); + } } else { var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id)); if (groupInfo instanceof GroupInfoV1) { @@ -500,7 +520,8 @@ public class SignalAccount implements Closeable { groupStore.updateGroup(groupInfo); } } - } catch (Exception ignored) { + } catch (Exception e) { + logger.warn("Failed to read legacy thread info: {}", e.getMessage()); } } } @@ -533,8 +554,6 @@ public class SignalAccount implements Closeable { .put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize())) .put("registered", registered) .putPOJO("groupStore", groupStore) - .putPOJO("contactStore", contactStore) - .putPOJO("profileStore", profileStore) .putPOJO("stickerStore", stickerStore); try { try (var output = new ByteArrayOutputStream()) { @@ -602,8 +621,8 @@ public class SignalAccount implements Closeable { return groupStore; } - public JsonContactsStore getContactStore() { - return contactStore; + public ContactsStore getContactStore() { + return recipientStore; } public RecipientStore getRecipientStore() { @@ -611,7 +630,7 @@ public class SignalAccount implements Closeable { } public ProfileStore getProfileStore() { - return profileStore; + return recipientStore; } public StickerStore getStickerStore() { @@ -638,6 +657,10 @@ public class SignalAccount implements Closeable { return new SignalServiceAddress(uuid, username); } + public RecipientId getSelfRecipientId() { + return recipientStore.resolveRecipientTrusted(getSelfAddress()); + } + public int getDeviceId() { return deviceId; } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/ContactsStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/ContactsStore.java new file mode 100644 index 00000000..25d429e8 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/ContactsStore.java @@ -0,0 +1,16 @@ +package org.asamk.signal.manager.storage.contacts; + +import org.asamk.signal.manager.storage.recipients.Contact; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.whispersystems.libsignal.util.Pair; + +import java.util.List; + +public interface ContactsStore { + + void storeContact(RecipientId recipientId, Contact contact); + + Contact getContact(RecipientId recipientId); + + List> getContacts(); +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/JsonContactsStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/JsonContactsStore.java deleted file mode 100644 index b80dfe95..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/JsonContactsStore.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.asamk.signal.manager.storage.contacts; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import org.whispersystems.signalservice.api.push.SignalServiceAddress; - -import java.util.ArrayList; -import java.util.List; - -public class JsonContactsStore { - - @JsonProperty("contacts") - private List contacts = new ArrayList<>(); - - public void updateContact(ContactInfo contact) { - final var contactAddress = contact.getAddress(); - for (var i = 0; i < contacts.size(); i++) { - if (contacts.get(i).getAddress().matches(contactAddress)) { - contacts.set(i, contact); - return; - } - } - - contacts.add(contact); - } - - public ContactInfo getContact(SignalServiceAddress address) { - for (var contact : contacts) { - if (contact.getAddress().matches(address)) { - if (contact.uuid == null) { - contact.uuid = address.getUuid().orNull(); - } else if (contact.number == null) { - contact.number = address.getNumber().orNull(); - } - - return contact; - } - } - return null; - } - - public List getContacts() { - return new ArrayList<>(contacts); - } - - /** - * Remove all contacts from the store - */ - public void clear() { - contacts.clear(); - } -} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/ContactInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java similarity index 82% rename from lib/src/main/java/org/asamk/signal/manager/storage/contacts/ContactInfo.java rename to lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java index 4dd132f7..a90f87e0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/ContactInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java @@ -9,7 +9,7 @@ import java.util.UUID; import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY; -public class ContactInfo { +public class LegacyContactInfo { @JsonProperty public String name; @@ -38,12 +38,7 @@ public class ContactInfo { @JsonProperty(defaultValue = "false") public boolean archived; - public ContactInfo() { - } - - public ContactInfo(SignalServiceAddress address) { - this.number = address.getNumber().orNull(); - this.uuid = address.getUuid().orNull(); + public LegacyContactInfo() { } @JsonIgnore diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyJsonContactsStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyJsonContactsStore.java new file mode 100644 index 00000000..229e1e97 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyJsonContactsStore.java @@ -0,0 +1,19 @@ +package org.asamk.signal.manager.storage.contacts; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +public class LegacyJsonContactsStore { + + @JsonProperty("contacts") + private final List contacts = new ArrayList<>(); + + private LegacyJsonContactsStore() { + } + + public List getContacts() { + return contacts; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java new file mode 100644 index 00000000..8e1d5c88 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java @@ -0,0 +1,75 @@ +package org.asamk.signal.manager.storage.profiles; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; +import org.signal.zkgroup.profiles.ProfileKeyCredential; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +public class LegacyProfileStore { + + private static final ObjectMapper jsonProcessor = new ObjectMapper(); + + @JsonProperty("profiles") + @JsonDeserialize(using = ProfileStoreDeserializer.class) + private final List profiles = new ArrayList<>(); + + public List getProfileEntries() { + return profiles; + } + + public static class ProfileStoreDeserializer extends JsonDeserializer> { + + @Override + public List deserialize( + JsonParser jsonParser, DeserializationContext deserializationContext + ) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + var profileEntries = new ArrayList(); + + if (node.isArray()) { + for (var entry : node) { + var name = entry.hasNonNull("name") ? entry.get("name").asText() : null; + var uuid = entry.hasNonNull("uuid") ? UuidUtil.parseOrNull(entry.get("uuid").asText()) : null; + final var serviceAddress = new SignalServiceAddress(uuid, name); + ProfileKey profileKey = null; + try { + profileKey = new ProfileKey(Base64.getDecoder().decode(entry.get("profileKey").asText())); + } catch (InvalidInputException ignored) { + } + ProfileKeyCredential profileKeyCredential = null; + if (entry.hasNonNull("profileKeyCredential")) { + try { + profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder() + .decode(entry.get("profileKeyCredential").asText())); + } catch (Throwable ignored) { + } + } + var lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong(); + var profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class); + profileEntries.add(new LegacySignalProfileEntry(serviceAddress, + profileKey, + lastUpdateTimestamp, + profile, + profileKeyCredential)); + } + } + + return profileEntries; + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfileEntry.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java similarity index 82% rename from lib/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfileEntry.java rename to lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java index a81fbcb5..1e2f7ec8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfileEntry.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java @@ -4,7 +4,7 @@ import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -public class SignalProfileEntry { +public class LegacySignalProfileEntry { private final SignalServiceAddress serviceAddress; @@ -16,9 +16,7 @@ public class SignalProfileEntry { private final ProfileKeyCredential profileKeyCredential; - private boolean requestPending; - - public SignalProfileEntry( + public LegacySignalProfileEntry( final SignalServiceAddress serviceAddress, final ProfileKey profileKey, final long lastUpdateTimestamp, @@ -51,12 +49,4 @@ public class SignalProfileEntry { public ProfileKeyCredential getProfileKeyCredential() { return profileKeyCredential; } - - public boolean isRequestPending() { - return requestPending; - } - - public void setRequestPending(final boolean requestPending) { - this.requestPending = requestPending; - } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java index dc69a7ee..c600f0fb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java @@ -1,156 +1,21 @@ package org.asamk.signal.manager.storage.profiles; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -import org.signal.zkgroup.InvalidInputException; +import org.asamk.signal.manager.storage.recipients.Profile; +import org.asamk.signal.manager.storage.recipients.RecipientId; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.UuidUtil; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; - -public class ProfileStore { - - private static final ObjectMapper jsonProcessor = new ObjectMapper(); - - @JsonProperty("profiles") - @JsonDeserialize(using = ProfileStoreDeserializer.class) - @JsonSerialize(using = ProfileStoreSerializer.class) - private final List profiles = new ArrayList<>(); - - public SignalProfileEntry getProfileEntry(SignalServiceAddress serviceAddress) { - for (var entry : profiles) { - if (entry.getServiceAddress().matches(serviceAddress)) { - return entry; - } - } - return null; - } - - public ProfileKey getProfileKey(SignalServiceAddress serviceAddress) { - for (var entry : profiles) { - if (entry.getServiceAddress().matches(serviceAddress)) { - return entry.getProfileKey(); - } - } - return null; - } - - public void updateProfile( - SignalServiceAddress serviceAddress, - ProfileKey profileKey, - long now, - SignalProfile profile, - ProfileKeyCredential profileKeyCredential - ) { - var newEntry = new SignalProfileEntry(serviceAddress, profileKey, now, profile, profileKeyCredential); - for (var i = 0; i < profiles.size(); i++) { - if (profiles.get(i).getServiceAddress().matches(serviceAddress)) { - profiles.set(i, newEntry); - return; - } - } - - profiles.add(newEntry); - } - - public void storeProfileKey(SignalServiceAddress serviceAddress, ProfileKey profileKey) { - var newEntry = new SignalProfileEntry(serviceAddress, profileKey, 0, null, null); - for (var i = 0; i < profiles.size(); i++) { - if (profiles.get(i).getServiceAddress().matches(serviceAddress)) { - if (!profiles.get(i).getProfileKey().equals(profileKey)) { - profiles.set(i, newEntry); - } - return; - } - } - - profiles.add(newEntry); - } - public static class ProfileStoreDeserializer extends JsonDeserializer> { +public interface ProfileStore { - @Override - public List deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext - ) throws IOException { - JsonNode node = jsonParser.getCodec().readTree(jsonParser); + Profile getProfile(RecipientId recipientId); - var addresses = new ArrayList(); + ProfileKey getProfileKey(RecipientId recipientId); - if (node.isArray()) { - for (var entry : node) { - var name = entry.hasNonNull("name") ? entry.get("name").asText() : null; - var uuid = entry.hasNonNull("uuid") ? UuidUtil.parseOrNull(entry.get("uuid").asText()) : null; - final var serviceAddress = new SignalServiceAddress(uuid, name); - ProfileKey profileKey = null; - try { - profileKey = new ProfileKey(Base64.getDecoder().decode(entry.get("profileKey").asText())); - } catch (InvalidInputException ignored) { - } - ProfileKeyCredential profileKeyCredential = null; - if (entry.hasNonNull("profileKeyCredential")) { - try { - profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder() - .decode(entry.get("profileKeyCredential").asText())); - } catch (Throwable ignored) { - } - } - var lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong(); - var profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class); - addresses.add(new SignalProfileEntry(serviceAddress, - profileKey, - lastUpdateTimestamp, - profile, - profileKeyCredential)); - } - } + ProfileKeyCredential getProfileKeyCredential(RecipientId recipientId); - return addresses; - } - } + void storeProfile(RecipientId recipientId, Profile profile); - public static class ProfileStoreSerializer extends JsonSerializer> { + void storeProfileKey(RecipientId recipientId, ProfileKey profileKey); - @Override - public void serialize( - List profiles, JsonGenerator json, SerializerProvider serializerProvider - ) throws IOException { - json.writeStartArray(); - for (var profileEntry : profiles) { - final var address = profileEntry.getServiceAddress(); - json.writeStartObject(); - if (address.getNumber().isPresent()) { - json.writeStringField("name", address.getNumber().get()); - } - if (address.getUuid().isPresent()) { - json.writeStringField("uuid", address.getUuid().get().toString()); - } - json.writeStringField("profileKey", - Base64.getEncoder().encodeToString(profileEntry.getProfileKey().serialize())); - json.writeNumberField("lastUpdateTimestamp", profileEntry.getLastUpdateTimestamp()); - json.writeObjectField("profile", profileEntry.getProfile()); - if (profileEntry.getProfileKeyCredential() != null) { - json.writeStringField("profileKeyCredential", - Base64.getEncoder().encodeToString(profileEntry.getProfileKeyCredential().serialize())); - } - json.writeEndObject(); - } - json.writeEndArray(); - } - } + void storeProfileKeyCredential(RecipientId recipientId, ProfileKeyCredential profileKeyCredential); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfile.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfile.java index 45201e18..60d9aefc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfile.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfile.java @@ -3,12 +3,11 @@ package org.asamk.signal.manager.storage.profiles; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; - public class SignalProfile { @JsonProperty - private final String identityKey; + @JsonIgnore + private String identityKey; @JsonProperty private final String name; @@ -29,28 +28,6 @@ public class SignalProfile { private final Capabilities capabilities; public SignalProfile( - final String identityKey, - final String name, - final String about, - final String aboutEmoji, - final String unidentifiedAccess, - final boolean unrestrictedUnidentifiedAccess, - final SignalServiceProfile.Capabilities capabilities - ) { - this.identityKey = identityKey; - this.name = name; - this.about = about; - this.aboutEmoji = aboutEmoji; - this.unidentifiedAccess = unidentifiedAccess; - this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; - this.capabilities = new Capabilities(); - this.capabilities.storage = capabilities.isStorage(); - this.capabilities.gv1Migration = capabilities.isGv1Migration(); - this.capabilities.gv2 = capabilities.isGv2(); - } - - public SignalProfile( - @JsonProperty("identityKey") final String identityKey, @JsonProperty("name") final String name, @JsonProperty("about") final String about, @JsonProperty("aboutEmoji") final String aboutEmoji, @@ -58,7 +35,6 @@ public class SignalProfile { @JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess, @JsonProperty("capabilities") final Capabilities capabilities ) { - this.identityKey = identityKey; this.name = name; this.about = about; this.aboutEmoji = aboutEmoji; @@ -67,17 +43,24 @@ public class SignalProfile { this.capabilities = capabilities; } - public String getIdentityKey() { - return identityKey; - } + public String getGivenName() { + if (name == null) { + return null; + } + + String[] parts = name.split("\0"); - public String getName() { - return name; + return parts.length < 1 ? null : parts[0]; } - public String getDisplayName() { - // First name and last name (if set) are separated by a NULL char + trim space in case only one is filled - return name == null ? "" : name.replace("\0", " ").trim(); + public String getFamilyName() { + if (name == null) { + return null; + } + + String[] parts = name.split("\0"); + + return parts.length < 2 ? null : parts[1]; } public String getAbout() { @@ -100,31 +83,6 @@ public class SignalProfile { return capabilities; } - @Override - public String toString() { - return "SignalProfile{" - + "identityKey='" - + identityKey - + '\'' - + ", name='" - + name - + '\'' - + ", about='" - + about - + '\'' - + ", aboutEmoji='" - + aboutEmoji - + '\'' - + ", unidentifiedAccess='" - + unidentifiedAccess - + '\'' - + ", unrestrictedUnidentifiedAccess=" - + unrestrictedUnidentifiedAccess - + ", capabilities=" - + capabilities - + '}'; - } - public static class Capabilities { @JsonIgnore diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Contact.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Contact.java new file mode 100644 index 00000000..f2867c9f --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Contact.java @@ -0,0 +1,111 @@ +package org.asamk.signal.manager.storage.recipients; + +public class Contact { + + private final String name; + + private final String color; + + private final int messageExpirationTime; + + private final boolean blocked; + + private final boolean archived; + + public Contact( + final String name, + final String color, + final int messageExpirationTime, + final boolean blocked, + final boolean archived + ) { + this.name = name; + this.color = color; + this.messageExpirationTime = messageExpirationTime; + this.blocked = blocked; + this.archived = archived; + } + + private Contact(final Builder builder) { + name = builder.name; + color = builder.color; + messageExpirationTime = builder.messageExpirationTime; + blocked = builder.blocked; + archived = builder.archived; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static Builder newBuilder(final Contact copy) { + Builder builder = new Builder(); + builder.name = copy.getName(); + builder.color = copy.getColor(); + builder.messageExpirationTime = copy.getMessageExpirationTime(); + builder.blocked = copy.isBlocked(); + builder.archived = copy.isArchived(); + return builder; + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } + + public int getMessageExpirationTime() { + return messageExpirationTime; + } + + public boolean isBlocked() { + return blocked; + } + + public boolean isArchived() { + return archived; + } + + public static final class Builder { + + private String name; + private String color; + private int messageExpirationTime; + private boolean blocked; + private boolean archived; + + private Builder() { + } + + public Builder withName(final String val) { + name = val; + return this; + } + + public Builder withColor(final String val) { + color = val; + return this; + } + + public Builder withMessageExpirationTime(final int val) { + messageExpirationTime = val; + return this; + } + + public Builder withBlocked(final boolean val) { + blocked = val; + return this; + } + + public Builder withArchived(final boolean val) { + archived = val; + return this; + } + + public Contact build() { + return new Contact(this); + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java new file mode 100644 index 00000000..dcdc8964 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java @@ -0,0 +1,199 @@ +package org.asamk.signal.manager.storage.recipients; + +import org.whispersystems.signalservice.internal.util.Util; + +import java.util.Collections; +import java.util.Set; + +public class Profile { + + private final long lastUpdateTimestamp; + + private final String givenName; + + private final String familyName; + + private final String about; + + private final String aboutEmoji; + + private final UnidentifiedAccessMode unidentifiedAccessMode; + + private final Set capabilities; + + public Profile( + final long lastUpdateTimestamp, + final String givenName, + final String familyName, + final String about, + final String aboutEmoji, + final UnidentifiedAccessMode unidentifiedAccessMode, + final Set capabilities + ) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + this.givenName = givenName; + this.familyName = familyName; + this.about = about; + this.aboutEmoji = aboutEmoji; + this.unidentifiedAccessMode = unidentifiedAccessMode; + this.capabilities = capabilities; + } + + private Profile(final Builder builder) { + lastUpdateTimestamp = builder.lastUpdateTimestamp; + givenName = builder.givenName; + familyName = builder.familyName; + about = builder.about; + aboutEmoji = builder.aboutEmoji; + unidentifiedAccessMode = builder.unidentifiedAccessMode; + capabilities = builder.capabilities; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static Builder newBuilder(final Profile copy) { + Builder builder = new Builder(); + builder.lastUpdateTimestamp = copy.getLastUpdateTimestamp(); + builder.givenName = copy.getGivenName(); + builder.familyName = copy.getFamilyName(); + builder.about = copy.getAbout(); + builder.aboutEmoji = copy.getAboutEmoji(); + builder.unidentifiedAccessMode = copy.getUnidentifiedAccessMode(); + builder.capabilities = copy.getCapabilities(); + return builder; + } + + public long getLastUpdateTimestamp() { + return lastUpdateTimestamp; + } + + public String getGivenName() { + return givenName; + } + + public String getFamilyName() { + return familyName; + } + + public String getInternalServiceName() { + if (familyName == null) { + return givenName == null ? "" : givenName; + } + return String.join("\0", givenName == null ? "" : givenName, familyName); + } + + public String getDisplayName() { + final var noGivenName = Util.isEmpty(givenName); + final var noFamilyName = Util.isEmpty(familyName); + + if (noGivenName && noFamilyName) { + return ""; + } else if (noGivenName) { + return familyName; + } else if (noFamilyName) { + return givenName; + } + + return givenName + " " + familyName; + } + + public String getAbout() { + return about; + } + + public String getAboutEmoji() { + return aboutEmoji; + } + + public UnidentifiedAccessMode getUnidentifiedAccessMode() { + return unidentifiedAccessMode; + } + + public Set getCapabilities() { + return capabilities; + } + + public enum UnidentifiedAccessMode { + UNKNOWN, + DISABLED, + ENABLED, + UNRESTRICTED; + + static UnidentifiedAccessMode valueOfOrUnknown(String value) { + try { + return valueOf(value); + } catch (IllegalArgumentException ignored) { + return UNKNOWN; + } + } + } + + public enum Capability { + gv2, + storage, + gv1Migration; + + static Capability valueOfOrNull(String value) { + try { + return valueOf(value); + } catch (IllegalArgumentException ignored) { + return null; + } + } + } + + public static final class Builder { + + private String givenName; + private String familyName; + private String about; + private String aboutEmoji; + private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; + private Set capabilities = Collections.emptySet(); + private long lastUpdateTimestamp = 0; + + private Builder() { + } + + public Builder withGivenName(final String val) { + givenName = val; + return this; + } + + public Builder withFamilyName(final String val) { + familyName = val; + return this; + } + + public Builder withAbout(final String val) { + about = val; + return this; + } + + public Builder withAboutEmoji(final String val) { + aboutEmoji = val; + return this; + } + + public Builder withUnidentifiedAccessMode(final UnidentifiedAccessMode val) { + unidentifiedAccessMode = val; + return this; + } + + public Builder withCapabilities(final Set val) { + capabilities = val; + return this; + } + + public Profile build() { + return new Profile(this); + } + + public Builder withLastUpdateTimestamp(final long val) { + lastUpdateTimestamp = val; + return this; + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java new file mode 100644 index 00000000..3ccf8210 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java @@ -0,0 +1,131 @@ +package org.asamk.signal.manager.storage.recipients; + +import org.signal.zkgroup.profiles.ProfileKey; +import org.signal.zkgroup.profiles.ProfileKeyCredential; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public class Recipient { + + private final RecipientId recipientId; + + private final SignalServiceAddress address; + + private final Contact contact; + + private final ProfileKey profileKey; + + private final ProfileKeyCredential profileKeyCredential; + + private final Profile profile; + + public Recipient( + final RecipientId recipientId, + final SignalServiceAddress address, + final Contact contact, + final ProfileKey profileKey, + final ProfileKeyCredential profileKeyCredential, + final Profile profile + ) { + this.recipientId = recipientId; + this.address = address; + this.contact = contact; + this.profileKey = profileKey; + this.profileKeyCredential = profileKeyCredential; + this.profile = profile; + } + + private Recipient(final Builder builder) { + recipientId = builder.recipientId; + address = builder.address; + contact = builder.contact; + profileKey = builder.profileKey; + profileKeyCredential = builder.profileKeyCredential; + profile = builder.profile; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static Builder newBuilder(final Recipient copy) { + Builder builder = new Builder(); + builder.recipientId = copy.getRecipientId(); + builder.address = copy.getAddress(); + builder.contact = copy.getContact(); + builder.profileKey = copy.getProfileKey(); + builder.profileKeyCredential = copy.getProfileKeyCredential(); + builder.profile = copy.getProfile(); + return builder; + } + + public RecipientId getRecipientId() { + return recipientId; + } + + public SignalServiceAddress getAddress() { + return address; + } + + public Contact getContact() { + return contact; + } + + public ProfileKey getProfileKey() { + return profileKey; + } + + public ProfileKeyCredential getProfileKeyCredential() { + return profileKeyCredential; + } + + public Profile getProfile() { + return profile; + } + + public static final class Builder { + + private RecipientId recipientId; + private SignalServiceAddress address; + private Contact contact; + private ProfileKey profileKey; + private ProfileKeyCredential profileKeyCredential; + private Profile profile; + + private Builder() { + } + + public Builder withRecipientId(final RecipientId val) { + recipientId = val; + return this; + } + + public Builder withAddress(final SignalServiceAddress val) { + address = val; + return this; + } + + public Builder withContact(final Contact val) { + contact = val; + return this; + } + + public Builder withProfileKey(final ProfileKey val) { + profileKey = val; + return this; + } + + public Builder withProfileKeyCredential(final ProfileKeyCredential val) { + profileKeyCredential = val; + return this; + } + + public Builder withProfile(final Profile val) { + profile = val; + return this; + } + + public Recipient build() { + return new Recipient(this); + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 7235b662..b400e45d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -3,6 +3,11 @@ package org.asamk.signal.manager.storage.recipients; import com.fasterxml.jackson.databind.ObjectMapper; import org.asamk.signal.manager.storage.Utils; +import org.asamk.signal.manager.storage.contacts.ContactsStore; +import org.asamk.signal.manager.storage.profiles.ProfileStore; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; +import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.util.Pair; @@ -17,14 +22,17 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; -public class RecipientStore { +public class RecipientStore implements ContactsStore, ProfileStore { private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class); @@ -32,7 +40,7 @@ public class RecipientStore { private final File file; private final RecipientMergeHandler recipientMergeHandler; - private final Map recipients; + private final Map recipients; private final Map recipientsMerged = new HashMap<>(); private long lastId; @@ -40,16 +48,57 @@ public class RecipientStore { public static RecipientStore load(File file, RecipientMergeHandler recipientMergeHandler) throws IOException { final var objectMapper = Utils.createStorageObjectMapper(); try (var inputStream = new FileInputStream(file)) { - var storage = objectMapper.readValue(inputStream, Storage.class); - return new RecipientStore(objectMapper, - file, - recipientMergeHandler, - storage.recipients.stream() - .collect(Collectors.toMap(r -> new RecipientId(r.id), - r -> new SignalServiceAddress(org.whispersystems.libsignal.util.guava.Optional.fromNullable( - r.uuid).transform(UuidUtil::parseOrThrow), - org.whispersystems.libsignal.util.guava.Optional.fromNullable(r.name)))), - storage.lastId); + final var storage = objectMapper.readValue(inputStream, Storage.class); + final var recipients = storage.recipients.stream().map(r -> { + final var recipientId = new RecipientId(r.id); + final var address = new SignalServiceAddress(org.whispersystems.libsignal.util.guava.Optional.fromNullable( + r.uuid).transform(UuidUtil::parseOrThrow), + org.whispersystems.libsignal.util.guava.Optional.fromNullable(r.number)); + + Contact contact = null; + if (r.contact != null) { + contact = new Contact(r.contact.name, + r.contact.color, + r.contact.messageExpirationTime, + r.contact.blocked, + r.contact.archived); + } + + ProfileKey profileKey = null; + if (r.profileKey != null) { + try { + profileKey = new ProfileKey(Base64.getDecoder().decode(r.profileKey)); + } catch (InvalidInputException ignored) { + } + } + + ProfileKeyCredential profileKeyCredential = null; + if (r.profileKeyCredential != null) { + try { + profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder() + .decode(r.profileKeyCredential)); + } catch (Throwable ignored) { + } + } + + Profile profile = null; + if (r.profile != null) { + profile = new Profile(r.profile.lastUpdateTimestamp, + r.profile.givenName, + r.profile.familyName, + r.profile.about, + r.profile.aboutEmoji, + Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode), + r.profile.capabilities.stream() + .map(Profile.Capability::valueOfOrNull) + .filter(Objects::nonNull) + .collect(Collectors.toSet())); + } + + return new Recipient(recipientId, address, contact, profileKey, profileKeyCredential, profile); + }).collect(Collectors.toMap(Recipient::getRecipientId, r -> r)); + + return new RecipientStore(objectMapper, file, recipientMergeHandler, recipients, storage.lastId); } catch (FileNotFoundException e) { logger.debug("Creating new recipient store."); return new RecipientStore(objectMapper, file, recipientMergeHandler, new HashMap<>(), 0); @@ -60,7 +109,7 @@ public class RecipientStore { final ObjectMapper objectMapper, final File file, final RecipientMergeHandler recipientMergeHandler, - final Map recipients, + final Map recipients, final long lastId ) { this.objectMapper = objectMapper; @@ -71,6 +120,12 @@ public class RecipientStore { } public SignalServiceAddress resolveServiceAddress(RecipientId recipientId) { + synchronized (recipients) { + return getRecipient(recipientId).getAddress(); + } + } + + public Recipient getRecipient(RecipientId recipientId) { synchronized (recipients) { while (recipientsMerged.containsKey(recipientId)) { recipientId = recipientsMerged.get(recipientId); @@ -92,11 +147,11 @@ public class RecipientStore { return resolveRecipient(new SignalServiceAddress(null, number), false); } - public RecipientId resolveRecipient(SignalServiceAddress address) { + public RecipientId resolveRecipientTrusted(SignalServiceAddress address) { return resolveRecipient(address, true); } - public List resolveRecipients(List addresses) { + public List resolveRecipientsTrusted(List addresses) { final List recipientIds; final List> toBeMerged = new ArrayList<>(); synchronized (recipients) { @@ -114,10 +169,84 @@ public class RecipientStore { return recipientIds; } - public RecipientId resolveRecipientUntrusted(SignalServiceAddress address) { + public RecipientId resolveRecipient(SignalServiceAddress address) { return resolveRecipient(address, false); } + @Override + public void storeContact(final RecipientId recipientId, final Contact contact) { + synchronized (recipients) { + final var recipient = recipients.get(recipientId); + storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withContact(contact).build()); + } + } + + @Override + public Contact getContact(final RecipientId recipientId) { + final var recipient = getRecipient(recipientId); + return recipient == null ? null : recipient.getContact(); + } + + @Override + public List> getContacts() { + return recipients.entrySet() + .stream() + .filter(e -> e.getValue().getContact() != null) + .map(e -> new Pair<>(e.getKey(), e.getValue().getContact())) + .collect(Collectors.toList()); + } + + @Override + public Profile getProfile(final RecipientId recipientId) { + final var recipient = getRecipient(recipientId); + return recipient == null ? null : recipient.getProfile(); + } + + @Override + public ProfileKey getProfileKey(final RecipientId recipientId) { + final var recipient = getRecipient(recipientId); + return recipient == null ? null : recipient.getProfileKey(); + } + + @Override + public ProfileKeyCredential getProfileKeyCredential(final RecipientId recipientId) { + final var recipient = getRecipient(recipientId); + return recipient == null ? null : recipient.getProfileKeyCredential(); + } + + @Override + public void storeProfile(final RecipientId recipientId, final Profile profile) { + synchronized (recipients) { + final var recipient = recipients.get(recipientId); + storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withProfile(profile).build()); + } + } + + @Override + public void storeProfileKey(final RecipientId recipientId, final ProfileKey profileKey) { + synchronized (recipients) { + final var recipient = recipients.get(recipientId); + storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withProfileKey(profileKey).build()); + } + } + + @Override + public void storeProfileKeyCredential( + final RecipientId recipientId, final ProfileKeyCredential profileKeyCredential + ) { + synchronized (recipients) { + final var recipient = recipients.get(recipientId); + storeRecipientLocked(recipientId, + Recipient.newBuilder(recipient).withProfileKeyCredential(profileKeyCredential).build()); + } + } + + public boolean isEmpty() { + synchronized (recipients) { + return recipients.isEmpty(); + } + } + /** * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source. * Has no effect, if the address contains only a number or a uuid. @@ -141,20 +270,20 @@ public class RecipientStore { SignalServiceAddress address, boolean isHighTrust ) { final var byNumber = !address.getNumber().isPresent() - ? Optional.empty() - : findByName(address.getNumber().get()); + ? Optional.empty() + : findByNameLocked(address.getNumber().get()); final var byUuid = !address.getUuid().isPresent() - ? Optional.empty() - : findByUuid(address.getUuid().get()); + ? Optional.empty() + : findByUuidLocked(address.getUuid().get()); if (byNumber.isEmpty() && byUuid.isEmpty()) { logger.debug("Got new recipient, both uuid and number are unknown"); if (isHighTrust || !address.getUuid().isPresent() || !address.getNumber().isPresent()) { - return new Pair<>(addNewRecipient(address), Optional.empty()); + return new Pair<>(addNewRecipientLocked(address), Optional.empty()); } - return new Pair<>(addNewRecipient(new SignalServiceAddress(address.getUuid().get(), null)), + return new Pair<>(addNewRecipientLocked(new SignalServiceAddress(address.getUuid().get(), null)), Optional.empty()); } @@ -162,79 +291,138 @@ public class RecipientStore { || !address.getUuid().isPresent() || !address.getNumber().isPresent() || byNumber.equals(byUuid)) { - return new Pair<>(byUuid.orElseGet(byNumber::get), Optional.empty()); + return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty()); } if (byNumber.isEmpty()) { logger.debug("Got recipient existing with uuid, updating with high trust number"); - recipients.put(byUuid.get(), address); - save(); - return new Pair<>(byUuid.get(), Optional.empty()); + updateRecipientAddressLocked(byUuid.get().getRecipientId(), address); + return new Pair<>(byUuid.get().getRecipientId(), Optional.empty()); } if (byUuid.isEmpty()) { logger.debug("Got recipient existing with number, updating with high trust uuid"); - recipients.put(byNumber.get(), address); - save(); - return new Pair<>(byNumber.get(), Optional.empty()); + updateRecipientAddressLocked(byNumber.get().getRecipientId(), address); + return new Pair<>(byNumber.get().getRecipientId(), Optional.empty()); } - final var byNumberAddress = recipients.get(byNumber.get()); - if (byNumberAddress.getUuid().isPresent()) { + if (byNumber.get().getAddress().getUuid().isPresent()) { logger.debug( "Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number"); - recipients.put(byNumber.get(), new SignalServiceAddress(byNumberAddress.getUuid().get(), null)); - recipients.put(byUuid.get(), address); - save(); - return new Pair<>(byUuid.get(), Optional.empty()); + updateRecipientAddressLocked(byNumber.get().getRecipientId(), + new SignalServiceAddress(byNumber.get().getAddress().getUuid().get(), null)); + updateRecipientAddressLocked(byUuid.get().getRecipientId(), address); + return new Pair<>(byUuid.get().getRecipientId(), Optional.empty()); } logger.debug("Got separate recipients for high trust number and uuid, need to merge them"); - recipients.put(byUuid.get(), address); - recipients.remove(byNumber.get()); - save(); - return new Pair<>(byUuid.get(), byNumber); + updateRecipientAddressLocked(byUuid.get().getRecipientId(), address); + mergeRecipientsLocked(byUuid.get().getRecipientId(), byNumber.get().getRecipientId()); + return new Pair<>(byUuid.get().getRecipientId(), byNumber.map(Recipient::getRecipientId)); } - private RecipientId addNewRecipient(final SignalServiceAddress serviceAddress) { - final var nextRecipientId = nextId(); - recipients.put(nextRecipientId, serviceAddress); - save(); + private RecipientId addNewRecipientLocked(final SignalServiceAddress serviceAddress) { + final var nextRecipientId = nextIdLocked(); + storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, serviceAddress, null, null, null, null)); return nextRecipientId; } - private Optional findByName(final String number) { + private void updateRecipientAddressLocked( + final RecipientId recipientId, final SignalServiceAddress address + ) { + final var nextRecipientId = nextIdLocked(); + final var recipient = recipients.get(recipientId); + storeRecipientLocked(nextRecipientId, Recipient.newBuilder(recipient).withAddress(address).build()); + } + + private void storeRecipientLocked( + final RecipientId recipientId, final Recipient recipient + ) { + recipients.put(recipientId, recipient); + saveLocked(); + } + + private void mergeRecipientsLocked(RecipientId recipientId, RecipientId toBeMergedRecipientId) { + final var recipient = recipients.get(recipientId); + final var toBeMergedRecipient = recipients.get(toBeMergedRecipientId); + recipients.put(recipientId, + new Recipient(recipientId, + recipient.getAddress(), + recipient.getContact() != null ? recipient.getContact() : toBeMergedRecipient.getContact(), + recipient.getProfileKey() != null + ? recipient.getProfileKey() + : toBeMergedRecipient.getProfileKey(), + recipient.getProfileKeyCredential() != null + ? recipient.getProfileKeyCredential() + : toBeMergedRecipient.getProfileKeyCredential(), + recipient.getProfile() != null ? recipient.getProfile() : toBeMergedRecipient.getProfile())); + recipients.remove(toBeMergedRecipientId); + saveLocked(); + } + + private Optional findByNameLocked(final String number) { return recipients.entrySet() .stream() - .filter(entry -> entry.getValue().getNumber().isPresent() && number.equals(entry.getValue() + .filter(entry -> entry.getValue().getAddress().getNumber().isPresent() && number.equals(entry.getValue() + .getAddress() .getNumber() .get())) .findFirst() - .map(Map.Entry::getKey); + .map(Map.Entry::getValue); } - private Optional findByUuid(final UUID uuid) { + private Optional findByUuidLocked(final UUID uuid) { return recipients.entrySet() .stream() - .filter(entry -> entry.getValue().getUuid().isPresent() && uuid.equals(entry.getValue() + .filter(entry -> entry.getValue().getAddress().getUuid().isPresent() && uuid.equals(entry.getValue() + .getAddress() .getUuid() .get())) .findFirst() - .map(Map.Entry::getKey); + .map(Map.Entry::getValue); } - private RecipientId nextId() { + private RecipientId nextIdLocked() { return new RecipientId(++this.lastId); } - private void save() { - var storage = new Storage(recipients.entrySet() - .stream() - .map(pair -> new Storage.Recipient(pair.getKey().getId(), - pair.getValue().getNumber().orNull(), - pair.getValue().getUuid().transform(UUID::toString).orNull())) - .collect(Collectors.toList()), lastId); + private void saveLocked() { + final var base64 = Base64.getEncoder(); + var storage = new Storage(recipients.entrySet().stream().map(pair -> { + final var recipient = pair.getValue(); + final var contact = recipient.getContact() == null + ? null + : new Storage.Recipient.Contact(recipient.getContact().getName(), + recipient.getContact().getColor(), + recipient.getContact().getMessageExpirationTime(), + recipient.getContact().isBlocked(), + recipient.getContact().isArchived()); + final var profile = recipient.getProfile() == null + ? null + : new Storage.Recipient.Profile(recipient.getProfile().getLastUpdateTimestamp(), + recipient.getProfile().getGivenName(), + recipient.getProfile().getFamilyName(), + recipient.getProfile().getAbout(), + recipient.getProfile().getAboutEmoji(), + recipient.getProfile().getUnidentifiedAccessMode().name(), + recipient.getProfile() + .getCapabilities() + .stream() + .map(Enum::name) + .collect(Collectors.toSet())); + return new Storage.Recipient(pair.getKey().getId(), + recipient.getAddress().getNumber().orNull(), + recipient.getAddress().getUuid().transform(UUID::toString).orNull(), + recipient.getProfileKey() == null + ? null + : base64.encodeToString(recipient.getProfileKey().serialize()), + recipient.getProfileKeyCredential() == null + ? null + : base64.encodeToString(recipient.getProfileKeyCredential().serialize()), + contact, + profile); + }).collect(Collectors.toList()), lastId); // Write to memory first to prevent corrupting the file in case of serialization errors try (var inMemoryOutput = new ByteArrayOutputStream()) { @@ -249,17 +437,11 @@ public class RecipientStore { } } - public boolean isEmpty() { - synchronized (recipients) { - return recipients.isEmpty(); - } - } - private static class Storage { - private List recipients; + public List recipients; - private long lastId; + public long lastId; // For deserialization private Storage() { @@ -270,40 +452,96 @@ public class RecipientStore { this.lastId = lastId; } - public List getRecipients() { - return recipients; - } - - public long getLastId() { - return lastId; - } + private static class Recipient { - public static class Recipient { - - private long id; - private String name; - private String uuid; + public long id; + public String number; + public String uuid; + public String profileKey; + public String profileKeyCredential; + public Contact contact; + public Profile profile; // For deserialization private Recipient() { } - public Recipient(final long id, final String name, final String uuid) { + public Recipient( + final long id, + final String number, + final String uuid, + final String profileKey, + final String profileKeyCredential, + final Contact contact, + final Profile profile + ) { this.id = id; - this.name = name; + this.number = number; this.uuid = uuid; + this.profileKey = profileKey; + this.profileKeyCredential = profileKeyCredential; + this.contact = contact; + this.profile = profile; } - public long getId() { - return id; - } + private static class Contact { - public String getName() { - return name; + public String name; + public String color; + public int messageExpirationTime; + public boolean blocked; + public boolean archived; + + // For deserialization + public Contact() { + } + + public Contact( + final String name, + final String color, + final int messageExpirationTime, + final boolean blocked, + final boolean archived + ) { + this.name = name; + this.color = color; + this.messageExpirationTime = messageExpirationTime; + this.blocked = blocked; + this.archived = archived; + } } - public String getUuid() { - return uuid; + private static class Profile { + + public long lastUpdateTimestamp; + public String givenName; + public String familyName; + public String about; + public String aboutEmoji; + public String unidentifiedAccessMode; + public Set capabilities; + + // For deserialization + private Profile() { + } + + public Profile( + final long lastUpdateTimestamp, + final String givenName, + final String familyName, + final String about, + final String aboutEmoji, + final String unidentifiedAccessMode, + final Set capabilities + ) { + this.lastUpdateTimestamp = lastUpdateTimestamp; + this.givenName = givenName; + this.familyName = familyName; + this.about = about; + this.aboutEmoji = aboutEmoji; + this.unidentifiedAccessMode = unidentifiedAccessMode; + this.capabilities = capabilities; + } } } } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java index 63820b51..244f34f6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java @@ -1,16 +1,19 @@ package org.asamk.signal.manager.util; -import org.asamk.signal.manager.storage.profiles.SignalProfile; +import org.asamk.signal.manager.storage.recipients.Profile; import org.signal.zkgroup.profiles.ProfileKey; +import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import java.util.Base64; +import java.util.Date; +import java.util.HashSet; public class ProfileUtils { - public static SignalProfile decryptProfile( + public static Profile decryptProfile( final ProfileKey profileKey, final SignalServiceProfile encryptedProfile ) { var profileCipher = new ProfileCipher(profileKey); @@ -28,13 +31,28 @@ public class ProfileUtils { } catch (IllegalArgumentException e) { unidentifiedAccess = null; } - return new SignalProfile(encryptedProfile.getIdentityKey(), - name, + final var nameParts = splitName(name); + final var capabilities = new HashSet(); + if (encryptedProfile.getCapabilities().isGv1Migration()) { + capabilities.add(Profile.Capability.gv1Migration); + } + if (encryptedProfile.getCapabilities().isGv2()) { + capabilities.add(Profile.Capability.gv2); + } + if (encryptedProfile.getCapabilities().isStorage()) { + capabilities.add(Profile.Capability.storage); + } + return new Profile(new Date().getTime(), + nameParts.first(), + nameParts.second(), about, aboutEmoji, - unidentifiedAccess, - encryptedProfile.isUnrestrictedUnidentifiedAccess(), - encryptedProfile.getCapabilities()); + encryptedProfile.isUnrestrictedUnidentifiedAccess() + ? Profile.UnidentifiedAccessMode.UNRESTRICTED + : unidentifiedAccess != null + ? Profile.UnidentifiedAccessMode.ENABLED + : Profile.UnidentifiedAccessMode.DISABLED, + capabilities); } catch (InvalidCiphertextException e) { return null; } @@ -51,4 +69,17 @@ public class ProfileUtils { return null; } } + + private static Pair splitName(String name) { + String[] parts = name.split("\0"); + + switch (parts.length) { + case 0: + return new Pair<>(null, null); + case 1: + return new Pair<>(parts[0], null); + default: + return new Pair<>(parts[0], parts[1]); + } + } } diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 0093ab9b..e7a21b88 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -87,7 +87,7 @@ public interface Signal extends DBusInterface { void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure; - boolean isContactBlocked(final String number); + boolean isContactBlocked(final String number) throws Error.InvalidNumber; boolean isGroupBlocked(final byte[] groupId); diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 91df4b06..2aa7dfca 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -18,6 +18,7 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; import java.util.Base64; @@ -667,7 +668,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { private String formatContact(SignalServiceAddress address) { final var number = address.getLegacyIdentifier(); - var name = m.getContactOrProfileName(number); + String name = null; + try { + name = m.getContactOrProfileName(number); + } catch (InvalidNumberException ignored) { + } if (name == null || name.isEmpty()) { return number; } else { diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index 4b27e17d..577db3db 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -18,7 +18,10 @@ public class ListContactsCommand implements LocalCommand { var contacts = m.getContacts(); for (var c : contacts) { - writer.println("Number: {} Name: {} Blocked: {}", c.number, c.name, c.blocked); + writer.println("Number: {} Name: {} Blocked: {}", + m.resolveSignalServiceAddress(c.first()).getLegacyIdentifier(), + c.second().getName(), + c.second().isBlocked()); } } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index f50363cb..e58f7829 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -11,6 +11,7 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.util.ErrorUtils; import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -248,7 +249,7 @@ public class DbusSignalImpl implements Signal { public String getContactName(final String number) { try { return m.getContactOrProfileName(number); - } catch (Exception e) { + } catch (InvalidNumberException e) { throw new Error.InvalidNumber(e.getMessage()); } } @@ -383,11 +384,10 @@ public class DbusSignalImpl implements Signal { // all numbers the system knows @Override public List listNumbers() { - return Stream.concat(m.getIdentities() - .stream() - .map(IdentityInfo::getRecipientId) + return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId), + m.getContacts().stream().map(Pair::first)) .map(m::resolveSignalServiceAddress) - .map(a -> a.getNumber().orNull()), m.getContacts().stream().map(c -> c.number)) + .map(a -> a.getNumber().orNull()) .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); @@ -399,8 +399,8 @@ public class DbusSignalImpl implements Signal { var numbers = new ArrayList(); var contacts = m.getContacts(); for (var c : contacts) { - if (c.name != null && c.name.equals(name)) { - numbers.add(c.number); + if (name.equals(c.second().getName())) { + numbers.add(m.resolveSignalServiceAddress(c.first()).getLegacyIdentifier()); } } // Try profiles if no contact name was found @@ -449,13 +449,11 @@ public class DbusSignalImpl implements Signal { @Override public boolean isContactBlocked(final String number) { - var contacts = m.getContacts(); - for (var c : contacts) { - if (c.number.equals(number)) { - return c.blocked; - } + try { + return m.isContactBlocked(number); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); } - return false; } @Override