Skip to content

Commit

Permalink
Server signed group v2 changes sent and received P2P.
Browse files Browse the repository at this point in the history
  • Loading branch information
alan-signal authored and greyson-signal committed May 29, 2020
1 parent ec8d5de commit 2f93209
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 49 deletions.
Expand Up @@ -148,11 +148,12 @@ public static void ejectFromGroup(@NonNull Context context, @NonNull GroupId.V2
public static void updateGroupFromServer(@NonNull Context context,
@NonNull GroupMasterKey groupMasterKey,
int version,
long timestamp)
long timestamp,
@Nullable byte[] signedGroupChange)
throws GroupChangeBusyException, IOException, GroupNotAMemberException
{
try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
updater.updateLocalToServerVersion(version, timestamp);
updater.updateLocalToServerVersion(version, timestamp, signedGroupChange);
}
}

Expand Down
Expand Up @@ -6,6 +6,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;

import com.google.protobuf.InvalidProtocolBufferException;

import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.Member;
Expand Down Expand Up @@ -144,7 +146,7 @@ class GroupCreator implements Closeable {
groupDatabase.onAvatarUpdated(groupId, avatar != null);
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);

return sendGroupUpdate(masterKey, decryptedGroup, null);
return sendGroupUpdate(masterKey, decryptedGroup, null, null);
} catch (VerificationFailedException | InvalidGroupStateException e) {
throw new GroupChangeFailedException(e);
}
Expand Down Expand Up @@ -354,7 +356,7 @@ private GroupChange.Actions.Builder resolveConflict(@NonNull GroupChange.Actions
throws IOException, GroupNotAMemberException, GroupChangeFailedException
{
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) {
throw new GroupChangeFailedException();
Expand Down Expand Up @@ -390,24 +392,24 @@ private GroupManager.GroupActionResult commitChange(@NonNull GroupChange.Actions
throw new IOException(e);
}

commitToServer(changeActions);
GroupChange signedGroupChange = commitToServer(changeActions);
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
{
try {
groupsV2Api.patchGroup(change, groupSecretParams, authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
} catch (NotInGroupException e) {
Log.w(TAG, e);
throw new GroupNotAMemberException(e);
} catch (AuthorizationFailedException e) {
Log.w(TAG, e);
throw new GroupInsufficientRightsException(e);
} catch (VerificationFailedException | InvalidGroupStateException e) {
} catch (VerificationFailedException e) {
Log.w(TAG, e);
throw new GroupChangeFailedException(e);
}
Expand All @@ -430,11 +432,25 @@ class GroupUpdater implements Closeable {
}

@WorkerThread
void updateLocalToServerVersion(int version, long timestamp)
void updateLocalToServerVersion(int version, long timestamp, @Nullable byte[] signedGroupChange)
throws IOException, GroupNotAMemberException
{
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
.updateLocalGroupToRevision(version, timestamp);
new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
.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
Expand All @@ -445,11 +461,12 @@ public void close() throws IOException {

private @NonNull GroupManager.GroupActionResult sendGroupUpdate(@NonNull GroupMasterKey masterKey,
@NonNull DecryptedGroup decryptedGroup,
@Nullable DecryptedGroupChange plainGroupChange)
@Nullable DecryptedGroupChange plainGroupChange,
@Nullable GroupChange signedGroupChange)
{
GroupId.V2 groupId = GroupId.v2(masterKey);
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,
decryptedGroupV2Context,
null,
Expand Down
Expand Up @@ -8,6 +8,7 @@

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.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
Expand Down Expand Up @@ -48,16 +49,20 @@ public static int findVersionWeWereAdded(@NonNull DecryptedGroup group, @NonNull

public static DecryptedGroupV2Context createDecryptedGroupV2Context(@NonNull GroupMasterKey masterKey,
@NonNull DecryptedGroup decryptedGroup,
@Nullable DecryptedGroupChange plainGroupChange)
@Nullable DecryptedGroupChange plainGroupChange,
@Nullable GroupChange signedServerChange)
{
int version = plainGroupChange != null ? plainGroupChange.getVersion() : decryptedGroup.getVersion();
SignalServiceProtos.GroupContextV2 groupContext = SignalServiceProtos.GroupContextV2.newBuilder()
.setMasterKey(ByteString.copyFrom(masterKey.serialize()))
.setRevision(version)
.build();
SignalServiceProtos.GroupContextV2.Builder contextBuilder = SignalServiceProtos.GroupContextV2.newBuilder()
.setMasterKey(ByteString.copyFrom(masterKey.serialize()))
.setRevision(version);

if (signedServerChange != null) {
contextBuilder.setGroupChange(signedServerChange.toByteString());
}

DecryptedGroupV2Context.Builder builder = DecryptedGroupV2Context.newBuilder()
.setContext(groupContext)
.setContext(contextBuilder.build())
.setGroupState(decryptedGroup);

if (plainGroupChange != null) {
Expand Down
Expand Up @@ -132,21 +132,47 @@ private StateProcessorForGroup(@NonNull GroupMasterKey groupMasterKey) {
*/
@WorkerThread
public GroupUpdateResult updateLocalGroupToRevision(final int revision,
final long timestamp)
final long timestamp,
@Nullable DecryptedGroupChange signedGroupChange)
throws IOException, GroupNotAMemberException
{
if (localIsAtLeast(revision)) {
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null);
}

GlobalGroupState inputGroupState;
try {
inputGroupState = queryServer();
} 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;
GlobalGroupState inputGroupState = null;

DecryptedGroup localState = groupDatabase.getGroup(groupId)
.transform(g -> g.requireV2GroupProperties().getDecryptedGroup())
.orNull();

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);
DecryptedGroup newLocalState = advanceGroupStateResult.getNewGlobalGroupState().getLocalState();

Expand Down Expand Up @@ -185,7 +211,7 @@ private void insertGroupLeave() {
.addDeleteMembers(UuidUtil.toByteString(selfUuid))
.build();

DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, simulatedGroupState, simulatedGroupChange);
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, simulatedGroupState, simulatedGroupChange, null);
OutgoingGroupUpdateMessage leaveMessage = new OutgoingGroupUpdateMessage(groupRecipient,
decryptedGroupV2Context,
null,
Expand Down Expand Up @@ -245,7 +271,7 @@ private void updateLocalDatabaseGroupState(@NonNull GlobalGroupState inputGroupS

private void insertUpdateMessages(long timestamp, Collection<GroupLogEntry> processedLogEntries) {
for (GroupLogEntry entry : processedLogEntries) {
storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, entry.getGroup(), entry.getChange()), timestamp);
storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, entry.getGroup(), entry.getChange(), null), timestamp);
}
}

Expand All @@ -266,15 +292,12 @@ private void persistLearnedProfileKeys(@NonNull GlobalGroupState globalGroupStat
}
}

private GlobalGroupState queryServer()
private @NonNull GlobalGroupState queryServer(@Nullable DecryptedGroup localState)
throws IOException, GroupNotAMemberException
{
DecryptedGroup latestServerGroup;
List<GroupLogEntry> history;
UUID selfUuid = Recipient.self().getUuid().get();
DecryptedGroup localState = groupDatabase.getGroup(groupId)
.transform(g -> g.requireV2GroupProperties().getDecryptedGroup())
.orNull();
UUID selfUuid = Recipient.self().getUuid().get();

try {
latestServerGroup = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfUuid, groupSecretParams));
Expand Down
Expand Up @@ -445,7 +445,7 @@ private boolean groupV2PreProcessMessage(@NonNull SignalServiceContent content,
throws IOException, GroupChangeBusyException
{
try {
GroupManager.updateGroupFromServer(context, groupMasterKey, groupV2.getRevision(), content.getTimestamp());
GroupManager.updateGroupFromServer(context, groupMasterKey, groupV2.getRevision(), content.getTimestamp(), groupV2.getSignedGroupChange());
return true;
} catch (GroupNotAMemberException e) {
Log.w(TAG, "Ignoring message for a group we're not in");
Expand Down
Expand Up @@ -87,7 +87,7 @@ public void onRun() throws IOException, GroupNotAMemberException, GroupChangeBus
return;
}

GroupManager.updateGroupFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis());
GroupManager.updateGroupFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null);
}

@Override
Expand Down
Expand Up @@ -1072,10 +1072,17 @@ private GroupContext createGroupContent(SignalServiceGroup group) throws IOExcep
}

private static GroupContextV2 createGroupContent(SignalServiceGroupV2 group) {
return GroupContextV2.newBuilder()
.setMasterKey(ByteString.copyFrom(group.getMasterKey().serialize()))
.setRevision(group.getRevision())
.build();
GroupContextV2.Builder builder = GroupContextV2.newBuilder()
.setMasterKey(ByteString.copyFrom(group.getMasterKey().serialize()))
.setRevision(group.getRevision());


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 {
Expand Down
Expand Up @@ -132,15 +132,11 @@ public String uploadAvatar(byte[] avatar,
return form.getKey();
}

public DecryptedGroupChange patchGroup(GroupChange.Actions groupChange,
GroupSecretParams groupSecretParams,
GroupsV2AuthorizationString authorization)
throws IOException, VerificationFailedException, InvalidGroupStateException
public GroupChange patchGroup(GroupChange.Actions groupChange,
GroupsV2AuthorizationString authorization)
throws IOException
{
GroupChange groupChanges = socket.patchGroupsV2Group(groupChange, authorization.toString());

return groupsOperations.forGroup(groupSecretParams)
.decryptChange(groupChanges, true);
return socket.patchGroupsV2Group(groupChange, authorization.toString());
}

private static HashMap<Integer, AuthCredentialResponse> parseCredentialResponse(CredentialResponse credentialResponse)
Expand Down

0 comments on commit 2f93209

Please sign in to comment.