All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.whispersystems.signalservice.api.groupsv2.GroupsV2Api Maven / Gradle / Ivy

There is a newer version: 2.15.3_unofficial_107
Show newest version
package org.whispersystems.signalservice.api.groupsv2;

import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.auth.AuthCredentialPresentation;
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPni;
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations;
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse;
import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.storageservice.protos.groups.AvatarUploadAttributes;
import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupAttributeBlob;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupChanges;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.GroupJoinInfo;
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.whispersystems.signalservice.api.NetworkResult;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException;

import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.annotation.Nonnull;

import okio.ByteString;

public class GroupsV2Api {

  private final PushServiceSocket  socket;
  private final GroupsV2Operations groupsOperations;

  public GroupsV2Api(PushServiceSocket socket, GroupsV2Operations groupsOperations) {
    this.socket           = socket;
    this.groupsOperations = groupsOperations;
  }

  /**
   * Provides 7 days of credentials, which you should cache.
   */
  public CredentialResponseMaps getCredentials(long todaySeconds)
      throws IOException
  {
    return parseCredentialResponse(socket.retrieveGroupsV2Credentials(todaySeconds));
  }

  /**
   * Create an auth token from a credential response.
   */
  public GroupsV2AuthorizationString getGroupsV2AuthorizationString(ACI aci,
                                                                    PNI pni,
                                                                    long redemptionTimeSeconds,
                                                                    GroupSecretParams groupSecretParams,
                                                                    AuthCredentialWithPniResponse authCredentialWithPniResponse)
      throws VerificationFailedException
  {
    ClientZkAuthOperations     authOperations             = groupsOperations.getAuthOperations();
    AuthCredentialWithPni      authCredentialWithPni      = authOperations.receiveAuthCredentialWithPniAsServiceId(aci.getLibSignalAci(), pni.getLibSignalPni(), redemptionTimeSeconds, authCredentialWithPniResponse);
    AuthCredentialPresentation authCredentialPresentation = authOperations.createAuthCredentialPresentation(new SecureRandom(), groupSecretParams, authCredentialWithPni);

    return new GroupsV2AuthorizationString(groupSecretParams, authCredentialPresentation);
  }

  public void putNewGroup(GroupsV2Operations.NewGroup newGroup,
                          GroupsV2AuthorizationString authorization)
      throws IOException
  {
    Group group = newGroup.getNewGroupMessage();

    if (newGroup.getAvatar().isPresent()) {
      String cdnKey = uploadAvatar(newGroup.getAvatar().get(), newGroup.getGroupSecretParams(), authorization);

      group = group.newBuilder()
                    .avatar(cdnKey)
                    .build();
    }

    socket.putNewGroupsV2Group(group, authorization);
  }

  public NetworkResult getGroupAsResult(GroupSecretParams groupSecretParams, GroupsV2AuthorizationString authorization) {
    return NetworkResult.fromFetch(() -> getGroup(groupSecretParams, authorization));
  }

  public DecryptedGroup getGroup(GroupSecretParams groupSecretParams,
                                 GroupsV2AuthorizationString authorization)
      throws IOException, InvalidGroupStateException, VerificationFailedException
  {
    Group group = socket.getGroupsV2Group(authorization);

    return groupsOperations.forGroup(groupSecretParams)
                           .decryptGroup(group);
  }

  public GroupHistoryPage getGroupHistoryPage(GroupSecretParams groupSecretParams,
                                              int fromRevision,
                                              GroupsV2AuthorizationString authorization,
                                              boolean includeFirstState)
      throws IOException, InvalidGroupStateException, VerificationFailedException
  {
    PushServiceSocket.GroupHistory     group           = socket.getGroupsV2GroupHistory(fromRevision, authorization, GroupsV2Operations.HIGHEST_KNOWN_EPOCH, includeFirstState);
    List      result          = new ArrayList<>(group.getGroupChanges().groupChanges.size());
    GroupsV2Operations.GroupOperations groupOperations = groupsOperations.forGroup(groupSecretParams);

    for (GroupChanges.GroupChangeState change : group.getGroupChanges().groupChanges) {
      DecryptedGroup       decryptedGroup  = change.groupState != null ? groupOperations.decryptGroup(change.groupState) : null;
      DecryptedGroupChange decryptedChange = change.groupChange != null ? groupOperations.decryptChange(change.groupChange, false).orElse(null) : null;

      result.add(new DecryptedGroupChangeLog(decryptedGroup, decryptedChange));
    }

    return new GroupHistoryPage(result, GroupHistoryPage.PagingData.forGroupHistory(group));
  }

