Refactor contact and profile store

This commit is contained in:
AsamK 2021-04-30 22:17:13 +02:00
parent a96bd91770
commit 224d8194cc
31 changed files with 1393 additions and 729 deletions

View file

@ -1,6 +1,7 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import org.asamk.signal.manager.groups.GroupIdV1; import org.asamk.signal.manager.groups.GroupIdV1;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Objects; import java.util.Objects;
@ -160,15 +161,15 @@ class SendGroupInfoAction implements HandleAction {
class RetrieveProfileAction implements HandleAction { class RetrieveProfileAction implements HandleAction {
private final SignalServiceAddress address; private final RecipientId recipientId;
public RetrieveProfileAction(final SignalServiceAddress address) { public RetrieveProfileAction(final RecipientId recipientId) {
this.address = address; this.recipientId = recipientId;
} }
@Override @Override
public void execute(Manager m) throws Throwable { public void execute(Manager m) throws Throwable {
m.getRecipientProfile(address, true); m.getRecipientProfile(recipientId, true);
} }
@Override @Override
@ -178,11 +179,11 @@ class RetrieveProfileAction implements HandleAction {
final RetrieveProfileAction that = (RetrieveProfileAction) o; final RetrieveProfileAction that = (RetrieveProfileAction) o;
return address.equals(that.address); return recipientId.equals(that.recipientId);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return address.hashCode(); return recipientId.hashCode();
} }
} }

View file

