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;
}
final var recipientIds = context.getRecipientHelper().resolveRecipients(recipients);
final var selfRecipientId = account.getSelfRecipientId();
boolean shouldRotateProfileKey = false;
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();
}
@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) {
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();
}

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;
}
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 {

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.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;
@ -45,6 +46,7 @@ import java.io.FileInputStream;
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;
@ -340,6 +342,36 @@ class GroupV2Helper {
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,

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.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;
@ -18,6 +19,7 @@ import org.asamk.signal.manager.actions.SendSyncConfigurationAction;
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;
@ -246,6 +248,13 @@ public final class IncomingMessageHandler {
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,

View file

@ -4,11 +4,15 @@ import com.google.protobuf.InvalidProtocolBufferException;
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;
@ -57,6 +61,35 @@ public final class ProfileHelper {
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);
}
@ -106,11 +139,12 @@ public final class ProfileHelper {
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,
@ -134,13 +168,14 @@ public final class ProfileHelper {
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()
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)
: AvatarUploadParams.unchanged(false);
: avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false);
final var paymentsAddress = Optional.ofNullable(newProfile.getPaymentAddress()).map(data -> {
try {
return SignalServiceProtos.PaymentAddress.parseFrom(data);
@ -148,6 +183,7 @@ public final class ProfileHelper {
return null;
}
});
logger.debug("Uploading new profile");
final var avatarPath = dependencies.getAccountManager()
.setVersionedProfile(account.getAci(),
account.getProfileKey(),
@ -156,7 +192,7 @@ public final class ProfileHelper {
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));
}

View file

@ -139,6 +139,21 @@ public class SendHelper {
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
) {

View file

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

View file

@ -373,7 +373,7 @@ public class SignalAccount implements Closeable {
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) {
@ -1266,6 +1266,7 @@ public class SignalAccount implements Closeable {
}
this.profileKey = profileKey;
save();
getProfileStore().storeSelfProfileKey(getSelfRecipientId(), getProfileKey());
}
public byte[] getSelfUnidentifiedAccessKey() {

View file

@ -15,6 +15,8 @@ public interface ProfileStore {
void storeProfile(RecipientId recipientId, Profile profile);
void storeSelfProfileKey(RecipientId recipientId, ProfileKey profileKey);
void storeProfileKey(RecipientId recipientId, ProfileKey profileKey);
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
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()) && (
@ -339,13 +348,15 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
return;
}
final var newRecipient = Recipient.newBuilder(recipient)
final var builder = Recipient.newBuilder(recipient)
.withProfileKey(profileKey)
.withProfileKeyCredential(null)
.withProfile(recipient.getProfile() == null
.withProfileKeyCredential(null);
if (resetProfile) {
builder.withProfile(recipient.getProfile() == null
? null
: Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build())
.build();
: Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build());
}
final var newRecipient = builder.build();
storeRecipientLocked(recipientId, newRecipient);
}
}