  public NetworkResult getGroupJoinedAt(@Nonnull GroupsV2AuthorizationString authorization) {
    return NetworkResult.fromFetch(() -> socket.getGroupJoinedAtRevision(authorization));
  }

  public DecryptedGroupJoinInfo getGroupJoinInfo(GroupSecretParams groupSecretParams,
                                                 Optional password,
                                                 GroupsV2AuthorizationString authorization)
      throws IOException, GroupLinkNotActiveException
  {
    try {
      GroupJoinInfo                      joinInfo        = socket.getGroupJoinInfo(password, authorization);
      GroupsV2Operations.GroupOperations groupOperations = groupsOperations.forGroup(groupSecretParams);

      return groupOperations.decryptGroupJoinInfo(joinInfo);
    } catch (ForbiddenException e) {
      throw new GroupLinkNotActiveException(null, e.getReason());
    }
  }

  public String uploadAvatar(byte[] avatar,
                             GroupSecretParams groupSecretParams,
                             GroupsV2AuthorizationString authorization)
      throws IOException
  {
    AvatarUploadAttributes form = socket.getGroupsV2AvatarUploadForm(authorization.toString());

    byte[] cipherText;
    try {
      cipherText = new ClientZkGroupCipher(groupSecretParams).encryptBlob(new GroupAttributeBlob.Builder().avatar(ByteString.of(avatar)).build().encode());
    } catch (VerificationFailedException e) {
      throw new AssertionError(e);
    }

    socket.uploadGroupV2Avatar(cipherText, form);

    return form.key;
  }

  public GroupChange patchGroup(GroupChange.Actions groupChange,
                                GroupsV2AuthorizationString authorization,
                                Optional groupLinkPassword)
      throws IOException
  {
    return socket.patchGroupsV2Group(groupChange, authorization.toString(), groupLinkPassword);
  }

  public GroupExternalCredential getGroupExternalCredential(GroupsV2AuthorizationString authorization)
      throws IOException
  {
    return socket.getGroupExternalCredential(authorization);
  }

  private static CredentialResponseMaps parseCredentialResponse(CredentialResponse credentialResponse)
      throws IOException
  {
    HashMap  credentials         = new HashMap<>();
    HashMap callLinkCredentials = new HashMap<>();

    for (TemporalCredential credential : credentialResponse.getCredentials()) {
      AuthCredentialWithPniResponse authCredentialWithPniResponse;
      try {
        authCredentialWithPniResponse = new AuthCredentialWithPniResponse(credential.getCredential());
      } catch (InvalidInputException e) {
        throw new IOException(e);
      }

      credentials.put(credential.getRedemptionTime(), authCredentialWithPniResponse);
    }

    for (TemporalCredential credential : credentialResponse.getCallLinkAuthCredentials()) {
      CallLinkAuthCredentialResponse callLinkAuthCredentialResponse;
      try {
        callLinkAuthCredentialResponse = new CallLinkAuthCredentialResponse(credential.getCredential());
      } catch (InvalidInputException e) {
        throw new IOException(e);
      }

      callLinkCredentials.put(credential.getRedemptionTime(), callLinkAuthCredentialResponse);
    }

    return new CredentialResponseMaps(credentials, callLinkCredentials);
  }

  public static class CredentialResponseMaps {
    private final Map  authCredentialWithPniResponseHashMap;
    private final Map callLinkAuthCredentialResponseHashMap;

    public CredentialResponseMaps(Map authCredentialWithPniResponseHashMap,
                                  Map callLinkAuthCredentialResponseHashMap)
    {
      this.authCredentialWithPniResponseHashMap  = authCredentialWithPniResponseHashMap;
      this.callLinkAuthCredentialResponseHashMap = callLinkAuthCredentialResponseHashMap;
    }

    public Map getAuthCredentialWithPniResponseHashMap() {
      return authCredentialWithPniResponseHashMap;
    }

    public Map getCallLinkAuthCredentialResponseHashMap() {
      return callLinkAuthCredentialResponseHashMap;
    }

    public CredentialResponseMaps createUnmodifiableCopy() {
      return new CredentialResponseMaps(
          Map.copyOf(authCredentialWithPniResponseHashMap),
          Map.copyOf(callLinkAuthCredentialResponseHashMap)
      );
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy