Rotate profile key after blocking a contact/group

This commit is contained in:
AsamK 2022-05-18 12:19:06 +02:00
parent b1e56faab2
commit cf1626ea31
12 changed files with 218 additions and 19 deletions

View file

@ -694,27 +694,48 @@ class ManagerImpl implements Manager {
return; return;
} }
final var recipientIds = context.getRecipientHelper().resolveRecipients(recipients); final var recipientIds = context.getRecipientHelper().resolveRecipients(recipients);
final var selfRecipientId = account.getSelfRecipientId();
boolean shouldRotateProfileKey = false;
for (final var recipientId : recipientIds) { for (final var recipientId : recipientIds) {
context.getContactHelper().setContactBlocked(recipientId, blocked); 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(); context.getSyncHelper().sendBlockedList();
} }
@Override @Override
public void setGroupsBlocked( public void setGroupsBlocked(
final Collection<GroupId> groupIds, final boolean blocked final Collection<GroupId> groupIds, final boolean blocked
) throws GroupNotFoundException, NotMasterDeviceException { ) throws GroupNotFoundException, NotMasterDeviceException, IOException {
if (!account.isMasterDevice()) { if (!account.isMasterDevice()) {
throw new NotMasterDeviceException(); throw new NotMasterDeviceException();
} }
if (groupIds.size() == 0) { if (groupIds.size() == 0) {
return; return;
} }
boolean shouldRotateProfileKey = false;
for (final var groupId : groupIds) { for (final var groupId : groupIds) {
context.getGroupHelper().setGroupBlocked(groupId, blocked); 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(); context.getSyncHelper().sendBlockedList();
} }

View file

@ -0,0 +1,33 @@
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);
}
}

View file

@ -0,0 +1,20 @@
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();
}
}

View file

