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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -1,10 +1,10 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface UnidentifiedAccessProvider {
Optional<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 org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.contacts.ContactInfo;
import org.asamk.signal.manager.storage.contacts.JsonContactsStore;
import org.asamk.signal.manager.storage.contacts.ContactsStore;
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.JsonGroupStore;
import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
import org.asamk.signal.manager.storage.messageCache.MessageCache;
import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore;
import org.asamk.signal.manager.storage.profiles.LegacyProfileStore;
import org.asamk.signal.manager.storage.profiles.ProfileStore;
import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore;
import org.asamk.signal.manager.storage.protocol.SignalProtocolStore;
import org.asamk.signal.manager.storage.recipients.Contact;
import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.recipients.RecipientStore;
import org.asamk.signal.manager.storage.sessions.SessionStore;
@ -45,6 +48,7 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.storage.StorageKey;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@ -57,9 +61,9 @@ import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class SignalAccount implements Closeable {
@ -88,9 +92,7 @@ public class SignalAccount implements Closeable {
private SessionStore sessionStore;
private IdentityKeyStore identityKeyStore;
private JsonGroupStore groupStore;
private JsonContactsStore contactStore;
private RecipientStore recipientStore;
private ProfileStore profileStore;
private StickerStore stickerStore;
private MessageCache messageCache;
@ -136,7 +138,6 @@ public class SignalAccount implements Closeable {
account.username = username;
account.profileKey = profileKey;
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.contactStore = new JsonContactsStore();
account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
account::mergeRecipients);
account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
@ -151,7 +152,6 @@ public class SignalAccount implements Closeable {
account.signedPreKeyStore,
account.sessionStore,
account.identityKeyStore);
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore();
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
@ -188,9 +188,9 @@ public class SignalAccount implements Closeable {
account.profileKey = profileKey;
account.deviceId = deviceId;
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
account.contactStore = new JsonContactsStore();
account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
account::mergeRecipients);
account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
@ -203,7 +203,6 @@ public class SignalAccount implements Closeable {
account.signedPreKeyStore,
account.sessionStore,
account.identityKeyStore);
account.profileStore = new ProfileStore();
account.stickerStore = new StickerStore();
account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
@ -222,23 +221,8 @@ public class SignalAccount implements Closeable {
setProfileKey(KeyUtils.createProfileKey());
save();
}
// Store profile keys only in profile store
for (var contact : getContactStore().getContacts()) {
var profileKeyString = contact.profileKey;
if (profileKeyString == null) {
continue;
}
final ProfileKey profileKey;
try {
profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString));
} catch (InvalidInputException ignored) {
continue;
}
contact.profileKey = null;
getProfileStore().storeProfileKey(contact.getAddress(), profileKey);
}
// Ensure our profile key is stored in profile store
getProfileStore().storeProfileKey(getSelfAddress(), getProfileKey());
getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey());
}
private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
@ -354,13 +338,15 @@ public class SignalAccount implements Closeable {
}
recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
var legacyRecipientStoreNode = rootNode.get("recipientStore");
if (legacyRecipientStoreNode != null) {
logger.debug("Migrating legacy recipient store.");
var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class);
if (legacyRecipientStore != null) {
recipientStore.resolveRecipients(legacyRecipientStore.getAddresses());
recipientStore.resolveRecipientsTrusted(legacyRecipientStore.getAddresses());
}
recipientStore.resolveRecipientTrusted(getSelfAddress());
}
var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
@ -414,9 +400,9 @@ public class SignalAccount implements Closeable {
identityKeyPair,
registrationId);
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
logger.debug("Migrating identity session store.");
logger.debug("Migrating legacy identity session store.");
for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) {
RecipientId recipientId = recipientStore.resolveRecipient(identity.getAddress());
RecipientId recipientId = recipientStore.resolveRecipientTrusted(identity.getAddress());
identityKeyStore.saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded());
identityKeyStore.setIdentityTrustLevel(recipientId,
identity.getIdentityKey(),
@ -436,20 +422,67 @@ public class SignalAccount implements Closeable {
groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
}
var contactStoreNode = rootNode.get("contactStore");
if (contactStoreNode != null) {
contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
}
if (contactStore == null) {
contactStore = new JsonContactsStore();
if (rootNode.hasNonNull("contactStore")) {
logger.debug("Migrating legacy contact store.");
final var contactStoreNode = rootNode.get("contactStore");
final var contactStore = jsonProcessor.convertValue(contactStoreNode, LegacyJsonContactsStore.class);
for (var contact : contactStore.getContacts()) {
final var recipientId = recipientStore.resolveRecipientTrusted(contact.getAddress());
recipientStore.storeContact(recipientId,
new Contact(contact.name,
contact.color,
contact.messageExpirationTime,
contact.blocked,
contact.archived));
// Store profile keys only in profile store
var profileKeyString = contact.profileKey;
if (profileKeyString != null) {
final ProfileKey profileKey;
try {
profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString));
getProfileStore().storeProfileKey(recipientId, profileKey);
} catch (InvalidInputException e) {
logger.warn("Failed to parse legacy contact profile key: {}", e.getMessage());
}
}
}
}
var profileStoreNode = rootNode.get("profileStore");
if (profileStoreNode != null) {
profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class);
}
if (profileStore == null) {
profileStore = new ProfileStore();
if (rootNode.hasNonNull("profileStore")) {
logger.debug("Migrating legacy profile store.");
var profileStoreNode = rootNode.get("profileStore");
final var legacyProfileStore = jsonProcessor.convertValue(profileStoreNode, LegacyProfileStore.class);
for (var profileEntry : legacyProfileStore.getProfileEntries()) {
var recipientId = recipientStore.resolveRecipient(profileEntry.getServiceAddress());
recipientStore.storeProfileKey(recipientId, profileEntry.getProfileKey());
recipientStore.storeProfileKeyCredential(recipientId, profileEntry.getProfileKeyCredential());
final var profile = profileEntry.getProfile();
if (profile != null) {
final var capabilities = new HashSet<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");
@ -460,24 +493,6 @@ public class SignalAccount implements Closeable {
stickerStore = new StickerStore();
}
if (recipientStore.isEmpty()) {
recipientStore.resolveRecipient(getSelfAddress());
recipientStore.resolveRecipients(contactStore.getContacts()
.stream()
.map(ContactInfo::getAddress)
.collect(Collectors.toList()));
for (var group : groupStore.getGroups()) {
if (group instanceof GroupInfoV1) {
var groupInfoV1 = (GroupInfoV1) group;
groupInfoV1.members = groupInfoV1.members.stream()
.map(m -> recipientStore.resolveServiceAddress(m))
.collect(Collectors.toSet());
}
}
}
messageCache = new MessageCache(getMessageCachePath(dataPath, username));
var threadStoreNode = rootNode.get("threadStore");
@ -489,10 +504,15 @@ public class SignalAccount implements Closeable {
continue;
}
try {
var contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id));
if (contactInfo != null) {
contactInfo.messageExpirationTime = thread.messageExpirationTime;
contactStore.updateContact(contactInfo);
if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) {
final var recipientId = recipientStore.resolveRecipient(thread.id);
var contact = recipientStore.getContact(recipientId);
if (contact != null) {
recipientStore.storeContact(recipientId,
Contact.newBuilder(contact)
.withMessageExpirationTime(thread.messageExpirationTime)
.build());
}
} else {
var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
if (groupInfo instanceof GroupInfoV1) {
@ -500,7 +520,8 @@ public class SignalAccount implements Closeable {
groupStore.updateGroup(groupInfo);
}
}
} catch (Exception ignored) {
} catch (Exception e) {
logger.warn("Failed to read legacy thread info: {}", e.getMessage());
}
}
}
@ -533,8 +554,6 @@ public class SignalAccount implements Closeable {
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
.put("registered", registered)
.putPOJO("groupStore", groupStore)
.putPOJO("contactStore", contactStore)
.putPOJO("profileStore", profileStore)
.putPOJO("stickerStore", stickerStore);
try {
try (var output = new ByteArrayOutputStream()) {
@ -602,8 +621,8 @@ public class SignalAccount implements Closeable {
return groupStore;
}
public JsonContactsStore getContactStore() {
return contactStore;
public ContactsStore getContactStore() {
return recipientStore;
}
public RecipientStore getRecipientStore() {
@ -611,7 +630,7 @@ public class SignalAccount implements Closeable {
}
public ProfileStore getProfileStore() {
return profileStore;
return recipientStore;
}
public StickerStore getStickerStore() {
@ -638,6 +657,10 @@ public class SignalAccount implements Closeable {
return new SignalServiceAddress(uuid, username);
}
public RecipientId getSelfRecipientId() {
return recipientStore.resolveRecipientTrusted(getSelfAddress());
}
public int getDeviceId() {
return deviceId;
}

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;
public class ContactInfo {
public class LegacyContactInfo {
@JsonProperty
public String name;
@ -38,12 +38,7 @@ public class ContactInfo {
@JsonProperty(defaultValue = "false")
public boolean archived;
public ContactInfo() {
}
public ContactInfo(SignalServiceAddress address) {
this.number = address.getNumber().orNull();
this.uuid = address.getUuid().orNull();
public LegacyContactInfo() {
}
@JsonIgnore

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

View file

@ -1,156 +1,21 @@
package org.asamk.signal.manager.storage.profiles;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.signal.zkgroup.InvalidInputException;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
public interface ProfileStore {
public class ProfileStore {
Profile getProfile(RecipientId recipientId);
private static final ObjectMapper jsonProcessor = new ObjectMapper();
ProfileKey getProfileKey(RecipientId recipientId);
@JsonProperty("profiles")
@JsonDeserialize(using = ProfileStoreDeserializer.class)
@JsonSerialize(using = ProfileStoreSerializer.class)
private final List<SignalProfileEntry> profiles = new ArrayList<>();
ProfileKeyCredential getProfileKeyCredential(RecipientId recipientId);
public SignalProfileEntry getProfileEntry(SignalServiceAddress serviceAddress) {
for (var entry : profiles) {
if (entry.getServiceAddress().matches(serviceAddress)) {
return entry;
}
}
return null;
}
void storeProfile(RecipientId recipientId, Profile profile);
public ProfileKey getProfileKey(SignalServiceAddress serviceAddress) {
for (var entry : profiles) {
if (entry.getServiceAddress().matches(serviceAddress)) {
return entry.getProfileKey();
}
}
return null;
}
void storeProfileKey(RecipientId recipientId, ProfileKey profileKey);
public void updateProfile(
SignalServiceAddress serviceAddress,
ProfileKey profileKey,
long now,
SignalProfile profile,
ProfileKeyCredential profileKeyCredential
) {
var newEntry = new SignalProfileEntry(serviceAddress, profileKey, now, profile, profileKeyCredential);
for (var i = 0; i < profiles.size(); i++) {
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
profiles.set(i, newEntry);
return;
}
}
profiles.add(newEntry);
}
public void storeProfileKey(SignalServiceAddress serviceAddress, ProfileKey profileKey) {
var newEntry = new SignalProfileEntry(serviceAddress, profileKey, 0, null, null);
for (var i = 0; i < profiles.size(); i++) {
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
if (!profiles.get(i).getProfileKey().equals(profileKey)) {
profiles.set(i, newEntry);
}
return;
}
}
profiles.add(newEntry);
}
public static class ProfileStoreDeserializer extends JsonDeserializer<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();
}
}
void storeProfileKeyCredential(RecipientId recipientId, ProfileKeyCredential profileKeyCredential);
}

View file

@ -3,12 +3,11 @@ package org.asamk.signal.manager.storage.profiles;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
public class SignalProfile {
@JsonProperty
private final String identityKey;
@JsonIgnore
private String identityKey;
@JsonProperty
private final String name;
@ -29,28 +28,6 @@ public class SignalProfile {
private final Capabilities capabilities;
public SignalProfile(
final String identityKey,
final String name,
final String about,
final String aboutEmoji,
final String unidentifiedAccess,
final boolean unrestrictedUnidentifiedAccess,
final SignalServiceProfile.Capabilities capabilities
) {
this.identityKey = identityKey;
this.name = name;
this.about = about;
this.aboutEmoji = aboutEmoji;
this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
this.capabilities = new Capabilities();
this.capabilities.storage = capabilities.isStorage();
this.capabilities.gv1Migration = capabilities.isGv1Migration();
this.capabilities.gv2 = capabilities.isGv2();
}
public SignalProfile(
@JsonProperty("identityKey") final String identityKey,
@JsonProperty("name") final String name,
@JsonProperty("about") final String about,
@JsonProperty("aboutEmoji") final String aboutEmoji,
@ -58,7 +35,6 @@ public class SignalProfile {
@JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess,
@JsonProperty("capabilities") final Capabilities capabilities
) {
this.identityKey = identityKey;
this.name = name;
this.about = about;
this.aboutEmoji = aboutEmoji;
@ -67,17 +43,24 @@ public class SignalProfile {
this.capabilities = capabilities;
}
public String getIdentityKey() {
return identityKey;
public String getGivenName() {
if (name == null) {
return null;
}
String[] parts = name.split("\0");
return parts.length < 1 ? null : parts[0];
}
public String getName() {
return name;
}
public String getFamilyName() {
if (name == null) {
return null;
}
public String getDisplayName() {
// First name and last name (if set) are separated by a NULL char + trim space in case only one is filled
return name == null ? "" : name.replace("\0", " ").trim();
String[] parts = name.split("\0");
return parts.length < 2 ? null : parts[1];
}
public String getAbout() {
@ -100,31 +83,6 @@ public class SignalProfile {
return capabilities;
}
@Override
public String toString() {
return "SignalProfile{"
+ "identityKey='"
+ identityKey
+ '\''
+ ", name='"
+ name
+ '\''
+ ", about='"
+ about
+ '\''
+ ", aboutEmoji='"
+ aboutEmoji
+ '\''
+ ", unidentifiedAccess='"
+ unidentifiedAccess
+ '\''
+ ", unrestrictedUnidentifiedAccess="
+ unrestrictedUnidentifiedAccess
+ ", capabilities="
+ capabilities
+ '}';
}
public static class Capabilities {
@JsonIgnore

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 org.asamk.signal.manager.storage.Utils;
import org.asamk.signal.manager.storage.contacts.ContactsStore;
import org.asamk.signal.manager.storage.profiles.ProfileStore;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.Pair;
@ -17,14 +22,17 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class RecipientStore {
public class RecipientStore implements ContactsStore, ProfileStore {
private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class);
@ -32,7 +40,7 @@ public class RecipientStore {
private final File file;
private final RecipientMergeHandler recipientMergeHandler;
private final Map<RecipientId, SignalServiceAddress> recipients;
private final Map<RecipientId, Recipient> recipients;
private final Map<RecipientId, RecipientId> recipientsMerged = new HashMap<>();
private long lastId;
@ -40,16 +48,57 @@ public class RecipientStore {
public static RecipientStore load(File file, RecipientMergeHandler recipientMergeHandler) throws IOException {
final var objectMapper = Utils.createStorageObjectMapper();
try (var inputStream = new FileInputStream(file)) {
var storage = objectMapper.readValue(inputStream, Storage.class);
return new RecipientStore(objectMapper,
file,
recipientMergeHandler,
storage.recipients.stream()
.collect(Collectors.toMap(r -> new RecipientId(r.id),
r -> new SignalServiceAddress(org.whispersystems.libsignal.util.guava.Optional.fromNullable(
r.uuid).transform(UuidUtil::parseOrThrow),
org.whispersystems.libsignal.util.guava.Optional.fromNullable(r.name)))),
storage.lastId);
final var storage = objectMapper.readValue(inputStream, Storage.class);
final var recipients = storage.recipients.stream().map(r -> {
final var recipientId = new RecipientId(r.id);
final var address = new SignalServiceAddress(org.whispersystems.libsignal.util.guava.Optional.fromNullable(
r.uuid).transform(UuidUtil::parseOrThrow),
org.whispersystems.libsignal.util.guava.Optional.fromNullable(r.number));
Contact contact = null;
if (r.contact != null) {
contact = new Contact(r.contact.name,
r.contact.color,
r.contact.messageExpirationTime,
r.contact.blocked,
r.contact.archived);
}
ProfileKey profileKey = null;
if (r.profileKey != null) {
try {
profileKey = new ProfileKey(Base64.getDecoder().decode(r.profileKey));
} catch (InvalidInputException ignored) {
}
}
ProfileKeyCredential profileKeyCredential = null;
if (r.profileKeyCredential != null) {
try {
profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder()
.decode(r.profileKeyCredential));
} catch (Throwable ignored) {
}
}
Profile profile = null;
if (r.profile != null) {
profile = new Profile(r.profile.lastUpdateTimestamp,
r.profile.givenName,
r.profile.familyName,
r.profile.about,
r.profile.aboutEmoji,
Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode),
r.profile.capabilities.stream()
.map(Profile.Capability::valueOfOrNull)
.filter(Objects::nonNull)
.collect(Collectors.toSet()));
}
return new Recipient(recipientId, address, contact, profileKey, profileKeyCredential, profile);
}).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));
return new RecipientStore(objectMapper, file, recipientMergeHandler, recipients, storage.lastId);
} catch (FileNotFoundException e) {
logger.debug("Creating new recipient store.");
return new RecipientStore(objectMapper, file, recipientMergeHandler, new HashMap<>(), 0);
@ -60,7 +109,7 @@ public class RecipientStore {
final ObjectMapper objectMapper,
final File file,
final RecipientMergeHandler recipientMergeHandler,
final Map<RecipientId, SignalServiceAddress> recipients,
final Map<RecipientId, Recipient> recipients,
final long lastId
) {
this.objectMapper = objectMapper;
@ -71,6 +120,12 @@ public class RecipientStore {
}
public SignalServiceAddress resolveServiceAddress(RecipientId recipientId) {
synchronized (recipients) {
return getRecipient(recipientId).getAddress();
}
}
public Recipient getRecipient(RecipientId recipientId) {
synchronized (recipients) {
while (recipientsMerged.containsKey(recipientId)) {
recipientId = recipientsMerged.get(recipientId);
@ -92,11 +147,11 @@ public class RecipientStore {
return resolveRecipient(new SignalServiceAddress(null, number), false);
}
public RecipientId resolveRecipient(SignalServiceAddress address) {
public RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
return resolveRecipient(address, true);
}
public List<RecipientId> resolveRecipients(List<SignalServiceAddress> addresses) {
public List<RecipientId> resolveRecipientsTrusted(List<SignalServiceAddress> addresses) {
final List<RecipientId> recipientIds;
final List<Pair<RecipientId, RecipientId>> toBeMerged = new ArrayList<>();
synchronized (recipients) {
@ -114,10 +169,84 @@ public class RecipientStore {
return recipientIds;
}
public RecipientId resolveRecipientUntrusted(SignalServiceAddress address) {
public RecipientId resolveRecipient(SignalServiceAddress address) {
return resolveRecipient(address, false);
}
@Override
public void storeContact(final RecipientId recipientId, final Contact contact) {
synchronized (recipients) {
final var recipient = recipients.get(recipientId);
storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withContact(contact).build());
}
}
@Override
public Contact getContact(final RecipientId recipientId) {
final var recipient = getRecipient(recipientId);
return recipient == null ? null : recipient.getContact();
}
@Override
public List<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.
* Has no effect, if the address contains only a number or a uuid.
@ -141,20 +270,20 @@ public class RecipientStore {
SignalServiceAddress address, boolean isHighTrust
) {
final var byNumber = !address.getNumber().isPresent()
? Optional.<RecipientId>empty()
: findByName(address.getNumber().get());
? Optional.<Recipient>empty()
: findByNameLocked(address.getNumber().get());
final var byUuid = !address.getUuid().isPresent()
? Optional.<RecipientId>empty()
: findByUuid(address.getUuid().get());
? Optional.<Recipient>empty()
: findByUuidLocked(address.getUuid().get());
if (byNumber.isEmpty() && byUuid.isEmpty()) {
logger.debug("Got new recipient, both uuid and number are unknown");
if (isHighTrust || !address.getUuid().isPresent() || !address.getNumber().isPresent()) {
return new Pair<>(addNewRecipient(address), Optional.empty());
return new Pair<>(addNewRecipientLocked(address), Optional.empty());
}
return new Pair<>(addNewRecipient(new SignalServiceAddress(address.getUuid().get(), null)),
return new Pair<>(addNewRecipientLocked(new SignalServiceAddress(address.getUuid().get(), null)),
Optional.empty());
}
@ -162,79 +291,138 @@ public class RecipientStore {
|| !address.getUuid().isPresent()
|| !address.getNumber().isPresent()
|| byNumber.equals(byUuid)) {
return new Pair<>(byUuid.orElseGet(byNumber::get), Optional.empty());
return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty());
}
if (byNumber.isEmpty()) {
logger.debug("Got recipient existing with uuid, updating with high trust number");
recipients.put(byUuid.get(), address);
save();
return new Pair<>(byUuid.get(), Optional.empty());
updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
}
if (byUuid.isEmpty()) {
logger.debug("Got recipient existing with number, updating with high trust uuid");
recipients.put(byNumber.get(), address);
save();
return new Pair<>(byNumber.get(), Optional.empty());
updateRecipientAddressLocked(byNumber.get().getRecipientId(), address);
return new Pair<>(byNumber.get().getRecipientId(), Optional.empty());
}
final var byNumberAddress = recipients.get(byNumber.get());
if (byNumberAddress.getUuid().isPresent()) {
if (byNumber.get().getAddress().getUuid().isPresent()) {
logger.debug(
"Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number");
recipients.put(byNumber.get(), new SignalServiceAddress(byNumberAddress.getUuid().get(), null));
recipients.put(byUuid.get(), address);
save();
return new Pair<>(byUuid.get(), Optional.empty());
updateRecipientAddressLocked(byNumber.get().getRecipientId(),
new SignalServiceAddress(byNumber.get().getAddress().getUuid().get(), null));
updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
}
logger.debug("Got separate recipients for high trust number and uuid, need to merge them");
recipients.put(byUuid.get(), address);
recipients.remove(byNumber.get());
save();
return new Pair<>(byUuid.get(), byNumber);
updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
mergeRecipientsLocked(byUuid.get().getRecipientId(), byNumber.get().getRecipientId());
return new Pair<>(byUuid.get().getRecipientId(), byNumber.map(Recipient::getRecipientId));
}
private RecipientId addNewRecipient(final SignalServiceAddress serviceAddress) {
final var nextRecipientId = nextId();
recipients.put(nextRecipientId, serviceAddress);
save();
private RecipientId addNewRecipientLocked(final SignalServiceAddress serviceAddress) {
final var nextRecipientId = nextIdLocked();
storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, serviceAddress, null, null, null, null));
return nextRecipientId;
}
private Optional<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()
.stream()
.filter(entry -> entry.getValue().getNumber().isPresent() && number.equals(entry.getValue()
.filter(entry -> entry.getValue().getAddress().getNumber().isPresent() && number.equals(entry.getValue()
.getAddress()
.getNumber()
.get()))
.findFirst()
.map(Map.Entry::getKey);
.map(Map.Entry::getValue);
}
private Optional<RecipientId> findByUuid(final UUID uuid) {
private Optional<Recipient> findByUuidLocked(final UUID uuid) {
return recipients.entrySet()
.stream()
.filter(entry -> entry.getValue().getUuid().isPresent() && uuid.equals(entry.getValue()
.filter(entry -> entry.getValue().getAddress().getUuid().isPresent() && uuid.equals(entry.getValue()
.getAddress()
.getUuid()
.get()))
.findFirst()
.map(Map.Entry::getKey);
.map(Map.Entry::getValue);
}
private RecipientId nextId() {
private RecipientId nextIdLocked() {
return new RecipientId(++this.lastId);
}
private void save() {
var storage = new Storage(recipients.entrySet()
.stream()
.map(pair -> new Storage.Recipient(pair.getKey().getId(),
pair.getValue().getNumber().orNull(),
pair.getValue().getUuid().transform(UUID::toString).orNull()))
.collect(Collectors.toList()), lastId);
private void saveLocked() {
final var base64 = Base64.getEncoder();
var storage = new Storage(recipients.entrySet().stream().map(pair -> {
final var recipient = pair.getValue();
final var contact = recipient.getContact() == null
? null
: new Storage.Recipient.Contact(recipient.getContact().getName(),
recipient.getContact().getColor(),
recipient.getContact().getMessageExpirationTime(),
recipient.getContact().isBlocked(),
recipient.getContact().isArchived());
final var profile = recipient.getProfile() == null
? null
: new Storage.Recipient.Profile(recipient.getProfile().getLastUpdateTimestamp(),
recipient.getProfile().getGivenName(),
recipient.getProfile().getFamilyName(),
recipient.getProfile().getAbout(),
recipient.getProfile().getAboutEmoji(),
recipient.getProfile().getUnidentifiedAccessMode().name(),
recipient.getProfile()
.getCapabilities()
.stream()
.map(Enum::name)
.collect(Collectors.toSet()));
return new Storage.Recipient(pair.getKey().getId(),
recipient.getAddress().getNumber().orNull(),
recipient.getAddress().getUuid().transform(UUID::toString).orNull(),
recipient.getProfileKey() == null
? null
: base64.encodeToString(recipient.getProfileKey().serialize()),
recipient.getProfileKeyCredential() == null
? null
: base64.encodeToString(recipient.getProfileKeyCredential().serialize()),
contact,
profile);
}).collect(Collectors.toList()), lastId);
// Write to memory first to prevent corrupting the file in case of serialization errors
try (var inMemoryOutput = new ByteArrayOutputStream()) {
@ -249,17 +437,11 @@ public class RecipientStore {
}
}
public boolean isEmpty() {
synchronized (recipients) {
return recipients.isEmpty();
}
}
private static class Storage {
private List<Recipient> recipients;
public List<Recipient> recipients;
private long lastId;
public long lastId;
// For deserialization
private Storage() {
@ -270,40 +452,96 @@ public class RecipientStore {
this.lastId = lastId;
}
public List<Recipient> getRecipients() {
return recipients;
}
private static class Recipient {
public long getLastId() {
return lastId;
}
public static class Recipient {
private long id;
private String name;
private String uuid;
public long id;
public String number;
public String uuid;
public String profileKey;
public String profileKeyCredential;
public Contact contact;
public Profile profile;
// For deserialization
private Recipient() {
}
public Recipient(final long id, final String name, final String uuid) {
public Recipient(
final long id,
final String number,
final String uuid,
final String profileKey,
final String profileKeyCredential,
final Contact contact,
final Profile profile
) {
this.id = id;
this.name = name;
this.number = number;
this.uuid = uuid;
this.profileKey = profileKey;
this.profileKeyCredential = profileKeyCredential;
this.contact = contact;
this.profile = profile;
}
public long getId() {
return id;
private static class Contact {
public String 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() {
return name;
}
private static class Profile {
public String getUuid() {
return uuid;
public long lastUpdateTimestamp;
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;
import org.asamk.signal.manager.storage.profiles.SignalProfile;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import java.util.Base64;
import java.util.Date;
import java.util.HashSet;
public class ProfileUtils {
public static SignalProfile decryptProfile(
public static Profile decryptProfile(
final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
) {
var profileCipher = new ProfileCipher(profileKey);
@ -28,13 +31,28 @@ public class ProfileUtils {
} catch (IllegalArgumentException e) {
unidentifiedAccess = null;
}
return new SignalProfile(encryptedProfile.getIdentityKey(),
name,
final var nameParts = splitName(name);
final var capabilities = new HashSet<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,
aboutEmoji,
unidentifiedAccess,
encryptedProfile.isUnrestrictedUnidentifiedAccess(),
encryptedProfile.getCapabilities());
encryptedProfile.isUnrestrictedUnidentifiedAccess()
? Profile.UnidentifiedAccessMode.UNRESTRICTED
: unidentifiedAccess != null
? Profile.UnidentifiedAccessMode.ENABLED
: Profile.UnidentifiedAccessMode.DISABLED,
capabilities);
} catch (InvalidCiphertextException e) {
return null;
}
@ -51,4 +69,17 @@ public class ProfileUtils {
return null;
}
}
private static Pair<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;
boolean isContactBlocked(final String number);
boolean isContactBlocked(final String number) throws Error.InvalidNumber;
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.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.util.Base64;
@ -667,7 +668,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
private String formatContact(SignalServiceAddress address) {
final var number = address.getLegacyIdentifier();
var name = m.getContactOrProfileName(number);
String name = null;
try {
name = m.getContactOrProfileName(number);
} catch (InvalidNumberException ignored) {
}
if (name == null || name.isEmpty()) {
return number;
} else {

View file

@ -18,7 +18,10 @@ public class ListContactsCommand implements LocalCommand {
var contacts = m.getContacts();
for (var c : contacts) {
writer.println("Number: {} Name: {} Blocked: {}", c.number, c.name, c.blocked);
writer.println("Number: {} Name: {} Blocked: {}",
m.resolveSignalServiceAddress(c.first()).getLegacyIdentifier(),
c.second().getName(),
c.second().isBlocked());
}
}
}

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