mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 20:18:34 +00:00
Server signed group v2 changes sent and received P2P.
This commit is contained in:
parent
ec8d5defd4
commit
2f9320989a
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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));
|
||||||
|
@ -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");
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user