@ -30,13 +30,13 @@ import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.ProfileHelper;
import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
import org.asamk.signal.manager.storage.SignalAccount; 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.GroupInfo;
import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.manager.storage.messageCache.CachedMessage; 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.recipients.RecipientId;
import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.storage.stickers.Sticker;
import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.AttachmentUtils;
@ -243,13 +243,15 @@ public class Manager implements Closeable {
this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey, this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
unidentifiedAccessHelper::getAccessFor, unidentifiedAccessHelper::getAccessFor,
unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(), unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(),
() -> messageReceiver); () -> messageReceiver,
this::resolveSignalServiceAddress);
this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential, this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
this::getRecipientProfile, this::getRecipientProfile,
account::getSelfAddress, account::getSelfRecipientId,
groupsV2Operations, groupsV2Operations,
groupsV2Api, groupsV2Api,
this::getGroupAuthForToday); this::getGroupAuthForToday,
this::resolveSignalServiceAddress);
this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); 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 * if it's Optional.absent(), the avatar will be removed
*/ */
public void setProfile(String name, String about, String aboutEmoji, Optional<File> avatar) throws IOException { public void setProfile(String name, String about, String aboutEmoji, Optional<File> avatar) throws IOException {
var profileEntry = account.getProfileStore().getProfileEntry(getSelfAddress()); var profile = getRecipientProfile(account.getSelfRecipientId());
var profile = profileEntry == null ? null : profileEntry.getProfile(); var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
var newProfile = new SignalProfile(profile == null ? null : profile.getIdentityKey(), if (name != null) {
name != null ? name : profile == null || profile.getName() == null ? "" : profile.getName(), builder.withGivenName(name);
about != null ? about : profile == null || profile.getAbout() == null ? "" : profile.getAbout(), builder.withFamilyName(null);
aboutEmoji != null }
? aboutEmoji if (about != null) {
: profile == null || profile.getAboutEmoji() == null ? "" : profile.getAboutEmoji(), builder.withAbout(about);
profile == null ? null : profile.getUnidentifiedAccess(), }
account.isUnrestrictedUnidentifiedAccess(), if (aboutEmoji != null) {
profile == null ? null : profile.getCapabilities()); builder.withAboutEmoji(aboutEmoji);
}
var newProfile = builder.build();
try (final var streamDetails = avatar == null try (final var streamDetails = avatar == null
? avatarStore.retrieveProfileAvatar(getSelfAddress()) ? avatarStore.retrieveProfileAvatar(getSelfAddress())
: avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) { : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
accountManager.setVersionedProfile(account.getUuid(), accountManager.setVersionedProfile(account.getUuid(),
account.getProfileKey(), account.getProfileKey(),
newProfile.getName(), newProfile.getInternalServiceName(),
newProfile.getAbout(), newProfile.getAbout(),
newProfile.getAboutEmoji(), newProfile.getAboutEmoji(),
streamDetails); streamDetails);
@ -386,12 +390,7 @@ public class Manager implements Closeable {
avatarStore.deleteProfileAvatar(getSelfAddress()); avatarStore.deleteProfileAvatar(getSelfAddress());
} }
} }
account.getProfileStore() account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile);
.updateProfile(getSelfAddress(),
account.getProfileKey(),
System.currentTimeMillis(),
newProfile,
profileEntry == null ? null : profileEntry.getProfileKeyCredential());
try { try {
sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
@ -527,99 +526,124 @@ public class Manager implements Closeable {
ServiceConfig.AUTOMATIC_NETWORK_RETRY); ServiceConfig.AUTOMATIC_NETWORK_RETRY);
} }
public SignalProfile getRecipientProfile( public Profile getRecipientProfile(
SignalServiceAddress address SignalServiceAddress address
) { ) {
return getRecipientProfile(address, false); return getRecipientProfile(resolveRecipient(address), false);
} }
SignalProfile getRecipientProfile( public Profile getRecipientProfile(
SignalServiceAddress address, boolean force RecipientId recipientId
) { ) {
var profileEntry = account.getProfileStore().getProfileEntry(address); return getRecipientProfile(recipientId, false);
if (profileEntry == null) { }
// retrieve profile to get identity key
retrieveEncryptedProfile(address); private final Set<RecipientId> pendingProfileRequest = new HashSet<>();
Profile getRecipientProfile(
RecipientId recipientId, boolean force
) {
var profileKey = account.getProfileStore().getProfileKey(recipientId);
if (profileKey == null) {
if (force) {
// retrieve profile to get identity key
retrieveEncryptedProfile(recipientId);
}
return null; return null;
} }
var now = new Date().getTime(); var profile = account.getProfileStore().getProfile(recipientId);
// 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);
}
if (encryptedProfile == null) {
return null;
}
final var profileKey = profileEntry.getProfileKey(); var now = new Date().getTime();
final var profile = decryptProfileAndDownloadAvatar(address, profileKey, encryptedProfile); // Profiles are cached for 24h before retrieving them again, unless forced
account.getProfileStore() if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) {
.updateProfile(address, profileKey, now, profile, profileEntry.getProfileKeyCredential());
return profile; return profile;
} }
return profileEntry.getProfile();
synchronized (pendingProfileRequest) {
if (pendingProfileRequest.contains(recipientId)) {
return profile;
}
pendingProfileRequest.add(recipientId);
}
final SignalServiceProfile encryptedProfile;
try {
encryptedProfile = retrieveEncryptedProfile(recipientId);
} finally {
synchronized (pendingProfileRequest) {
pendingProfileRequest.remove(recipientId);
}
}
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 { try {
final var profile = profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE) return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile();
.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;
} catch (IOException e) { } catch (IOException e) {
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage()); logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
return null; return null;
} }
} }
private ProfileKeyCredential getRecipientProfileKeyCredential(SignalServiceAddress address) { private ProfileAndCredential retrieveProfileAndCredential(
var profileEntry = account.getProfileStore().getProfileEntry(address); final RecipientId recipientId, final SignalServiceProfile.RequestType requestType
if (profileEntry == null) { ) throws IOException {
return null; final var profileAndCredential = profileHelper.retrieveProfileSync(recipientId, requestType);
} final var profile = profileAndCredential.getProfile();
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(); try {
final var profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull(); account.getIdentityKeyStore()
final var profile = decryptProfileAndDownloadAvatar(address, .saveIdentity(recipientId,
profileEntry.getProfileKey(), new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
profileAndCredential.getProfile()); new Date());
account.getProfileStore() } catch (InvalidKeyException ignored) {
.updateProfile(address, profileEntry.getProfileKey(), now, profile, profileKeyCredential); logger.warn("Got invalid identity key in profile for {}",
return profileKeyCredential; resolveSignalServiceAddress(recipientId).getLegacyIdentifier());
} }
return profileEntry.getProfileKeyCredential(); return profileAndCredential;
} }
private SignalProfile decryptProfileAndDownloadAvatar( private ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) {
final SignalServiceAddress address, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile 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;
}
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().storeProfile(recipientId, profile);
}
return profileKeyCredential;
}
private Profile decryptProfileAndDownloadAvatar(
final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
) { ) {
if (encryptedProfile.getAvatar() != null) { if (encryptedProfile.getAvatar() != null) {
downloadProfileAvatar(address, encryptedProfile.getAvatar(), profileKey); downloadProfileAvatar(resolveSignalServiceAddress(recipientId), encryptedProfile.getAvatar(), profileKey);
} }
return ProfileUtils.decryptProfile(profileKey, encryptedProfile); return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
@ -729,19 +753,23 @@ public class Manager implements Closeable {
) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
return sendUpdateGroupMessage(groupId, return sendUpdateGroupMessage(groupId,
name, name,
members == null ? null : getSignalServiceAddresses(members), members == null
? null
: getSignalServiceAddresses(members).stream()
.map(this::resolveRecipient)
.collect(Collectors.toSet()),
avatarFile); avatarFile);
} }
private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage( private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage(
GroupId groupId, String name, Collection<SignalServiceAddress> members, File avatarFile GroupId groupId, String name, Set<RecipientId> members, File avatarFile
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
GroupInfo g; GroupInfo g;
SignalServiceDataMessage.Builder messageBuilder; SignalServiceDataMessage.Builder messageBuilder;
if (groupId == null) { if (groupId == null) {
// Create new group // Create new group
var gv2 = groupHelper.createGroupV2(name == null ? "" : name, var gv2 = groupHelper.createGroupV2(name == null ? "" : name,
members == null ? List.of() : members, members == null ? Set.of() : members,
avatarFile); avatarFile);
if (gv2 == null) { if (gv2 == null) {
var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
@ -774,7 +802,7 @@ public class Manager implements Closeable {
final var newMembers = new HashSet<>(members); final var newMembers = new HashSet<>(members);
newMembers.removeAll(group.getMembers() newMembers.removeAll(group.getMembers()
.stream() .stream()
.map(this::resolveSignalServiceAddress) .map(this::resolveRecipient)
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
if (newMembers.size() > 0) { if (newMembers.size() > 0) {
var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers); var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers);
@ -810,18 +838,18 @@ public class Manager implements Closeable {
} }
private void updateGroupV1( private void updateGroupV1(
final GroupInfoV1 g, final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final File avatarFile
final String name,
final Collection<SignalServiceAddress> members,
final File avatarFile
) throws IOException { ) throws IOException {
if (name != null) { if (name != null) {
g.name = name; g.name = name;
} }
if (members != null) { if (members != null) {
final var memberAddresses = members.stream()
.map(this::resolveSignalServiceAddress)
.collect(Collectors.toList());
final var newE164Members = new HashSet<String>(); final var newE164Members = new HashSet<String>();
for (var member : members) { for (var member : memberAddresses) {
if (g.isMember(member) || !member.getNumber().isPresent()) { if (g.isMember(member) || !member.getNumber().isPresent()) {
continue; continue;
} }
@ -837,7 +865,7 @@ public class Manager implements Closeable {
+ " to group: Not registered on Signal"); + " to group: Not registered on Signal");
} }
g.addMembers(members); g.addMembers(memberAddresses);
} }
if (avatarFile != null) { if (avatarFile != null) {
@ -973,7 +1001,7 @@ public class Manager implements Closeable {
System.currentTimeMillis()); System.currentTimeMillis());
createMessageSender().sendReceipt(remoteAddress, createMessageSender().sendReceipt(remoteAddress,
unidentifiedAccessHelper.getAccessFor(remoteAddress), unidentifiedAccessHelper.getAccessFor(resolveRecipient(remoteAddress)),
receiptMessage); receiptMessage);
} }
@ -1053,36 +1081,26 @@ public class Manager implements Closeable {
} }
public String getContactName(String number) throws InvalidNumberException { public String getContactName(String number) throws InvalidNumberException {
var contact = account.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number)); var contact = account.getContactStore().getContact(canonicalizeAndResolveRecipient(number));
if (contact == null) { return contact == null || contact.getName() == null ? "" : contact.getName();
return "";
} else {
return contact.name;
}
} }
public void setContactName(String number, String name) throws InvalidNumberException { public void setContactName(String number, String name) throws InvalidNumberException {
final var address = canonicalizeAndResolveSignalServiceAddress(number); final var recipientId = canonicalizeAndResolveRecipient(number);
var contact = account.getContactStore().getContact(address); var contact = account.getContactStore().getContact(recipientId);
if (contact == null) { final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
contact = new ContactInfo(address); account.getContactStore().storeContact(recipientId, builder.withName(name).build());
}
contact.name = name;
account.getContactStore().updateContact(contact);
account.save(); account.save();
} }
public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException { public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException {
setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked); setContactBlocked(canonicalizeAndResolveRecipient(number), blocked);
} }
private void setContactBlocked(SignalServiceAddress address, boolean blocked) { private void setContactBlocked(RecipientId recipientId, boolean blocked) {
var contact = account.getContactStore().getContact(address); var contact = account.getContactStore().getContact(recipientId);
if (contact == null) { final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
contact = new ContactInfo(address); account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build());
}
contact.blocked = blocked;
account.getContactStore().updateContact(contact);
account.save(); account.save();
} }
@ -1097,15 +1115,14 @@ public class Manager implements Closeable {
account.save(); account.save();
} }
/** private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) {
* Change the expiration timer for a contact var contact = account.getContactStore().getContact(recipientId);
*/ if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) {
public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) throws IOException { return;
var contact = account.getContactStore().getContact(address); }
contact.messageExpirationTime = messageExpirationTimer; final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
account.getContactStore().updateContact(contact); account.getContactStore()
sendExpirationTimerUpdate(address); .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build());
account.save();
} }
private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException { private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException {
@ -1119,8 +1136,10 @@ public class Manager implements Closeable {
public void setExpirationTimer( public void setExpirationTimer(
String number, int messageExpirationTimer String number, int messageExpirationTimer
) throws IOException, InvalidNumberException { ) throws IOException, InvalidNumberException {
var address = canonicalizeAndResolveSignalServiceAddress(number); var recipientId = canonicalizeAndResolveRecipient(number);
setExpirationTimer(address, messageExpirationTimer); setExpirationTimer(recipientId, messageExpirationTimer);
sendExpirationTimerUpdate(resolveSignalServiceAddress(recipientId));
account.save();
} }
/** /**
@ -1298,6 +1317,7 @@ public class Manager implements Closeable {
SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients
) throws IOException { ) throws IOException {
recipients = recipients.stream().map(this::resolveSignalServiceAddress).collect(Collectors.toSet()); 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(); final var timestamp = System.currentTimeMillis();
messageBuilder.withTimestamp(timestamp); messageBuilder.withTimestamp(timestamp);
getOrCreateMessagePipe(); getOrCreateMessagePipe();
@ -1310,7 +1330,7 @@ public class Manager implements Closeable {
var messageSender = createMessageSender(); var messageSender = createMessageSender();
final var isRecipientUpdate = false; final var isRecipientUpdate = false;
var result = messageSender.sendMessage(new ArrayList<>(recipients), var result = messageSender.sendMessage(new ArrayList<>(recipients),
unidentifiedAccessHelper.getAccessFor(recipients), unidentifiedAccessHelper.getAccessFor(recipientIds),
isRecipientUpdate, isRecipientUpdate,
message); message);
@ -1332,8 +1352,8 @@ public class Manager implements Closeable {
messageBuilder.withProfileKey(account.getProfileKey().serialize()); messageBuilder.withProfileKey(account.getProfileKey().serialize());
var results = new ArrayList<SendMessageResult>(recipients.size()); var results = new ArrayList<SendMessageResult>(recipients.size());
for (var address : recipients) { for (var address : recipients) {
final var contact = account.getContactStore().getContact(address); final var contact = account.getContactStore().getContact(resolveRecipient(address));
final var expirationTime = contact != null ? contact.messageExpirationTime : 0; final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
messageBuilder.withExpiration(expirationTime); messageBuilder.withExpiration(expirationTime);
message = messageBuilder.build(); message = messageBuilder.build();
results.add(sendMessage(address, message)); results.add(sendMessage(address, message));
@ -1358,10 +1378,10 @@ public class Manager implements Closeable {
getOrCreateMessagePipe(); getOrCreateMessagePipe();
getOrCreateUnidentifiedMessagePipe(); getOrCreateUnidentifiedMessagePipe();
try { try {
final var address = getSelfAddress(); final var recipientId = account.getSelfRecipientId();
final var contact = account.getContactStore().getContact(address); final var contact = account.getContactStore().getContact(recipientId);
final var expirationTime = contact != null ? contact.messageExpirationTime : 0; final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
messageBuilder.withExpiration(expirationTime); messageBuilder.withExpiration(expirationTime);
var message = messageBuilder.build(); var message = messageBuilder.build();
@ -1377,7 +1397,7 @@ public class Manager implements Closeable {
var recipient = account.getSelfAddress(); var recipient = account.getSelfAddress();
final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(recipient); final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(resolveRecipient(recipient));
var transcript = new SentTranscriptMessage(Optional.of(recipient), var transcript = new SentTranscriptMessage(Optional.of(recipient),
message.getTimestamp(), message.getTimestamp(),
message, message,
@ -1404,7 +1424,9 @@ public class Manager implements Closeable {
var messageSender = createMessageSender(); var messageSender = createMessageSender();
try { try {
return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message); return messageSender.sendMessage(address,
unidentifiedAccessHelper.getAccessFor(resolveRecipient(address)),
message);
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {
return SendMessageResult.identityFailure(address, e.getIdentityKey()); return SendMessageResult.identityFailure(address, e.getIdentityKey());
} }
@ -1520,14 +1542,7 @@ public class Manager implements Closeable {
// disappearing message timer already stored in the DecryptedGroup // disappearing message timer already stored in the DecryptedGroup
} }
} else if (conversationPartnerAddress != null) { } else if (conversationPartnerAddress != null) {
var contact = account.getContactStore().getContact(conversationPartnerAddress); setExpirationTimer(resolveRecipient(conversationPartnerAddress), message.getExpiresInSeconds());
if (contact == null) {
contact = new ContactInfo(conversationPartnerAddress);
}
if (contact.messageExpirationTime != message.getExpiresInSeconds()) {
contact.messageExpirationTime = message.getExpiresInSeconds();
account.getContactStore().updateContact(contact);
}
} }
} }
if (!ignoreAttachments) { if (!ignoreAttachments) {
@ -1554,7 +1569,7 @@ public class Manager implements Closeable {
if (source.matches(account.getSelfAddress())) { if (source.matches(account.getSelfAddress())) {
this.account.setProfileKey(profileKey); this.account.setProfileKey(profileKey);
} }
this.account.getProfileStore().storeProfileKey(source, profileKey); this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey);
} }
if (message.getPreviews().isPresent()) { if (message.getPreviews().isPresent()) {
final var previews = message.getPreviews().get(); final var previews = message.getPreviews().get();
@ -1632,7 +1647,7 @@ public class Manager implements Closeable {
private void storeProfileKeysFromMembers(final DecryptedGroup group) { private void storeProfileKeysFromMembers(final DecryptedGroup group) {
for (var member : group.getMembersList()) { 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)); .toByteArray()), null));
try { try {
account.getProfileStore() account.getProfileStore()
@ -1789,7 +1804,7 @@ public class Manager implements Closeable {
if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) { if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
final var recipientId = resolveRecipient(((org.whispersystems.libsignal.UntrustedIdentityException) exception) final var recipientId = resolveRecipient(((org.whispersystems.libsignal.UntrustedIdentityException) exception)
.getName()); .getName());
queuedActions.add(new RetrieveProfileAction(resolveSignalServiceAddress(recipientId))); queuedActions.add(new RetrieveProfileAction(recipientId));
if (!envelope.hasSource()) { if (!envelope.hasSource()) {
try { try {
cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId);
@ -1816,8 +1831,8 @@ public class Manager implements Closeable {
} else { } else {
return false; return false;
} }
var sourceContact = account.getContactStore().getContact(source); final var recipientId = resolveRecipient(source);
if (sourceContact != null && sourceContact.blocked) { if (isContactBlocked(recipientId)) {
return true; return true;
} }
@ -1834,6 +1849,16 @@ public class Manager implements Closeable {
return false; 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( private boolean isNotAGroupMember(
SignalServiceEnvelope envelope, SignalServiceContent content SignalServiceEnvelope envelope, SignalServiceContent content
) { ) {
@ -1876,8 +1901,6 @@ public class Manager implements Closeable {
} else { } else {
sender = content.getSender(); sender = content.getSender();
} }
// Store uuid if we don't have it already
resolveSignalServiceAddress(sender);
if (content.getDataMessage().isPresent()) { if (content.getDataMessage().isPresent()) {
var message = content.getDataMessage().get(); var message = content.getDataMessage().get();
@ -1974,7 +1997,7 @@ public class Manager implements Closeable {
if (syncMessage.getBlockedList().isPresent()) { if (syncMessage.getBlockedList().isPresent()) {
final var blockedListMessage = syncMessage.getBlockedList().get(); final var blockedListMessage = syncMessage.getBlockedList().get();
for (var address : blockedListMessage.getAddresses()) { for (var address : blockedListMessage.getAddresses()) {
setContactBlocked(resolveSignalServiceAddress(address), true); setContactBlocked(resolveRecipient(address), true);
} }
for (var groupId : blockedListMessage.getGroupIds() for (var groupId : blockedListMessage.getGroupIds()
.stream() .stream()
@ -2001,19 +2024,19 @@ public class Manager implements Closeable {
if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
account.setProfileKey(c.getProfileKey().get()); account.setProfileKey(c.getProfileKey().get());
} }
final var address = resolveSignalServiceAddress(c.getAddress()); final var recipientId = resolveRecipientTrusted(c.getAddress());
var contact = account.getContactStore().getContact(address); var contact = account.getContactStore().getContact(recipientId);
if (contact == null) { final var builder = contact == null
contact = new ContactInfo(address); ? Contact.newBuilder()
} : Contact.newBuilder(contact);
if (c.getName().isPresent()) { if (c.getName().isPresent()) {
contact.name = c.getName().get(); builder.withName(c.getName().get());
} }
if (c.getColor().isPresent()) { if (c.getColor().isPresent()) {
contact.color = c.getColor().get(); builder.withColor(c.getColor().get());
} }
if (c.getProfileKey().isPresent()) { if (c.getProfileKey().isPresent()) {
account.getProfileStore().storeProfileKey(address, c.getProfileKey().get()); account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
} }
if (c.getVerified().isPresent()) { if (c.getVerified().isPresent()) {
final var verifiedMessage = c.getVerified().get(); final var verifiedMessage = c.getVerified().get();
@ -2023,15 +2046,14 @@ public class Manager implements Closeable {
TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
} }
if (c.getExpirationTimer().isPresent()) { if (c.getExpirationTimer().isPresent()) {
contact.messageExpirationTime = c.getExpirationTimer().get(); builder.withMessageExpirationTime(c.getExpirationTimer().get());
} }
contact.blocked = c.isBlocked(); builder.withBlocked(c.isBlocked());
contact.inboxPosition = c.getInboxPosition().orNull(); builder.withArchived(c.isArchived());
contact.archived = c.isArchived(); account.getContactStore().storeContact(recipientId, builder.build());
account.getContactStore().updateContact(contact);
if (c.getAvatar().isPresent()) { 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()) { if (syncMessage.getFetchType().isPresent()) {
switch (syncMessage.getFetchType().get()) { switch (syncMessage.getFetchType().get()) {
case LOCAL_PROFILE: case LOCAL_PROFILE:
getRecipientProfile(getSelfAddress(), true); getRecipientProfile(account.getSelfRecipientId(), true);
case STORAGE_MANIFEST: case STORAGE_MANIFEST:
// TODO // TODO
} }
@ -2294,28 +2316,31 @@ public class Manager implements Closeable {
try { try {
try (OutputStream fos = new FileOutputStream(contactsFile)) { try (OutputStream fos = new FileOutputStream(contactsFile)) {
var out = new DeviceContactsOutputStream(fos); 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; VerifiedMessage verifiedMessage = null;
var currentIdentity = account.getIdentityKeyStore()
.getIdentity(resolveRecipientTrusted(record.getAddress()));
if (currentIdentity != null) { if (currentIdentity != null) {
verifiedMessage = new VerifiedMessage(record.getAddress(), verifiedMessage = new VerifiedMessage(address,
currentIdentity.getIdentityKey(), currentIdentity.getIdentityKey(),
currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getTrustLevel().toVerifiedState(),
currentIdentity.getDateAdded().getTime()); currentIdentity.getDateAdded().getTime());
} }
var profileKey = account.getProfileStore().getProfileKey(record.getAddress()); var profileKey = account.getProfileStore().getProfileKey(recipientId);
out.write(new DeviceContact(record.getAddress(), out.write(new DeviceContact(address,
Optional.fromNullable(record.name), Optional.fromNullable(contact.getName()),
createContactAvatarAttachment(record.getAddress()), createContactAvatarAttachment(address),
Optional.fromNullable(record.color), Optional.fromNullable(contact.getColor()),
Optional.fromNullable(verifiedMessage), Optional.fromNullable(verifiedMessage),
Optional.fromNullable(profileKey), Optional.fromNullable(profileKey),
record.blocked, contact.isBlocked(),
Optional.of(record.messageExpirationTime), Optional.of(contact.getMessageExpirationTime()),
Optional.fromNullable(record.inboxPosition), Optional.absent(),
record.archived)); contact.isArchived()));
} }
if (account.getProfileKey() != null) { if (account.getProfileKey() != null) {
@ -2356,8 +2381,8 @@ public class Manager implements Closeable {
void sendBlockedList() throws IOException, UntrustedIdentityException { void sendBlockedList() throws IOException, UntrustedIdentityException {
var addresses = new ArrayList<SignalServiceAddress>(); var addresses = new ArrayList<SignalServiceAddress>();
for (var record : account.getContactStore().getContacts()) { for (var record : account.getContactStore().getContacts()) {
if (record.blocked) { if (record.second().isBlocked()) {
addresses.add(record.getAddress()); addresses.add(resolveSignalServiceAddress(record.first()));
} }
} }
var groupIds = new ArrayList<byte[]>(); var groupIds = new ArrayList<byte[]>();
@ -2379,22 +2404,25 @@ public class Manager implements Closeable {
sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
} }
public List<ContactInfo> getContacts() { public List<Pair<RecipientId, Contact>> getContacts() {
return account.getContactStore().getContacts(); return account.getContactStore().getContacts();
} }
public String getContactOrProfileName(String number) { public String getContactOrProfileName(String number) throws InvalidNumberException {
final var address = Utils.getSignalServiceAddressFromIdentifier(number); final var recipientId = canonicalizeAndResolveRecipient(number);
final var recipient = account.getRecipientStore().getRecipient(recipientId);
final var contact = account.getContactStore().getContact(address); if (recipient == null) {
if (contact != null && !Util.isEmpty(contact.name)) { return null;
return contact.name;
} }
final var profileEntry = account.getProfileStore().getProfileEntry(address); if (recipient.getContact() != null && !Util.isEmpty(recipient.getContact().getName())) {
if (profileEntry != null && profileEntry.getProfile() != null) { return recipient.getContact().getName();
return profileEntry.getProfile().getDisplayName();
} }
if (recipient.getProfile() != null && recipient.getProfile() != null) {
return recipient.getProfile().getDisplayName();
}
return null; return null;
} }
@ -2530,11 +2558,11 @@ public class Manager implements Closeable {
} }
public RecipientId resolveRecipient(SignalServiceAddress address) { public RecipientId resolveRecipient(SignalServiceAddress address) {
return account.getRecipientStore().resolveRecipientUntrusted(address); return account.getRecipientStore().resolveRecipient(address);
} }
private RecipientId resolveRecipientTrusted(SignalServiceAddress address) { private RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
return account.getRecipientStore().resolveRecipient(address); return account.getRecipientStore().resolveRecipientTrusted(address);
} }
@Override @Override

View file

@ -165,7 +165,7 @@ public class RegistrationManager implements Closeable {
account.setUuid(UuidUtil.parseOrNull(response.getUuid())); account.setUuid(UuidUtil.parseOrNull(response.getUuid()));
account.setRegistrationLockPin(pin); account.setRegistrationLockPin(pin);
account.getSessionStore().archiveAllSessions(); 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(); final var publicKey = account.getIdentityKeyPair().getPublicKey();
account.getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date()); account.getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED); account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);

View file

@ -5,7 +5,8 @@ import com.google.protobuf.InvalidProtocolBufferException;
import org.asamk.signal.manager.groups.GroupLinkPassword; import org.asamk.signal.manager.groups.GroupLinkPassword;
import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.storage.groups.GroupInfoV2; 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.asamk.signal.manager.util.IOUtils;
import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.GroupChange;
@ -38,7 +39,6 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Collection;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -51,7 +51,7 @@ public class GroupHelper {
private final ProfileProvider profileProvider; private final ProfileProvider profileProvider;
private final SelfAddressProvider selfAddressProvider; private final SelfRecipientIdProvider selfRecipientIdProvider;
private final GroupsV2Operations groupsV2Operations; private final GroupsV2Operations groupsV2Operations;
@ -59,20 +59,24 @@ public class GroupHelper {
private final GroupAuthorizationProvider groupAuthorizationProvider; private final GroupAuthorizationProvider groupAuthorizationProvider;
private final SignalServiceAddressResolver addressResolver;
public GroupHelper( public GroupHelper(
final ProfileKeyCredentialProvider profileKeyCredentialProvider, final ProfileKeyCredentialProvider profileKeyCredentialProvider,
final ProfileProvider profileProvider, final ProfileProvider profileProvider,
final SelfAddressProvider selfAddressProvider, final SelfRecipientIdProvider selfRecipientIdProvider,
final GroupsV2Operations groupsV2Operations, final GroupsV2Operations groupsV2Operations,
final GroupsV2Api groupsV2Api, final GroupsV2Api groupsV2Api,
final GroupAuthorizationProvider groupAuthorizationProvider final GroupAuthorizationProvider groupAuthorizationProvider,
final SignalServiceAddressResolver addressResolver
) { ) {
this.profileKeyCredentialProvider = profileKeyCredentialProvider; this.profileKeyCredentialProvider = profileKeyCredentialProvider;
this.profileProvider = profileProvider; this.profileProvider = profileProvider;
this.selfAddressProvider = selfAddressProvider; this.selfRecipientIdProvider = selfRecipientIdProvider;
this.groupsV2Operations = groupsV2Operations; this.groupsV2Operations = groupsV2Operations;
this.groupsV2Api = groupsV2Api; this.groupsV2Api = groupsV2Api;
this.groupAuthorizationProvider = groupAuthorizationProvider; this.groupAuthorizationProvider = groupAuthorizationProvider;
this.addressResolver = addressResolver;
} }
public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) { public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
@ -97,7 +101,7 @@ public class GroupHelper {
} }
public GroupInfoV2 createGroupV2( public GroupInfoV2 createGroupV2(
String name, Collection<SignalServiceAddress> members, File avatarFile String name, Set<RecipientId> members, File avatarFile
) throws IOException { ) throws IOException {
final var avatarBytes = readAvatarBytes(avatarFile); final var avatarBytes = readAvatarBytes(avatarFile);
final var newGroup = buildNewGroupV2(name, members, avatarBytes); final var newGroup = buildNewGroupV2(name, members, avatarBytes);
@ -139,9 +143,9 @@ public class GroupHelper {
} }
private GroupsV2Operations.NewGroup buildNewGroupV2( private GroupsV2Operations.NewGroup buildNewGroupV2(
String name, Collection<SignalServiceAddress> members, byte[] avatar String name, Set<RecipientId> members, byte[] avatar
) { ) {
final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfAddressProvider.getSelfAddress()); final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientIdProvider.getSelfRecipientId());
if (profileKeyCredential == null) { if (profileKeyCredential == null) {
logger.warn("Cannot create a V2 group as self does not have a versioned profile"); logger.warn("Cannot create a V2 group as self does not have a versioned profile");
return null; return null;
@ -149,10 +153,11 @@ public class GroupHelper {
if (!areMembersValid(members)) return null; if (!areMembersValid(members)) return null;
var self = new GroupCandidate(selfAddressProvider.getSelfAddress().getUuid().orNull(), var self = new GroupCandidate(addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
Optional.fromNullable(profileKeyCredential)); .getUuid()
.orNull(), Optional.fromNullable(profileKeyCredential));
var candidates = members.stream() 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)))) Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -166,8 +171,9 @@ public class GroupHelper {
0); 0);
} }
private boolean areMembersValid(final Collection<SignalServiceAddress> members) { private boolean areMembersValid(final Set<RecipientId> members) {
final var noUuidCapability = members.stream() final var noUuidCapability = members.stream()
.map(addressResolver::resolveSignalServiceAddress)
.filter(address -> !address.getUuid().isPresent()) .filter(address -> !address.getUuid().isPresent())
.map(SignalServiceAddress::getLegacyIdentifier) .map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -179,11 +185,11 @@ public class GroupHelper {
final var noGv2Capability = members.stream() final var noGv2Capability = members.stream()
.map(profileProvider::getProfile) .map(profileProvider::getProfile)
.filter(profile -> profile != null && !profile.getCapabilities().gv2) .filter(profile -> profile != null && !profile.getCapabilities().contains(Profile.Capability.gv2))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
if (noGv2Capability.size() > 0) { if (noGv2Capability.size() > 0) {
logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}", 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; return false;
} }
@ -206,7 +212,8 @@ public class GroupHelper {
change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey)); 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()) { if (uuid.isPresent()) {
change.setSourceUuid(UuidUtil.toByteString(uuid.get())); change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
} }
@ -215,7 +222,7 @@ public class GroupHelper {
} }
public Pair<DecryptedGroup, GroupChange> updateGroupV2( public Pair<DecryptedGroup, GroupChange> updateGroupV2(
GroupInfoV2 groupInfoV2, Set<SignalServiceAddress> newMembers GroupInfoV2 groupInfoV2, Set<RecipientId> newMembers
) throws IOException { ) throws IOException {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
var groupOperations = groupsV2Operations.forGroup(groupSecretParams); var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
@ -225,24 +232,25 @@ public class GroupHelper {
} }
var candidates = newMembers.stream() 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)))) Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
final var change = groupOperations.createModifyGroupMembershipChange(candidates, final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
selfAddressProvider.getSelfAddress().getUuid().get()); .getUuid()
.get();
final var change = groupOperations.createModifyGroupMembershipChange(candidates, uuid);
final var uuid = this.selfAddressProvider.getSelfAddress().getUuid(); change.setSourceUuid(UuidUtil.toByteString(uuid));
if (uuid.isPresent()) {
change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
}
return commitChange(groupInfoV2, change); return commitChange(groupInfoV2, change);
} }
public Pair<DecryptedGroup, GroupChange> leaveGroup(GroupInfoV2 groupInfoV2) throws IOException { public Pair<DecryptedGroup, GroupChange> leaveGroup(GroupInfoV2 groupInfoV2) throws IOException {
var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList(); 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); var selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfUuid);
if (selfPendingMember.isPresent()) { if (selfPendingMember.isPresent()) {
@ -260,8 +268,8 @@ public class GroupHelper {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
final var groupOperations = groupsV2Operations.forGroup(groupSecretParams); final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
final var selfAddress = this.selfAddressProvider.getSelfAddress(); final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfAddress); final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
if (profileKeyCredential == null) { if (profileKeyCredential == null) {
throw new IOException("Cannot join a V2 group as self does not have a versioned profile"); 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.createGroupJoinRequest(profileKeyCredential)
: groupOperations.createGroupJoinDirect(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); return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
} }
@ -280,15 +290,15 @@ public class GroupHelper {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
final var groupOperations = groupsV2Operations.forGroup(groupSecretParams); final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
final var selfAddress = this.selfAddressProvider.getSelfAddress(); final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfAddress); final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
if (profileKeyCredential == null) { if (profileKeyCredential == null) {
throw new IOException("Cannot join a V2 group as self does not have a versioned profile"); throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
} }
final var change = groupOperations.createAcceptInviteChange(profileKeyCredential); final var change = groupOperations.createAcceptInviteChange(profileKeyCredential);
final var uuid = selfAddress.getUuid(); final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientId).getUuid();
if (uuid.isPresent()) { if (uuid.isPresent()) {
change.setSourceUuid(UuidUtil.toByteString(uuid.get())); change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
} }
@ -330,7 +340,9 @@ public class GroupHelper {
try { try {
decryptedChange = groupOperations.decryptChange(changeActions, decryptedChange = groupOperations.decryptChange(changeActions,
selfAddressProvider.getSelfAddress().getUuid().get()); addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
.getUuid()
.get());
decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange); decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) { } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
throw new IOException(e); throw new IOException(e);

View file

@ -1,5 +1,6 @@
package org.asamk.signal.manager.helper; package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
@ -27,23 +28,27 @@ public final class ProfileHelper {
private final MessageReceiverProvider messageReceiverProvider; private final MessageReceiverProvider messageReceiverProvider;
private final SignalServiceAddressResolver addressResolver;
public ProfileHelper( public ProfileHelper(
final ProfileKeyProvider profileKeyProvider, final ProfileKeyProvider profileKeyProvider,
final UnidentifiedAccessProvider unidentifiedAccessProvider, final UnidentifiedAccessProvider unidentifiedAccessProvider,
final MessagePipeProvider messagePipeProvider, final MessagePipeProvider messagePipeProvider,
final MessageReceiverProvider messageReceiverProvider final MessageReceiverProvider messageReceiverProvider,
final SignalServiceAddressResolver addressResolver
) { ) {
this.profileKeyProvider = profileKeyProvider; this.profileKeyProvider = profileKeyProvider;
this.unidentifiedAccessProvider = unidentifiedAccessProvider; this.unidentifiedAccessProvider = unidentifiedAccessProvider;
this.messagePipeProvider = messagePipeProvider; this.messagePipeProvider = messagePipeProvider;
this.messageReceiverProvider = messageReceiverProvider; this.messageReceiverProvider = messageReceiverProvider;
this.addressResolver = addressResolver;
} }
public ProfileAndCredential retrieveProfileSync( public ProfileAndCredential retrieveProfileSync(
SignalServiceAddress recipient, SignalServiceProfile.RequestType requestType RecipientId recipientId, SignalServiceProfile.RequestType requestType
) throws IOException { ) throws IOException {
try { try {
return retrieveProfile(recipient, requestType).get(10, TimeUnit.SECONDS); return retrieveProfile(recipientId, requestType).get(10, TimeUnit.SECONDS);
} catch (ExecutionException e) { } catch (ExecutionException e) {
if (e.getCause() instanceof PushNetworkException) { if (e.getCause() instanceof PushNetworkException) {
throw (PushNetworkException) e.getCause(); throw (PushNetworkException) e.getCause();
@ -58,11 +63,12 @@ public final class ProfileHelper {
} }
public ListenableFuture<ProfileAndCredential> retrieveProfile( public ListenableFuture<ProfileAndCredential> retrieveProfile(
SignalServiceAddress address, SignalServiceProfile.RequestType requestType RecipientId recipientId, SignalServiceProfile.RequestType requestType
) { ) {
var unidentifiedAccess = getUnidentifiedAccess(address); var unidentifiedAccess = getUnidentifiedAccess(recipientId);
var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(address)); var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId));
final var address = addressResolver.resolveSignalServiceAddress(recipientId);
if (unidentifiedAccess.isPresent()) { if (unidentifiedAccess.isPresent()) {
return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address, return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address,
profileKey, profileKey,
@ -126,8 +132,8 @@ public final class ProfileHelper {
} }
} }
private Optional<UnidentifiedAccess> getUnidentifiedAccess(SignalServiceAddress recipient) { private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) {
var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipient); var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId);
if (unidentifiedAccess.isPresent()) { if (unidentifiedAccess.isPresent()) {
return unidentifiedAccess.get().getTargetUnidentifiedAccess(); return unidentifiedAccess.get().getTargetUnidentifiedAccess();

View file

@ -1,9 +1,9 @@
package org.asamk.signal.manager.helper; package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface ProfileKeyCredentialProvider { public interface ProfileKeyCredentialProvider {
ProfileKeyCredential getProfileKeyCredential(SignalServiceAddress address); ProfileKeyCredential getProfileKeyCredential(RecipientId recipientId);
} }

View file

@ -1,9 +1,9 @@
package org.asamk.signal.manager.helper; package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface ProfileKeyProvider { public interface ProfileKeyProvider {
ProfileKey getProfileKey(SignalServiceAddress address); ProfileKey getProfileKey(RecipientId address);
} }

View file

@ -1,9 +1,9 @@
package org.asamk.signal.manager.helper; package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.storage.profiles.SignalProfile; import org.asamk.signal.manager.storage.recipients.Profile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.asamk.signal.manager.storage.recipients.RecipientId;
public interface ProfileProvider { public interface ProfileProvider {
SignalProfile getProfile(SignalServiceAddress address); Profile getProfile(RecipientId address);
} }

View file

@ -1,8 +0,0 @@
package org.asamk.signal.manager.helper;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface SelfAddressProvider {
SignalServiceAddress getSelfAddress();
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.storage.recipients.RecipientId;
public interface SelfRecipientIdProvider {
RecipientId getSelfRecipientId();
}

View file

@ -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);
}

View file

@ -1,10 +1,10 @@
package org.asamk.signal.manager.helper; package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -38,22 +38,25 @@ public class UnidentifiedAccessHelper {
return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey()); return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey());
} }
public byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { public byte[] getTargetUnidentifiedAccessKey(RecipientId recipient) {
var theirProfileKey = profileKeyProvider.getProfileKey(recipient);
if (theirProfileKey == null) {
return null;
}
var targetProfile = profileProvider.getProfile(recipient); var targetProfile = profileProvider.getProfile(recipient);
if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) { if (targetProfile == null) {
return null; return null;
} }
if (targetProfile.isUnrestrictedUnidentifiedAccess()) { switch (targetProfile.getUnidentifiedAccessMode()) {
return createUnrestrictedUnidentifiedAccess(); case ENABLED:
} var theirProfileKey = profileKeyProvider.getProfileKey(recipient);
if (theirProfileKey == null) {
return null;
}
return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
case UNRESTRICTED:
return createUnrestrictedUnidentifiedAccess();
default:
return null;
}
} }
public Optional<UnidentifiedAccessPair> getAccessForSync() { public Optional<UnidentifiedAccessPair> getAccessForSync() {
@ -73,11 +76,11 @@ public class UnidentifiedAccessHelper {
} }
} }
public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<SignalServiceAddress> recipients) { public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<RecipientId> recipients) {
return recipients.stream().map(this::getAccessFor).collect(Collectors.toList()); return recipients.stream().map(this::getAccessFor).collect(Collectors.toList());
} }
public Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress recipient) { public Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipient) {
var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate(); var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();

View file

@ -1,10 +1,10 @@
package org.asamk.signal.manager.helper; package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface UnidentifiedAccessProvider { public interface UnidentifiedAccessProvider {
Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress address); Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipientId);
} }

View file

@ -10,18 +10,21 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.contacts.ContactInfo; import org.asamk.signal.manager.storage.contacts.ContactsStore;
import org.asamk.signal.manager.storage.contacts.JsonContactsStore; import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.JsonGroupStore; import org.asamk.signal.manager.storage.groups.JsonGroupStore;
import org.asamk.signal.manager.storage.identities.IdentityKeyStore; import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
import org.asamk.signal.manager.storage.messageCache.MessageCache; import org.asamk.signal.manager.storage.messageCache.MessageCache;
import org.asamk.signal.manager.storage.prekeys.PreKeyStore; import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore; 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.profiles.ProfileStore;
import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore; import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore;
import org.asamk.signal.manager.storage.protocol.SignalProtocolStore; 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.LegacyRecipientStore;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientStore; import org.asamk.signal.manager.storage.recipients.RecipientStore;
import org.asamk.signal.manager.storage.sessions.SessionStore; 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.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.api.storage.StorageKey;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -57,9 +61,9 @@ import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.nio.channels.FileLock; import java.nio.channels.FileLock;
import java.util.Base64; import java.util.Base64;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
public class SignalAccount implements Closeable { public class SignalAccount implements Closeable {
@ -88,9 +92,7 @@ public class SignalAccount implements Closeable {
private SessionStore sessionStore; private SessionStore sessionStore;
private IdentityKeyStore identityKeyStore; private IdentityKeyStore identityKeyStore;
private JsonGroupStore groupStore; private JsonGroupStore groupStore;
private JsonContactsStore contactStore;
private RecipientStore recipientStore; private RecipientStore recipientStore;
private ProfileStore profileStore;
private StickerStore stickerStore; private StickerStore stickerStore;
private MessageCache messageCache; private MessageCache messageCache;
@ -136,7 +138,6 @@ public class SignalAccount implements Closeable {
account.username = username; account.username = username;
account.profileKey = profileKey; account.profileKey = profileKey;
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username)); account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.contactStore = new JsonContactsStore();
account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
account::mergeRecipients); account::mergeRecipients);
account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username)); account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
@ -151,7 +152,6 @@ public class SignalAccount implements Closeable {
account.signedPreKeyStore, account.signedPreKeyStore,
account.sessionStore, account.sessionStore,
account.identityKeyStore); account.identityKeyStore);
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore(); account.stickerStore = new StickerStore();
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username)); account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
@ -188,9 +188,9 @@ public class SignalAccount implements Closeable {
account.profileKey = profileKey; account.profileKey = profileKey;
account.deviceId = deviceId; account.deviceId = deviceId;
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username)); account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.contactStore = new JsonContactsStore();
account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
account::mergeRecipients); account::mergeRecipients);
account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username)); account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username)); account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
account.sessionStore = new SessionStore(getSessionsPath(dataPath, username), account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
@ -203,7 +203,6 @@ public class SignalAccount implements Closeable {
account.signedPreKeyStore, account.signedPreKeyStore,
account.sessionStore, account.sessionStore,
account.identityKeyStore); account.identityKeyStore);
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore(); account.stickerStore = new StickerStore();
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username)); account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
@ -222,23 +221,8 @@ public class SignalAccount implements Closeable {
setProfileKey(KeyUtils.createProfileKey()); setProfileKey(KeyUtils.createProfileKey());
save(); 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 // Ensure our profile key is stored in profile store
getProfileStore().storeProfileKey(getSelfAddress(), getProfileKey()); getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey());
} }
private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
@ -354,13 +338,15 @@ public class SignalAccount implements Closeable {
} }
recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients); recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
var legacyRecipientStoreNode = rootNode.get("recipientStore"); var legacyRecipientStoreNode = rootNode.get("recipientStore");
if (legacyRecipientStoreNode != null) { if (legacyRecipientStoreNode != null) {
logger.debug("Migrating legacy recipient store."); logger.debug("Migrating legacy recipient store.");
var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class); var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class);
if (legacyRecipientStore != null) { if (legacyRecipientStore != null) {
recipientStore.resolveRecipients(legacyRecipientStore.getAddresses()); recipientStore.resolveRecipientsTrusted(legacyRecipientStore.getAddresses());
} }
recipientStore.resolveRecipientTrusted(getSelfAddress());
} }
var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore") var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
@ -414,9 +400,9 @@ public class SignalAccount implements Closeable {
identityKeyPair, identityKeyPair,
registrationId); registrationId);
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) { 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()) { 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.saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded());
identityKeyStore.setIdentityTrustLevel(recipientId, identityKeyStore.setIdentityTrustLevel(recipientId,
identity.getIdentityKey(), identity.getIdentityKey(),
@ -436,20 +422,67 @@ public class SignalAccount implements Closeable {
groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username)); groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
} }
var contactStoreNode = rootNode.get("contactStore"); if (rootNode.hasNonNull("contactStore")) {
if (contactStoreNode != null) { logger.debug("Migrating legacy contact store.");
contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class); final var contactStoreNode = rootNode.get("contactStore");
} final var contactStore = jsonProcessor.convertValue(contactStoreNode, LegacyJsonContactsStore.class);
if (contactStore == null) { for (var contact : contactStore.getContacts()) {
contactStore = new JsonContactsStore(); 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 (rootNode.hasNonNull("profileStore")) {
if (profileStoreNode != null) { logger.debug("Migrating legacy profile store.");
profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class); var profileStoreNode = rootNode.get("profileStore");
} final var legacyProfileStore = jsonProcessor.convertValue(profileStoreNode, LegacyProfileStore.class);
if (profileStore == null) { for (var profileEntry : legacyProfileStore.getProfileEntries()) {
profileStore = new ProfileStore(); 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<Profile.Capability>();
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"); var stickerStoreNode = rootNode.get("stickerStore");
@ -460,24 +493,6 @@ public class SignalAccount implements Closeable {
stickerStore = new StickerStore(); 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)); messageCache = new MessageCache(getMessageCachePath(dataPath, username));
var threadStoreNode = rootNode.get("threadStore"); var threadStoreNode = rootNode.get("threadStore");
@ -489,10 +504,15 @@ public class SignalAccount implements Closeable {
continue; continue;
} }
try { try {
var contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id)); if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) {
if (contactInfo != null) { final var recipientId = recipientStore.resolveRecipient(thread.id);
contactInfo.messageExpirationTime = thread.messageExpirationTime; var contact = recipientStore.getContact(recipientId);
contactStore.updateContact(contactInfo); if (contact != null) {
recipientStore.storeContact(recipientId,
Contact.newBuilder(contact)
.withMessageExpirationTime(thread.messageExpirationTime)
.build());
}
} else { } else {
var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id)); var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
if (groupInfo instanceof GroupInfoV1) { if (groupInfo instanceof GroupInfoV1) {
@ -500,7 +520,8 @@ public class SignalAccount implements Closeable {
groupStore.updateGroup(groupInfo); 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("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
.put("registered", registered) .put("registered", registered)
.putPOJO("groupStore", groupStore) .putPOJO("groupStore", groupStore)
.putPOJO("contactStore", contactStore)
.putPOJO("profileStore", profileStore)
.putPOJO("stickerStore", stickerStore); .putPOJO("stickerStore", stickerStore);
try { try {
try (var output = new ByteArrayOutputStream()) { try (var output = new ByteArrayOutputStream()) {
@ -602,8 +621,8 @@ public class SignalAccount implements Closeable {
return groupStore; return groupStore;
} }
public JsonContactsStore getContactStore() { public ContactsStore getContactStore() {
return contactStore; return recipientStore;
} }
public RecipientStore getRecipientStore() { public RecipientStore getRecipientStore() {
@ -611,7 +630,7 @@ public class SignalAccount implements Closeable {
} }
public ProfileStore getProfileStore() { public ProfileStore getProfileStore() {
return profileStore; return recipientStore;
} }
public StickerStore getStickerStore() { public StickerStore getStickerStore() {
@ -638,6 +657,10 @@ public class SignalAccount implements Closeable {
return new SignalServiceAddress(uuid, username); return new SignalServiceAddress(uuid, username);
} }
public RecipientId getSelfRecipientId() {
return recipientStore.resolveRecipientTrusted(getSelfAddress());
}
public int getDeviceId() { public int getDeviceId() {
return deviceId; return deviceId;
} }

View file

@ -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<Pair<RecipientId, Contact>> getContacts();
}

View file

@ -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<ContactInfo> 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<ContactInfo> getContacts() {
return new ArrayList<>(contacts);
}
/**
* Remove all contacts from the store
*/
public void clear() {
contacts.clear();
}
}

View file

@ -9,7 +9,7 @@ import java.util.UUID;
import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY; import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY;
public class ContactInfo { public class LegacyContactInfo {
@JsonProperty @JsonProperty
public String name; public String name;
@ -38,12 +38,7 @@ public class ContactInfo {
@JsonProperty(defaultValue = "false") @JsonProperty(defaultValue = "false")
public boolean archived; public boolean archived;
public ContactInfo() { public LegacyContactInfo() {
}
public ContactInfo(SignalServiceAddress address) {
this.number = address.getNumber().orNull();
this.uuid = address.getUuid().orNull();
} }
@JsonIgnore @JsonIgnore

View file

@ -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<LegacyContactInfo> contacts = new ArrayList<>();
private LegacyJsonContactsStore() {
}
public List<LegacyContactInfo> getContacts() {
return contacts;
}
}

View file

@ -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<LegacySignalProfileEntry> profiles = new ArrayList<>();
public List<LegacySignalProfileEntry> getProfileEntries() {
return profiles;
}
public static class ProfileStoreDeserializer extends JsonDeserializer<List<LegacySignalProfileEntry>> {
@Override
public List<LegacySignalProfileEntry> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
var profileEntries = new ArrayList<LegacySignalProfileEntry>();
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;
}
}
}

View file

@ -4,7 +4,7 @@ import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class SignalProfileEntry { public class LegacySignalProfileEntry {
private final SignalServiceAddress serviceAddress; private final SignalServiceAddress serviceAddress;
@ -16,9 +16,7 @@ public class SignalProfileEntry {
private final ProfileKeyCredential profileKeyCredential; private final ProfileKeyCredential profileKeyCredential;
private boolean requestPending; public LegacySignalProfileEntry(
public SignalProfileEntry(
final SignalServiceAddress serviceAddress, final SignalServiceAddress serviceAddress,
final ProfileKey profileKey, final ProfileKey profileKey,
final long lastUpdateTimestamp, final long lastUpdateTimestamp,
@ -51,12 +49,4 @@ public class SignalProfileEntry {
public ProfileKeyCredential getProfileKeyCredential() { public ProfileKeyCredential getProfileKeyCredential() {
return profileKeyCredential; return profileKeyCredential;
} }
public boolean isRequestPending() {
return requestPending;
}
public void setRequestPending(final boolean requestPending) {
this.requestPending = requestPending;
}
} }

View file

@ -1,156 +1,21 @@
package org.asamk.signal.manager.storage.profiles; package org.asamk.signal.manager.storage.profiles;
import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.signal.manager.storage.recipients.Profile;
import com.fasterxml.jackson.core.JsonGenerator; import org.asamk.signal.manager.storage.recipients.RecipientId;
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.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException; public interface ProfileStore {
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
public class ProfileStore { Profile getProfile(RecipientId recipientId);
private static final ObjectMapper jsonProcessor = new ObjectMapper(); ProfileKey getProfileKey(RecipientId recipientId);
@JsonProperty("profiles") ProfileKeyCredential getProfileKeyCredential(RecipientId recipientId);
@JsonDeserialize(using = ProfileStoreDeserializer.class)
@JsonSerialize(using = ProfileStoreSerializer.class)
private final List<SignalProfileEntry> profiles = new ArrayList<>();
public SignalProfileEntry getProfileEntry(SignalServiceAddress serviceAddress) { void storeProfile(RecipientId recipientId, Profile profile);
for (var entry : profiles) {
if (entry.getServiceAddress().matches(serviceAddress)) {
return entry;
}
}
return null;
}
public ProfileKey getProfileKey(SignalServiceAddress serviceAddress) { void storeProfileKey(RecipientId recipientId, ProfileKey profileKey);
for (var entry : profiles) {
if (entry.getServiceAddress().matches(serviceAddress)) {
return entry.getProfileKey();
}
}
return null;
}
public void updateProfile( void storeProfileKeyCredential(RecipientId recipientId, ProfileKeyCredential profileKeyCredential);
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<List<SignalProfileEntry>> {
@Override
public List<SignalProfileEntry> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
var addresses = new ArrayList<SignalProfileEntry>();
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));
}
}
return addresses;
}
}
public static class ProfileStoreSerializer extends JsonSerializer<List<SignalProfileEntry>> {
@Override
public void serialize(
List<SignalProfileEntry> 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();
}
}
} }

View file

@ -3,12 +3,11 @@ package org.asamk.signal.manager.storage.profiles;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
public class SignalProfile { public class SignalProfile {
@JsonProperty @JsonProperty
private final String identityKey; @JsonIgnore
private String identityKey;
@JsonProperty @JsonProperty
private final String name; private final String name;
@ -29,28 +28,6 @@ public class SignalProfile {
private final Capabilities capabilities; private final Capabilities capabilities;
public SignalProfile( 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("name") final String name,
@JsonProperty("about") final String about, @JsonProperty("about") final String about,
@JsonProperty("aboutEmoji") final String aboutEmoji, @JsonProperty("aboutEmoji") final String aboutEmoji,
@ -58,7 +35,6 @@ public class SignalProfile {
@JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess, @JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess,
@JsonProperty("capabilities") final Capabilities capabilities @JsonProperty("capabilities") final Capabilities capabilities
) { ) {
this.identityKey = identityKey;
this.name = name; this.name = name;
this.about = about; this.about = about;
this.aboutEmoji = aboutEmoji; this.aboutEmoji = aboutEmoji;
@ -67,17 +43,24 @@ public class SignalProfile {
this.capabilities = capabilities; this.capabilities = capabilities;
} }
public String getIdentityKey() { public String getGivenName() {
return identityKey; if (name == null) {
return null;
}
String[] parts = name.split("\0");
return parts.length < 1 ? null : parts[0];
} }
public String getName() { public String getFamilyName() {
return name; if (name == null) {
} return null;
}
public String getDisplayName() { String[] parts = name.split("\0");
// 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(); return parts.length < 2 ? null : parts[1];
} }
public String getAbout() { public String getAbout() {
@ -100,31 +83,6 @@ public class SignalProfile {
return capabilities; 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 { public static class Capabilities {
@JsonIgnore @JsonIgnore

View file

@ -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);
}
}
}

View file

@ -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<Capability> capabilities;
public Profile(
final long lastUpdateTimestamp,
final String givenName,
final String familyName,
final String about,
final String aboutEmoji,
final UnidentifiedAccessMode unidentifiedAccessMode,
final Set<Capability> 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<Capability> 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<Capability> 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<Capability> val) {
capabilities = val;
return this;
}
public Profile build() {
return new Profile(this);
}
public Builder withLastUpdateTimestamp(final long val) {
lastUpdateTimestamp = val;
return this;
}
}
}

View file

@ -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);
}
}
}

View file

@ -3,6 +3,11 @@ package org.asamk.signal.manager.storage.recipients;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.asamk.signal.manager.storage.Utils; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.Pair;
@ -17,14 +22,17 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class RecipientStore { public class RecipientStore implements ContactsStore, ProfileStore {
private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class); private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class);
@ -32,7 +40,7 @@ public class RecipientStore {
private final File file; private final File file;
private final RecipientMergeHandler recipientMergeHandler; private final RecipientMergeHandler recipientMergeHandler;
private final Map<RecipientId, SignalServiceAddress> recipients; private final Map<RecipientId, Recipient> recipients;
private final Map<RecipientId, RecipientId> recipientsMerged = new HashMap<>(); private final Map<RecipientId, RecipientId> recipientsMerged = new HashMap<>();
private long lastId; private long lastId;
@ -40,16 +48,57 @@ public class RecipientStore {
public static RecipientStore load(File file, RecipientMergeHandler recipientMergeHandler) throws IOException { public static RecipientStore load(File file, RecipientMergeHandler recipientMergeHandler) throws IOException {
final var objectMapper = Utils.createStorageObjectMapper(); final var objectMapper = Utils.createStorageObjectMapper();
try (var inputStream = new FileInputStream(file)) { try (var inputStream = new FileInputStream(file)) {
var storage = objectMapper.readValue(inputStream, Storage.class); final var storage = objectMapper.readValue(inputStream, Storage.class);
return new RecipientStore(objectMapper, final var recipients = storage.recipients.stream().map(r -> {
file, final var recipientId = new RecipientId(r.id);
recipientMergeHandler, final var address = new SignalServiceAddress(org.whispersystems.libsignal.util.guava.Optional.fromNullable(
storage.recipients.stream() r.uuid).transform(UuidUtil::parseOrThrow),
.collect(Collectors.toMap(r -> new RecipientId(r.id), org.whispersystems.libsignal.util.guava.Optional.fromNullable(r.number));
r -> new SignalServiceAddress(org.whispersystems.libsignal.util.guava.Optional.fromNullable(
r.uuid).transform(UuidUtil::parseOrThrow), Contact contact = null;
org.whispersystems.libsignal.util.guava.Optional.fromNullable(r.name)))), if (r.contact != null) {
storage.lastId); 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) { } catch (FileNotFoundException e) {
logger.debug("Creating new recipient store."); logger.debug("Creating new recipient store.");
return new RecipientStore(objectMapper, file, recipientMergeHandler, new HashMap<>(), 0); return new RecipientStore(objectMapper, file, recipientMergeHandler, new HashMap<>(), 0);
@ -60,7 +109,7 @@ public class RecipientStore {
final ObjectMapper objectMapper, final ObjectMapper objectMapper,
final File file, final File file,
final RecipientMergeHandler recipientMergeHandler, final RecipientMergeHandler recipientMergeHandler,
final Map<RecipientId, SignalServiceAddress> recipients, final Map<RecipientId, Recipient> recipients,
final long lastId final long lastId
) { ) {
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
@ -71,6 +120,12 @@ public class RecipientStore {
} }
public SignalServiceAddress resolveServiceAddress(RecipientId recipientId) { public SignalServiceAddress resolveServiceAddress(RecipientId recipientId) {
synchronized (recipients) {
return getRecipient(recipientId).getAddress();
}
}
public Recipient getRecipient(RecipientId recipientId) {
synchronized (recipients) { synchronized (recipients) {
while (recipientsMerged.containsKey(recipientId)) { while (recipientsMerged.containsKey(recipientId)) {
recipientId = recipientsMerged.get(recipientId); recipientId = recipientsMerged.get(recipientId);
@ -92,11 +147,11 @@ public class RecipientStore {
return resolveRecipient(new SignalServiceAddress(null, number), false); return resolveRecipient(new SignalServiceAddress(null, number), false);
} }
public RecipientId resolveRecipient(SignalServiceAddress address) { public RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
return resolveRecipient(address, true); return resolveRecipient(address, true);
} }
public List<RecipientId> resolveRecipients(List<SignalServiceAddress> addresses) { public List<RecipientId> resolveRecipientsTrusted(List<SignalServiceAddress> addresses) {
final List<RecipientId> recipientIds; final List<RecipientId> recipientIds;
final List<Pair<RecipientId, RecipientId>> toBeMerged = new ArrayList<>(); final List<Pair<RecipientId, RecipientId>> toBeMerged = new ArrayList<>();
synchronized (recipients) { synchronized (recipients) {
@ -114,10 +169,84 @@ public class RecipientStore {
return recipientIds; return recipientIds;
} }
public RecipientId resolveRecipientUntrusted(SignalServiceAddress address) { public RecipientId resolveRecipient(SignalServiceAddress address) {
return resolveRecipient(address, false); 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<Pair<RecipientId, Contact>> 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. * @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. * Has no effect, if the address contains only a number or a uuid.
@ -141,20 +270,20 @@ public class RecipientStore {
SignalServiceAddress address, boolean isHighTrust SignalServiceAddress address, boolean isHighTrust
) { ) {
final var byNumber = !address.getNumber().isPresent() final var byNumber = !address.getNumber().isPresent()
? Optional.<RecipientId>empty() ? Optional.<Recipient>empty()
: findByName(address.getNumber().get()); : findByNameLocked(address.getNumber().get());
final var byUuid = !address.getUuid().isPresent() final var byUuid = !address.getUuid().isPresent()
? Optional.<RecipientId>empty() ? Optional.<Recipient>empty()
: findByUuid(address.getUuid().get()); : findByUuidLocked(address.getUuid().get());
if (byNumber.isEmpty() && byUuid.isEmpty()) { if (byNumber.isEmpty() && byUuid.isEmpty()) {
logger.debug("Got new recipient, both uuid and number are unknown"); logger.debug("Got new recipient, both uuid and number are unknown");
if (isHighTrust || !address.getUuid().isPresent() || !address.getNumber().isPresent()) { 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()); Optional.empty());
} }
@ -162,79 +291,138 @@ public class RecipientStore {
|| !address.getUuid().isPresent() || !address.getUuid().isPresent()
|| !address.getNumber().isPresent() || !address.getNumber().isPresent()
|| byNumber.equals(byUuid)) { || 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()) { if (byNumber.isEmpty()) {
logger.debug("Got recipient existing with uuid, updating with high trust number"); logger.debug("Got recipient existing with uuid, updating with high trust number");
recipients.put(byUuid.get(), address); updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
save(); return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
return new Pair<>(byUuid.get(), Optional.empty());
} }
if (byUuid.isEmpty()) { if (byUuid.isEmpty()) {
logger.debug("Got recipient existing with number, updating with high trust uuid"); logger.debug("Got recipient existing with number, updating with high trust uuid");
recipients.put(byNumber.get(), address); updateRecipientAddressLocked(byNumber.get().getRecipientId(), address);
save(); return new Pair<>(byNumber.get().getRecipientId(), Optional.empty());
return new Pair<>(byNumber.get(), Optional.empty());
} }
final var byNumberAddress = recipients.get(byNumber.get()); if (byNumber.get().getAddress().getUuid().isPresent()) {
if (byNumberAddress.getUuid().isPresent()) {
logger.debug( logger.debug(
"Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number"); "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)); updateRecipientAddressLocked(byNumber.get().getRecipientId(),
recipients.put(byUuid.get(), address); new SignalServiceAddress(byNumber.get().getAddress().getUuid().get(), null));
save(); updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
return new Pair<>(byUuid.get(), Optional.empty()); return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
} }
logger.debug("Got separate recipients for high trust number and uuid, need to merge them"); logger.debug("Got separate recipients for high trust number and uuid, need to merge them");
recipients.put(byUuid.get(), address); updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
recipients.remove(byNumber.get()); mergeRecipientsLocked(byUuid.get().getRecipientId(), byNumber.get().getRecipientId());
save(); return new Pair<>(byUuid.get().getRecipientId(), byNumber.map(Recipient::getRecipientId));
return new Pair<>(byUuid.get(), byNumber);
} }
private RecipientId addNewRecipient(final SignalServiceAddress serviceAddress) { private RecipientId addNewRecipientLocked(final SignalServiceAddress serviceAddress) {
final var nextRecipientId = nextId(); final var nextRecipientId = nextIdLocked();
recipients.put(nextRecipientId, serviceAddress); storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, serviceAddress, null, null, null, null));
save();
return nextRecipientId; return nextRecipientId;
} }
private Optional<RecipientId> 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<Recipient> findByNameLocked(final String number) {
return recipients.entrySet() return recipients.entrySet()
.stream() .stream()
.filter(entry -> entry.getValue().getNumber().isPresent() && number.equals(entry.getValue() .filter(entry -> entry.getValue().getAddress().getNumber().isPresent() && number.equals(entry.getValue()
.getAddress()
.getNumber() .getNumber()
.get())) .get()))
.findFirst() .findFirst()
.map(Map.Entry::getKey); .map(Map.Entry::getValue);
} }
private Optional<RecipientId> findByUuid(final UUID uuid) { private Optional<Recipient> findByUuidLocked(final UUID uuid) {
return recipients.entrySet() return recipients.entrySet()
.stream() .stream()
.filter(entry -> entry.getValue().getUuid().isPresent() && uuid.equals(entry.getValue() .filter(entry -> entry.getValue().getAddress().getUuid().isPresent() && uuid.equals(entry.getValue()
.getAddress()
.getUuid() .getUuid()
.get())) .get()))
.findFirst() .findFirst()
.map(Map.Entry::getKey); .map(Map.Entry::getValue);
} }
private RecipientId nextId() { private RecipientId nextIdLocked() {
return new RecipientId(++this.lastId); return new RecipientId(++this.lastId);
} }
private void save() { private void saveLocked() {
var storage = new Storage(recipients.entrySet() final var base64 = Base64.getEncoder();
.stream() var storage = new Storage(recipients.entrySet().stream().map(pair -> {
.map(pair -> new Storage.Recipient(pair.getKey().getId(), final var recipient = pair.getValue();
pair.getValue().getNumber().orNull(), final var contact = recipient.getContact() == null
pair.getValue().getUuid().transform(UUID::toString).orNull())) ? null
.collect(Collectors.toList()), lastId); : 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 // Write to memory first to prevent corrupting the file in case of serialization errors
try (var inMemoryOutput = new ByteArrayOutputStream()) { 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 static class Storage {
private List<Recipient> recipients; public List<Recipient> recipients;
private long lastId; public long lastId;
// For deserialization // For deserialization
private Storage() { private Storage() {
@ -270,40 +452,96 @@ public class RecipientStore {
this.lastId = lastId; this.lastId = lastId;
} }
public List<Recipient> getRecipients() { private static class Recipient {
return recipients;
}
public long getLastId() { public long id;
return lastId; public String number;
} public String uuid;
public String profileKey;
public static class Recipient { public String profileKeyCredential;
public Contact contact;
private long id; public Profile profile;
private String name;
private String uuid;
// For deserialization // For deserialization
private Recipient() { 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.id = id;
this.name = name; this.number = number;
this.uuid = uuid; this.uuid = uuid;
this.profileKey = profileKey;
this.profileKeyCredential = profileKeyCredential;
this.contact = contact;
this.profile = profile;
} }
public long getId() { private static class Contact {
return id;
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 getName() { private static class Profile {
return name;
}
public String getUuid() { public long lastUpdateTimestamp;
return uuid; public String givenName;
public String familyName;
public String about;
public String aboutEmoji;
public String unidentifiedAccessMode;
public Set<String> 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<String> capabilities
) {
this.lastUpdateTimestamp = lastUpdateTimestamp;
this.givenName = givenName;
this.familyName = familyName;
this.about = about;
this.aboutEmoji = aboutEmoji;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.capabilities = capabilities;
}
} }
} }
} }

View file

@ -1,16 +1,19 @@
package org.asamk.signal.manager.util; 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.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import java.util.Base64; import java.util.Base64;
import java.util.Date;
import java.util.HashSet;
public class ProfileUtils { public class ProfileUtils {
public static SignalProfile decryptProfile( public static Profile decryptProfile(
final ProfileKey profileKey, final SignalServiceProfile encryptedProfile final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
) { ) {
var profileCipher = new ProfileCipher(profileKey); var profileCipher = new ProfileCipher(profileKey);
@ -28,13 +31,28 @@ public class ProfileUtils {
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
unidentifiedAccess = null; unidentifiedAccess = null;
} }
return new SignalProfile(encryptedProfile.getIdentityKey(), final var nameParts = splitName(name);
name, final var capabilities = new HashSet<Profile.Capability>();
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, about,
aboutEmoji, aboutEmoji,
unidentifiedAccess, encryptedProfile.isUnrestrictedUnidentifiedAccess()
encryptedProfile.isUnrestrictedUnidentifiedAccess(), ? Profile.UnidentifiedAccessMode.UNRESTRICTED
encryptedProfile.getCapabilities()); : unidentifiedAccess != null
? Profile.UnidentifiedAccessMode.ENABLED
: Profile.UnidentifiedAccessMode.DISABLED,
capabilities);
} catch (InvalidCiphertextException e) { } catch (InvalidCiphertextException e) {
return null; return null;
} }
@ -51,4 +69,17 @@ public class ProfileUtils {
return null; return null;
} }
} }
private static Pair<String, String> 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]);
}
}
} }

View file

@ -87,7 +87,7 @@ public interface Signal extends DBusInterface {
void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure; 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); boolean isGroupBlocked(final byte[] groupId);

View file

@ -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.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException; import java.io.IOException;
import java.util.Base64; import java.util.Base64;
@ -667,7 +668,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
private String formatContact(SignalServiceAddress address) { private String formatContact(SignalServiceAddress address) {
final var number = address.getLegacyIdentifier(); 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()) { if (name == null || name.isEmpty()) {
return number; return number;
} else { } else {

View file

@ -18,7 +18,10 @@ public class ListContactsCommand implements LocalCommand {
var contacts = m.getContacts(); var contacts = m.getContacts();
for (var c : contacts) { 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());
} }
} }
} }

View file

@ -11,6 +11,7 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.ErrorUtils;
import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
@ -248,7 +249,7 @@ public class DbusSignalImpl implements Signal {
public String getContactName(final String number) { public String getContactName(final String number) {
try { try {
return m.getContactOrProfileName(number); return m.getContactOrProfileName(number);
} catch (Exception e) { } catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage()); throw new Error.InvalidNumber(e.getMessage());
} }
} }
@ -383,11 +384,10 @@ public class DbusSignalImpl implements Signal {
// all numbers the system knows // all numbers the system knows
@Override @Override
public List<String> listNumbers() { public List<String> listNumbers() {
return Stream.concat(m.getIdentities() return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId),
.stream() m.getContacts().stream().map(Pair::first))
.map(IdentityInfo::getRecipientId)
.map(m::resolveSignalServiceAddress) .map(m::resolveSignalServiceAddress)
.map(a -> a.getNumber().orNull()), m.getContacts().stream().map(c -> c.number)) .map(a -> a.getNumber().orNull())
.filter(Objects::nonNull) .filter(Objects::nonNull)
.distinct() .distinct()
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -399,8 +399,8 @@ public class DbusSignalImpl implements Signal {
var numbers = new ArrayList<String>(); var numbers = new ArrayList<String>();
var contacts = m.getContacts(); var contacts = m.getContacts();
for (var c : contacts) { for (var c : contacts) {
if (c.name != null && c.name.equals(name)) { if (name.equals(c.second().getName())) {
numbers.add(c.number); numbers.add(m.resolveSignalServiceAddress(c.first()).getLegacyIdentifier());
} }
} }
// Try profiles if no contact name was found // Try profiles if no contact name was found
@ -449,13 +449,11 @@ public class DbusSignalImpl implements Signal {
@Override @Override
public boolean isContactBlocked(final String number) { public boolean isContactBlocked(final String number) {
var contacts = m.getContacts(); try {
for (var c : contacts) { return m.isContactBlocked(number);
if (c.number.equals(number)) { } catch (InvalidNumberException e) {
return c.blocked; throw new Error.InvalidNumber(e.getMessage());
}
} }
return false;
} }
@Override @Override