/*
- Copyright (C) 2015-2020 AsamK and contributors
+ Copyright (C) 2015-2021 AsamK and contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
*/
package org.asamk.signal.manager;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
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.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.groups.GroupInfo;
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
+import org.asamk.signal.manager.storage.messageCache.CachedMessage;
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.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.ProfileUtils;
+import org.asamk.signal.manager.util.StickerUtils;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
import org.signal.libsignal.metadata.InvalidMetadataVersionException;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.InvalidVersionException;
-import org.whispersystems.libsignal.ecc.Curve;
-import org.whispersystems.libsignal.ecc.ECKeyPair;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
-import org.whispersystems.libsignal.util.KeyHelper;
-import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
-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.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
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.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.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload;
-import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload.StickerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
+import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
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.storage.StorageKey;
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.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
-import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Hex;
-import org.whispersystems.util.Base64;
+import org.whispersystems.signalservice.internal.util.Util;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
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.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
-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;
public class Manager implements Closeable {
- final static Logger logger = LoggerFactory.getLogger(Manager.class);
+ private 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;
- 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 SignalAccount account;
+ private final SignalServiceAccountManager accountManager;
+ private final GroupsV2Api groupsV2Api;
private final GroupsV2Operations groupsV2Operations;
+ private final SignalServiceMessageReceiver messageReceiver;
+ private final ClientZkProfileOperations clientZkProfileOperations;
- private SignalServiceMessageReceiver messageReceiver = null;
private SignalServiceMessagePipe messagePipe = null;
private SignalServiceMessagePipe unidentifiedMessagePipe = null;
private final UnidentifiedAccessHelper unidentifiedAccessHelper;
private final ProfileHelper profileHelper;
private final GroupHelper groupHelper;
+ private final PinHelper pinHelper;
+ private final AvatarStore avatarStore;
+ private final AttachmentStore attachmentStore;
- public Manager(
+ Manager(
SignalAccount account,
PathConfig pathConfig,
SignalServiceConfiguration serviceConfiguration,
String userAgent
) {
this.account = account;
- this.pathConfig = pathConfig;
this.serviceConfiguration = serviceConfiguration;
this.userAgent = userAgent;
this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create(
serviceConfiguration)) : null;
- this.accountManager = createSignalServiceAccountManager();
+ final SleepTimer timer = new UptimeSleepTimer();
+ this.accountManager = new SignalServiceAccountManager(serviceConfiguration,
+ new DynamicCredentialsProvider(account.getUuid(),
+ account.getUsername(),
+ account.getPassword(),
+ account.getSignalingKey(),
+ account.getDeviceId()),
+ userAgent,
+ groupsV2Operations,
+ ServiceConfig.AUTOMATIC_NETWORK_RETRY,
+ timer);
this.groupsV2Api = accountManager.getGroupsV2Api();
+ final KeyBackupService keyBackupService = ServiceConfig.createKeyBackupService(accountManager);
+ this.pinHelper = new PinHelper(keyBackupService);
+ this.clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create(serviceConfiguration)
+ .getProfileOperations() : null;
+ this.messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration,
+ account.getUuid(),
+ account.getUsername(),
+ account.getPassword(),
+ account.getDeviceId(),
+ account.getSignalingKey(),
+ userAgent,
+ null,
+ timer,
+ clientZkProfileOperations,
+ ServiceConfig.AUTOMATIC_NETWORK_RETRY);
this.account.setResolver(this::resolveSignalServiceAddress);
this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
unidentifiedAccessHelper::getAccessFor,
unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(),
- this::getOrCreateMessageReceiver);
+ () -> messageReceiver);
this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
this::getRecipientProfile,
account::getSelfAddress,
groupsV2Operations,
groupsV2Api,
this::getGroupAuthForToday);
+ this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
+ this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
}
public String getUsername() {
return account.getSelfAddress();
}
- private SignalServiceAccountManager createSignalServiceAccountManager() {
- return new SignalServiceAccountManager(serviceConfiguration,
- new DynamicCredentialsProvider(account.getUuid(),
- account.getUsername(),
- account.getPassword(),
- null,
- account.getDeviceId()),
- userAgent,
- groupsV2Operations,
- timer);
- }
-
private IdentityKeyPair getIdentityKeyPair() {
return account.getSignalProtocolStore().getIdentityKeyPair();
}
return account.getDeviceId();
}
- private File getMessageCachePath() {
- return SignalAccount.getMessageCachePath(pathConfig.getDataPath(), account.getUsername());
- }
-
- private File getMessageCachePath(String sender) {
- if (sender == null || sender.isEmpty()) {
- return getMessageCachePath();
- }
-
- return new File(getMessageCachePath(), sender.replace("/", "_"));
- }
-
- private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException {
- File cachePath = getMessageCachePath(sender);
- IOUtils.createPrivateDirectories(cachePath);
- return new File(cachePath, now + "_" + timestamp);
- }
-
public static Manager init(
String username, File settingsPath, SignalServiceConfiguration serviceConfiguration, String userAgent
- ) throws IOException {
+ ) throws IOException, NotRegisteredException {
PathConfig pathConfig = PathConfig.createDefault(settingsPath);
if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) {
- IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
- int registrationId = KeyHelper.generateRegistrationId(false);
-
- ProfileKey profileKey = KeyUtils.createProfileKey();
- SignalAccount account = SignalAccount.create(pathConfig.getDataPath(),
- username,
- identityKey,
- registrationId,
- profileKey);
- account.save();
-
- return new Manager(account, pathConfig, serviceConfiguration, userAgent);
+ throw new NotRegisteredException();
}
SignalAccount account = SignalAccount.load(pathConfig.getDataPath(), username);
- Manager m = new Manager(account, pathConfig, serviceConfiguration, userAgent);
-
- m.migrateLegacyConfigs();
+ if (!account.isRegistered()) {
+ throw new NotRegisteredException();
+ }
- return m;
+ return new Manager(account, pathConfig, serviceConfiguration, userAgent);
}
- private void migrateLegacyConfigs() {
- if (account.getProfileKey() == null && isRegistered()) {
- // Old config file, creating new profile key
- account.setProfileKey(KeyUtils.createProfileKey());
- account.save();
- }
- // Store profile keys only in profile store
- for (ContactInfo contact : account.getContactStore().getContacts()) {
- String profileKeyString = contact.profileKey;
- if (profileKeyString == null) {
- continue;
- }
- final ProfileKey profileKey;
- try {
- profileKey = new ProfileKey(Base64.decode(profileKeyString));
- } catch (InvalidInputException | IOException e) {
- continue;
- }
- contact.profileKey = null;
- account.getProfileStore().storeProfileKey(contact.getAddress(), profileKey);
+ public static List<String> getAllLocalUsernames(File settingsPath) {
+ PathConfig pathConfig = PathConfig.createDefault(settingsPath);
+ final File dataPath = pathConfig.getDataPath();
+ final File[] files = dataPath.listFiles();
+
+ if (files == null) {
+ return List.of();
}
- // Ensure our profile key is stored in profile store
- account.getProfileStore().storeProfileKey(getSelfAddress(), account.getProfileKey());
+
+ return Arrays.stream(files)
+ .filter(File::isFile)
+ .map(File::getName)
+ .filter(file -> PhoneNumberFormatter.isValidNumber(file, null))
+ .collect(Collectors.toList());
}
public void checkAccountState() throws IOException {
- if (account.isRegistered()) {
- if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) {
- refreshPreKeys();
- account.save();
- }
- if (account.getUuid() == null) {
- account.setUuid(accountManager.getOwnUuid());
- account.save();
- }
- updateAccountAttributes();
+ if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) {
+ refreshPreKeys();
+ account.save();
}
+ if (account.getUuid() == null) {
+ account.setUuid(accountManager.getOwnUuid());
+ account.save();
+ }
+ updateAccountAttributes();
}
- public boolean isRegistered() {
- return account.isRegistered();
- }
-
- 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();
+ /**
+ * 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 get the contacts to check if they're 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
+ Map<String, UUID> contactDetails = getRegisteredUsers(numbers);
- if (voiceVerification) {
- accountManager.requestVoiceVerificationCode(Locale.getDefault(),
- Optional.fromNullable(captcha),
- Optional.absent());
- } else {
- accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent());
- }
+ Set<String> registeredUsers = contactDetails.keySet();
- account.setRegistered(false);
- account.save();
+ return numbers.stream().collect(Collectors.toMap(x -> x, registeredUsers::contains));
}
public void updateAccountAttributes() throws IOException {
accountManager.setAccountAttributes(account.getSignalingKey(),
account.getSignalProtocolStore().getLocalRegistrationId(),
true,
- account.getRegistrationLockPin(),
- account.getRegistrationLock(),
- unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(),
- unrestrictedUnidentifiedAccess,
+ // set legacy pin only if no KBS master key is set
+ account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null,
+ account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
+ account.getSelfUnidentifiedAccessKey(),
+ account.isUnrestrictedUnidentifiedAccess(),
capabilities,
- discoverableByPhoneNumber);
+ account.isDiscoverableByPhoneNumber());
}
- public void setProfile(String name, File avatar) throws IOException {
- try (final StreamDetails streamDetails = avatar == null ? null : Utils.createStreamDetailsFromFile(avatar)) {
- accountManager.setVersionedProfile(account.getUuid(), account.getProfileKey(), name, streamDetails);
+ /**
+ * @param avatar if avatar is null the image from the local avatar store is used (if present),
+ * if it's Optional.absent(), the avatar will be removed
+ */
+ public void setProfile(String name, Optional<File> avatar) throws IOException {
+ // TODO
+ String about = null;
+ String aboutEmoji = null;
+
+ try (final StreamDetails streamDetails = avatar == null
+ ? avatarStore.retrieveProfileAvatar(getSelfAddress())
+ : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
+ accountManager.setVersionedProfile(account.getUuid(),
+ account.getProfileKey(),
+ name,
+ about,
+ aboutEmoji,
+ streamDetails);
+ }
+
+ if (avatar != null) {
+ if (avatar.isPresent()) {
+ avatarStore.storeProfileAvatar(getSelfAddress(),
+ outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream));
+ } else {
+ avatarStore.deleteProfileAvatar(getSelfAddress());
+ }
+ }
+
+ try {
+ sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
+ } catch (UntrustedIdentityException ignored) {
}
}
// If this is the master device, other users can't send messages to this number anymore.
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
accountManager.setGcmId(Optional.absent());
+ accountManager.deleteAccount();
account.setRegistered(false);
account.save();
account.save();
}
- private List<PreKeyRecord> generatePreKeys() {
- List<PreKeyRecord> records = new ArrayList<>(ServiceConfig.PREKEY_BATCH_SIZE);
-
- final int offset = account.getPreKeyIdOffset();
- for (int i = 0; i < ServiceConfig.PREKEY_BATCH_SIZE; i++) {
- int preKeyId = (offset + i) % Medium.MAX_VALUE;
- ECKeyPair keyPair = Curve.generateKeyPair();
- PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
-
- records.add(record);
- }
-
- account.addPreKeys(records);
- account.save();
-
- return records;
- }
-
- 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);
-
- account.addSignedPreKey(record);
- account.save();
-
- return record;
- } catch (InvalidKeyException e) {
- throw new AssertionError(e);
+ public void setRegistrationLockPin(Optional<String> pin) throws IOException, UnauthenticatedResponseException {
+ if (!account.isMasterDevice()) {
+ throw new RuntimeException("Only master device can set a PIN");
}
- }
+ if (pin.isPresent()) {
+ final MasterKey masterKey = account.getPinMasterKey() != null
+ ? account.getPinMasterKey()
+ : KeyUtils.createMasterKey();
- public void verifyAccount(String verificationCode, String pin) throws IOException {
- 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,
- unidentifiedAccessHelper.getSelfUnidentifiedAccessKey(),
- unrestrictedUnidentifiedAccess,
- capabilities,
- discoverableByPhoneNumber);
-
- 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.setRegistrationLockPin(pin);
- account.getSignalProtocolStore()
- .saveIdentity(account.getSelfAddress(),
- getIdentityKeyPair().getPublicKey(),
- TrustLevel.TRUSTED_VERIFIED);
-
- refreshPreKeys();
- account.save();
- }
+ pinHelper.setRegistrationLockPin(pin.get(), masterKey);
- public void setRegistrationLockPin(Optional<String> pin) throws IOException {
- if (pin.isPresent()) {
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 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 List<PreKeyRecord> generatePreKeys() {
+ final int offset = account.getPreKeyIdOffset();
+
+ List<PreKeyRecord> records = KeyUtils.generatePreKeyRecords(offset, ServiceConfig.PREKEY_BATCH_SIZE);
+ account.addPreKeys(records);
+ account.save();
+
+ return records;
}
- private SignalServiceMessageReceiver getOrCreateMessageReceiver() {
- if (messageReceiver == null) {
- messageReceiver = createMessageReceiver();
- }
- return messageReceiver;
+ private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
+ final int signedPreKeyId = account.getNextSignedPreKeyId();
+
+ SignedPreKeyRecord record = KeyUtils.generateSignedPreKeyRecord(identityKeyPair, signedPreKeyId);
+ account.addSignedPreKey(record);
+ account.save();
+
+ return record;
}
private SignalServiceMessagePipe getOrCreateMessagePipe() {
if (messagePipe == null) {
- messagePipe = getOrCreateMessageReceiver().createMessagePipe();
+ messagePipe = messageReceiver.createMessagePipe();
}
return messagePipe;
}
private SignalServiceMessagePipe getOrCreateUnidentifiedMessagePipe() {
if (unidentifiedMessagePipe == null) {
- unidentifiedMessagePipe = getOrCreateMessageReceiver().createUnidentifiedMessagePipe();
+ unidentifiedMessagePipe = messageReceiver.createUnidentifiedMessagePipe();
}
return unidentifiedMessagePipe;
}
private SignalServiceMessageSender createMessageSender() {
- final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create(
- serviceConfiguration).getProfileOperations() : null;
final ExecutorService executor = null;
return new SignalServiceMessageSender(serviceConfiguration,
account.getUuid(),
Optional.absent(),
clientZkProfileOperations,
executor,
- ServiceConfig.MAX_ENVELOPE_SIZE);
+ ServiceConfig.MAX_ENVELOPE_SIZE,
+ ServiceConfig.AUTOMATIC_NETWORK_RETRY);
}
- private SignalServiceProfile getEncryptedRecipientProfile(SignalServiceAddress address) throws IOException {
- return profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE).getProfile();
+ private SignalProfile getRecipientProfile(
+ SignalServiceAddress address
+ ) {
+ return getRecipientProfile(address, false);
}
private SignalProfile getRecipientProfile(
- SignalServiceAddress address
+ SignalServiceAddress address, boolean force
) {
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
+ // Profiles are cached for 24h before retrieving them again
if (!profileEntry.isRequestPending() && (
- profileEntry.getProfile() == null || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000
+ force
+ || profileEntry.getProfile() == null
+ || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000
)) {
- ProfileKey profileKey = profileEntry.getProfileKey();
profileEntry.setRequestPending(true);
- SignalProfile profile;
+ final SignalServiceProfile encryptedProfile;
try {
- profile = retrieveRecipientProfile(address, profileKey);
+ encryptedProfile = profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE)
+ .getProfile();
} catch (IOException e) {
logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
- profileEntry.setRequestPending(false);
return null;
+ } finally {
+ profileEntry.setRequestPending(false);
}
- profileEntry.setRequestPending(false);
+
+ final ProfileKey profileKey = profileEntry.getProfileKey();
+ final SignalProfile profile = decryptProfileAndDownloadAvatar(address, profileKey, encryptedProfile);
account.getProfileStore()
.updateProfile(address, profileKey, now, profile, profileEntry.getProfileKeyCredential());
return profile;
long now = new Date().getTime();
final ProfileKeyCredential profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
- final SignalProfile profile = decryptProfile(address,
+ final SignalProfile profile = decryptProfileAndDownloadAvatar(address,
profileEntry.getProfileKey(),
profileAndCredential.getProfile());
account.getProfileStore()
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(
+ private SignalProfile decryptProfileAndDownloadAvatar(
final SignalServiceAddress address, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
) {
- File avatarFile = null;
- try {
- avatarFile = encryptedProfile.getAvatar() == null
- ? null
- : retrieveProfileAvatar(address, encryptedProfile.getAvatar(), profileKey);
- } catch (Throwable e) {
- logger.warn("Failed to retrieve profile avatar, ignoring: {}", e.getMessage());
+ if (encryptedProfile.getAvatar() != null) {
+ downloadProfileAvatar(address, encryptedProfile.getAvatar(), profileKey);
}
- ProfileCipher profileCipher = new ProfileCipher(profileKey);
- try {
- 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,
- unidentifiedAccess,
- encryptedProfile.isUnrestrictedUnidentifiedAccess(),
- encryptedProfile.getCapabilities());
- } catch (InvalidCiphertextException e) {
- return null;
- }
+ return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
}
private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(GroupId groupId) throws IOException {
- File file = getGroupAvatarFile(groupId);
- if (!file.exists()) {
+ final StreamDetails streamDetails = avatarStore.retrieveGroupAvatar(groupId);
+ if (streamDetails == null) {
return Optional.absent();
}
- return Optional.of(AttachmentUtils.createAttachment(file));
+ return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
}
- private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(String number) throws IOException {
- File file = getContactAvatarFile(number);
- if (!file.exists()) {
+ private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
+ final StreamDetails streamDetails = avatarStore.retrieveContactAvatar(address);
+ if (streamDetails == null) {
return Optional.absent();
}
- return Optional.of(AttachmentUtils.createAttachment(file));
+ return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
}
private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
- GroupInfo g = account.getGroupStore().getGroup(groupId);
+ GroupInfo g = getGroup(groupId);
if (g == null) {
throw new GroupNotFoundException(groupId);
}
}
private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
- GroupInfo g = account.getGroupStore().getGroup(groupId);
+ GroupInfo g = getGroup(groupId);
if (g == null) {
throw new GroupNotFoundException(groupId);
}
return account.getGroupStore().getGroups();
}
- public Pair<Long, List<SendMessageResult>> sendGroupMessage(
- SignalServiceDataMessage.Builder messageBuilder, GroupId groupId
- ) throws IOException, GroupNotFoundException, NotAGroupMemberException {
- final GroupInfo g = getGroupForSending(groupId);
-
- GroupUtils.setGroupContext(messageBuilder, g);
- messageBuilder.withExpiration(g.getMessageExpirationTime());
-
- return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
- }
-
public Pair<Long, List<SendMessageResult>> sendGroupMessage(
String messageText, List<String> attachments, GroupId groupId
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
return sendGroupMessage(messageBuilder, groupId);
}
- public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
+ public Pair<Long, List<SendMessageResult>> sendGroupMessage(
+ SignalServiceDataMessage.Builder messageBuilder, GroupId groupId
+ ) throws IOException, GroupNotFoundException, NotAGroupMemberException {
+ final GroupInfo g = getGroupForSending(groupId);
+
+ GroupUtils.setGroupContext(messageBuilder, g);
+ messageBuilder.withExpiration(g.getMessageExpirationTime());
+ return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
+ }
+
+ public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
SignalServiceDataMessage.Builder messageBuilder;
final GroupInfo g = getGroupForUpdating(groupId);
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
}
+ public Pair<GroupId, List<SendMessageResult>> updateGroup(
+ GroupId groupId, String name, List<String> members, File avatarFile
+ ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
+ return sendUpdateGroupMessage(groupId,
+ name,
+ members == null ? null : getSignalServiceAddresses(members),
+ avatarFile);
+ }
+
private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage(
- GroupId groupId, String name, Collection<SignalServiceAddress> members, String avatarFile
+ GroupId groupId, String name, Collection<SignalServiceAddress> members, File avatarFile
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
GroupInfo g;
SignalServiceDataMessage.Builder messageBuilder;
if (groupId == null) {
// Create new group
- GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile);
+ GroupInfoV2 gv2 = groupHelper.createGroupV2(name == null ? "" : name,
+ members == null ? List.of() : members,
+ avatarFile);
if (gv2 == null) {
GroupInfoV1 gv1 = new GroupInfoV1(GroupIdV1.createRandom());
- gv1.addMembers(Collections.singleton(account.getSelfAddress()));
+ gv1.addMembers(List.of(account.getSelfAddress()));
updateGroupV1(gv1, name, members, avatarFile);
messageBuilder = getGroupUpdateMessageBuilder(gv1);
g = gv1;
} else {
+ if (avatarFile != null) {
+ avatarStore.storeGroupAvatar(gv2.getGroupId(),
+ outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
+ }
messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
g = gv2;
}
Pair<DecryptedGroup, GroupChange> groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2,
name,
avatarFile);
+ if (avatarFile != null) {
+ avatarStore.storeGroupAvatar(groupInfoV2.getGroupId(),
+ outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
+ }
result = sendUpdateGroupMessage(groupInfoV2,
groupGroupChangePair.first(),
groupGroupChangePair.second());
return new Pair<>(g.getGroupId(), result.second());
}
+ private void updateGroupV1(
+ final GroupInfoV1 g,
+ final String name,
+ final Collection<SignalServiceAddress> members,
+ final File avatarFile
+ ) throws IOException {
+ if (name != null) {
+ g.name = name;
+ }
+
+ if (members != null) {
+ final Set<String> newE164Members = new HashSet<>();
+ for (SignalServiceAddress member : members) {
+ if (g.isMember(member) || !member.getNumber().isPresent()) {
+ continue;
+ }
+ newE164Members.add(member.getNumber().get());
+ }
+
+ final Map<String, UUID> registeredUsers = getRegisteredUsers(newE164Members);
+ if (registeredUsers.size() != newE164Members.size()) {
+ // Some of the new members are not registered on Signal
+ newE164Members.removeAll(registeredUsers.keySet());
+ throw new IOException("Failed to add members "
+ + String.join(", ", newE164Members)
+ + " to group: Not registered on Signal");
+ }
+
+ g.addMembers(members);
+ }
+
+ if (avatarFile != null) {
+ avatarStore.storeGroupAvatar(g.getGroupId(),
+ outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
+ }
+ }
+
public Pair<GroupId, List<SendMessageResult>> joinGroup(
GroupInviteLinkUrl inviteLinkUrl
) throws IOException, GroupLinkNotActiveException {
return new Pair<>(group.getGroupId(), result.second());
}
+ private static int currentTimeDays() {
+ return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
+ }
+
+ 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);
+ try {
+ return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
+ today,
+ groupSecretParams,
+ authCredentialResponse);
+ } catch (VerificationFailedException e) {
+ throw new IOException(e);
+ }
+ }
+
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;
- }
-
- if (members != null) {
- final Set<String> newE164Members = new HashSet<>();
- for (SignalServiceAddress member : members) {
- if (g.isMember(member) || !member.getNumber().isPresent()) {
- continue;
- }
- newE164Members.add(member.getNumber().get());
- }
-
- final List<ContactTokenDetails> contacts = accountManager.getContacts(newE164Members);
- if (contacts.size() != newE164Members.size()) {
- // Some of the new members are not registered on Signal
- for (ContactTokenDetails contact : contacts) {
- newE164Members.remove(contact.getNumber());
- }
- 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.getGroupId());
- Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
- }
+ account.getGroupStore().updateGroup(group);
+ return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfAddress()));
}
- Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
+ Pair<Long, List<SendMessageResult>> sendGroupInfoMessage(
GroupIdV1 groupId, SignalServiceAddress recipient
) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
GroupInfoV1 g;
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 {
.withName(g.name)
.withMembers(new ArrayList<>(g.getMembers()));
- File aFile = getGroupAvatarFile(g.getGroupId());
- if (aFile.exists()) {
- try {
- group.withAvatar(AttachmentUtils.createAttachment(aFile));
- } catch (IOException e) {
- throw new AttachmentInvalidException(aFile.toString(), e);
+ try {
+ final Optional<SignalServiceAttachmentStream> attachment = createGroupAvatarAttachment(g.getGroupId());
+ if (attachment.isPresent()) {
+ group.withAvatar(attachment.get());
}
+ } catch (IOException e) {
+ throw new AttachmentInvalidException(g.getGroupId().toBase64(), e);
}
return 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 {
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
- Collections.singletonList(messageId),
+ List.of(messageId),
System.currentTimeMillis());
createMessageSender().sendReceipt(remoteAddress,
return sendMessage(messageBuilder, getSignalServiceAddresses(recipients));
}
+ public Pair<Long, SendMessageResult> sendSelfMessage(
+ String messageText, List<String> attachments
+ ) throws IOException, AttachmentInvalidException {
+ final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
+ .withBody(messageText);
+ if (attachments != null) {
+ messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments));
+ }
+ return sendSelfMessage(messageBuilder);
+ }
+
public Pair<Long, List<SendMessageResult>> sendMessageReaction(
String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List<String> recipients
) throws IOException, InvalidNumberException {
account.save();
}
- 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);
- }
-
/**
* Change the expiration timer for a contact
*/
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 group
*/
public void setExpirationTimer(GroupId groupId, int messageExpirationTimer) {
- GroupInfo g = account.getGroupStore().getGroup(groupId);
+ GroupInfo g = getGroup(groupId);
if (g instanceof GroupInfoV1) {
GroupInfoV1 groupInfoV1 = (GroupInfoV1) g;
groupInfoV1.messageExpirationTime = messageExpirationTimer;
* @return if successful, returns the URL to install the sticker pack in the signal app
*/
public String uploadStickerPack(File path) throws IOException, StickerPackInvalidException {
- SignalServiceStickerManifestUpload manifest = getSignalServiceStickerManifestUpload(path);
+ SignalServiceStickerManifestUpload manifest = StickerUtils.getSignalServiceStickerManifestUpload(path);
SignalServiceMessageSender messageSender = createMessageSender();
}
}
- private SignalServiceStickerManifestUpload getSignalServiceStickerManifestUpload(
- final File file
- ) throws IOException, StickerPackInvalidException {
- ZipFile zip = null;
- String rootPath = null;
-
- if (file.getName().endsWith(".zip")) {
- zip = new ZipFile(file);
- } else if (file.getName().equals("manifest.json")) {
- rootPath = file.getParent();
- } else {
- throw new StickerPackInvalidException("Could not find manifest.json");
- }
-
- JsonStickerPack pack = parseStickerPack(rootPath, zip);
-
- if (pack.stickers == null) {
- throw new StickerPackInvalidException("Must set a 'stickers' field.");
- }
-
- if (pack.stickers.isEmpty()) {
- throw new StickerPackInvalidException("Must include stickers.");
- }
-
- List<StickerInfo> stickers = new ArrayList<>(pack.stickers.size());
- for (JsonStickerPack.JsonSticker sticker : pack.stickers) {
- if (sticker.file == null) {
- throw new StickerPackInvalidException("Must set a 'file' field on each sticker.");
- }
-
- Pair<InputStream, Long> data;
- try {
- data = getInputStreamAndLength(rootPath, zip, sticker.file);
- } catch (IOException ignored) {
- throw new StickerPackInvalidException("Could not find find " + sticker.file);
- }
-
- String contentType = Utils.getFileMimeType(new File(sticker.file), null);
- StickerInfo stickerInfo = new StickerInfo(data.first(),
- data.second(),
- Optional.fromNullable(sticker.emoji).or(""),
- contentType);
- stickers.add(stickerInfo);
- }
-
- StickerInfo cover = null;
- if (pack.cover != null) {
- if (pack.cover.file == null) {
- throw new StickerPackInvalidException("Must set a 'file' field on the cover.");
- }
-
- Pair<InputStream, Long> data;
- try {
- data = getInputStreamAndLength(rootPath, zip, pack.cover.file);
- } catch (IOException ignored) {
- throw new StickerPackInvalidException("Could not find find " + pack.cover.file);
- }
-
- String contentType = Utils.getFileMimeType(new File(pack.cover.file), null);
- cover = new StickerInfo(data.first(),
- data.second(),
- Optional.fromNullable(pack.cover.emoji).or(""),
- contentType);
- }
-
- return new SignalServiceStickerManifestUpload(pack.title, pack.author, cover, stickers);
- }
-
- private static JsonStickerPack parseStickerPack(String rootPath, ZipFile zip) throws IOException {
- InputStream inputStream;
- if (zip != null) {
- inputStream = zip.getInputStream(zip.getEntry("manifest.json"));
- } else {
- inputStream = new FileInputStream((new File(rootPath, "manifest.json")));
- }
- return new ObjectMapper().readValue(inputStream, JsonStickerPack.class);
- }
-
- 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());
- } else {
- final File file = new File(rootPath, subfile);
- return new Pair<>(new FileInputStream(file), file.length());
- }
- }
-
void requestSyncGroups() throws IOException {
SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder()
.setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS)
try {
sendSyncMessage(message);
} catch (UntrustedIdentityException e) {
- e.printStackTrace();
+ throw new AssertionError(e);
}
}
try {
sendSyncMessage(message);
} catch (UntrustedIdentityException e) {
- e.printStackTrace();
+ throw new AssertionError(e);
}
}
try {
sendSyncMessage(message);
} catch (UntrustedIdentityException e) {
- e.printStackTrace();
+ throw new AssertionError(e);
}
}
try {
sendSyncMessage(message);
} catch (UntrustedIdentityException e) {
- e.printStackTrace();
+ throw new AssertionError(e);
+ }
+ }
+
+ void requestSyncKeys() throws IOException {
+ SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder()
+ .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS)
+ .build();
+ SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
+ try {
+ sendSyncMessage(message);
+ } catch (UntrustedIdentityException e) {
+ throw new AssertionError(e);
}
}
private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
final Set<SignalServiceAddress> signalServiceAddresses = new HashSet<>(numbers.size());
- final Set<SignalServiceAddress> missingUuids = new HashSet<>();
+ final Set<SignalServiceAddress> addressesMissingUuid = new HashSet<>();
for (String number : numbers) {
final SignalServiceAddress resolvedAddress = canonicalizeAndResolveSignalServiceAddress(number);
if (resolvedAddress.getUuid().isPresent()) {
signalServiceAddresses.add(resolvedAddress);
} else {
- missingUuids.add(resolvedAddress);
+ addressesMissingUuid.add(resolvedAddress);
}
}
+ final Set<String> numbersMissingUuid = addressesMissingUuid.stream()
+ .map(a -> a.getNumber().get())
+ .collect(Collectors.toSet());
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) {
+ registeredUsers = getRegisteredUsers(numbersMissingUuid);
+ } catch (IOException e) {
logger.warn("Failed to resolve uuids from server, ignoring: {}", e.getMessage());
- registeredUsers = new HashMap<>();
+ registeredUsers = Map.of();
}
- for (SignalServiceAddress address : missingUuids) {
+ for (SignalServiceAddress address : addressesMissingUuid) {
final String number = address.getNumber().get();
if (registeredUsers.containsKey(number)) {
final SignalServiceAddress newAddress = resolveSignalServiceAddress(new SignalServiceAddress(
return signalServiceAddresses;
}
+ private Map<String, UUID> getRegisteredUsers(final Set<String> numbersMissingUuid) throws IOException {
+ try {
+ return accountManager.getRegisteredUsers(getIasKeyStore(), numbersMissingUuid, CDS_MRENCLAVE);
+ } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) {
+ throw new IOException(e);
+ }
+ }
+
private Pair<Long, List<SendMessageResult>> sendMessage(
SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients
) throws IOException {
.saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
e.getIdentityKey(),
TrustLevel.UNTRUSTED);
- return new Pair<>(timestamp, Collections.emptyList());
+ return new Pair<>(timestamp, List.of());
}
} else {
// Send to all individually, so sync messages are sent correctly
+ messageBuilder.withProfileKey(account.getProfileKey().serialize());
List<SendMessageResult> results = new ArrayList<>(recipients.size());
for (SignalServiceAddress address : recipients) {
- ContactInfo contact = account.getContactStore().getContact(address);
- if (contact != null) {
- messageBuilder.withExpiration(contact.messageExpirationTime);
- messageBuilder.withProfileKey(account.getProfileKey().serialize());
- } else {
- messageBuilder.withExpiration(0);
- messageBuilder.withProfileKey(null);
- }
+ final ContactInfo contact = account.getContactStore().getContact(address);
+ final int expirationTime = contact != null ? contact.messageExpirationTime : 0;
+ messageBuilder.withExpiration(expirationTime);
message = messageBuilder.build();
- if (address.matches(account.getSelfAddress())) {
- results.add(sendSelfMessage(message));
- } else {
- results.add(sendMessage(address, message));
- }
+ results.add(sendMessage(address, message));
}
return new Pair<>(timestamp, results);
}
}
}
+ private Pair<Long, SendMessageResult> sendSelfMessage(
+ SignalServiceDataMessage.Builder messageBuilder
+ ) throws IOException {
+ final long timestamp = System.currentTimeMillis();
+ messageBuilder.withTimestamp(timestamp);
+ getOrCreateMessagePipe();
+ getOrCreateUnidentifiedMessagePipe();
+ try {
+ final SignalServiceAddress address = getSelfAddress();
+
+ final ContactInfo contact = account.getContactStore().getContact(address);
+ final int expirationTime = contact != null ? contact.messageExpirationTime : 0;
+ messageBuilder.withExpiration(expirationTime);
+
+ SignalServiceDataMessage message = messageBuilder.build();
+ final SendMessageResult result = sendSelfMessage(message);
+ return new Pair<>(timestamp, result);
+ } finally {
+ account.save();
+ }
+ }
+
private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException {
SignalServiceMessageSender messageSender = createMessageSender();
message.getTimestamp(),
message,
message.getExpiresInSeconds(),
- Collections.singletonMap(recipient, unidentifiedAccess.isPresent()),
+ Map.of(recipient, unidentifiedAccess.isPresent()),
false);
SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
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);
+ final IdentityKey untrustedIdentity = identityException.getUntrustedIdentity();
+ if (untrustedIdentity != null) {
+ account.getSignalProtocolStore()
+ .saveIdentity(resolveSignalServiceAddress(identityException.getName()),
+ untrustedIdentity,
+ TrustLevel.UNTRUSTED);
+ }
throw identityException;
}
throw new AssertionError(e);
account.getSignalProtocolStore().deleteAllSessions(source);
}
- private static int currentTimeDays() {
- return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
- }
-
- 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);
- try {
- return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
- today,
- groupSecretParams,
- authCredentialResponse);
- } catch (VerificationFailedException e) {
- throw new IOException(e);
- }
- }
-
private List<HandleAction> handleSignalServiceDataMessage(
SignalServiceDataMessage message,
boolean isSync,
if (message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
GroupIdV1 groupId = GroupId.v1(groupInfo.getGroupId());
- GroupInfo group = account.getGroupStore().getGroup(groupId);
+ GroupInfo group = getGroup(groupId);
if (group == null || group instanceof GroupInfoV1) {
GroupInfoV1 groupV1 = (GroupInfoV1) group;
switch (groupInfo.getType()) {
if (groupInfo.getAvatar().isPresent()) {
SignalServiceAttachment avatar = groupInfo.getAvatar().get();
- if (avatar.isPointer()) {
- try {
- retrieveGroupAvatarAttachment(avatar.asPointer(), groupV1.getGroupId());
- } catch (IOException | InvalidMessageException | MissingConfigurationException e) {
- logger.warn("Failed to retrieve avatar for group {}, ignoring: {}",
- groupId.toBase64(),
- e.getMessage());
- }
- }
+ downloadGroupAvatar(avatar, groupV1.getGroupId());
}
if (groupInfo.getName().isPresent()) {
}
case REQUEST_INFO:
if (groupV1 != null && !isSync) {
- actions.add(new SendGroupUpdateAction(source, groupV1.getGroupId()));
+ actions.add(new SendGroupInfoAction(source, groupV1.getGroupId()));
}
break;
}
}
if (message.getAttachments().isPresent() && !ignoreAttachments) {
for (SignalServiceAttachment attachment : message.getAttachments().get()) {
- if (attachment.isPointer()) {
- try {
- retrieveAttachment(attachment.asPointer());
- } catch (IOException | InvalidMessageException | MissingConfigurationException e) {
- logger.warn("Failed to retrieve attachment ({}), ignoring: {}",
- attachment.asPointer().getRemoteId(),
- e.getMessage());
- }
- }
+ downloadAttachment(attachment);
}
}
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
if (message.getPreviews().isPresent()) {
final List<SignalServiceDataMessage.Preview> previews = message.getPreviews().get();
for (SignalServiceDataMessage.Preview preview : previews) {
- if (preview.getImage().isPresent() && preview.getImage().get().isPointer()) {
- SignalServiceAttachmentPointer attachment = preview.getImage().get().asPointer();
- try {
- retrieveAttachment(attachment);
- } catch (IOException | InvalidMessageException | MissingConfigurationException e) {
- logger.warn("Failed to retrieve preview image ({}), ignoring: {}",
- attachment.getRemoteId(),
- e.getMessage());
- }
+ if (preview.getImage().isPresent()) {
+ downloadAttachment(preview.getImage().get());
}
}
}
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());
- }
+ final SignalServiceAttachment thumbnail = quotedAttachment.getThumbnail();
+ if (thumbnail != null) {
+ downloadAttachment(thumbnail);
}
}
}
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams);
- GroupInfo groupInfo = account.getGroupStore().getGroup(groupId);
+ GroupInfo groupInfo = 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
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());
- }
+ downloadGroupAvatar(groupId, groupSecretParams, avatar);
}
}
groupInfoV2.setGroup(group);
}
}
- private void retryFailedReceivedMessages(
- ReceiveMessageHandler handler, boolean ignoreAttachments
- ) {
- final File cachePath = getMessageCachePath();
- if (!cachePath.exists()) {
- return;
- }
- for (final File dir : Objects.requireNonNull(cachePath.listFiles())) {
- if (!dir.isDirectory()) {
- retryFailedReceivedMessage(handler, ignoreAttachments, dir);
- continue;
- }
-
- for (final File fileEntry : Objects.requireNonNull(dir.listFiles())) {
- if (!fileEntry.isFile()) {
- continue;
- }
- retryFailedReceivedMessage(handler, ignoreAttachments, fileEntry);
- }
- // Try to delete directory if empty
- dir.delete();
+ private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
+ for (CachedMessage cachedMessage : account.getMessageCache().getCachedMessages()) {
+ retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage);
}
}
private void retryFailedReceivedMessage(
- final ReceiveMessageHandler handler, final boolean ignoreAttachments, final File fileEntry
+ final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage
) {
- SignalServiceEnvelope envelope;
- try {
- envelope = MessageCacheUtils.loadEnvelope(fileEntry);
- if (envelope == null) {
- return;
- }
- } catch (IOException e) {
- e.printStackTrace();
+ SignalServiceEnvelope envelope = cachedMessage.loadEnvelope();
+ if (envelope == null) {
return;
}
SignalServiceContent content = null;
return;
} catch (Exception er) {
// All other errors are not recoverable, so delete the cached message
- try {
- Files.delete(fileEntry.toPath());
- } catch (IOException e) {
- logger.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry, e.getMessage());
- }
+ cachedMessage.delete();
return;
}
List<HandleAction> actions = handleMessage(envelope, content, ignoreAttachments);
try {
action.execute(this);
} catch (Throwable e) {
- e.printStackTrace();
+ logger.warn("Message action failed.", e);
}
}
}
account.save();
handler.handleMessage(envelope, content, null);
- try {
- Files.delete(fileEntry.toPath());
- } catch (IOException e) {
- logger.warn("Failed to delete cached message file “{}”, ignoring: {}", fileEntry, e.getMessage());
- }
+ cachedMessage.delete();
}
public void receiveMessages(
Set<HandleAction> queuedActions = null;
- getOrCreateMessagePipe();
+ final SignalServiceMessagePipe messagePipe = getOrCreateMessagePipe();
boolean hasCaughtUpWithOldMessages = false;
SignalServiceEnvelope envelope;
SignalServiceContent content = null;
Exception exception = null;
- final long now = new Date().getTime();
+ final CachedMessage[] cachedMessage = {null};
try {
Optional<SignalServiceEnvelope> result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> {
// store message on disk, before acknowledging receipt to the server
- try {
- String source = envelope1.getSourceE164().isPresent() ? envelope1.getSourceE164().get() : "";
- File cacheFile = getMessageCacheFile(source, now, envelope1.getTimestamp());
- MessageCacheUtils.storeEnvelope(envelope1, cacheFile);
- } catch (IOException e) {
- logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage());
- }
+ cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1);
});
if (result.isPresent()) {
envelope = result.get();
try {
action.execute(this);
} catch (Throwable e) {
- e.printStackTrace();
+ logger.warn("Message action failed.", e);
}
}
account.save();
try {
action.execute(this);
} catch (Throwable e) {
- e.printStackTrace();
+ logger.warn("Message action failed.", e);
}
}
} else {
}
}
account.save();
- if (!isMessageBlocked(envelope, content)) {
+ if (isMessageBlocked(envelope, content)) {
+ logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp());
+ } else if (isNotAGroupMember(envelope, content)) {
+ logger.info("Ignoring a message from a non group member: {}", envelope.getTimestamp());
+ } else {
handler.handleMessage(envelope, content, exception);
}
if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
- File cacheFile = null;
- try {
- String source = envelope.getSourceE164().isPresent() ? envelope.getSourceE164().get() : "";
- cacheFile = getMessageCacheFile(source, now, envelope.getTimestamp());
- Files.delete(cacheFile.toPath());
- // Try to delete directory if empty
- getMessageCachePath().delete();
- } catch (IOException e) {
- logger.warn("Failed to delete cached message file “{}”, ignoring: {}", cacheFile, e.getMessage());
+ if (cachedMessage[0] != null) {
+ cachedMessage[0].delete();
}
}
}
return true;
}
+ if (content != null && content.getDataMessage().isPresent()) {
+ SignalServiceDataMessage message = content.getDataMessage().get();
+ if (message.getGroupContext().isPresent()) {
+ GroupId groupId = GroupUtils.getGroupId(message.getGroupContext().get());
+ GroupInfo group = getGroup(groupId);
+ if (group != null && group.isBlocked()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean isNotAGroupMember(
+ SignalServiceEnvelope envelope, SignalServiceContent content
+ ) {
+ SignalServiceAddress source;
+ if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
+ source = envelope.getSourceAddress();
+ } else if (content != null) {
+ source = content.getSender();
+ } else {
+ return false;
+ }
+
if (content != null && content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
if (message.getGroupContext().isPresent()) {
if (message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
- if (groupInfo.getType() != SignalServiceGroup.Type.DELIVER) {
+ if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) {
return false;
}
}
GroupId groupId = GroupUtils.getGroupId(message.getGroupContext().get());
- GroupInfo group = account.getGroupStore().getGroup(groupId);
- if (group != null && group.isBlocked()) {
+ GroupInfo group = getGroup(groupId);
+ if (group != null && !group.isMember(source)) {
return true;
}
}
File tmpFile = null;
try {
tmpFile = IOUtils.createTempFile();
- try (InputStream attachmentAsStream = retrieveAttachmentAsStream(syncMessage.getGroups()
- .get()
- .asPointer(), tmpFile)) {
+ final SignalServiceAttachment groupsMessage = syncMessage.getGroups().get();
+ try (InputStream attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(),
+ tmpFile)) {
DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream);
DeviceGroup g;
while ((g = s.read()) != null) {
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.getGroupId());
+ downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId());
}
syncGroup.inboxPosition = g.getInboxPosition().orNull();
syncGroup.archived = g.isArchived();
logger.warn("Failed to handle received sync groups “{}”, ignoring: {}",
tmpFile,
e.getMessage());
- e.printStackTrace();
} finally {
if (tmpFile != null) {
try {
account.getContactStore().updateContact(contact);
if (c.getAvatar().isPresent()) {
- retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
+ downloadContactAvatar(c.getAvatar().get(), contact.getAddress());
}
}
}
} catch (Exception e) {
- e.printStackTrace();
+ logger.warn("Failed to handle received sync contacts “{}”, ignoring: {}",
+ tmpFile,
+ e.getMessage());
} finally {
if (tmpFile != null) {
try {
account.getStickerStore().updateSticker(sticker);
}
}
+ if (syncMessage.getFetchType().isPresent()) {
+ switch (syncMessage.getFetchType().get()) {
+ case LOCAL_PROFILE:
+ getRecipientProfile(getSelfAddress(), true);
+ case STORAGE_MANIFEST:
+ // TODO
+ }
+ }
+ if (syncMessage.getKeys().isPresent()) {
+ final KeysMessage keysMessage = syncMessage.getKeys().get();
+ if (keysMessage.getStorageService().isPresent()) {
+ final StorageKey storageKey = keysMessage.getStorageService().get();
+ account.setStorageKey(storageKey);
+ }
+ }
if (syncMessage.getConfiguration().isPresent()) {
// TODO
}
return actions;
}
- private File getContactAvatarFile(String number) {
- return new File(pathConfig.getAvatarsPath(), "contact-" + number);
+ private void downloadContactAvatar(SignalServiceAttachment avatar, SignalServiceAddress address) {
+ try {
+ avatarStore.storeContactAvatar(address, outputStream -> retrieveAttachment(avatar, outputStream));
+ } catch (IOException e) {
+ logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage());
+ }
}
- 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 AttachmentUtils.retrieveAttachment(stream, getContactAvatarFile(number));
+ private void downloadGroupAvatar(SignalServiceAttachment avatar, GroupId groupId) {
+ try {
+ avatarStore.storeGroupAvatar(groupId, outputStream -> retrieveAttachment(avatar, outputStream));
+ } catch (IOException e) {
+ logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage());
}
}
- private File getGroupAvatarFile(GroupId groupId) {
- return new File(pathConfig.getAvatarsPath(), "group-" + groupId.toBase64().replace("/", "_"));
+ private void downloadGroupAvatar(GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey) {
+ try {
+ avatarStore.storeGroupAvatar(groupId,
+ outputStream -> retrieveGroupV2Avatar(groupSecretParams, cdnKey, outputStream));
+ } catch (IOException e) {
+ logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage());
+ }
}
- 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 AttachmentUtils.retrieveAttachment(stream, getGroupAvatarFile(groupId));
+ private void downloadProfileAvatar(
+ SignalServiceAddress address, String avatarPath, ProfileKey profileKey
+ ) {
+ try {
+ avatarStore.storeProfileAvatar(address,
+ outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream));
+ } catch (Throwable e) {
+ logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage());
+ }
+ }
+
+ public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
+ return attachmentStore.getAttachmentFile(attachmentId);
+ }
+
+ private void downloadAttachment(final SignalServiceAttachment attachment) {
+ if (!attachment.isPointer()) {
+ logger.warn("Invalid state, can't store an attachment stream.");
+ }
+
+ SignalServiceAttachmentPointer pointer = attachment.asPointer();
+ if (pointer.getPreview().isPresent()) {
+ final byte[] preview = pointer.getPreview().get();
+ try {
+ attachmentStore.storeAttachmentPreview(pointer.getRemoteId(),
+ outputStream -> outputStream.write(preview, 0, preview.length));
+ } catch (IOException e) {
+ logger.warn("Failed to download attachment preview, ignoring: {}", e.getMessage());
+ }
+ }
+
+ try {
+ attachmentStore.storeAttachment(pointer.getRemoteId(),
+ outputStream -> retrieveAttachmentPointer(pointer, outputStream));
+ } catch (IOException e) {
+ logger.warn("Failed to download attachment ({}), ignoring: {}", pointer.getRemoteId(), e.getMessage());
}
}
- private File retrieveGroupAvatar(
- GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey
+ private void retrieveGroupV2Avatar(
+ GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream
) 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,
+ try (InputStream input = messageReceiver.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);
- }
+ outputStream.write(decryptedData);
} finally {
try {
Files.delete(tmpFile.toPath());
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
+ private void retrieveProfileAvatar(
+ String avatarPath, ProfileKey profileKey, OutputStream outputStream
) throws IOException {
- IOUtils.createPrivateDirectories(pathConfig.getAvatarsPath());
- SignalServiceMessageReceiver receiver = getOrCreateMessageReceiver();
- File outputFile = getProfileAvatarFile(address);
-
File tmpFile = IOUtils.createTempFile();
- try (InputStream input = receiver.retrieveProfileAvatar(avatarPath,
+ try (InputStream input = messageReceiver.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);
+ IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
} finally {
try {
Files.delete(tmpFile.toPath());
e.getMessage());
}
}
- return outputFile;
- }
-
- public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
- return new File(pathConfig.getAttachmentsPath(), attachmentId.toString());
}
- private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException, MissingConfigurationException {
- IOUtils.createPrivateDirectories(pathConfig.getAttachmentsPath());
- return retrieveAttachment(pointer, getAttachmentFile(pointer.getRemoteId()), true);
- }
-
- 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)) {
- byte[] preview = pointer.getPreview().get();
- output.write(preview, 0, preview.length);
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- return null;
- }
+ private void retrieveAttachment(
+ final SignalServiceAttachment attachment, final OutputStream outputStream
+ ) throws IOException {
+ if (attachment.isPointer()) {
+ SignalServiceAttachmentPointer pointer = attachment.asPointer();
+ retrieveAttachmentPointer(pointer, outputStream);
+ } else {
+ SignalServiceAttachmentStream stream = attachment.asStream();
+ IOUtils.copyStream(stream.getInputStream(), outputStream);
}
+ }
- final SignalServiceMessageReceiver messageReceiver = getOrCreateMessageReceiver();
-
+ private void retrieveAttachmentPointer(
+ SignalServiceAttachmentPointer pointer, OutputStream outputStream
+ ) throws IOException {
File tmpFile = IOUtils.createTempFile();
- try (InputStream input = messageReceiver.retrieveAttachment(pointer,
- tmpFile,
- ServiceConfig.MAX_ATTACHMENT_SIZE)) {
- IOUtils.copyStreamToFile(input, outputFile);
+ try (InputStream input = retrieveAttachmentAsStream(pointer, tmpFile)) {
+ IOUtils.copyStream(input, outputStream);
+ } catch (MissingConfigurationException | InvalidMessageException e) {
+ throw new IOException(e);
} finally {
try {
Files.delete(tmpFile.toPath());
e.getMessage());
}
}
- return outputFile;
}
private InputStream retrieveAttachmentAsStream(
SignalServiceAttachmentPointer pointer, File tmpFile
) throws IOException, InvalidMessageException, MissingConfigurationException {
- final SignalServiceMessageReceiver messageReceiver = getOrCreateMessageReceiver();
return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
}
try {
try (OutputStream fos = new FileOutputStream(groupsFile)) {
DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos);
- for (GroupInfo record : account.getGroupStore().getGroups()) {
+ for (GroupInfo record : getGroups()) {
if (record instanceof GroupInfoV1) {
GroupInfoV1 groupInfo = (GroupInfoV1) record;
out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
ProfileKey profileKey = account.getProfileStore().getProfileKey(record.getAddress());
out.write(new DeviceContact(record.getAddress(),
Optional.fromNullable(record.name),
- createContactAvatarAttachment(record.number),
+ createContactAvatarAttachment(record.getAddress()),
Optional.fromNullable(record.color),
Optional.fromNullable(verifiedMessage),
Optional.fromNullable(profileKey),
}
}
List<byte[]> groupIds = new ArrayList<>();
- for (GroupInfo record : account.getGroupStore().getGroups()) {
+ for (GroupInfo record : getGroups()) {
if (record.isBlocked()) {
groupIds.add(record.getGroupId().serialize());
}
return account.getContactStore().getContacts();
}
- public ContactInfo getContact(String number) {
- return account.getContactStore().getContact(Utils.getSignalServiceAddressFromIdentifier(number));
+ public String getContactOrProfileName(String number) {
+ final SignalServiceAddress address = Utils.getSignalServiceAddressFromIdentifier(number);
+
+ final ContactInfo contact = account.getContactStore().getContact(address);
+ if (contact != null && !Util.isEmpty(contact.name)) {
+ return contact.name;
+ }
+
+ final SignalProfileEntry profileEntry = account.getProfileStore().getProfileEntry(address);
+ if (profileEntry != null && profileEntry.getProfile() != null) {
+ return profileEntry.getProfile().getName();
+ }
+
+ return null;
}
public GroupInfo getGroup(GroupId groupId) {
- return account.getGroupStore().getGroup(groupId);
+ final GroupInfo group = account.getGroupStore().getGroup(groupId);
+ if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
+ final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
+ ((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams));
+ account.getGroupStore().updateGroup(group);
+ }
+ return group;
}
public List<IdentityInfo> getIdentities() {
try {
sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
} catch (IOException | UntrustedIdentityException e) {
- e.printStackTrace();
+ logger.warn("Failed to send verification sync message: {}", e.getMessage());
}
account.save();
return true;
try {
sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
} catch (IOException | UntrustedIdentityException e) {
- e.printStackTrace();
+ logger.warn("Failed to send verification sync message: {}", e.getMessage());
}
account.save();
return true;
try {
sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
} catch (IOException | UntrustedIdentityException e) {
- e.printStackTrace();
+ logger.warn("Failed to send verification sync message: {}", e.getMessage());
}
}
}
theirIdentityKey);
}
- void saveAccount() {
- account.save();
- }
-
public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException {
String canonicalizedNumber = UuidUtil.isUuid(identifier)
? identifier
@Override
public void close() throws IOException {
+ close(true);
+ }
+
+ void close(boolean closeAccount) throws IOException {
if (messagePipe != null) {
messagePipe.shutdown();
messagePipe = null;
unidentifiedMessagePipe = null;
}
- account.close();
+ if (closeAccount && account != null) {
+ account.close();
+ }
+ account = null;
}
public interface ReceiveMessageHandler {