Server signed group v2 changes sent and received P2P.

This commit is contained in:
Alan Evans 2020-05-19 16:02:24 -03:00 committed by Greyson Parrelli
parent ec8d5defd4
commit 2f9320989a
8 changed files with 98 additions and 49 deletions

View File

@ -148,11 +148,12 @@ public final class GroupManager {
public static void updateGroupFromServer(@NonNull Context context, public static void updateGroupFromServer(@NonNull Context context,
@NonNull GroupMasterKey groupMasterKey, @NonNull GroupMasterKey groupMasterKey,
int version, int version,
long timestamp) long timestamp,
@Nullable byte[] signedGroupChange)
throws GroupChangeBusyException, IOException, GroupNotAMemberException throws GroupChangeBusyException, IOException, GroupNotAMemberException
{ {
try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) { try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
updater.updateLocalToServerVersion(version, timestamp); updater.updateLocalToServerVersion(version, timestamp, signedGroupChange);
} }
} }

View File

@ -6,6 +6,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.Member;
@ -144,7 +146,7 @@ final class GroupManagerV2 {
groupDatabase.onAvatarUpdated(groupId, avatar != null); groupDatabase.onAvatarUpdated(groupId, avatar != null);
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);
return sendGroupUpdate(masterKey, decryptedGroup, null); return sendGroupUpdate(masterKey, decryptedGroup, null, null);
} catch (VerificationFailedException | InvalidGroupStateException e) { } catch (VerificationFailedException | InvalidGroupStateException e) {
throw new GroupChangeFailedException(e); throw new GroupChangeFailedException(e);
} }
@ -354,7 +356,7 @@ final class GroupManagerV2 {
throws IOException, GroupNotAMemberException, GroupChangeFailedException throws IOException, GroupNotAMemberException, GroupChangeFailedException
{ {
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(groupMasterKey) GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(groupMasterKey)
.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis()); .updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), null);
if (groupUpdateResult.getGroupState() != GroupsV2StateProcessor.GroupState.GROUP_UPDATED || groupUpdateResult.getLatestServer() == null) { if (groupUpdateResult.getGroupState() != GroupsV2StateProcessor.GroupState.GROUP_UPDATED || groupUpdateResult.getLatestServer() == null) {
throw new GroupChangeFailedException(); throw new GroupChangeFailedException();
@ -390,24 +392,24 @@ final class GroupManagerV2 {
throw new IOException(e); throw new IOException(e);
} }
commitToServer(changeActions); GroupChange signedGroupChange = commitToServer(changeActions);
groupDatabase.update(groupId, decryptedGroupState); groupDatabase.update(groupId, decryptedGroupState);
return sendGroupUpdate(groupMasterKey, decryptedGroupState, decryptedChange); return sendGroupUpdate(groupMasterKey, decryptedGroupState, decryptedChange, signedGroupChange);
} }
private void commitToServer(GroupChange.Actions change) private GroupChange commitToServer(GroupChange.Actions change)
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
{ {
try { try {
groupsV2Api.patchGroup(change, groupSecretParams, authorization.getAuthorizationForToday(selfUuid, groupSecretParams)); return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
} catch (NotInGroupException e) { } catch (NotInGroupException e) {
Log.w(TAG, e); Log.w(TAG, e);
throw new GroupNotAMemberException(e); throw new GroupNotAMemberException(e);
} catch (AuthorizationFailedException e) { } catch (AuthorizationFailedException e) {
Log.w(TAG, e); Log.w(TAG, e);
throw new GroupInsufficientRightsException(e); throw new GroupInsufficientRightsException(e);
} catch (VerificationFailedException | InvalidGroupStateException e) { } catch (VerificationFailedException e) {
Log.w(TAG, e); Log.w(TAG, e);
throw new GroupChangeFailedException(e); throw new GroupChangeFailedException(e);
} }
@ -430,11 +432,25 @@ final class GroupManagerV2 {
} }
@WorkerThread @WorkerThread
void updateLocalToServerVersion(int version, long timestamp) void updateLocalToServerVersion(int version, long timestamp, @Nullable byte[] signedGroupChange)
throws IOException, GroupNotAMemberException throws IOException, GroupNotAMemberException
{ {
new GroupsV2StateProcessor(context).forGroup(groupMasterKey) new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
.updateLocalGroupToRevision(version, timestamp); .updateLocalGroupToRevision(version, timestamp, getDecryptedGroupChange(signedGroupChange));
}
private DecryptedGroupChange getDecryptedGroupChange(@Nullable byte[] signedGroupChange) {
if (signedGroupChange != null) {
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
try {
return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true);
} catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
Log.w(TAG, "Unable to verify supplied group change", e);
}
}
return null;
} }
@Override @Override
@ -445,11 +461,12 @@ final class GroupManagerV2 {
private @NonNull GroupManager.GroupActionResult sendGroupUpdate(@NonNull GroupMasterKey masterKey, private @NonNull GroupManager.GroupActionResult sendGroupUpdate(@NonNull GroupMasterKey masterKey,
@NonNull DecryptedGroup decryptedGroup, @NonNull DecryptedGroup decryptedGroup,
@Nullable DecryptedGroupChange plainGroupChange) @Nullable DecryptedGroupChange plainGroupChange,
@Nullable GroupChange signedGroupChange)
{ {
GroupId.V2 groupId = GroupId.v2(masterKey); GroupId.V2 groupId = GroupId.v2(masterKey);
Recipient groupRecipient = Recipient.externalGroup(context, groupId); Recipient groupRecipient = Recipient.externalGroup(context, groupId);
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, decryptedGroup, plainGroupChange); DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, decryptedGroup, plainGroupChange, signedGroupChange);
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient,
decryptedGroupV2Context, decryptedGroupV2Context,
null, null,

View File

@ -8,6 +8,7 @@ import androidx.annotation.WorkerThread;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.signal.storageservice.protos.groups.GroupChange;
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.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedMember;
@ -48,16 +49,20 @@ public final class GroupProtoUtil {
public static DecryptedGroupV2Context createDecryptedGroupV2Context(@NonNull GroupMasterKey masterKey, public static DecryptedGroupV2Context createDecryptedGroupV2Context(@NonNull GroupMasterKey masterKey,
@NonNull DecryptedGroup decryptedGroup, @NonNull DecryptedGroup decryptedGroup,
@Nullable DecryptedGroupChange plainGroupChange) @Nullable DecryptedGroupChange plainGroupChange,
@Nullable GroupChange signedServerChange)
{ {
int version = plainGroupChange != null ? plainGroupChange.getVersion() : decryptedGroup.getVersion(); int version = plainGroupChange != null ? plainGroupChange.getVersion() : decryptedGroup.getVersion();
SignalServiceProtos.GroupContextV2 groupContext = SignalServiceProtos.GroupContextV2.newBuilder() SignalServiceProtos.GroupContextV2.Builder contextBuilder = SignalServiceProtos.GroupContextV2.newBuilder()
.setMasterKey(ByteString.copyFrom(masterKey.serialize())) .setMasterKey(ByteString.copyFrom(masterKey.serialize()))
.setRevision(version) .setRevision(version);
.build();
if (signedServerChange != null) {
contextBuilder.setGroupChange(signedServerChange.toByteString());
}
DecryptedGroupV2Context.Builder builder = DecryptedGroupV2Context.newBuilder() DecryptedGroupV2Context.Builder builder = DecryptedGroupV2Context.newBuilder()
.setContext(groupContext) .setContext(contextBuilder.build())
.setGroupState(decryptedGroup); .setGroupState(decryptedGroup);
if (plainGroupChange != null) { if (plainGroupChange != null) {

View File

@ -132,21 +132,47 @@ public final class GroupsV2StateProcessor {
*/ */
@WorkerThread @WorkerThread
public GroupUpdateResult updateLocalGroupToRevision(final int revision, public GroupUpdateResult updateLocalGroupToRevision(final int revision,
final long timestamp) final long timestamp,
@Nullable DecryptedGroupChange signedGroupChange)
throws IOException, GroupNotAMemberException throws IOException, GroupNotAMemberException
{ {
if (localIsAtLeast(revision)) { if (localIsAtLeast(revision)) {
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null); return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null);
} }
GlobalGroupState inputGroupState; GlobalGroupState inputGroupState = null;
try {
inputGroupState = queryServer(); DecryptedGroup localState = groupDatabase.getGroup(groupId)
} catch (GroupNotAMemberException e) { .transform(g -> g.requireV2GroupProperties().getDecryptedGroup())
Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message"); .orNull();
insertGroupLeave();
throw e; if (signedGroupChange != null &&
localState != null &&
localState.getVersion() + 1 == signedGroupChange.getVersion() &&
revision == signedGroupChange.getVersion())
{
try {
Log.i(TAG, "Applying P2P group change");
DecryptedGroup newState = DecryptedGroupUtil.apply(localState, signedGroupChange);
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new GroupLogEntry(newState, signedGroupChange)));
} catch (DecryptedGroupUtil.NotAbleToApplyChangeException e) {
Log.w(TAG, "Unable to apply P2P group change", e);
}
} }
if (inputGroupState == null) {
try {
inputGroupState = queryServer(localState);
} catch (GroupNotAMemberException e) {
Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message");
insertGroupLeave();
throw e;
}
} else {
Log.i(TAG, "Saved server query for group change");
}
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(inputGroupState, revision); AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(inputGroupState, revision);
DecryptedGroup newLocalState = advanceGroupStateResult.getNewGlobalGroupState().getLocalState(); DecryptedGroup newLocalState = advanceGroupStateResult.getNewGlobalGroupState().getLocalState();
@ -185,7 +211,7 @@ public final class GroupsV2StateProcessor {
.addDeleteMembers(UuidUtil.toByteString(selfUuid)) .addDeleteMembers(UuidUtil.toByteString(selfUuid))
.build(); .build();
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, simulatedGroupState, simulatedGroupChange); DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, simulatedGroupState, simulatedGroupChange, null);
OutgoingGroupUpdateMessage leaveMessage = new OutgoingGroupUpdateMessage(groupRecipient, OutgoingGroupUpdateMessage leaveMessage = new OutgoingGroupUpdateMessage(groupRecipient,
decryptedGroupV2Context, decryptedGroupV2Context,
null, null,
@ -245,7 +271,7 @@ public final class GroupsV2StateProcessor {
private void insertUpdateMessages(long timestamp, Collection<GroupLogEntry> processedLogEntries) { private void insertUpdateMessages(long timestamp, Collection<GroupLogEntry> processedLogEntries) {
for (GroupLogEntry entry : processedLogEntries) { for (GroupLogEntry entry : processedLogEntries) {
storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, entry.getGroup(), entry.getChange()), timestamp); storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, entry.getGroup(), entry.getChange(), null), timestamp);
} }
} }
@ -266,15 +292,12 @@ public final class GroupsV2StateProcessor {
} }
} }
private GlobalGroupState queryServer() private @NonNull GlobalGroupState queryServer(@Nullable DecryptedGroup localState)
throws IOException, GroupNotAMemberException throws IOException, GroupNotAMemberException
{ {
DecryptedGroup latestServerGroup; DecryptedGroup latestServerGroup;
List<GroupLogEntry> history; List<GroupLogEntry> history;
UUID selfUuid = Recipient.self().getUuid().get(); UUID selfUuid = Recipient.self().getUuid().get();
DecryptedGroup localState = groupDatabase.getGroup(groupId)
.transform(g -> g.requireV2GroupProperties().getDecryptedGroup())
.orNull();
try { try {
latestServerGroup = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfUuid, groupSecretParams)); latestServerGroup = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfUuid, groupSecretParams));

