return;
}
final var recipientIds = context.getRecipientHelper().resolveRecipients(recipients);
+ final var selfRecipientId = account.getSelfRecipientId();
+ boolean shouldRotateProfileKey = false;
for (final var recipientId : recipientIds) {
+ if (context.getContactHelper().isContactBlocked(recipientId) == blocked) {
+ continue;
+ }
context.getContactHelper().setContactBlocked(recipientId, blocked);
+ // if we don't have a common group with the blocked contact we need to rotate the profile key
+ shouldRotateProfileKey = blocked && (
+ shouldRotateProfileKey || account.getGroupStore()
+ .getGroups()
+ .stream()
+ .noneMatch(g -> g.isMember(selfRecipientId) && g.isMember(recipientId))
+ );
+ }
+ if (shouldRotateProfileKey) {
+ context.getProfileHelper().rotateProfileKey();
}
- // TODO cycle our profile key, if we're not together in a group with recipient
context.getSyncHelper().sendBlockedList();
}
@Override
public void setGroupsBlocked(
final Collection<GroupId> groupIds, final boolean blocked
- ) throws GroupNotFoundException, NotMasterDeviceException {
+ ) throws GroupNotFoundException, NotMasterDeviceException, IOException {
if (!account.isMasterDevice()) {
throw new NotMasterDeviceException();
}
if (groupIds.size() == 0) {
return;
}
+ boolean shouldRotateProfileKey = false;
for (final var groupId : groupIds) {
+ if (context.getGroupHelper().isGroupBlocked(groupId) == blocked) {
+ continue;
+ }
context.getGroupHelper().setGroupBlocked(groupId, blocked);
+ shouldRotateProfileKey = blocked;
+ }
+ if (shouldRotateProfileKey) {
+ context.getProfileHelper().rotateProfileKey();
}
- // TODO cycle our profile key
context.getSyncHelper().sendBlockedList();
}
--- /dev/null
+package org.asamk.signal.manager.actions;
+
+import org.asamk.signal.manager.helper.Context;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
+
+import java.util.Objects;
+
+public class SendProfileKeyAction implements HandleAction {
+
+ private final RecipientId recipientId;
+
+ public SendProfileKeyAction(final RecipientId recipientId) {
+ this.recipientId = recipientId;
+ }
+
+ @Override
+ public void execute(Context context) throws Throwable {
+ context.getSendHelper().sendProfileKey(recipientId);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final SendProfileKeyAction that = (SendProfileKeyAction) o;
+ return recipientId.equals(that.recipientId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(recipientId);
+ }
+}
--- /dev/null
+package org.asamk.signal.manager.actions;
+
+import org.asamk.signal.manager.helper.Context;
+
+public class UpdateAccountAttributesAction implements HandleAction {
+
+ private static final UpdateAccountAttributesAction INSTANCE = new UpdateAccountAttributesAction();
+
+ private UpdateAccountAttributesAction() {
+ }
+
+ public static UpdateAccountAttributesAction create() {
+ return INSTANCE;
+ }
+
+ @Override
+ public void execute(Context context) throws Throwable {
+ context.getAccountHelper().updateAccountAttributes();
+ }
+}
return result;
}
+ public void updateGroupProfileKey(GroupIdV2 groupId) throws GroupNotFoundException, NotAGroupMemberException, IOException {
+ var group = getGroupForUpdating(groupId);
+
+ if (group instanceof GroupInfoV2 groupInfoV2) {
+ Pair<DecryptedGroup, GroupChange> groupChangePair;
+ try {
+ groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2);
+ } catch (ConflictException e) {
+ // Detected conflicting update, refreshing group and trying again
+ groupInfoV2 = (GroupInfoV2) getGroup(groupId, true);
+ groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2);
+ }
+ if (groupChangePair != null) {
+ sendUpdateGroupV2Message(groupInfoV2, groupChangePair.first(), groupChangePair.second());
+ }
+ }
+ }
+
public Pair<GroupId, SendGroupMessageResults> joinGroup(
GroupInviteLinkUrl inviteLinkUrl
) throws IOException, InactiveGroupLinkException {
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
+import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
return commitChange(groupInfoV2, change);
}
+ Pair<DecryptedGroup, GroupChange> updateSelfProfileKey(GroupInfoV2 groupInfoV2) throws IOException {
+ Optional<DecryptedMember> selfInGroup = groupInfoV2.getGroup() == null
+ ? Optional.empty()
+ : DecryptedGroupUtil.findMemberByUuid(groupInfoV2.getGroup().getMembersList(), getSelfAci().uuid());
+ if (selfInGroup.isEmpty()) {
+ logger.trace("Not updating group, self not in group " + groupInfoV2.getGroupId().toBase64());
+ return null;
+ }
+
+ final var profileKey = context.getAccount().getProfileKey();
+ if (Arrays.equals(profileKey.serialize(), selfInGroup.get().getProfileKey().toByteArray())) {
+ logger.trace("Not updating group, own Profile Key is already up to date in group "
+ + groupInfoV2.getGroupId().toBase64());
+ return null;
+ }
+ logger.debug("Updating own profile key in group " + groupInfoV2.getGroupId().toBase64());
+
+ final var selfRecipientId = context.getAccount().getSelfRecipientId();
+ final var profileKeyCredential = context.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId);
+ if (profileKeyCredential == null) {
+ logger.trace("Cannot update profile key as self does not have a versioned profile");
+ return null;
+ }
+
+ final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
+ final var change = groupOperations.createUpdateProfileKeyCredentialChange(profileKeyCredential);
+ change.setSourceUuid(getSelfAci().toByteString());
+ return commitChange(groupInfoV2, change);
+ }
+
GroupChange joinGroup(
GroupMasterKey groupMasterKey,
GroupLinkPassword groupLinkPassword,
import org.asamk.signal.manager.actions.SendGroupInfoAction;
import org.asamk.signal.manager.actions.SendGroupInfoRequestAction;
import org.asamk.signal.manager.actions.SendPniIdentityKeyAction;
+import org.asamk.signal.manager.actions.SendProfileKeyAction;
import org.asamk.signal.manager.actions.SendReceiptAction;
import org.asamk.signal.manager.actions.SendRetryMessageRequestAction;
import org.asamk.signal.manager.actions.SendSyncBlockedListAction;
import org.asamk.signal.manager.actions.SendSyncContactsAction;
import org.asamk.signal.manager.actions.SendSyncGroupsAction;
import org.asamk.signal.manager.actions.SendSyncKeysAction;
+import org.asamk.signal.manager.actions.UpdateAccountAttributesAction;
import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.StickerPackId;
if (content.isNeedsReceipt()) {
actions.add(new SendReceiptAction(sender, message.getTimestamp()));
+ } else {
+ // Message wasn't sent as unidentified sender message
+ final var contact = context.getAccount().getContactStore().getContact(sender);
+ if (contact != null && !contact.isBlocked() && contact.isProfileSharingEnabled()) {
+ actions.add(UpdateAccountAttributesAction.create());
+ actions.add(new SendProfileKeyAction(sender));
+ }
}
actions.addAll(handleSignalServiceDataMessage(message,
import org.asamk.signal.manager.SignalDependencies;
import org.asamk.signal.manager.config.ServiceConfig;
+import org.asamk.signal.manager.groups.GroupNotFoundException;
+import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.storage.SignalAccount;
+import org.asamk.signal.manager.storage.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.util.IOUtils;
+import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.ProfileUtils;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.IdentityKey;
this.context = context;
}
+ public void rotateProfileKey() throws IOException {
+ var profileKey = KeyUtils.createProfileKey();
+ account.setProfileKey(profileKey);
+ context.getAccountHelper().updateAccountAttributes();
+ setProfile(true, true, null, null, null, null, null);
+ // TODO update profile key in storage
+
+ final var recipientIds = account.getRecipientStore().getRecipientIdsWithEnabledProfileSharing();
+ for (final var recipientId : recipientIds) {
+ context.getSendHelper().sendProfileKey(recipientId);
+ }
+
+ final var selfRecipientId = account.getSelfRecipientId();
+ final var activeGroupIds = account.getGroupStore()
+ .getGroups()
+ .stream()
+ .filter(g -> g instanceof GroupInfoV2 && g.isMember(selfRecipientId))
+ .map(g -> (GroupInfoV2) g)
+ .map(GroupInfoV2::getGroupId)
+ .toList();
+ for (final var groupId : activeGroupIds) {
+ try {
+ context.getGroupHelper().updateGroupProfileKey(groupId);
+ } catch (GroupNotFoundException | NotAGroupMemberException | IOException e) {
+ logger.warn("Failed to update group profile key: {}", e.getMessage());
+ }
+ }
+ }
+
public Profile getRecipientProfile(RecipientId recipientId) {
return getRecipientProfile(recipientId, false);
}
public void setProfile(
String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar
) throws IOException {
- setProfile(true, givenName, familyName, about, aboutEmoji, avatar);
+ setProfile(true, false, givenName, familyName, about, aboutEmoji, avatar);
}
public void setProfile(
boolean uploadProfile,
+ boolean forceUploadAvatar,
String givenName,
final String familyName,
String about,
var newProfile = builder.build();
if (uploadProfile) {
- try (final var streamDetails = avatar != null && avatar.isPresent() ? Utils.createStreamDetailsFromFile(
- avatar.get()) : null) {
- final var avatarUploadParams = avatar == null
- ? AvatarUploadParams.unchanged(true)
- : avatar.isPresent()
- ? AvatarUploadParams.forAvatar(streamDetails)
- : AvatarUploadParams.unchanged(false);
+ final var streamDetails = avatar != null && avatar.isPresent()
+ ? Utils.createStreamDetailsFromFile(avatar.get())
+ : forceUploadAvatar && avatar == null ? context.getAvatarStore()
+ .retrieveProfileAvatar(account.getSelfRecipientAddress()) : null;
+ try (streamDetails) {
+ final var avatarUploadParams = streamDetails != null
+ ? AvatarUploadParams.forAvatar(streamDetails)
+ : avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false);
final var paymentsAddress = Optional.ofNullable(newProfile.getPaymentAddress()).map(data -> {
try {
return SignalServiceProtos.PaymentAddress.parseFrom(data);
return null;
}
});
+ logger.debug("Uploading new profile");
final var avatarPath = dependencies.getAccountManager()
.setVersionedProfile(account.getAci(),
account.getProfileKey(),
newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
paymentsAddress,
avatarUploadParams,
- List.of(/* TODO */));
+ List.of(/* TODO implement support for badges */));
if (!avatarUploadParams.keepTheSame) {
builder.withAvatarUrlPath(avatarPath.orElse(null));
}
return result;
}
+ public SendMessageResult sendProfileKey(RecipientId recipientId) {
+ logger.debug("Sending updated profile key to recipient: {}", recipientId);
+ final var profileKey = account.getProfileKey().serialize();
+ final var message = SignalServiceDataMessage.newBuilder()
+ .asProfileKeyUpdate(true)
+ .withProfileKey(profileKey)
+ .build();
+ return handleSendMessage(recipientId,
+ (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address,
+ unidentifiedAccess,
+ ContentHint.IMPLICIT,
+ message,
+ SignalServiceMessageSender.IndividualSendEvents.EMPTY));
+ }
+
public SendMessageResult sendRetryReceipt(
DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId
) {
context.getProfileHelper()
.setProfile(false,
+ false,
accountRecord.getGivenName().orElse(null),
accountRecord.getFamilyName().orElse(null),
null,
setProfileKey(KeyUtils.createProfileKey());
}
// Ensure our profile key is stored in profile store
- getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey());
+ getProfileStore().storeSelfProfileKey(getSelfRecipientId(), getProfileKey());
if (previousStorageVersion < 3) {
for (final var group : groupStore.getGroups()) {
if (group instanceof GroupInfoV2 && group.getDistributionId() == null) {
}
this.profileKey = profileKey;
save();
+ getProfileStore().storeSelfProfileKey(getSelfRecipientId(), getProfileKey());
}
public byte[] getSelfUnidentifiedAccessKey() {
void storeProfile(RecipientId recipientId, Profile profile);
+ void storeSelfProfileKey(RecipientId recipientId, ProfileKey profileKey);
+
void storeProfileKey(RecipientId recipientId, ProfileKey profileKey);
void storeProfileKeyCredential(RecipientId recipientId, ProfileKeyCredential profileKeyCredential);
}
}
+ @Override
+ public void storeSelfProfileKey(final RecipientId recipientId, final ProfileKey profileKey) {
+ storeProfileKey(recipientId, profileKey, false);
+ }
+
@Override
public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) {
+ storeProfileKey(recipientId, profileKey, true);
+ }
+
+ private void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey, boolean resetProfile) {
synchronized (recipients) {
final var recipient = recipients.get(recipientId);
if (profileKey != null && profileKey.equals(recipient.getProfileKey()) && (
return;
}
- final var newRecipient = Recipient.newBuilder(recipient)
+ final var builder = Recipient.newBuilder(recipient)
.withProfileKey(profileKey)
- .withProfileKeyCredential(null)
- .withProfile(recipient.getProfile() == null
- ? null
- : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build())
- .build();
+ .withProfileKeyCredential(null);
+ if (resetProfile) {
+ builder.withProfile(recipient.getProfile() == null
+ ? null
+ : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build());
+ }
+ final var newRecipient = builder.build();
storeRecipientLocked(recipientId, newRecipient);
}
}