@ -254,6 +254,24 @@ public class GroupHelper {
return result; 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( public Pair<GroupId, SendGroupMessageResults> joinGroup(
GroupInviteLinkUrl inviteLinkUrl GroupInviteLinkUrl inviteLinkUrl
) throws IOException, InactiveGroupLinkException { ) throws IOException, InactiveGroupLinkException {

View file

@ -25,6 +25,7 @@ import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; 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.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -45,6 +46,7 @@ import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -340,6 +342,36 @@ class GroupV2Helper {
return commitChange(groupInfoV2, change); 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( GroupChange joinGroup(
GroupMasterKey groupMasterKey, GroupMasterKey groupMasterKey,
GroupLinkPassword groupLinkPassword, GroupLinkPassword groupLinkPassword,

View file

@ -11,6 +11,7 @@ import org.asamk.signal.manager.actions.RetrieveStorageDataAction;
import org.asamk.signal.manager.actions.SendGroupInfoAction; import org.asamk.signal.manager.actions.SendGroupInfoAction;
import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; import org.asamk.signal.manager.actions.SendGroupInfoRequestAction;
import org.asamk.signal.manager.actions.SendPniIdentityKeyAction; 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.SendReceiptAction;
import org.asamk.signal.manager.actions.SendRetryMessageRequestAction; import org.asamk.signal.manager.actions.SendRetryMessageRequestAction;
import org.asamk.signal.manager.actions.SendSyncBlockedListAction; import org.asamk.signal.manager.actions.SendSyncBlockedListAction;
@ -18,6 +19,7 @@ import org.asamk.signal.manager.actions.SendSyncConfigurationAction;
import org.asamk.signal.manager.actions.SendSyncContactsAction; import org.asamk.signal.manager.actions.SendSyncContactsAction;
import org.asamk.signal.manager.actions.SendSyncGroupsAction; import org.asamk.signal.manager.actions.SendSyncGroupsAction;
import org.asamk.signal.manager.actions.SendSyncKeysAction; 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.MessageEnvelope;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.StickerPackId; import org.asamk.signal.manager.api.StickerPackId;
@ -246,6 +248,13 @@ public final class IncomingMessageHandler {
if (content.isNeedsReceipt()) { if (content.isNeedsReceipt()) {
actions.add(new SendReceiptAction(sender, message.getTimestamp())); 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, actions.addAll(handleSignalServiceDataMessage(message,

View file

@ -4,11 +4,15 @@ import com.google.protobuf.InvalidProtocolBufferException;
import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.SignalDependencies;
import org.asamk.signal.manager.config.ServiceConfig; 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.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.util.IOUtils; 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.ProfileUtils;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKey;
@ -57,6 +61,35 @@ public final class ProfileHelper {
this.context = context; 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) { public Profile getRecipientProfile(RecipientId recipientId) {
return getRecipientProfile(recipientId, false); return getRecipientProfile(recipientId, false);
} }
@ -106,11 +139,12 @@ public final class ProfileHelper {
public void setProfile( public void setProfile(
String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar
) throws IOException { ) throws IOException {
setProfile(true, givenName, familyName, about, aboutEmoji, avatar); setProfile(true, false, givenName, familyName, about, aboutEmoji, avatar);
} }
public void setProfile( public void setProfile(
boolean uploadProfile, boolean uploadProfile,
boolean forceUploadAvatar,
String givenName, String givenName,
final String familyName, final String familyName,
String about, String about,
@ -134,13 +168,14 @@ public final class ProfileHelper {
var newProfile = builder.build(); var newProfile = builder.build();
if (uploadProfile) { if (uploadProfile) {
try (final var streamDetails = avatar != null && avatar.isPresent() ? Utils.createStreamDetailsFromFile( final var streamDetails = avatar != null && avatar.isPresent()
avatar.get()) : null) { ? Utils.createStreamDetailsFromFile(avatar.get())
final var avatarUploadParams = avatar == null : forceUploadAvatar && avatar == null ? context.getAvatarStore()
? AvatarUploadParams.unchanged(true) .retrieveProfileAvatar(account.getSelfRecipientAddress()) : null;
: avatar.isPresent() try (streamDetails) {
final var avatarUploadParams = streamDetails != null
? AvatarUploadParams.forAvatar(streamDetails) ? AvatarUploadParams.forAvatar(streamDetails)
: AvatarUploadParams.unchanged(false); : avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false);
final var paymentsAddress = Optional.ofNullable(newProfile.getPaymentAddress()).map(data -> { final var paymentsAddress = Optional.ofNullable(newProfile.getPaymentAddress()).map(data -> {
try { try {
return SignalServiceProtos.PaymentAddress.parseFrom(data); return SignalServiceProtos.PaymentAddress.parseFrom(data);
@ -148,6 +183,7 @@ public final class ProfileHelper {
return null; return null;
} }
}); });
logger.debug("Uploading new profile");
final var avatarPath = dependencies.getAccountManager() final var avatarPath = dependencies.getAccountManager()
.setVersionedProfile(account.getAci(), .setVersionedProfile(account.getAci(),
account.getProfileKey(), account.getProfileKey(),
@ -156,7 +192,7 @@ public final class ProfileHelper {
newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
paymentsAddress, paymentsAddress,
avatarUploadParams, avatarUploadParams,
List.of(/* TODO */)); List.of(/* TODO implement support for badges */));
if (!avatarUploadParams.keepTheSame) { if (!avatarUploadParams.keepTheSame) {
builder.withAvatarUrlPath(avatarPath.orElse(null)); builder.withAvatarUrlPath(avatarPath.orElse(null));
} }

View file

@ -139,6 +139,21 @@ public class SendHelper {
return result; 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( public SendMessageResult sendRetryReceipt(
DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId
) { ) {

View file

@ -229,6 +229,7 @@ public class StorageHelper {
context.getProfileHelper() context.getProfileHelper()
.setProfile(false, .setProfile(false,
false,
accountRecord.getGivenName().orElse(null), accountRecord.getGivenName().orElse(null),
accountRecord.getFamilyName().orElse(null), accountRecord.getFamilyName().orElse(null),
null, null,

View file

@ -373,7 +373,7 @@ public class SignalAccount implements Closeable {
setProfileKey(KeyUtils.createProfileKey()); setProfileKey(KeyUtils.createProfileKey());
} }
// Ensure our profile key is stored in profile store // Ensure our profile key is stored in profile store
getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey()); getProfileStore().storeSelfProfileKey(getSelfRecipientId(), getProfileKey());
if (previousStorageVersion < 3) { if (previousStorageVersion < 3) {
for (final var group : groupStore.getGroups()) { for (final var group : groupStore.getGroups()) {
if (group instanceof GroupInfoV2 && group.getDistributionId() == null) { if (group instanceof GroupInfoV2 && group.getDistributionId() == null) {
@ -1266,6 +1266,7 @@ public class SignalAccount implements Closeable {
} }
this.profileKey = profileKey; this.profileKey = profileKey;
save(); save();
getProfileStore().storeSelfProfileKey(getSelfRecipientId(), getProfileKey());
} }
public byte[] getSelfUnidentifiedAccessKey() { public byte[] getSelfUnidentifiedAccessKey() {

View file

@ -15,6 +15,8 @@ public interface ProfileStore {
void storeProfile(RecipientId recipientId, Profile profile); void storeProfile(RecipientId recipientId, Profile profile);
void storeSelfProfileKey(RecipientId recipientId, ProfileKey profileKey);
void storeProfileKey(RecipientId recipientId, ProfileKey profileKey); void storeProfileKey(RecipientId recipientId, ProfileKey profileKey);
void storeProfileKeyCredential(RecipientId recipientId, ProfileKeyCredential profileKeyCredential); void storeProfileKeyCredential(RecipientId recipientId, ProfileKeyCredential profileKeyCredential);

View file

@ -325,8 +325,17 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
} }
} }
@Override
public void storeSelfProfileKey(final RecipientId recipientId, final ProfileKey profileKey) {
storeProfileKey(recipientId, profileKey, false);
}
@Override @Override
public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) { public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) {
storeProfileKey(recipientId, profileKey, true);
}
private void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey, boolean resetProfile) {
synchronized (recipients) { synchronized (recipients) {
final var recipient = recipients.get(recipientId); final var recipient = recipients.get(recipientId);
if (profileKey != null && profileKey.equals(recipient.getProfileKey()) && ( if (profileKey != null && profileKey.equals(recipient.getProfileKey()) && (
@ -339,13 +348,15 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
return; return;
} }
final var newRecipient = Recipient.newBuilder(recipient) final var builder = Recipient.newBuilder(recipient)
.withProfileKey(profileKey) .withProfileKey(profileKey)
.withProfileKeyCredential(null) .withProfileKeyCredential(null);
.withProfile(recipient.getProfile() == null if (resetProfile) {
builder.withProfile(recipient.getProfile() == null
? null ? null
: Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build()) : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build());
.build(); }
final var newRecipient = builder.build();
storeRecipientLocked(recipientId, newRecipient); storeRecipientLocked(recipientId, newRecipient);
} }
} }