View File

@ -445,7 +445,7 @@ public final class PushProcessMessageJob extends BaseJob {
throws IOException, GroupChangeBusyException throws IOException, GroupChangeBusyException
{ {
try { try {
GroupManager.updateGroupFromServer(context, groupMasterKey, groupV2.getRevision(), content.getTimestamp()); GroupManager.updateGroupFromServer(context, groupMasterKey, groupV2.getRevision(), content.getTimestamp(), groupV2.getSignedGroupChange());
return true; return true;
} catch (GroupNotAMemberException e) { } catch (GroupNotAMemberException e) {
Log.w(TAG, "Ignoring message for a group we're not in"); Log.w(TAG, "Ignoring message for a group we're not in");

View File

@ -87,7 +87,7 @@ public final class RequestGroupV2InfoJob extends BaseJob {
return; return;
} }
GroupManager.updateGroupFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis()); GroupManager.updateGroupFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null);
} }
@Override @Override

View File

@ -1072,10 +1072,17 @@ public class SignalServiceMessageSender {
} }
private static GroupContextV2 createGroupContent(SignalServiceGroupV2 group) { private static GroupContextV2 createGroupContent(SignalServiceGroupV2 group) {
return GroupContextV2.newBuilder() GroupContextV2.Builder builder = GroupContextV2.newBuilder()
.setMasterKey(ByteString.copyFrom(group.getMasterKey().serialize())) .setMasterKey(ByteString.copyFrom(group.getMasterKey().serialize()))
.setRevision(group.getRevision()) .setRevision(group.getRevision());
.build();
byte[] signedGroupChange = group.getSignedGroupChange();
if (signedGroupChange != null && signedGroupChange.length <= 2048) {
builder.setGroupChange(ByteString.copyFrom(signedGroupChange));
}
return builder.build();
} }
private List<DataMessage.Contact> createSharedContactContent(List<SharedContact> contacts) throws IOException { private List<DataMessage.Contact> createSharedContactContent(List<SharedContact> contacts) throws IOException {

View File

@ -132,15 +132,11 @@ public final class GroupsV2Api {
return form.getKey(); return form.getKey();
} }
public DecryptedGroupChange patchGroup(GroupChange.Actions groupChange, public GroupChange patchGroup(GroupChange.Actions groupChange,
GroupSecretParams groupSecretParams, GroupsV2AuthorizationString authorization)
GroupsV2AuthorizationString authorization) throws IOException
throws IOException, VerificationFailedException, InvalidGroupStateException
{ {
GroupChange groupChanges = socket.patchGroupsV2Group(groupChange, authorization.toString()); return socket.patchGroupsV2Group(groupChange, authorization.toString());
return groupsOperations.forGroup(groupSecretParams)
.decryptChange(groupChanges, true);
} }
private static HashMap<Integer, AuthCredentialResponse> parseCredentialResponse(CredentialResponse credentialResponse) private static HashMap<Integer, AuthCredentialResponse> parseCredentialResponse(CredentialResponse credentialResponse)