import com.fasterxml.jackson.databind.ObjectMapper;
-import org.asamk.signal.storage.SignalAccount;
-import org.asamk.signal.storage.contacts.ContactInfo;
-import org.asamk.signal.storage.groups.GroupInfo;
-import org.asamk.signal.storage.groups.GroupInfoV1;
-import org.asamk.signal.storage.groups.GroupInfoV2;
-import org.asamk.signal.storage.profiles.SignalProfile;
-import org.asamk.signal.storage.profiles.SignalProfileEntry;
-import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
-import org.asamk.signal.storage.stickers.Sticker;
-import org.asamk.signal.util.IOUtils;
-import org.asamk.signal.util.Util;
+import org.asamk.signal.manager.groups.GroupId;
+import org.asamk.signal.manager.groups.GroupIdV1;
+import org.asamk.signal.manager.groups.GroupIdV2;
+import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
+import org.asamk.signal.manager.groups.GroupNotFoundException;
+import org.asamk.signal.manager.groups.GroupUtils;
+import org.asamk.signal.manager.groups.NotAGroupMemberException;
+import org.asamk.signal.manager.helper.GroupHelper;
+import org.asamk.signal.manager.helper.PinHelper;
+import org.asamk.signal.manager.helper.ProfileHelper;
+import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
+import org.asamk.signal.manager.storage.SignalAccount;
+import org.asamk.signal.manager.storage.contacts.ContactInfo;
+import org.asamk.signal.manager.storage.groups.GroupInfo;
+import org.asamk.signal.manager.storage.groups.GroupInfoV1;
+import org.asamk.signal.manager.storage.groups.GroupInfoV2;
+import org.asamk.signal.manager.storage.profiles.SignalProfile;
+import org.asamk.signal.manager.storage.profiles.SignalProfileEntry;
+import org.asamk.signal.manager.storage.protocol.IdentityInfo;
+import org.asamk.signal.manager.storage.stickers.Sticker;
+import org.asamk.signal.manager.util.AttachmentUtils;
+import org.asamk.signal.manager.util.IOUtils;
+import org.asamk.signal.manager.util.KeyUtils;
+import org.asamk.signal.manager.util.MessageCacheUtils;
+import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
import org.signal.libsignal.metadata.InvalidMetadataVersionException;
import org.signal.libsignal.metadata.ProtocolDuplicateMessageException;
import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException;
-import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
+import org.signal.libsignal.metadata.certificate.CertificateValidator;
+import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
+import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
+import org.signal.zkgroup.profiles.ProfileKeyCredential;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.KbsPinData;
+import org.whispersystems.signalservice.api.KeyBackupService;
+import org.whispersystems.signalservice.api.KeyBackupServicePinException;
+import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
-import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
+import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
-import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
+import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
+import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
+import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
+import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
+import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
+import org.whispersystems.signalservice.internal.push.LockedException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
+import java.security.KeyStore;
+import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
+import static org.asamk.signal.manager.ServiceConfig.CDS_MRENCLAVE;
import static org.asamk.signal.manager.ServiceConfig.capabilities;
+import static org.asamk.signal.manager.ServiceConfig.getIasKeyStore;
public class Manager implements Closeable {
+ final static Logger logger = LoggerFactory.getLogger(Manager.class);
+
private final SleepTimer timer = new UptimeSleepTimer();
+ private final CertificateValidator certificateValidator = new CertificateValidator(ServiceConfig.getUnidentifiedSenderTrustRoot());
+
private final SignalServiceConfiguration serviceConfiguration;
private final String userAgent;
+ // TODO make configurable
+ private final boolean discoverableByPhoneNumber = true;
+ private final boolean unrestrictedUnidentifiedAccess = false;
+
private final SignalAccount account;
private final PathConfig pathConfig;
private SignalServiceAccountManager accountManager;
private GroupsV2Api groupsV2Api;
+ private final GroupsV2Operations groupsV2Operations;
+
+ private SignalServiceMessageReceiver messageReceiver = null;
private SignalServiceMessagePipe messagePipe = null;
private SignalServiceMessagePipe unidentifiedMessagePipe = null;
- private final boolean discoverableByPhoneNumber = true;
- public Manager(SignalAccount account, PathConfig pathConfig, SignalServiceConfiguration serviceConfiguration, String userAgent) {
+ private final UnidentifiedAccessHelper unidentifiedAccessHelper;
+ private final ProfileHelper profileHelper;
+ private final GroupHelper groupHelper;
+ private PinHelper pinHelper;
+
+ Manager(
+ SignalAccount account,
+ PathConfig pathConfig,
+ SignalServiceConfiguration serviceConfiguration,
+ String userAgent
+ ) {
this.account = account;
this.pathConfig = pathConfig;
this.serviceConfiguration = serviceConfiguration;
this.userAgent = userAgent;
- this.accountManager = createSignalServiceAccountManager();
- this.groupsV2Api = accountManager.getGroupsV2Api();
+ this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create(
+ serviceConfiguration)) : null;
+ createSignalServiceAccountManager();
this.account.setResolver(this::resolveSignalServiceAddress);
+
+ this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
+ account.getProfileStore()::getProfileKey,
+ this::getRecipientProfile,
+ this::getSenderCertificate);
+ this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
+ unidentifiedAccessHelper::getAccessFor,
+ unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(),
+ this::getOrCreateMessageReceiver);
+ this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
+ this::getRecipientProfile,
+ account::getSelfAddress,
+ groupsV2Operations,
+ groupsV2Api,
+ this::getGroupAuthForToday);
}
public String getUsername() {
return account.getSelfAddress();
}
- private SignalServiceAccountManager createSignalServiceAccountManager() {
- GroupsV2Operations groupsV2Operations = capabilities.isGv2()
- ? new GroupsV2Operations(ClientZkOperations.create(serviceConfiguration))
- : null;
-
- return new SignalServiceAccountManager(serviceConfiguration,
- new DynamicCredentialsProvider(account.getUuid(), account.getUsername(), account.getPassword(), null, account.getDeviceId()),
+ private void createSignalServiceAccountManager() {
+ this.accountManager = new SignalServiceAccountManager(serviceConfiguration,
+ new DynamicCredentialsProvider(account.getUuid(),
+ account.getUsername(),
+ account.getPassword(),
+ null,
+ account.getDeviceId()),
userAgent,
groupsV2Operations,
timer);
+ this.groupsV2Api = accountManager.getGroupsV2Api();
+ this.pinHelper = new PinHelper(createKeyBackupService());
+ }
+
+ private KeyBackupService createKeyBackupService() {
+ KeyStore keyStore = ServiceConfig.getIasKeyStore();
+
+ return accountManager.getKeyBackupService(keyStore,
+ ServiceConfig.KEY_BACKUP_ENCLAVE_NAME,
+ ServiceConfig.KEY_BACKUP_SERVICE_ID,
+ ServiceConfig.KEY_BACKUP_MRENCLAVE,
+ 10);
}
private IdentityKeyPair getIdentityKeyPair() {
return account.getDeviceId();
}
- private String getMessageCachePath() {
- return pathConfig.getDataPath() + "/" + account.getUsername() + ".d/msg-cache";
+ private File getMessageCachePath() {
+ return SignalAccount.getMessageCachePath(pathConfig.getDataPath(), account.getUsername());
}
- private String getMessageCachePath(String sender) {
+ private File getMessageCachePath(String sender) {
if (sender == null || sender.isEmpty()) {
return getMessageCachePath();
}
- return getMessageCachePath() + "/" + sender.replace("/", "_");
+ return new File(getMessageCachePath(), sender.replace("/", "_"));
}
private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException {
- String cachePath = getMessageCachePath(sender);
+ File cachePath = getMessageCachePath(sender);
IOUtils.createPrivateDirectories(cachePath);
- return new File(cachePath + "/" + now + "_" + timestamp);
+ return new File(cachePath, now + "_" + timestamp);
}
- public static Manager init(String username, String settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent) throws IOException {
+ public static Manager init(
+ String username, File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent
+ ) throws IOException {
PathConfig pathConfig = PathConfig.createDefault(settingsPath);
if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) {
int registrationId = KeyHelper.generateRegistrationId(false);
ProfileKey profileKey = KeyUtils.createProfileKey();
- SignalAccount account = SignalAccount.create(pathConfig.getDataPath(), username, identityKey, registrationId, profileKey);
+ SignalAccount account = SignalAccount.create(pathConfig.getDataPath(),
+ username,
+ identityKey,
+ registrationId,
+ profileKey);
account.save();
return new Manager(account, pathConfig, serviceConfiguration, userAgent);
contact.profileKey = null;
account.getProfileStore().storeProfileKey(contact.getAddress(), profileKey);
}
+ // Ensure our profile key is stored in profile store
+ account.getProfileStore().storeProfileKey(getSelfAddress(), account.getProfileKey());
}
public void checkAccountState() throws IOException {
return account.isRegistered();
}
+ /**
+ * This is used for checking a set of phone numbers for registration on Signal
+ *
+ * @param numbers The set of phone number in question
+ * @return A map of numbers to booleans. True if registered, false otherwise. Should never be null
+ * @throws IOException if its unable to check if the users are registered
+ */
+ public Map<String, Boolean> areUsersRegistered(Set<String> numbers) throws IOException {
+ // Note "contactDetails" has no optionals. It only gives us info on users who are registered
+ List<ContactTokenDetails> contactDetails = this.accountManager.getContacts(numbers);
+
+ // Make the initial map with all numbers set to false for now
+ Map<String, Boolean> usersRegistered = numbers.stream().collect(Collectors.toMap(x -> x, x -> false));
+
+ // Override the contacts we did obtain
+ for (ContactTokenDetails contactDetail : contactDetails) {
+ usersRegistered.put(contactDetail.getNumber(), true);
+ }
+
+ return usersRegistered;
+ }
+
public void register(boolean voiceVerification, String captcha) throws IOException {
account.setPassword(KeyUtils.createPassword());
// Resetting UUID, because registering doesn't work otherwise
account.setUuid(null);
- accountManager = createSignalServiceAccountManager();
- this.groupsV2Api = accountManager.getGroupsV2Api();
+ createSignalServiceAccountManager();
if (voiceVerification) {
- accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.fromNullable(captcha), Optional.absent());
+ accountManager.requestVoiceVerificationCode(Locale.getDefault(),
+ Optional.fromNullable(captcha),
+ Optional.absent());
} else {
accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent());
}
}
public void updateAccountAttributes() throws IOException {
- accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), account.getRegistrationLock(), getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber);
+ accountManager.setAccountAttributes(account.getSignalingKey(),
+ account.getSignalProtocolStore().getLocalRegistrationId(),
+ true,
+ // set legacy pin only if no KBS master key is set
+ account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null,
+ account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
+ unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(),
+ unrestrictedUnidentifiedAccess,
+ capabilities,
+ discoverableByPhoneNumber);
}
public void setProfile(String name, File avatar) throws IOException {
}
public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException {
- Utils.DeviceLinkInfo info = Utils.parseDeviceLinkUri(linkUri);
+ DeviceLinkInfo info = DeviceLinkInfo.parseDeviceLinkUri(linkUri);
addDevice(info.deviceIdentifier, info.deviceKey);
}
IdentityKeyPair identityKeyPair = getIdentityKeyPair();
String verificationCode = accountManager.getNewDeviceVerificationCode();
- accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey().serialize()), verificationCode);
+ accountManager.addDevice(deviceIdentifier,
+ deviceKey,
+ identityKeyPair,
+ Optional.of(account.getProfileKey().serialize()),
+ verificationCode);
account.setMultiDevice(true);
account.save();
}
private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
try {
ECKeyPair keyPair = Curve.generateKeyPair();
- byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
- SignedPreKeyRecord record = new SignedPreKeyRecord(account.getNextSignedPreKeyId(), System.currentTimeMillis(), keyPair, signature);
+ byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(),
+ keyPair.getPublicKey().serialize());
+ SignedPreKeyRecord record = new SignedPreKeyRecord(account.getNextSignedPreKeyId(),
+ System.currentTimeMillis(),
+ keyPair,
+ signature);
account.addSignedPreKey(record);
account.save();
}
}
- public void verifyAccount(String verificationCode, String pin) throws IOException {
+ public void verifyAccount(
+ String verificationCode,
+ String pin
+ ) throws IOException, KeyBackupSystemNoDataException, KeyBackupServicePinException {
verificationCode = verificationCode.replace("-", "");
account.setSignalingKey(KeyUtils.createSignalingKey());
- // TODO make unrestricted unidentified access configurable
- VerifyAccountResponse response = accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, null, getSelfUnidentifiedAccessKey(), false, capabilities, discoverableByPhoneNumber);
+ VerifyAccountResponse response;
+ try {
+ response = verifyAccountWithCode(verificationCode, pin, null);
+ } catch (LockedException e) {
+ if (pin == null) {
+ throw e;
+ }
+
+ KbsPinData registrationLockData = pinHelper.getRegistrationLockData(pin, e);
+ if (registrationLockData == null) {
+ throw e;
+ }
+
+ String registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
+ try {
+ response = verifyAccountWithCode(verificationCode, null, registrationLock);
+ } catch (LockedException _e) {
+ throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
+ }
+ account.setPinMasterKey(registrationLockData.getMasterKey());
+ }
- UUID uuid = UuidUtil.parseOrNull(response.getUuid());
// TODO response.isStorageCapable()
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
+
account.setRegistered(true);
- account.setUuid(uuid);
+ account.setUuid(UuidUtil.parseOrNull(response.getUuid()));
account.setRegistrationLockPin(pin);
- account.getSignalProtocolStore().saveIdentity(account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), TrustLevel.TRUSTED_VERIFIED);
+ account.getSignalProtocolStore()
+ .saveIdentity(account.getSelfAddress(),
+ getIdentityKeyPair().getPublicKey(),
+ TrustLevel.TRUSTED_VERIFIED);
refreshPreKeys();
account.save();
}
- public void setRegistrationLockPin(Optional<String> pin) throws IOException {
+ private VerifyAccountResponse verifyAccountWithCode(
+ final String verificationCode, final String legacyPin, final String registrationLock
+ ) throws IOException {
+ return accountManager.verifyAccountWithCode(verificationCode,
+ account.getSignalingKey(),
+ account.getSignalProtocolStore().getLocalRegistrationId(),
+ true,
+ legacyPin,
+ registrationLock,
+ unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(),
+ unrestrictedUnidentifiedAccess,
+ capabilities,
+ discoverableByPhoneNumber);
+ }
+
+ public void setRegistrationLockPin(Optional<String> pin) throws IOException, UnauthenticatedResponseException {
if (pin.isPresent()) {
+ final MasterKey masterKey = account.getPinMasterKey() != null
+ ? account.getPinMasterKey()
+ : KeyUtils.createMasterKey();
+
+ pinHelper.setRegistrationLockPin(pin.get(), masterKey);
+
account.setRegistrationLockPin(pin.get());
- throw new RuntimeException("Not implemented anymore, will be replaced with KBS");
+ account.setPinMasterKey(masterKey);
} else {
- account.setRegistrationLockPin(null);
+ // Remove legacy registration lock
accountManager.removeRegistrationLockV1();
+
+ // Remove KBS Pin
+ pinHelper.removeRegistrationLockPin();
+
+ account.setRegistrationLockPin(null);
+ account.setPinMasterKey(null);
}
account.save();
}
accountManager.setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys);
}
- private SignalServiceMessageReceiver getMessageReceiver() {
- final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2()
- ? ClientZkOperations.create(serviceConfiguration).getProfileOperations()
- : null;
- return new SignalServiceMessageReceiver(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId(), account.getSignalingKey(), userAgent, null, timer, clientZkProfileOperations);
+ private SignalServiceMessageReceiver createMessageReceiver() {
+ final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create(
+ serviceConfiguration).getProfileOperations() : null;
+ return new SignalServiceMessageReceiver(serviceConfiguration,
+ account.getUuid(),
+ account.getUsername(),
+ account.getPassword(),
+ account.getDeviceId(),
+ account.getSignalingKey(),
+ userAgent,
+ null,
+ timer,
+ clientZkProfileOperations);
}
- private SignalServiceMessageSender getMessageSender() {
- final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2()
- ? ClientZkOperations.create(serviceConfiguration).getProfileOperations()
- : null;
- final ExecutorService executor = null;
- return new SignalServiceMessageSender(serviceConfiguration, account.getUuid(), account.getUsername(), account.getPassword(),
- account.getDeviceId(), account.getSignalProtocolStore(), userAgent, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent(), clientZkProfileOperations, executor, ServiceConfig.MAX_ENVELOPE_SIZE);
+ private SignalServiceMessageReceiver getOrCreateMessageReceiver() {
+ if (messageReceiver == null) {
+ messageReceiver = createMessageReceiver();
+ }
+ return messageReceiver;
}
- private SignalServiceProfile getEncryptedRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
- SignalServiceMessagePipe pipe = unidentifiedMessagePipe != null && unidentifiedAccess.isPresent() ? unidentifiedMessagePipe
- : messagePipe;
-
- if (pipe != null) {
- try {
- return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile();
- } catch (IOException | InterruptedException | ExecutionException | TimeoutException ignored) {
- }
+ private SignalServiceMessagePipe getOrCreateMessagePipe() {
+ if (messagePipe == null) {
+ messagePipe = getOrCreateMessageReceiver().createMessagePipe();
}
+ return messagePipe;
+ }
- SignalServiceMessageReceiver receiver = getMessageReceiver();
- try {
- return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS).getProfile();
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- throw new IOException("Failed to retrieve profile", e);
+ private SignalServiceMessagePipe getOrCreateUnidentifiedMessagePipe() {
+ if (unidentifiedMessagePipe == null) {
+ unidentifiedMessagePipe = getOrCreateMessageReceiver().createUnidentifiedMessagePipe();
}
+ return unidentifiedMessagePipe;
}
- private SignalProfile getRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess, ProfileKey profileKey) throws IOException {
- SignalProfileEntry profileEntry = account.getProfileStore().getProfile(address);
+ private SignalServiceMessageSender createMessageSender() {
+ final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create(
+ serviceConfiguration).getProfileOperations() : null;
+ final ExecutorService executor = null;
+ return new SignalServiceMessageSender(serviceConfiguration,
+ account.getUuid(),
+ account.getUsername(),
+ account.getPassword(),
+ account.getDeviceId(),
+ account.getSignalProtocolStore(),
+ userAgent,
+ account.isMultiDevice(),
+ Optional.fromNullable(messagePipe),
+ Optional.fromNullable(unidentifiedMessagePipe),
+ Optional.absent(),
+ clientZkProfileOperations,
+ executor,
+ ServiceConfig.MAX_ENVELOPE_SIZE);
+ }
+
+ private SignalServiceProfile getEncryptedRecipientProfile(SignalServiceAddress address) throws IOException {
+ return profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE).getProfile();
+ }
+
+ private SignalProfile getRecipientProfile(
+ SignalServiceAddress address
+ ) {
+ SignalProfileEntry profileEntry = account.getProfileStore().getProfileEntry(address);
+ if (profileEntry == null) {
+ return null;
+ }
long now = new Date().getTime();
// Profiles are cache for 24h before retrieving them again
- if (profileEntry == null || profileEntry.getProfile() == null || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000) {
- SignalProfile profile = retrieveRecipientProfile(address, unidentifiedAccess, profileKey);
- account.getProfileStore().updateProfile(address, profileKey, now, profile);
+ if (!profileEntry.isRequestPending() && (
+ profileEntry.getProfile() == null || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000
+ )) {
+ ProfileKey profileKey = profileEntry.getProfileKey();
+ profileEntry.setRequestPending(true);
+ SignalProfile profile;
+ try {
+ profile = retrieveRecipientProfile(address, profileKey);
+ } catch (IOException e) {
+ logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
+ profileEntry.setRequestPending(false);
+ return null;
+ }
+ profileEntry.setRequestPending(false);
+ account.getProfileStore()
+ .updateProfile(address, profileKey, now, profile, profileEntry.getProfileKeyCredential());
return profile;
}
return profileEntry.getProfile();
}
- private SignalProfile retrieveRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess, ProfileKey profileKey) throws IOException {
- final SignalServiceProfile encryptedProfile = getEncryptedRecipientProfile(address, unidentifiedAccess);
+ private ProfileKeyCredential getRecipientProfileKeyCredential(SignalServiceAddress address) {
+ SignalProfileEntry profileEntry = account.getProfileStore().getProfileEntry(address);
+ if (profileEntry == null) {
+ return null;
+ }
+ if (profileEntry.getProfileKeyCredential() == null) {
+ ProfileAndCredential profileAndCredential;
+ try {
+ profileAndCredential = profileHelper.retrieveProfileSync(address,
+ SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL);
+ } catch (IOException e) {
+ logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
+ return null;
+ }
+ long now = new Date().getTime();
+ final ProfileKeyCredential profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
+ final SignalProfile profile = decryptProfile(address,
+ profileEntry.getProfileKey(),
+ profileAndCredential.getProfile());
+ account.getProfileStore()
+ .updateProfile(address, profileEntry.getProfileKey(), now, profile, profileKeyCredential);
+ return profileKeyCredential;
+ }
+ return profileEntry.getProfileKeyCredential();
+ }
+
+ private SignalProfile retrieveRecipientProfile(
+ SignalServiceAddress address, ProfileKey profileKey
+ ) throws IOException {
+ final SignalServiceProfile encryptedProfile = getEncryptedRecipientProfile(address);
+
+ return decryptProfile(address, profileKey, encryptedProfile);
+ }
+
+ private SignalProfile decryptProfile(
+ final SignalServiceAddress address, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
+ ) {
File avatarFile = null;
try {
- avatarFile = encryptedProfile.getAvatar() == null ? null : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey);
+ avatarFile = encryptedProfile.getAvatar() == null
+ ? null
+ : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey);
} catch (Throwable e) {
- System.err.println("Failed to retrieve profile avatar, ignoring: " + e.getMessage());
+ logger.warn("Failed to retrieve profile avatar, ignoring: {}", e.getMessage());
}
ProfileCipher profileCipher = new ProfileCipher(profileKey);
try {
- return new SignalProfile(
- encryptedProfile.getIdentityKey(),
- encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))),
+ String name;
+ try {
+ name = encryptedProfile.getName() == null
+ ? null
+ : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName())));
+ } catch (IOException e) {
+ name = null;
+ }
+ String unidentifiedAccess;
+ try {
+ unidentifiedAccess = encryptedProfile.getUnidentifiedAccess() == null
+ || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess()))
+ ? null
+ : encryptedProfile.getUnidentifiedAccess();
+ } catch (IOException e) {
+ unidentifiedAccess = null;
+ }
+ return new SignalProfile(encryptedProfile.getIdentityKey(),
+ name,
avatarFile,
- encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(),
+ unidentifiedAccess,
encryptedProfile.isUnrestrictedUnidentifiedAccess(),
encryptedProfile.getCapabilities());
} catch (InvalidCiphertextException e) {
}
}
- private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(byte[] groupId) throws IOException {
+ private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(GroupId groupId) throws IOException {
File file = getGroupAvatarFile(groupId);
if (!file.exists()) {
return Optional.absent();
}
- return Optional.of(Utils.createAttachment(file));
+ return Optional.of(AttachmentUtils.createAttachment(file));
}
private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(String number) throws IOException {
return Optional.absent();
}
- return Optional.of(Utils.createAttachment(file));
+ return Optional.of(AttachmentUtils.createAttachment(file));
}
- private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException {
+ private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
GroupInfo g = account.getGroupStore().getGroup(groupId);
if (g == null) {
throw new GroupNotFoundException(groupId);
return g;
}
+ private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
+ GroupInfo g = account.getGroupStore().getGroup(groupId);
+ if (g == null) {
+ throw new GroupNotFoundException(groupId);
+ }
+ if (!g.isMember(account.getSelfAddress()) && !g.isPendingMember(account.getSelfAddress())) {
+ throw new NotAGroupMemberException(groupId, g.getTitle());
+ }
+ return g;
+ }
+
public List<GroupInfo> getGroups() {
return account.getGroupStore().getGroups();
}
public Pair<Long, List<SendMessageResult>> sendGroupMessage(
- String messageText,
- List<String> attachments,
- byte[] groupId
- )
- throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
- final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
- if (attachments != null) {
- messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments));
- }
-
+ SignalServiceDataMessage.Builder messageBuilder, GroupId groupId
+ ) throws IOException, GroupNotFoundException, NotAGroupMemberException {
final GroupInfo g = getGroupForSending(groupId);
- setGroupContext(messageBuilder, g);
+ GroupUtils.setGroupContext(messageBuilder, g);
messageBuilder.withExpiration(g.getMessageExpirationTime());
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
}
- private void setGroupContext(final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo groupInfo) {
- if (groupInfo instanceof GroupInfoV1) {
- SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
- .withId(groupInfo.groupId)
- .build();
- messageBuilder.asGroupMessage(group);
- } else {
- final GroupInfoV2 groupInfoV2 = (GroupInfoV2) groupInfo;
- SignalServiceGroupV2 group = SignalServiceGroupV2.newBuilder(groupInfoV2.getMasterKey())
- .withRevision(groupInfoV2.getGroup() == null ? 0 : groupInfoV2.getGroup().getRevision())
- .build();
- messageBuilder.asGroupMessage(group);
+ public Pair<Long, List<SendMessageResult>> sendGroupMessage(
+ String messageText, List<String> attachments, GroupId groupId
+ ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
+ final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
+ .withBody(messageText);
+ if (attachments != null) {
+ messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments));
}
+
+ return sendGroupMessage(messageBuilder, groupId);
}
- public Pair<Long, List<SendMessageResult>> sendGroupMessageReaction(String emoji, boolean remove, String targetAuthor,
- long targetSentTimestamp, byte[] groupId)
- throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException {
- SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp);
+ public Pair<Long, List<SendMessageResult>> sendGroupMessageReaction(
+ String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, GroupId groupId
+ ) throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException {
+ SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji,
+ remove,
+ canonicalizeAndResolveSignalServiceAddress(targetAuthor),
+ targetSentTimestamp);
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.withReaction(reaction);
- final GroupInfo g = getGroupForSending(groupId);
- setGroupContext(messageBuilder, g);
- return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
+
+ return sendGroupMessage(messageBuilder, groupId);
}
- public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
- SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
- .withId(groupId)
- .build();
+ public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
- SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
- .asGroupMessage(group);
+ SignalServiceDataMessage.Builder messageBuilder;
- final GroupInfo g = getGroupForSending(groupId);
+ final GroupInfo g = getGroupForUpdating(groupId);
if (g instanceof GroupInfoV1) {
GroupInfoV1 groupInfoV1 = (GroupInfoV1) g;
+ SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
+ .withId(groupId.serialize())
+ .build();
+ messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
groupInfoV1.removeMember(account.getSelfAddress());
account.getGroupStore().updateGroup(groupInfoV1);
} else {
- throw new RuntimeException("TODO Not implemented!");
+ final GroupInfoV2 groupInfoV2 = (GroupInfoV2) g;
+ final Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2);
+ groupInfoV2.setGroup(groupGroupChangePair.first());
+ messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
+ account.getGroupStore().updateGroup(groupInfoV2);
}
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
}
- private Pair<byte[], List<SendMessageResult>> sendUpdateGroupMessage(byte[] groupId, String name, Collection<SignalServiceAddress> members, String avatarFile) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
- GroupInfoV1 g;
+ private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage(
+ GroupId groupId, String name, Collection<SignalServiceAddress> members, String avatarFile
+ ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
+ GroupInfo g;
+ SignalServiceDataMessage.Builder messageBuilder;
if (groupId == null) {
// Create new group
- g = new GroupInfoV1(KeyUtils.createGroupId());
- g.addMembers(Collections.singleton(account.getSelfAddress()));
+ GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile);
+ if (gv2 == null) {
+ GroupInfoV1 gv1 = new GroupInfoV1(GroupIdV1.createRandom());
+ gv1.addMembers(List.of(account.getSelfAddress()));
+ updateGroupV1(gv1, name, members, avatarFile);
+ messageBuilder = getGroupUpdateMessageBuilder(gv1);
+ g = gv1;
+ } else {
+ messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
+ g = gv2;
+ }
} else {
- GroupInfo group = getGroupForSending(groupId);
- if (!(group instanceof GroupInfoV1)) {
- throw new RuntimeException("TODO Not implemented!");
+ GroupInfo group = getGroupForUpdating(groupId);
+ if (group instanceof GroupInfoV2) {
+ final GroupInfoV2 groupInfoV2 = (GroupInfoV2) group;
+
+ Pair<Long, List<SendMessageResult>> result = null;
+ if (groupInfoV2.isPendingMember(getSelfAddress())) {
+ Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2);
+ result = sendUpdateGroupMessage(groupInfoV2,
+ groupGroupChangePair.first(),
+ groupGroupChangePair.second());
+ }
+
+ if (members != null) {
+ final Set<SignalServiceAddress> newMembers = new HashSet<>(members);
+ newMembers.removeAll(group.getMembers()
+ .stream()
+ .map(this::resolveSignalServiceAddress)
+ .collect(Collectors.toSet()));
+ if (newMembers.size() > 0) {
+ Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2,
+ newMembers);
+ result = sendUpdateGroupMessage(groupInfoV2,
+ groupGroupChangePair.first(),
+ groupGroupChangePair.second());
+ }
+ }
+ if (result == null || name != null || avatarFile != null) {
+ Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2,
+ name,
+ avatarFile);
+ result = sendUpdateGroupMessage(groupInfoV2,
+ groupGroupChangePair.first(),
+ groupGroupChangePair.second());
+ }
+
+ return new Pair<>(group.getGroupId(), result.second());
+ } else {
+ GroupInfoV1 gv1 = (GroupInfoV1) group;
+ updateGroupV1(gv1, name, members, avatarFile);
+ messageBuilder = getGroupUpdateMessageBuilder(gv1);
+ g = gv1;
}
- g = (GroupInfoV1) group;
}
+ account.getGroupStore().updateGroup(g);
+
+ final Pair<Long, List<SendMessageResult>> result = sendMessage(messageBuilder,
+ g.getMembersIncludingPendingWithout(account.getSelfAddress()));
+ return new Pair<>(g.getGroupId(), result.second());
+ }
+
+ public Pair<GroupId, List<SendMessageResult>> joinGroup(
+ GroupInviteLinkUrl inviteLinkUrl
+ ) throws IOException, GroupLinkNotActiveException {
+ return sendJoinGroupMessage(inviteLinkUrl);
+ }
+
+ private Pair<GroupId, List<SendMessageResult>> sendJoinGroupMessage(
+ GroupInviteLinkUrl inviteLinkUrl
+ ) throws IOException, GroupLinkNotActiveException {
+ final DecryptedGroupJoinInfo groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(),
+ inviteLinkUrl.getPassword());
+ final GroupChange groupChange = groupHelper.joinGroup(inviteLinkUrl.getGroupMasterKey(),
+ inviteLinkUrl.getPassword(),
+ groupJoinInfo);
+ final GroupInfoV2 group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
+ groupJoinInfo.getRevision() + 1,
+ groupChange.toByteArray());
+
+ if (group.getGroup() == null) {
+ // Only requested member, can't send update to group members
+ return new Pair<>(group.getGroupId(), List.of());
+ }
+
+ final Pair<Long, List<SendMessageResult>> result = sendUpdateGroupMessage(group, group.getGroup(), groupChange);
+
+ return new Pair<>(group.getGroupId(), result.second());
+ }
+
+ private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
+ GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
+ ) throws IOException {
+ group.setGroup(newDecryptedGroup);
+ final SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(group,
+ groupChange.toByteArray());
+ account.getGroupStore().updateGroup(group);
+ return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfAddress()));
+ }
+
+ private void updateGroupV1(
+ final GroupInfoV1 g,
+ final String name,
+ final Collection<SignalServiceAddress> members,
+ final String avatarFile
+ ) throws IOException {
if (name != null) {
g.name = name;
}
for (ContactTokenDetails contact : contacts) {
newE164Members.remove(contact.getNumber());
}
- throw new IOException("Failed to add members " + Util.join(", ", newE164Members) + " to group: Not registered on Signal");
+ throw new IOException("Failed to add members "
+ + String.join(", ", newE164Members)
+ + " to group: Not registered on Signal");
}
g.addMembers(members);
if (avatarFile != null) {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
- File aFile = getGroupAvatarFile(g.groupId);
+ File aFile = getGroupAvatarFile(g.getGroupId());
Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
-
- account.getGroupStore().updateGroup(g);
-
- SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
-
- final Pair<Long, List<SendMessageResult>> result = sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
- return new Pair<>(g.groupId, result.second());
}
- Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(byte[] groupId, SignalServiceAddress recipient) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
+ Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
+ GroupIdV1 groupId, SignalServiceAddress recipient
+ ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
GroupInfoV1 g;
GroupInfo group = getGroupForSending(groupId);
if (!(group instanceof GroupInfoV1)) {
- throw new RuntimeException("TODO Not implemented!");
+ throw new RuntimeException("Received an invalid group request for a v2 group!");
}
g = (GroupInfoV1) group;
SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
// Send group message only to the recipient who requested it
- return sendMessage(messageBuilder, Collections.singleton(recipient));
+ return sendMessage(messageBuilder, List.of(recipient));
}
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
- .withId(g.groupId)
+ .withId(g.getGroupId().serialize())
.withName(g.name)
.withMembers(new ArrayList<>(g.getMembers()));
- File aFile = getGroupAvatarFile(g.groupId);
+ File aFile = getGroupAvatarFile(g.getGroupId());
if (aFile.exists()) {
try {
- group.withAvatar(Utils.createAttachment(aFile));
+ group.withAvatar(AttachmentUtils.createAttachment(aFile));
} catch (IOException e) {
throw new AttachmentInvalidException(aFile.toString(), e);
}
return SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build())
- .withExpiration(g.messageExpirationTime);
+ .withExpiration(g.getMessageExpirationTime());
+ }
+
+ private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
+ SignalServiceGroupV2.Builder group = SignalServiceGroupV2.newBuilder(g.getMasterKey())
+ .withRevision(g.getGroup().getRevision())
+ .withSignedGroupChange(signedGroupChange);
+ return SignalServiceDataMessage.newBuilder()
+ .asGroupMessage(group.build())
+ .withExpiration(g.getMessageExpirationTime());
}
- Pair<Long, List<SendMessageResult>> sendGroupInfoRequest(byte[] groupId, SignalServiceAddress recipient) throws IOException {
+ Pair<Long, List<SendMessageResult>> sendGroupInfoRequest(
+ GroupIdV1 groupId, SignalServiceAddress recipient
+ ) throws IOException {
SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO)
- .withId(groupId);
+ .withId(groupId.serialize());
SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build());
// Send group info request message to the recipient who sent us a message with this groupId
- return sendMessage(messageBuilder, Collections.singleton(recipient));
+ return sendMessage(messageBuilder, List.of(recipient));
}
- void sendReceipt(SignalServiceAddress remoteAddress, long messageId) throws IOException, UntrustedIdentityException {
+ void sendReceipt(
+ SignalServiceAddress remoteAddress, long messageId
+ ) throws IOException, UntrustedIdentityException {
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
- Collections.singletonList(messageId),
+ List.of(messageId),
System.currentTimeMillis());
- getMessageSender().sendReceipt(remoteAddress, getAccessFor(remoteAddress), receiptMessage);
+ createMessageSender().sendReceipt(remoteAddress,
+ unidentifiedAccessHelper.getAccessFor(remoteAddress),
+ receiptMessage);
}
- public Pair<Long, List<SendMessageResult>> sendMessage(String messageText, List<String> attachments,
- List<String> recipients)
- throws IOException, AttachmentInvalidException, InvalidNumberException {
- final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
+ public Pair<Long, List<SendMessageResult>> sendMessage(
+ String messageText, List<String> attachments, List<String> recipients
+ ) throws IOException, AttachmentInvalidException, InvalidNumberException {
+ final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
+ .withBody(messageText);
if (attachments != null) {
- List<SignalServiceAttachment> attachmentStreams = Utils.getSignalServiceAttachments(attachments);
+ List<SignalServiceAttachment> attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments);
// Upload attachments here, so we only upload once even for multiple recipients
- SignalServiceMessageSender messageSender = getMessageSender();
+ SignalServiceMessageSender messageSender = createMessageSender();
List<SignalServiceAttachment> attachmentPointers = new ArrayList<>(attachmentStreams.size());
for (SignalServiceAttachment attachment : attachmentStreams) {
if (attachment.isStream()) {
return sendMessage(messageBuilder, getSignalServiceAddresses(recipients));
}
- public Pair<Long, List<SendMessageResult>> sendMessageReaction(String emoji, boolean remove, String targetAuthor,
- long targetSentTimestamp, List<String> recipients)
- throws IOException, InvalidNumberException {
- SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, remove, canonicalizeAndResolveSignalServiceAddress(targetAuthor), targetSentTimestamp);
+ public Pair<Long, List<SendMessageResult>> sendMessageReaction(
+ String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List<String> recipients
+ ) throws IOException, InvalidNumberException {
+ SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji,
+ remove,
+ canonicalizeAndResolveSignalServiceAddress(targetAuthor),
+ targetSentTimestamp);
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.withReaction(reaction);
return sendMessage(messageBuilder, getSignalServiceAddresses(recipients));
}
public Pair<Long, List<SendMessageResult>> sendEndSessionMessage(List<String> recipients) throws IOException, InvalidNumberException {
- SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
- .asEndSessionMessage();
+ SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage();
final Collection<SignalServiceAddress> signalServiceAddresses = getSignalServiceAddresses(recipients);
try {
account.save();
}
- public void setGroupBlocked(final byte[] groupId, final boolean blocked) throws GroupNotFoundException {
+ public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException {
GroupInfo group = getGroup(groupId);
if (group == null) {
throw new GroupNotFoundException(groupId);
account.save();
}
- public Pair<byte[], List<SendMessageResult>> updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
- if (groupId.length == 0) {
- groupId = null;
- }
- if (name.isEmpty()) {
- name = null;
- }
- if (members.isEmpty()) {
- members = null;
- }
- if (avatar.isEmpty()) {
- avatar = null;
- }
- return sendUpdateGroupMessage(groupId, name, members == null ? null : getSignalServiceAddresses(members), avatar);
+ public Pair<GroupId, List<SendMessageResult>> updateGroup(
+ GroupId groupId, String name, List<String> members, String avatar
+ ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
+ return sendUpdateGroupMessage(groupId,
+ name,
+ members == null ? null : getSignalServiceAddresses(members),
+ avatar);
}
/**
private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException {
final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
.asExpirationUpdate();
- sendMessage(messageBuilder, Collections.singleton(address));
+ sendMessage(messageBuilder, List.of(address));
}
/**
* Change the expiration timer for a contact
*/
- public void setExpirationTimer(String number, int messageExpirationTimer) throws IOException, InvalidNumberException {
+ public void setExpirationTimer(
+ String number, int messageExpirationTimer
+ ) throws IOException, InvalidNumberException {
SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number);
setExpirationTimer(address, messageExpirationTimer);
}
/**
* Change the expiration timer for a group
*/
- public void setExpirationTimer(byte[] groupId, int messageExpirationTimer) {
+ public void setExpirationTimer(GroupId groupId, int messageExpirationTimer) {
GroupInfo g = account.getGroupStore().getGroup(groupId);
if (g instanceof GroupInfoV1) {
GroupInfoV1 groupInfoV1 = (GroupInfoV1) g;
* @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file
* @return if successful, returns the URL to install the sticker pack in the signal app
*/
- public String uploadStickerPack(String path) throws IOException, StickerPackInvalidException {
+ public String uploadStickerPack(File path) throws IOException, StickerPackInvalidException {
SignalServiceStickerManifestUpload manifest = getSignalServiceStickerManifestUpload(path);
- SignalServiceMessageSender messageSender = getMessageSender();
+ SignalServiceMessageSender messageSender = createMessageSender();
byte[] packKey = KeyUtils.createStickerUploadKey();
String packId = messageSender.uploadStickerManifest(manifest, packKey);
account.save();
try {
- return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder.encode(packId, StandardCharsets.UTF_8) + "&pack_key=" + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8))
- .toString();
+ return new URI("https",
+ "signal.art",
+ "/addstickers/",
+ "pack_id=" + URLEncoder.encode(packId, StandardCharsets.UTF_8) + "&pack_key=" + URLEncoder.encode(
+ Hex.toStringCondensed(packKey),
+ StandardCharsets.UTF_8)).toString();
} catch (URISyntaxException e) {
throw new AssertionError(e);
}
}
- private SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload(final String path) throws IOException, StickerPackInvalidException {
+ private SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload(
+ final File file
+ ) throws IOException, StickerPackInvalidException {
ZipFile zip = null;
String rootPath = null;
- final File file = new File(path);
if (file.getName().endsWith(".zip")) {
zip = new ZipFile(file);
} else if (file.getName().equals("manifest.json")) {
}
String contentType = Utils.getFileMimeType(new File(sticker.file), null);
- StickerInfo stickerInfo = new StickerInfo(data.first(), data.second(), Optional.fromNullable(sticker.emoji).or(""), contentType);
+ StickerInfo stickerInfo = new StickerInfo(data.first(),
+ data.second(),
+ Optional.fromNullable(sticker.emoji).or(""),
+ contentType);
stickers.add(stickerInfo);
}
}
String contentType = Utils.getFileMimeType(new File(pack.cover.file), null);
- cover = new StickerInfo(data.first(), data.second(), Optional.fromNullable(pack.cover.emoji).or(""), contentType);
+ cover = new StickerInfo(data.first(),
+ data.second(),
+ Optional.fromNullable(pack.cover.emoji).or(""),
+ contentType);
}
- return new SignalServiceStickerManifestUpload(
- pack.title,
- pack.author,
- cover,
- stickers);
+ return new SignalServiceStickerManifestUpload(pack.title, pack.author, cover, stickers);
}
private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException {
return new ObjectMapper().readValue(inputStream, JsonStickerPack.class);
}
- private static Pair<InputStream, Long> getInputStreamAndLength(final String rootPath, final ZipFile zip, final String subfile) throws IOException {
+ private static Pair<InputStream, Long> getInputStreamAndLength(
+ final String rootPath, final ZipFile zip, final String subfile
+ ) throws IOException {
if (zip != null) {
final ZipEntry entry = zip.getEntry(subfile);
return new Pair<>(zip.getInputStream(entry), entry.getSize());
}
void requestSyncGroups() throws IOException {
- SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
+ SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder()
+ .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS)
+ .build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try {
sendSyncMessage(message);
}
void requestSyncContacts() throws IOException {
- SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
+ SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder()
+ .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS)
+ .build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try {
sendSyncMessage(message);
}
void requestSyncBlocked() throws IOException {
- SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED).build();
+ SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder()
+ .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED)
+ .build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try {
sendSyncMessage(message);
}
void requestSyncConfiguration() throws IOException {
- SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION).build();
+ SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder()
+ .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION)
+ .build();
SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
try {
sendSyncMessage(message);
try {
certificate = accountManager.getSenderCertificate();
} catch (IOException e) {
- System.err.println("Failed to get sender certificate: " + e);
+ logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
return null;
}
// TODO cache for a day
return certificate;
}
- private byte[] getSelfUnidentifiedAccessKey() {
- return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
- }
-
- private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
- ProfileKey theirProfileKey = account.getProfileStore().getProfileKey(recipient);
- if (theirProfileKey == null) {
- return null;
- }
- SignalProfile targetProfile;
- try {
- targetProfile = getRecipientProfile(recipient, Optional.absent(), theirProfileKey);
- } catch (IOException e) {
- System.err.println("Failed to get recipient profile: " + e);
- return null;
- }
-
- if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) {
- return null;
- }
-
- if (targetProfile.isUnrestrictedUnidentifiedAccess()) {
- return KeyUtils.createUnrestrictedUnidentifiedAccess();
- }
-
- return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
- }
-
- private Optional<UnidentifiedAccessPair> getAccessForSync() {
- byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
- byte[] selfUnidentifiedAccessCertificate = getSenderCertificate();
-
- if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
- return Optional.absent();
- }
-
+ private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException {
+ SignalServiceMessageSender messageSender = createMessageSender();
try {
- return Optional.of(new UnidentifiedAccessPair(
- new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate),
- new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)
- ));
- } catch (InvalidCertificateException e) {
- return Optional.absent();
- }
- }
-
- private List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<SignalServiceAddress> recipients) {
- List<Optional<UnidentifiedAccessPair>> result = new ArrayList<>(recipients.size());
- for (SignalServiceAddress recipient : recipients) {
- result.add(getAccessFor(recipient));
- }
- return result;
- }
-
- private Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress recipient) {
- byte[] recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
- byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
- byte[] selfUnidentifiedAccessCertificate = getSenderCertificate();
-
- if (recipientUnidentifiedAccessKey == null || selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
- return Optional.absent();
- }
-
- try {
- return Optional.of(new UnidentifiedAccessPair(
- new UnidentifiedAccess(recipientUnidentifiedAccessKey, selfUnidentifiedAccessCertificate),
- new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)
- ));
- } catch (InvalidCertificateException e) {
- return Optional.absent();
- }
- }
-
- private Optional<UnidentifiedAccess> getUnidentifiedAccess(SignalServiceAddress recipient) {
- Optional<UnidentifiedAccessPair> unidentifiedAccess = getAccessFor(recipient);
-
- if (unidentifiedAccess.isPresent()) {
- return unidentifiedAccess.get().getTargetUnidentifiedAccess();
- }
-
- return Optional.absent();
- }
-
- private void sendSyncMessage(SignalServiceSyncMessage message)
- throws IOException, UntrustedIdentityException {
- SignalServiceMessageSender messageSender = getMessageSender();
- try {
- messageSender.sendMessage(message, getAccessForSync());
+ messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
} catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ account.getSignalProtocolStore()
+ .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
+ e.getIdentityKey(),
+ TrustLevel.UNTRUSTED);
throw e;
}
}
private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
final Set<SignalServiceAddress> signalServiceAddresses = new HashSet<>(numbers.size());
+ final Set<SignalServiceAddress> missingUuids = new HashSet<>();
for (String number : numbers) {
- signalServiceAddresses.add(canonicalizeAndResolveSignalServiceAddress(number));
+ final SignalServiceAddress resolvedAddress = canonicalizeAndResolveSignalServiceAddress(number);
+ if (resolvedAddress.getUuid().isPresent()) {
+ signalServiceAddresses.add(resolvedAddress);
+ } else {
+ missingUuids.add(resolvedAddress);
+ }
}
+
+ Map<String, UUID> registeredUsers;
+ try {
+ registeredUsers = accountManager.getRegisteredUsers(getIasKeyStore(),
+ missingUuids.stream().map(a -> a.getNumber().get()).collect(Collectors.toSet()),
+ CDS_MRENCLAVE);
+ } catch (IOException | Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException e) {
+ logger.warn("Failed to resolve uuids from server, ignoring: {}", e.getMessage());
+ registeredUsers = new HashMap<>();
+ }
+
+ for (SignalServiceAddress address : missingUuids) {
+ final String number = address.getNumber().get();
+ if (registeredUsers.containsKey(number)) {
+ final SignalServiceAddress newAddress = resolveSignalServiceAddress(new SignalServiceAddress(
+ registeredUsers.get(number),
+ number));
+ signalServiceAddresses.add(newAddress);
+ } else {
+ signalServiceAddresses.add(address);
+ }
+ }
+
return signalServiceAddresses;
}
- private Pair<Long, List<SendMessageResult>> sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients)
- throws IOException {
+ private Pair<Long, List<SendMessageResult>> sendMessage(
+ SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients
+ ) throws IOException {
recipients = recipients.stream().map(this::resolveSignalServiceAddress).collect(Collectors.toSet());
final long timestamp = System.currentTimeMillis();
messageBuilder.withTimestamp(timestamp);
- if (messagePipe == null) {
- messagePipe = getMessageReceiver().createMessagePipe();
- }
- if (unidentifiedMessagePipe == null) {
- unidentifiedMessagePipe = getMessageReceiver().createUnidentifiedMessagePipe();
- }
+ getOrCreateMessagePipe();
+ getOrCreateUnidentifiedMessagePipe();
SignalServiceDataMessage message = null;
try {
message = messageBuilder.build();
if (message.getGroupContext().isPresent()) {
try {
- SignalServiceMessageSender messageSender = getMessageSender();
+ SignalServiceMessageSender messageSender = createMessageSender();
final boolean isRecipientUpdate = false;
- List<SendMessageResult> result = messageSender.sendMessage(new ArrayList<>(recipients), getAccessFor(recipients), isRecipientUpdate, message);
+ List<SendMessageResult> result = messageSender.sendMessage(new ArrayList<>(recipients),
+ unidentifiedAccessHelper.getAccessFor(recipients),
+ isRecipientUpdate,
+ message);
for (SendMessageResult r : result) {
if (r.getIdentityFailure() != null) {
- account.getSignalProtocolStore().saveIdentity(r.getAddress(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED);
+ account.getSignalProtocolStore()
+ .saveIdentity(r.getAddress(),
+ r.getIdentityFailure().getIdentityKey(),
+ TrustLevel.UNTRUSTED);
}
}
return new Pair<>(timestamp, result);
} catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
- return new Pair<>(timestamp, Collections.emptyList());
+ account.getSignalProtocolStore()
+ .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
+ e.getIdentityKey(),
+ TrustLevel.UNTRUSTED);
+ return new Pair<>(timestamp, List.of());
}
} else {
// Send to all individually, so sync messages are sent correctly
}
private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException {
- SignalServiceMessageSender messageSender = getMessageSender();
+ SignalServiceMessageSender messageSender = createMessageSender();
SignalServiceAddress recipient = account.getSelfAddress();
- final Optional<UnidentifiedAccessPair> unidentifiedAccess = getAccessFor(recipient);
+ final Optional<UnidentifiedAccessPair> unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(recipient);
SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient),
message.getTimestamp(),
message,
message.getExpiresInSeconds(),
- Collections.singletonMap(recipient, unidentifiedAccess.isPresent()),
+ Map.of(recipient, unidentifiedAccess.isPresent()),
false);
SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
try {
long startTime = System.currentTimeMillis();
messageSender.sendMessage(syncMessage, unidentifiedAccess);
- return SendMessageResult.success(recipient, unidentifiedAccess.isPresent(), false, System.currentTimeMillis() - startTime);
+ return SendMessageResult.success(recipient,
+ unidentifiedAccess.isPresent(),
+ false,
+ System.currentTimeMillis() - startTime);
} catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ account.getSignalProtocolStore()
+ .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
+ e.getIdentityKey(),
+ TrustLevel.UNTRUSTED);
return SendMessageResult.identityFailure(recipient, e.getIdentityKey());
}
}
- private SendMessageResult sendMessage(SignalServiceAddress address, SignalServiceDataMessage message) throws IOException {
- SignalServiceMessageSender messageSender = getMessageSender();
+ private SendMessageResult sendMessage(
+ SignalServiceAddress address, SignalServiceDataMessage message
+ ) throws IOException {
+ SignalServiceMessageSender messageSender = createMessageSender();
try {
- return messageSender.sendMessage(address, getAccessFor(address), message);
+ return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message);
} catch (UntrustedIdentityException e) {
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), e.getIdentityKey(), TrustLevel.UNTRUSTED);
+ account.getSignalProtocolStore()
+ .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
+ e.getIdentityKey(),
+ TrustLevel.UNTRUSTED);
return SendMessageResult.identityFailure(address, e.getIdentityKey());
}
}
private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, org.whispersystems.libsignal.UntrustedIdentityException {
- SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(), account.getSignalProtocolStore(), Utils.getCertificateValidator());
+ SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(),
+ account.getSignalProtocolStore(),
+ certificateValidator);
try {
return cipher.decrypt(envelope);
} catch (ProtocolUntrustedIdentityException e) {
if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
- org.whispersystems.libsignal.UntrustedIdentityException identityException = (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause();
- account.getSignalProtocolStore().saveIdentity(resolveSignalServiceAddress(identityException.getName()), identityException.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
+ org.whispersystems.libsignal.UntrustedIdentityException identityException = (org.whispersystems.libsignal.UntrustedIdentityException) e
+ .getCause();
+ account.getSignalProtocolStore()
+ .saveIdentity(resolveSignalServiceAddress(identityException.getName()),
+ identityException.getUntrustedIdentity(),
+ TrustLevel.UNTRUSTED);
throw identityException;
}
throw new AssertionError(e);
return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
}
- private GroupsV2AuthorizationString getGroupAuthForToday(final GroupSecretParams groupSecretParams) throws IOException, VerificationFailedException {
+ private GroupsV2AuthorizationString getGroupAuthForToday(
+ final GroupSecretParams groupSecretParams
+ ) throws IOException {
final int today = currentTimeDays();
// Returns credentials for the next 7 days
final HashMap<Integer, AuthCredentialResponse> credentials = groupsV2Api.getCredentials(today);
// TODO cache credentials until they expire
AuthCredentialResponse authCredentialResponse = credentials.get(today);
- return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(), today, groupSecretParams, authCredentialResponse);
- }
-
- private List<HandleAction> handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments) {
+ try {
+ return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
+ today,
+ groupSecretParams,
+ authCredentialResponse);
+ } catch (VerificationFailedException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private List<HandleAction> handleSignalServiceDataMessage(
+ SignalServiceDataMessage message,
+ boolean isSync,
+ SignalServiceAddress source,
+ SignalServiceAddress destination,
+ boolean ignoreAttachments
+ ) {
List<HandleAction> actions = new ArrayList<>();
if (message.getGroupContext().isPresent()) {
if (message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
- GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId());
+ GroupIdV1 groupId = GroupId.v1(groupInfo.getGroupId());
+ GroupInfo group = account.getGroupStore().getGroup(groupId);
if (group == null || group instanceof GroupInfoV1) {
GroupInfoV1 groupV1 = (GroupInfoV1) group;
switch (groupInfo.getType()) {
case UPDATE: {
if (groupV1 == null) {
- groupV1 = new GroupInfoV1(groupInfo.getGroupId());
+ groupV1 = new GroupInfoV1(groupId);
}
if (groupInfo.getAvatar().isPresent()) {
SignalServiceAttachment avatar = groupInfo.getAvatar().get();
if (avatar.isPointer()) {
try {
- retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.groupId);
+ retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.getGroupId());
} catch (IOException | InvalidMessageException | MissingConfigurationException e) {
- System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getRemoteId() + "): " + e.getMessage());
+ logger.warn("Failed to retrieve avatar for group {}, ignoring: {}",
+ groupId.toBase64(),
+ e.getMessage());
}
}
}
}
if (groupInfo.getMembers().isPresent()) {
- groupV1.addMembers(groupInfo.getMembers().get()
+ groupV1.addMembers(groupInfo.getMembers()
+ .get()
.stream()
.map(this::resolveSignalServiceAddress)
.collect(Collectors.toSet()));
}
case DELIVER:
if (groupV1 == null && !isSync) {
- actions.add(new SendGroupInfoRequestAction(source, groupInfo.getGroupId()));
+ actions.add(new SendGroupInfoRequestAction(source, groupId));
}
break;
case QUIT: {
}
case REQUEST_INFO:
if (groupV1 != null && !isSync) {
- actions.add(new SendGroupUpdateAction(source, groupV1.groupId));
+ actions.add(new SendGroupUpdateAction(source, groupV1.getGroupId()));
}
break;
}
} else {
- System.err.println("Received a group v1 message for a v2 group: " + group.getTitle());
+ // Received a group v1 message for a v2 group
}
}
if (message.getGroupContext().get().getGroupV2().isPresent()) {
final SignalServiceGroupV2 groupContext = message.getGroupContext().get().getGroupV2().get();
final GroupMasterKey groupMasterKey = groupContext.getMasterKey();
- final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
-
- byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
- GroupInfo groupInfo = account.getGroupStore().getGroup(groupId);
- if (groupInfo instanceof GroupInfoV1) {
- // TODO upgrade group
- } else if (groupInfo == null || groupInfo instanceof GroupInfoV2) {
- GroupInfoV2 groupInfoV2 = groupInfo == null
- ? new GroupInfoV2(groupId, groupMasterKey)
- : (GroupInfoV2) groupInfo;
-
- if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < groupContext.getRevision()) {
- // TODO check if revision is only 1 behind and a signedGroupChange is available
- try {
- final GroupsV2AuthorizationString groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
- final DecryptedGroup group = groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
- groupInfoV2.setGroup(group);
- for (DecryptedMember member : group.getMembersList()) {
- final SignalServiceAddress address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(member.getUuid().toByteArray()), null));
- try {
- account.getProfileStore().storeProfileKey(address, new ProfileKey(member.getProfileKey().toByteArray()));
- } catch (InvalidInputException ignored) {
- }
- }
- } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
- System.err.println("Failed to retrieve Group V2 info, ignoring ...");
- }
- account.getGroupStore().updateGroup(groupInfoV2);
- }
- }
+ getOrMigrateGroup(groupMasterKey,
+ groupContext.getRevision(),
+ groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null);
}
}
+
final SignalServiceAddress conversationPartnerAddress = isSync ? destination : source;
- if (message.isEndSession()) {
+ if (conversationPartnerAddress != null && message.isEndSession()) {
handleEndSession(conversationPartnerAddress);
}
if (message.isExpirationUpdate() || message.getBody().isPresent()) {
if (message.getGroupContext().isPresent()) {
if (message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
- GroupInfoV1 group = account.getGroupStore().getOrCreateGroupV1(groupInfo.getGroupId());
+ GroupInfoV1 group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId()));
if (group != null) {
if (group.messageExpirationTime != message.getExpiresInSeconds()) {
group.messageExpirationTime = message.getExpiresInSeconds();
} else if (message.getGroupContext().get().getGroupV2().isPresent()) {
// disappearing message timer already stored in the DecryptedGroup
}
- } else {
+ } else if (conversationPartnerAddress != null) {
ContactInfo contact = account.getContactStore().getContact(conversationPartnerAddress);
if (contact == null) {
contact = new ContactInfo(conversationPartnerAddress);
try {
retrieveAttachment(attachment.asPointer());
} catch (IOException | InvalidMessageException | MissingConfigurationException e) {
- System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getRemoteId() + "): " + e.getMessage());
+ logger.warn("Failed to retrieve attachment ({}), ignoring: {}",
+ attachment.asPointer().getRemoteId(),
+ e.getMessage());
}
}
}
try {
retrieveAttachment(attachment);
} catch (IOException | InvalidMessageException | MissingConfigurationException e) {
- System.err.println("Failed to retrieve attachment (" + attachment.getRemoteId() + "): " + e.getMessage());
+ logger.warn("Failed to retrieve preview image ({}), ignoring: {}",
+ attachment.getRemoteId(),
+ e.getMessage());
+ }
+ }
+ }
+ }
+ if (message.getQuote().isPresent()) {
+ final SignalServiceDataMessage.Quote quote = message.getQuote().get();
+
+ for (SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment : quote.getAttachments()) {
+ final SignalServiceAttachment attachment = quotedAttachment.getThumbnail();
+ if (attachment != null && attachment.isPointer()) {
+ try {
+ retrieveAttachment(attachment.asPointer());
+ } catch (IOException | InvalidMessageException | MissingConfigurationException e) {
+ logger.warn("Failed to retrieve quote attachment thumbnail ({}), ignoring: {}",
+ attachment.asPointer().getRemoteId(),
+ e.getMessage());
}
}
}
return actions;
}
- private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
- final File cachePath = new File(getMessageCachePath());
+ private GroupInfoV2 getOrMigrateGroup(
+ final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange
+ ) {
+ final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
+
+ GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams);
+ GroupInfo groupInfo = account.getGroupStore().getGroup(groupId);
+ final GroupInfoV2 groupInfoV2;
+ if (groupInfo instanceof GroupInfoV1) {
+ // Received a v2 group message for a v1 group, we need to locally migrate the group
+ account.getGroupStore().deleteGroup(groupInfo.getGroupId());
+ groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
+ logger.info("Locally migrated group {} to group v2, id: {}",
+ groupInfo.getGroupId().toBase64(),
+ groupInfoV2.getGroupId().toBase64());
+ } else if (groupInfo instanceof GroupInfoV2) {
+ groupInfoV2 = (GroupInfoV2) groupInfo;
+ } else {
+ groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
+ }
+
+ if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) {
+ DecryptedGroup group = null;
+ if (signedGroupChange != null
+ && groupInfoV2.getGroup() != null
+ && groupInfoV2.getGroup().getRevision() + 1 == revision) {
+ group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), signedGroupChange, groupMasterKey);
+ }
+ if (group == null) {
+ group = groupHelper.getDecryptedGroup(groupSecretParams);
+ }
+ if (group != null) {
+ storeProfileKeysFromMembers(group);
+ final String avatar = group.getAvatar();
+ if (avatar != null && !avatar.isEmpty()) {
+ try {
+ retrieveGroupAvatar(groupId, groupSecretParams, avatar);
+ } catch (IOException e) {
+ logger.warn("Failed to download group avatar, ignoring: {}", e.getMessage());
+ }
+ }
+ }
+ groupInfoV2.setGroup(group);
+ account.getGroupStore().updateGroup(groupInfoV2);
+ }
+
+ return groupInfoV2;
+ }
+
+ private void storeProfileKeysFromMembers(final DecryptedGroup group) {
+ for (DecryptedMember member : group.getMembersList()) {
+ final SignalServiceAddress address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(
+ member.getUuid().toByteArray()), null));
+ try {
+ account.getProfileStore()
+ .storeProfileKey(address, new ProfileKey(member.getProfileKey().toByteArray()));
+ } catch (InvalidInputException ignored) {
+ }
+ }
+ }
+
+ private void retryFailedReceivedMessages(
+ ReceiveMessageHandler handler, boolean ignoreAttachments
+ ) {
+ final File cachePath = getMessageCachePath();
if (!cachePath.exists()) {
return;
}
}
}
- private void retryFailedReceivedMessage(final ReceiveMessageHandler handler, final boolean ignoreAttachments, final File fileEntry) {
+ private void retryFailedReceivedMessage(
+ final ReceiveMessageHandler handler, final boolean ignoreAttachments, final File fileEntry
+ ) {
SignalServiceEnvelope envelope;
try {
- envelope = Utils.loadEnvelope(fileEntry);
+ envelope = MessageCacheUtils.loadEnvelope(fileEntry);
if (envelope == null) {
return;
}
try {
Files.delete(fileEntry.toPath());
} catch (IOException e) {
- System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage());
+ logger.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry, e.getMessage());
}
return;
}
try {
Files.delete(fileEntry.toPath());
} catch (IOException e) {
- System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage());
+ logger.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry, e.getMessage());
}
}
- public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException {
+ public void receiveMessages(
+ long timeout,
+ TimeUnit unit,
+ boolean returnOnTimeout,
+ boolean ignoreAttachments,
+ ReceiveMessageHandler handler
+ ) throws IOException {
retryFailedReceivedMessages(handler, ignoreAttachments);
- final SignalServiceMessageReceiver messageReceiver = getMessageReceiver();
Set<HandleAction> queuedActions = null;
- if (messagePipe == null) {
- messagePipe = messageReceiver.createMessagePipe();
- }
+ getOrCreateMessagePipe();
boolean hasCaughtUpWithOldMessages = false;
try {
String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : "";
File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp());
- Utils.storeEnvelope(envelope1, cacheFile);
+ MessageCacheUtils.storeEnvelope(envelope1, cacheFile);
} catch (IOException e) {
- System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage());
+ logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage());
}
});
if (result.isPresent()) {
continue;
}
} catch (TimeoutException e) {
- if (returnOnTimeout)
- return;
+ if (returnOnTimeout) return;
continue;
} catch (InvalidVersionException e) {
- System.err.println("Ignoring error: " + e.getMessage());
+ logger.warn("Error while receiving messages, ignoring: {}", e.getMessage());
continue;
}
cacheFile = getMessageCacheFile(source, now, envelope.getTimestamp());
Files.delete(cacheFile.toPath());
// Try to delete directory if empty
- new File(getMessageCachePath()).delete();
+ getMessageCachePath().delete();
} catch (IOException e) {
- System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage());
+ logger.warn("Failed to delete cached message file “{}”, ignoring: {}", cacheFile, e.getMessage());
}
}
}
}
- private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) {
+ private boolean isMessageBlocked(
+ SignalServiceEnvelope envelope, SignalServiceContent content
+ ) {
SignalServiceAddress source;
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
source = envelope.getSourceAddress();
if (content != null && content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
- if (message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1().isPresent()) {
- SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
- GroupInfo group = getGroup(groupInfo.getGroupId());
- return groupInfo.getType() == SignalServiceGroup.Type.DELIVER && group != null && group.isBlocked();
+ if (message.getGroupContext().isPresent()) {
+ if (message.getGroupContext().get().getGroupV1().isPresent()) {
+ SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
+ if (groupInfo.getType() != SignalServiceGroup.Type.DELIVER) {
+ return false;
+ }
+ }
+ GroupId groupId = GroupUtils.getGroupId(message.getGroupContext().get());
+ GroupInfo group = account.getGroupStore().getGroup(groupId);
+ if (group != null && group.isBlocked()) {
+ return true;
+ }
}
}
return false;
}
- private List<HandleAction> handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
+ private List<HandleAction> handleMessage(
+ SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments
+ ) {
List<HandleAction> actions = new ArrayList<>();
if (content != null) {
- SignalServiceAddress sender;
+ final SignalServiceAddress sender;
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
sender = envelope.getSourceAddress();
} else {
actions.add(new SendReceiptAction(sender, message.getTimestamp()));
}
- actions.addAll(handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments));
+ actions.addAll(handleSignalServiceDataMessage(message,
+ false,
+ sender,
+ account.getSelfAddress(),
+ ignoreAttachments));
}
if (content.getSyncMessage().isPresent()) {
account.setMultiDevice(true);
SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
if (syncMessage.getSent().isPresent()) {
SentTranscriptMessage message = syncMessage.getSent().get();
- actions.addAll(handleSignalServiceDataMessage(message.getMessage(), true, sender, message.getDestination().orNull(), ignoreAttachments));
+ final SignalServiceAddress destination = message.getDestination().orNull();
+ actions.addAll(handleSignalServiceDataMessage(message.getMessage(),
+ true,
+ sender,
+ destination,
+ ignoreAttachments));
}
if (syncMessage.getRequest().isPresent()) {
RequestMessage rm = syncMessage.getRequest().get();
File tmpFile = null;
try {
tmpFile = IOUtils.createTempFile();
- try (InputStream attachmentAsStream = retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile)) {
+ try (InputStream attachmentAsStream = retrieveAttachmentAsStream(syncMessage.getGroups()
+ .get()
+ .asPointer(), tmpFile)) {
DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream);
DeviceGroup g;
while ((g = s.read()) != null) {
- GroupInfoV1 syncGroup = account.getGroupStore().getOrCreateGroupV1(g.getId());
+ GroupInfoV1 syncGroup = account.getGroupStore()
+ .getOrCreateGroupV1(GroupId.v1(g.getId()));
if (syncGroup != null) {
if (g.getName().isPresent()) {
syncGroup.name = g.getName().get();
syncGroup.removeMember(account.getSelfAddress());
} else {
// Add ourself to the member set as it's marked as active
- syncGroup.addMembers(Collections.singleton(account.getSelfAddress()));
+ syncGroup.addMembers(List.of(account.getSelfAddress()));
}
syncGroup.blocked = g.isBlocked();
if (g.getColor().isPresent()) {
}
if (g.getAvatar().isPresent()) {
- retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
+ retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.getGroupId());
}
syncGroup.inboxPosition = g.getInboxPosition().orNull();
syncGroup.archived = g.isArchived();
}
}
} catch (Exception e) {
+ logger.warn("Failed to handle received sync groups “{}”, ignoring: {}",
+ tmpFile,
+ e.getMessage());
e.printStackTrace();
} finally {
if (tmpFile != null) {
try {
Files.delete(tmpFile.toPath());
} catch (IOException e) {
- System.err.println("Failed to delete received groups temp file “" + tmpFile + "”: " + e.getMessage());
+ logger.warn("Failed to delete received groups temp file “{}”, ignoring: {}",
+ tmpFile,
+ e.getMessage());
}
}
}
for (SignalServiceAddress address : blockedListMessage.getAddresses()) {
setContactBlocked(resolveSignalServiceAddress(address), true);
}
- for (byte[] groupId : blockedListMessage.getGroupIds()) {
+ for (GroupId groupId : blockedListMessage.getGroupIds()
+ .stream()
+ .map(GroupId::unknownVersion)
+ .collect(Collectors.toSet())) {
try {
setGroupBlocked(groupId, true);
} catch (GroupNotFoundException e) {
- System.err.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64.encodeBytes(groupId));
+ logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}",
+ groupId.toBase64());
}
}
}
try {
tmpFile = IOUtils.createTempFile();
final ContactsMessage contactsMessage = syncMessage.getContacts().get();
- try (InputStream attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream().asPointer(), tmpFile)) {
+ try (InputStream attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream()
+ .asPointer(), tmpFile)) {
DeviceContactsInputStream s = new DeviceContactsInputStream(attachmentAsStream);
if (contactsMessage.isComplete()) {
account.getContactStore().clear();
}
if (c.getVerified().isPresent()) {
final VerifiedMessage verifiedMessage = c.getVerified().get();
- account.getSignalProtocolStore().setIdentityTrustLevel(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
+ account.getSignalProtocolStore()
+ .setIdentityTrustLevel(verifiedMessage.getDestination(),
+ verifiedMessage.getIdentityKey(),
+ TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
if (c.getExpirationTimer().isPresent()) {
contact.messageExpirationTime = c.getExpirationTimer().get();
try {
Files.delete(tmpFile.toPath());
} catch (IOException e) {
- System.err.println("Failed to delete received contacts temp file “" + tmpFile + "”: " + e.getMessage());
+ logger.warn("Failed to delete received contacts temp file “{}”, ignoring: {}",
+ tmpFile,
+ e.getMessage());
}
}
}
}
if (syncMessage.getVerified().isPresent()) {
final VerifiedMessage verifiedMessage = syncMessage.getVerified().get();
- account.getSignalProtocolStore().setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage.getDestination()), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
+ account.getSignalProtocolStore()
+ .setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage.getDestination()),
+ verifiedMessage.getIdentityKey(),
+ TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
if (syncMessage.getStickerPackOperations().isPresent()) {
- final List<StickerPackOperationMessage> stickerPackOperationMessages = syncMessage.getStickerPackOperations().get();
+ final List<StickerPackOperationMessage> stickerPackOperationMessages = syncMessage.getStickerPackOperations()
+ .get();
for (StickerPackOperationMessage m : stickerPackOperationMessages) {
if (!m.getPackId().isPresent()) {
continue;
}
sticker = new Sticker(m.getPackId().get(), m.getPackKey().get());
}
- sticker.setInstalled(!m.getType().isPresent() || m.getType().get() == StickerPackOperationMessage.Type.INSTALL);
+ sticker.setInstalled(!m.getType().isPresent()
+ || m.getType().get() == StickerPackOperationMessage.Type.INSTALL);
account.getStickerStore().updateSticker(sticker);
}
}
return new File(pathConfig.getAvatarsPath(), "contact-" + number);
}
- private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException, MissingConfigurationException {
+ private File retrieveContactAvatarAttachment(
+ SignalServiceAttachment attachment, String number
+ ) throws IOException, InvalidMessageException, MissingConfigurationException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
if (attachment.isPointer()) {
SignalServiceAttachmentPointer pointer = attachment.asPointer();
return retrieveAttachment(pointer, getContactAvatarFile(number), false);
} else {
SignalServiceAttachmentStream stream = attachment.asStream();
- return Utils.retrieveAttachment(stream, getContactAvatarFile(number));
+ return AttachmentUtils.retrieveAttachment(stream, getContactAvatarFile(number));
}
}
- private File getGroupAvatarFile(byte[] groupId) {
- return new File(pathConfig.getAvatarsPath(), "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
+ private File getGroupAvatarFile(GroupId groupId) {
+ return new File(pathConfig.getAvatarsPath(), "group-" + groupId.toBase64().replace("/", "_"));
}
- private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException, MissingConfigurationException {
+ private File retrieveGroupAvatarAttachment(
+ SignalServiceAttachment attachment, GroupId groupId
+ ) throws IOException, InvalidMessageException, MissingConfigurationException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
if (attachment.isPointer()) {
SignalServiceAttachmentPointer pointer = attachment.asPointer();
return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
} else {
SignalServiceAttachmentStream stream = attachment.asStream();
- return Utils.retrieveAttachment(stream, getGroupAvatarFile(groupId));
+ return AttachmentUtils.retrieveAttachment(stream, getGroupAvatarFile(groupId));
}
}
+ private File retrieveGroupAvatar(
+ GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey
+ ) throws IOException {
+ IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
+ SignalServiceMessageReceiver receiver = getOrCreateMessageReceiver();
+ File outputFile = getGroupAvatarFile(groupId);
+ GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams);
+
+ File tmpFile = IOUtils.createTempFile();
+ tmpFile.deleteOnExit();
+ try (InputStream input = receiver.retrieveGroupsV2ProfileAvatar(cdnKey,
+ tmpFile,
+ ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
+ byte[] encryptedData = IOUtils.readFully(input);
+
+ byte[] decryptedData = groupOperations.decryptAvatar(encryptedData);
+ try (OutputStream output = new FileOutputStream(outputFile)) {
+ output.write(decryptedData);
+ }
+ } finally {
+ try {
+ Files.delete(tmpFile.toPath());
+ } catch (IOException e) {
+ logger.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}",
+ tmpFile,
+ e.getMessage());
+ }
+ }
+ return outputFile;
+ }
+
private File getProfileAvatarFile(SignalServiceAddress address) {
return new File(pathConfig.getAvatarsPath(), "profile-" + address.getLegacyIdentifier());
}
- private File retrieveProfileAvatar(SignalServiceAddress address, String avatarPath, ProfileKey profileKey) throws IOException {
+ private File retrieveProfileAvatar(
+ SignalServiceAddress address, String avatarPath, ProfileKey profileKey
+ ) throws IOException {
IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
- SignalServiceMessageReceiver receiver = getMessageReceiver();
+ SignalServiceMessageReceiver receiver = getOrCreateMessageReceiver();
File outputFile = getProfileAvatarFile(address);
File tmpFile = IOUtils.createTempFile();
- try (InputStream input = receiver.retrieveProfileAvatar(avatarPath, tmpFile, profileKey, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
+ try (InputStream input = receiver.retrieveProfileAvatar(avatarPath,
+ tmpFile,
+ profileKey,
+ ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
// Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
IOUtils.copyStreamToFile(input, outputFile, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
} finally {
try {
Files.delete(tmpFile.toPath());
} catch (IOException e) {
- System.err.println("Failed to delete received avatar temp file “" + tmpFile + "”: " + e.getMessage());
+ logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}",
+ tmpFile,
+ e.getMessage());
}
}
return outputFile;
return retrieveAttachment(pointer, getAttachmentFile(pointer.getRemoteId()), true);
}
- private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException, MissingConfigurationException {
+ private File retrieveAttachment(
+ SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview
+ ) throws IOException, InvalidMessageException, MissingConfigurationException {
if (storePreview && pointer.getPreview().isPresent()) {
File previewFile = new File(outputFile + ".preview");
try (OutputStream output = new FileOutputStream(previewFile)) {
}
}
- final SignalServiceMessageReceiver messageReceiver = getMessageReceiver();
+ final SignalServiceMessageReceiver messageReceiver = getOrCreateMessageReceiver();
File tmpFile = IOUtils.createTempFile();
- try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE)) {
+ try (InputStream input = messageReceiver.retrieveAttachment(pointer,
+ tmpFile,
+ ServiceConfig.MAX_ATTACHMENT_SIZE)) {
IOUtils.copyStreamToFile(input, outputFile);
} finally {
try {
Files.delete(tmpFile.toPath());
} catch (IOException e) {
- System.err.println("Failed to delete received attachment temp file “" + tmpFile + "”: " + e.getMessage());
+ logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}",
+ tmpFile,
+ e.getMessage());
}
}
return outputFile;
}
- private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException, MissingConfigurationException {
- final SignalServiceMessageReceiver messageReceiver = getMessageReceiver();
+ private InputStream retrieveAttachmentAsStream(
+ SignalServiceAttachmentPointer pointer, File tmpFile
+ ) throws IOException, InvalidMessageException, MissingConfigurationException {
+ final SignalServiceMessageReceiver messageReceiver = getOrCreateMessageReceiver();
return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
}
for (GroupInfo record : account.getGroupStore().getGroups()) {
if (record instanceof GroupInfoV1) {
GroupInfoV1 groupInfo = (GroupInfoV1) record;
- out.write(new DeviceGroup(groupInfo.groupId, Optional.fromNullable(groupInfo.name),
- new ArrayList<>(groupInfo.getMembers()), createGroupAvatarAttachment(groupInfo.groupId),
- groupInfo.isMember(account.getSelfAddress()), Optional.of(groupInfo.messageExpirationTime),
- Optional.fromNullable(groupInfo.color), groupInfo.blocked, Optional.fromNullable(groupInfo.inboxPosition), groupInfo.archived));
+ out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
+ Optional.fromNullable(groupInfo.name),
+ new ArrayList<>(groupInfo.getMembers()),
+ createGroupAvatarAttachment(groupInfo.getGroupId()),
+ groupInfo.isMember(account.getSelfAddress()),
+ Optional.of(groupInfo.messageExpirationTime),
+ Optional.fromNullable(groupInfo.color),
+ groupInfo.blocked,
+ Optional.fromNullable(groupInfo.inboxPosition),
+ groupInfo.archived));
}
}
}
try {
Files.delete(groupsFile.toPath());
} catch (IOException e) {
- System.err.println("Failed to delete groups temp file “" + groupsFile + "”: " + e.getMessage());
+ logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage());
}
}
}
DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos);
for (ContactInfo record : account.getContactStore().getContacts()) {
VerifiedMessage verifiedMessage = null;
- JsonIdentityKeyStore.Identity currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress());
+ IdentityInfo currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress());
if (currentIdentity != null) {
- verifiedMessage = new VerifiedMessage(record.getAddress(), currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime());
+ verifiedMessage = new VerifiedMessage(record.getAddress(),
+ currentIdentity.getIdentityKey(),
+ currentIdentity.getTrustLevel().toVerifiedState(),
+ currentIdentity.getDateAdded().getTime());
}
ProfileKey profileKey = account.getProfileStore().getProfileKey(record.getAddress());
- out.write(new DeviceContact(record.getAddress(), Optional.fromNullable(record.name),
- createContactAvatarAttachment(record.number), Optional.fromNullable(record.color),
- Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), record.blocked,
+ out.write(new DeviceContact(record.getAddress(),
+ Optional.fromNullable(record.name),
+ createContactAvatarAttachment(record.number),
+ Optional.fromNullable(record.color),
+ Optional.fromNullable(verifiedMessage),
+ Optional.fromNullable(profileKey),
+ record.blocked,
Optional.of(record.messageExpirationTime),
- Optional.fromNullable(record.inboxPosition), record.archived));
+ Optional.fromNullable(record.inboxPosition),
+ record.archived));
}
if (account.getProfileKey() != null) {
// Send our own profile key as well
out.write(new DeviceContact(account.getSelfAddress(),
- Optional.absent(), Optional.absent(),
- Optional.absent(), Optional.absent(),
+ Optional.absent(),
+ Optional.absent(),
+ Optional.absent(),
+ Optional.absent(),
Optional.of(account.getProfileKey()),
- false, Optional.absent(), Optional.absent(), false));
+ false,
+ Optional.absent(),
+ Optional.absent(),
+ false));
}
}
try {
Files.delete(contactsFile.toPath());
} catch (IOException e) {
- System.err.println("Failed to delete contacts temp file “" + contactsFile + "”: " + e.getMessage());
+ logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage());
}
}
}
List<byte[]> groupIds = new ArrayList<>();
for (GroupInfo record : account.getGroupStore().getGroups()) {
if (record.isBlocked()) {
- groupIds.add(record.groupId);
+ groupIds.add(record.getGroupId().serialize());
}
}
sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
}
- private void sendVerifiedMessage(SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel) throws IOException, UntrustedIdentityException {
- VerifiedMessage verifiedMessage = new VerifiedMessage(destination, identityKey, trustLevel.toVerifiedState(), System.currentTimeMillis());
+ private void sendVerifiedMessage(
+ SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
+ ) throws IOException, UntrustedIdentityException {
+ VerifiedMessage verifiedMessage = new VerifiedMessage(destination,
+ identityKey,
+ trustLevel.toVerifiedState(),
+ System.currentTimeMillis());
sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
}
}
public ContactInfo getContact(String number) {
- return account.getContactStore().getContact(Util.getSignalServiceAddressFromIdentifier(number));
+ return account.getContactStore().getContact(Utils.getSignalServiceAddressFromIdentifier(number));
}
- public GroupInfo getGroup(byte[] groupId) {
+ public GroupInfo getGroup(GroupId groupId) {
return account.getGroupStore().getGroup(groupId);
}
- public byte[] getGroupId(GroupMasterKey groupMasterKey) {
- final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
- return groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
- }
-
- public List<JsonIdentityKeyStore.Identity> getIdentities() {
+ public List<IdentityInfo> getIdentities() {
return account.getSignalProtocolStore().getIdentities();
}
- public List<JsonIdentityKeyStore.Identity> getIdentities(String number) throws InvalidNumberException {
+ public List<IdentityInfo> getIdentities(String number) throws InvalidNumberException {
return account.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number));
}
*/
public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException {
SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name);
- List<JsonIdentityKeyStore.Identity> ids = account.getSignalProtocolStore().getIdentities(address);
+ List<IdentityInfo> ids = account.getSignalProtocolStore().getIdentities(address);
if (ids == null) {
return false;
}
- for (JsonIdentityKeyStore.Identity id : ids) {
+ for (IdentityInfo id : ids) {
if (!Arrays.equals(id.getIdentityKey().serialize(), fingerprint)) {
continue;
}
- account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+ account.getSignalProtocolStore()
+ .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
try {
sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
} catch (IOException | UntrustedIdentityException e) {
*/
public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException {
SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name);
- List<JsonIdentityKeyStore.Identity> ids = account.getSignalProtocolStore().getIdentities(address);
+ List<IdentityInfo> ids = account.getSignalProtocolStore().getIdentities(address);
if (ids == null) {
return false;
}
- for (JsonIdentityKeyStore.Identity id : ids) {
+ for (IdentityInfo id : ids) {
if (!safetyNumber.equals(computeSafetyNumber(address, id.getIdentityKey()))) {
continue;
}
- account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+ account.getSignalProtocolStore()
+ .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
try {
sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
} catch (IOException | UntrustedIdentityException e) {
*/
public boolean trustIdentityAllKeys(String name) {
SignalServiceAddress address = resolveSignalServiceAddress(name);
- List<JsonIdentityKeyStore.Identity> ids = account.getSignalProtocolStore().getIdentities(address);
+ List<IdentityInfo> ids = account.getSignalProtocolStore().getIdentities(address);
if (ids == null) {
return false;
}
- for (JsonIdentityKeyStore.Identity id : ids) {
+ for (IdentityInfo id : ids) {
if (id.getTrustLevel() == TrustLevel.UNTRUSTED) {
- account.getSignalProtocolStore().setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
+ account.getSignalProtocolStore()
+ .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
try {
sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
} catch (IOException | UntrustedIdentityException e) {
return true;
}
- public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) {
- return Utils.computeSafetyNumber(account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey);
+ public String computeSafetyNumber(
+ SignalServiceAddress theirAddress, IdentityKey theirIdentityKey
+ ) {
+ return Utils.computeSafetyNumber(ServiceConfig.capabilities.isUuid(),
+ account.getSelfAddress(),
+ getIdentityKeyPair().getPublicKey(),
+ theirAddress,
+ theirIdentityKey);
}
void saveAccount() {
}
public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException {
- String canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier : Util.canonicalizeNumber(identifier, account.getUsername());
+ String canonicalizedNumber = UuidUtil.isUuid(identifier)
+ ? identifier
+ : PhoneNumberFormatter.formatNumber(identifier, account.getUsername());
return resolveSignalServiceAddress(canonicalizedNumber);
}
public SignalServiceAddress resolveSignalServiceAddress(String identifier) {
- SignalServiceAddress address = Util.getSignalServiceAddressFromIdentifier(identifier);
+ SignalServiceAddress address = Utils.getSignalServiceAddressFromIdentifier(identifier);
return resolveSignalServiceAddress(address);
}