Update libsignal-service-java

This commit is contained in:
AsamK 2021-08-15 21:04:03 +02:00
parent 7ea3900854
commit b810e303ec
13 changed files with 527 additions and 279 deletions

View file

@ -14,7 +14,7 @@ repositories {
} }
dependencies { dependencies {
api("com.github.turasa:signal-service-java:2.15.3_unofficial_24") api("com.github.turasa:signal-service-java:2.15.3_unofficial_25")
implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("com.google.protobuf:protobuf-javalite:3.10.0")
implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("org.bouncycastle:bcprov-jdk15on:1.69")
implementation("org.slf4j:slf4j-api:1.7.30") implementation("org.slf4j:slf4j-api:1.7.30")

View file

@ -65,14 +65,12 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException; import org.signal.libsignal.metadata.SelfSendException;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException; import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.groups.GroupSecretParams; import org.signal.zkgroup.groups.GroupSecretParams;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -86,19 +84,12 @@ import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.InvalidMessageStructureException;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; 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.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
@ -133,9 +124,8 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce
import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote; import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
@ -182,23 +172,13 @@ public class Manager implements Closeable {
private final static Logger logger = LoggerFactory.getLogger(Manager.class); private final static Logger logger = LoggerFactory.getLogger(Manager.class);
private final CertificateValidator certificateValidator;
private final ServiceEnvironmentConfig serviceEnvironmentConfig; private final ServiceEnvironmentConfig serviceEnvironmentConfig;
private final String userAgent; private final SignalDependencies dependencies;
private SignalAccount account; private SignalAccount account;
private final SignalServiceAccountManager accountManager;
private final GroupsV2Api groupsV2Api;
private final GroupsV2Operations groupsV2Operations;
private final SignalServiceMessageReceiver messageReceiver;
private final ClientZkProfileOperations clientZkProfileOperations;
private final ExecutorService executor = Executors.newCachedThreadPool(); private final ExecutorService executor = Executors.newCachedThreadPool();
private SignalServiceMessagePipe messagePipe = null;
private SignalServiceMessagePipe unidentifiedMessagePipe = null;
private final UnidentifiedAccessHelper unidentifiedAccessHelper; private final UnidentifiedAccessHelper unidentifiedAccessHelper;
private final ProfileHelper profileHelper; private final ProfileHelper profileHelper;
private final GroupV2Helper groupV2Helper; private final GroupV2Helper groupV2Helper;
@ -224,42 +204,19 @@ public class Manager implements Closeable {
) { ) {
this.account = account; this.account = account;
this.serviceEnvironmentConfig = serviceEnvironmentConfig; this.serviceEnvironmentConfig = serviceEnvironmentConfig;
this.certificateValidator = new CertificateValidator(serviceEnvironmentConfig.getUnidentifiedSenderTrustRoot());
this.userAgent = userAgent;
this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create(
serviceEnvironmentConfig.getSignalServiceConfiguration())) : null;
final SleepTimer timer = new UptimeSleepTimer();
this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
new DynamicCredentialsProvider(account.getUuid(),
account.getUsername(),
account.getPassword(),
account.getDeviceId()),
userAgent,
groupsV2Operations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY,
timer);
this.groupsV2Api = accountManager.getGroupsV2Api();
final var keyBackupService = accountManager.getKeyBackupService(ServiceConfig.getIasKeyStore(),
serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(),
serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(),
serviceEnvironmentConfig.getKeyBackupConfig().getMrenclave(),
10);
this.pinHelper = new PinHelper(keyBackupService); final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(),
this.clientZkProfileOperations = capabilities.isGv2()
? ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration())
.getProfileOperations()
: null;
this.messageReceiver = new SignalServiceMessageReceiver(serviceEnvironmentConfig.getSignalServiceConfiguration(),
account.getUuid(),
account.getUsername(), account.getUsername(),
account.getPassword(), account.getPassword(),
account.getDeviceId(), account.getDeviceId());
this.dependencies = new SignalDependencies(account.getSelfAddress(),
serviceEnvironmentConfig,
userAgent, userAgent,
null, credentialsProvider,
timer, account.getSignalProtocolStore(),
clientZkProfileOperations, executor,
ServiceConfig.AUTOMATIC_NETWORK_RETRY); sessionLock);
this.pinHelper = new PinHelper(dependencies.getKeyBackupService());
this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
account.getProfileStore()::getProfileKey, account.getProfileStore()::getProfileKey,
@ -267,14 +224,14 @@ public class Manager implements Closeable {
this::getSenderCertificate); this::getSenderCertificate);
this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey, this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
unidentifiedAccessHelper::getAccessFor, unidentifiedAccessHelper::getAccessFor,
unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(), dependencies::getProfileService,
() -> messageReceiver, dependencies::getMessageReceiver,
this::resolveSignalServiceAddress); this::resolveSignalServiceAddress);
this.groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, this.groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential,
this::getRecipientProfile, this::getRecipientProfile,
account::getSelfRecipientId, account::getSelfRecipientId,
groupsV2Operations, dependencies.getGroupsV2Operations(),
groupsV2Api, dependencies.getGroupsV2Api(),
this::getGroupAuthForToday, this::getGroupAuthForToday,
this::resolveSignalServiceAddress); this::resolveSignalServiceAddress);
this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
@ -350,11 +307,11 @@ public class Manager implements Closeable {
days); days);
} }
} }
if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { if (dependencies.getAccountManager().getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) {
refreshPreKeys(); refreshPreKeys();
} }
if (account.getUuid() == null) { if (account.getUuid() == null) {
account.setUuid(accountManager.getOwnUuid()); account.setUuid(dependencies.getAccountManager().getOwnUuid());
} }
updateAccountAttributes(); updateAccountAttributes();
} }
@ -376,17 +333,18 @@ public class Manager implements Closeable {
} }
public void updateAccountAttributes() throws IOException { public void updateAccountAttributes() throws IOException {
accountManager.setAccountAttributes(account.getEncryptedDeviceName(), dependencies.getAccountManager()
null, .setAccountAttributes(account.getEncryptedDeviceName(),
account.getLocalRegistrationId(), null,
true, account.getLocalRegistrationId(),
// set legacy pin only if no KBS master key is set true,
account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null, // set legacy pin only if no KBS master key is set
account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null,
account.getSelfUnidentifiedAccessKey(), account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
account.isUnrestrictedUnidentifiedAccess(), account.getSelfUnidentifiedAccessKey(),
capabilities, account.isUnrestrictedUnidentifiedAccess(),
account.isDiscoverableByPhoneNumber()); capabilities,
account.isDiscoverableByPhoneNumber());
} }
/** /**
@ -418,13 +376,14 @@ public class Manager implements Closeable {
try (final var streamDetails = avatar == null try (final var streamDetails = avatar == null
? avatarStore.retrieveProfileAvatar(getSelfAddress()) ? avatarStore.retrieveProfileAvatar(getSelfAddress())
: avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) { : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
accountManager.setVersionedProfile(account.getUuid(), dependencies.getAccountManager()
account.getProfileKey(), .setVersionedProfile(account.getUuid(),
newProfile.getInternalServiceName(), account.getProfileKey(),
newProfile.getAbout() == null ? "" : newProfile.getAbout(), newProfile.getInternalServiceName(),
newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), newProfile.getAbout() == null ? "" : newProfile.getAbout(),
Optional.absent(), newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
streamDetails); Optional.absent(),
streamDetails);
} }
if (avatar != null) { if (avatar != null) {
@ -447,19 +406,19 @@ public class Manager implements Closeable {
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
// If this is the master device, other users can't send messages to this number anymore. // 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. // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
accountManager.setGcmId(Optional.absent()); dependencies.getAccountManager().setGcmId(Optional.absent());
account.setRegistered(false); account.setRegistered(false);
} }
public void deleteAccount() throws IOException { public void deleteAccount() throws IOException {
accountManager.deleteAccount(); dependencies.getAccountManager().deleteAccount();
account.setRegistered(false); account.setRegistered(false);
} }
public List<Device> getLinkedDevices() throws IOException { public List<Device> getLinkedDevices() throws IOException {
var devices = accountManager.getDevices(); var devices = dependencies.getAccountManager().getDevices();
account.setMultiDevice(devices.size() > 1); account.setMultiDevice(devices.size() > 1);
var identityKey = account.getIdentityKeyPair().getPrivateKey(); var identityKey = account.getIdentityKeyPair().getPrivateKey();
return devices.stream().map(d -> { return devices.stream().map(d -> {
@ -476,8 +435,8 @@ public class Manager implements Closeable {
} }
public void removeLinkedDevices(int deviceId) throws IOException { public void removeLinkedDevices(int deviceId) throws IOException {
accountManager.removeDevice(deviceId); dependencies.getAccountManager().removeDevice(deviceId);
var devices = accountManager.getDevices(); var devices = dependencies.getAccountManager().getDevices();
account.setMultiDevice(devices.size() > 1); account.setMultiDevice(devices.size() > 1);
} }
@ -489,13 +448,14 @@ public class Manager implements Closeable {
private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
var identityKeyPair = getIdentityKeyPair(); var identityKeyPair = getIdentityKeyPair();
var verificationCode = accountManager.getNewDeviceVerificationCode(); var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
accountManager.addDevice(deviceIdentifier, dependencies.getAccountManager()
deviceKey, .addDevice(deviceIdentifier,
identityKeyPair, deviceKey,
Optional.of(account.getProfileKey().serialize()), identityKeyPair,
verificationCode); Optional.of(account.getProfileKey().serialize()),
verificationCode);
account.setMultiDevice(true); account.setMultiDevice(true);
} }
@ -513,7 +473,7 @@ public class Manager implements Closeable {
account.setRegistrationLockPin(pin.get(), masterKey); account.setRegistrationLockPin(pin.get(), masterKey);
} else { } else {
// Remove legacy registration lock // Remove legacy registration lock
accountManager.removeRegistrationLockV1(); dependencies.getAccountManager().removeRegistrationLockV1();
// Remove KBS Pin // Remove KBS Pin
pinHelper.removeRegistrationLockPin(); pinHelper.removeRegistrationLockPin();
@ -527,7 +487,7 @@ public class Manager implements Closeable {
final var identityKeyPair = getIdentityKeyPair(); final var identityKeyPair = getIdentityKeyPair();
var signedPreKeyRecord = generateSignedPreKey(identityKeyPair); var signedPreKeyRecord = generateSignedPreKey(identityKeyPair);
accountManager.setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys); dependencies.getAccountManager().setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys);
} }
private List<PreKeyRecord> generatePreKeys() { private List<PreKeyRecord> generatePreKeys() {
@ -548,39 +508,6 @@ public class Manager implements Closeable {
return record; return record;
} }
private SignalServiceMessagePipe getOrCreateMessagePipe() {
if (messagePipe == null) {
messagePipe = messageReceiver.createMessagePipe();
}
return messagePipe;
}
private SignalServiceMessagePipe getOrCreateUnidentifiedMessagePipe() {
if (unidentifiedMessagePipe == null) {
unidentifiedMessagePipe = messageReceiver.createUnidentifiedMessagePipe();
}
return unidentifiedMessagePipe;
}
private SignalServiceMessageSender createMessageSender() {
return new SignalServiceMessageSender(serviceEnvironmentConfig.getSignalServiceConfiguration(),
account.getUuid(),
account.getUsername(),
account.getPassword(),
account.getDeviceId(),
account.getSignalProtocolStore(),
sessionLock,
userAgent,
account.isMultiDevice(),
Optional.fromNullable(messagePipe),
Optional.fromNullable(unidentifiedMessagePipe),
Optional.absent(),
clientZkProfileOperations,
executor,
ServiceConfig.MAX_ENVELOPE_SIZE,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
}
public Profile getRecipientProfile( public Profile getRecipientProfile(
RecipientId recipientId RecipientId recipientId
) { ) {
@ -1180,14 +1107,15 @@ public class Manager implements Closeable {
) throws IOException { ) throws IOException {
final var today = currentTimeDays(); final var today = currentTimeDays();
// Returns credentials for the next 7 days // Returns credentials for the next 7 days
final var credentials = groupsV2Api.getCredentials(today); final var credentials = dependencies.getGroupsV2Api().getCredentials(today);
// TODO cache credentials until they expire // TODO cache credentials until they expire
var authCredentialResponse = credentials.get(today); var authCredentialResponse = credentials.get(today);
try { try {
return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(), return dependencies.getGroupsV2Api()
today, .getGroupsV2AuthorizationString(account.getUuid(),
groupSecretParams, today,
authCredentialResponse); groupSecretParams,
authCredentialResponse);
} catch (VerificationFailedException e) { } catch (VerificationFailedException e) {
throw new IOException(e); throw new IOException(e);
} }
@ -1264,9 +1192,10 @@ public class Manager implements Closeable {
List.of(messageId), List.of(messageId),
System.currentTimeMillis()); System.currentTimeMillis());
createMessageSender().sendReceipt(remoteAddress, dependencies.getMessageSender()
unidentifiedAccessHelper.getAccessFor(resolveRecipient(remoteAddress)), .sendReceipt(remoteAddress,
receiptMessage); unidentifiedAccessHelper.getAccessFor(resolveRecipient(remoteAddress)),
receiptMessage);
} }
public Pair<Long, List<SendMessageResult>> sendMessage( public Pair<Long, List<SendMessageResult>> sendMessage(
@ -1277,7 +1206,7 @@ public class Manager implements Closeable {
var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments);
// Upload attachments here, so we only upload once even for multiple recipients // Upload attachments here, so we only upload once even for multiple recipients
var messageSender = createMessageSender(); var messageSender = dependencies.getMessageSender();
var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size()); var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
for (var attachment : attachmentStreams) { for (var attachment : attachmentStreams) {
if (attachment.isStream()) { if (attachment.isStream()) {
@ -1351,11 +1280,6 @@ public class Manager implements Closeable {
} }
} }
public String getContactName(String number) throws InvalidNumberException {
var contact = account.getContactStore().getContact(canonicalizeAndResolveRecipient(number));
return contact == null || contact.getName() == null ? "" : contact.getName();
}
public void setContactName(String number, String name) throws InvalidNumberException, NotMasterDeviceException { public void setContactName(String number, String name) throws InvalidNumberException, NotMasterDeviceException {
if (!account.isMasterDevice()) { if (!account.isMasterDevice()) {
throw new NotMasterDeviceException(); throw new NotMasterDeviceException();
@ -1442,7 +1366,7 @@ public class Manager implements Closeable {
public String uploadStickerPack(File path) throws IOException, StickerPackInvalidException { public String uploadStickerPack(File path) throws IOException, StickerPackInvalidException {
var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path);
var messageSender = createMessageSender(); var messageSender = dependencies.getMessageSender();
var packKey = KeyUtils.createStickerUploadKey(); var packKey = KeyUtils.createStickerUploadKey();
var packIdString = messageSender.uploadStickerManifest(manifest, packKey); var packIdString = messageSender.uploadStickerManifest(manifest, packKey);
@ -1536,9 +1460,9 @@ public class Manager implements Closeable {
byte[] certificate; byte[] certificate;
try { try {
if (account.isPhoneNumberShared()) { if (account.isPhoneNumberShared()) {
certificate = accountManager.getSenderCertificate(); certificate = dependencies.getAccountManager().getSenderCertificate();
} else { } else {
certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy(); certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
} }
} catch (IOException e) { } catch (IOException e) {
logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage()); logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
@ -1549,7 +1473,7 @@ public class Manager implements Closeable {
} }
private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException {
var messageSender = createMessageSender(); var messageSender = dependencies.getMessageSender();
messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync()); messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync());
} }
@ -1604,9 +1528,10 @@ public class Manager implements Closeable {
private Map<String, UUID> getRegisteredUsers(final Set<String> numbers) throws IOException { private Map<String, UUID> getRegisteredUsers(final Set<String> numbers) throws IOException {
try { try {
return accountManager.getRegisteredUsers(ServiceConfig.getIasKeyStore(), return dependencies.getAccountManager()
numbers, .getRegisteredUsers(ServiceConfig.getIasKeyStore(),
serviceEnvironmentConfig.getCdsMrenclave()); numbers,
serviceEnvironmentConfig.getCdsMrenclave());
} catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) {
throw new IOException(e); throw new IOException(e);
} }
@ -1623,7 +1548,7 @@ public class Manager implements Closeable {
) throws IOException, UntrustedIdentityException { ) throws IOException, UntrustedIdentityException {
final var timestamp = System.currentTimeMillis(); final var timestamp = System.currentTimeMillis();
var message = new SignalServiceTypingMessage(action.toSignalService(), timestamp, Optional.absent()); var message = new SignalServiceTypingMessage(action.toSignalService(), timestamp, Optional.absent());
var messageSender = createMessageSender(); var messageSender = dependencies.getMessageSender();
for (var recipientId : recipientIds) { for (var recipientId : recipientIds) {
final var address = resolveSignalServiceAddress(recipientId); final var address = resolveSignalServiceAddress(recipientId);
messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message);
@ -1638,7 +1563,7 @@ public class Manager implements Closeable {
final var message = new SignalServiceTypingMessage(action.toSignalService(), final var message = new SignalServiceTypingMessage(action.toSignalService(),
timestamp, timestamp,
Optional.of(groupId.serialize())); Optional.of(groupId.serialize()));
final var messageSender = createMessageSender(); final var messageSender = dependencies.getMessageSender();
final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId())); final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId()));
final var addresses = recipientIdList.stream() final var addresses = recipientIdList.stream()
.map(this::resolveSignalServiceAddress) .map(this::resolveSignalServiceAddress)
@ -1651,14 +1576,13 @@ public class Manager implements Closeable {
) throws IOException { ) throws IOException {
final var timestamp = System.currentTimeMillis(); final var timestamp = System.currentTimeMillis();
messageBuilder.withTimestamp(timestamp); messageBuilder.withTimestamp(timestamp);
getOrCreateMessagePipe();
getOrCreateUnidentifiedMessagePipe();
SignalServiceDataMessage message = null; SignalServiceDataMessage message = null;
try { try {
message = messageBuilder.build(); message = messageBuilder.build();
if (message.getGroupContext().isPresent()) { if (message.getGroupContext().isPresent()) {
try { try {
var messageSender = createMessageSender(); var messageSender = dependencies.getMessageSender();
final var isRecipientUpdate = false; final var isRecipientUpdate = false;
final var recipientIdList = new ArrayList<>(recipientIds); final var recipientIdList = new ArrayList<>(recipientIds);
final var addresses = recipientIdList.stream() final var addresses = recipientIdList.stream()
@ -1668,7 +1592,9 @@ public class Manager implements Closeable {
unidentifiedAccessHelper.getAccessFor(recipientIdList), unidentifiedAccessHelper.getAccessFor(recipientIdList),
isRecipientUpdate, isRecipientUpdate,
ContentHint.DEFAULT, ContentHint.DEFAULT,
message); message,
sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()),
() -> false);
for (var r : result) { for (var r : result) {
if (r.getIdentityFailure() != null) { if (r.getIdentityFailure() != null) {
@ -1712,8 +1638,6 @@ public class Manager implements Closeable {
) throws IOException { ) throws IOException {
final var timestamp = System.currentTimeMillis(); final var timestamp = System.currentTimeMillis();
messageBuilder.withTimestamp(timestamp); messageBuilder.withTimestamp(timestamp);
getOrCreateMessagePipe();
getOrCreateUnidentifiedMessagePipe();
final var recipientId = account.getSelfRecipientId(); final var recipientId = account.getSelfRecipientId();
final var contact = account.getContactStore().getContact(recipientId); final var contact = account.getContactStore().getContact(recipientId);
@ -1726,7 +1650,7 @@ public class Manager implements Closeable {
} }
private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException { private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException {
var messageSender = createMessageSender(); var messageSender = dependencies.getMessageSender();
var recipientId = account.getSelfRecipientId(); var recipientId = account.getSelfRecipientId();
@ -1741,12 +1665,7 @@ public class Manager implements Closeable {
var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript);
try { try {
var startTime = System.currentTimeMillis(); return messageSender.sendSyncMessage(syncMessage, unidentifiedAccess);
messageSender.sendSyncMessage(syncMessage, unidentifiedAccess);
return SendMessageResult.success(recipient,
unidentifiedAccess.isPresent(),
false,
System.currentTimeMillis() - startTime);
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {
return SendMessageResult.identityFailure(recipient, e.getIdentityKey()); return SendMessageResult.identityFailure(recipient, e.getIdentityKey());
} }
@ -1755,7 +1674,7 @@ public class Manager implements Closeable {
private SendMessageResult sendMessage( private SendMessageResult sendMessage(
RecipientId recipientId, SignalServiceDataMessage message RecipientId recipientId, SignalServiceDataMessage message
) throws IOException { ) throws IOException {
var messageSender = createMessageSender(); var messageSender = dependencies.getMessageSender();
final var address = resolveSignalServiceAddress(recipientId); final var address = resolveSignalServiceAddress(recipientId);
try { try {
@ -1777,7 +1696,7 @@ public class Manager implements Closeable {
} }
private SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException { private SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException {
var messageSender = createMessageSender(); var messageSender = dependencies.getMessageSender();
final var address = resolveSignalServiceAddress(recipientId); final var address = resolveSignalServiceAddress(recipientId);
try { try {
@ -1793,12 +1712,8 @@ public class Manager implements Closeable {
} }
} }
private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException { private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException {
var cipher = new SignalServiceCipher(account.getSelfAddress(), return dependencies.getCipher().decrypt(envelope);
account.getSignalProtocolStore(),
sessionLock,
certificateValidator);
return cipher.decrypt(envelope);
} }
private void handleEndSession(RecipientId recipientId) { private void handleEndSession(RecipientId recipientId) {
@ -2082,7 +1997,8 @@ public class Manager implements Closeable {
Set<HandleAction> queuedActions = null; Set<HandleAction> queuedActions = null;
final var messagePipe = getOrCreateMessagePipe(); final var signalWebSocket = dependencies.getSignalWebSocket();
signalWebSocket.connect();
var hasCaughtUpWithOldMessages = false; var hasCaughtUpWithOldMessages = false;
@ -2094,7 +2010,7 @@ public class Manager implements Closeable {
account.setLastReceiveTimestamp(System.currentTimeMillis()); account.setLastReceiveTimestamp(System.currentTimeMillis());
logger.debug("Checking for new message from server"); logger.debug("Checking for new message from server");
try { try {
var result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> { var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> {
final var recipientId = envelope1.hasSource() final var recipientId = envelope1.hasSource()
? resolveRecipient(envelope1.getSourceIdentifier()) ? resolveRecipient(envelope1.getSourceIdentifier())
: null; : null;
@ -2132,6 +2048,10 @@ public class Manager implements Closeable {
} else { } else {
throw e; throw e;
} }
} catch (WebSocketUnavailableException e) {
logger.debug("Pipe unexpectedly unavailable, connecting");
signalWebSocket.connect();
continue;
} catch (TimeoutException e) { } catch (TimeoutException e) {
if (returnOnTimeout) return; if (returnOnTimeout) return;
continue; continue;
@ -2602,12 +2522,11 @@ public class Manager implements Closeable {
private void retrieveGroupV2Avatar( private void retrieveGroupV2Avatar(
GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream
) throws IOException { ) throws IOException {
var groupOperations = groupsV2Operations.forGroup(groupSecretParams); var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
var tmpFile = IOUtils.createTempFile(); var tmpFile = IOUtils.createTempFile();
try (InputStream input = messageReceiver.retrieveGroupsV2ProfileAvatar(cdnKey, try (InputStream input = dependencies.getMessageReceiver()
tmpFile, .retrieveGroupsV2ProfileAvatar(cdnKey, tmpFile, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
var encryptedData = IOUtils.readFully(input); var encryptedData = IOUtils.readFully(input);
var decryptedData = groupOperations.decryptAvatar(encryptedData); var decryptedData = groupOperations.decryptAvatar(encryptedData);
@ -2627,10 +2546,11 @@ public class Manager implements Closeable {
String avatarPath, ProfileKey profileKey, OutputStream outputStream String avatarPath, ProfileKey profileKey, OutputStream outputStream
) throws IOException { ) throws IOException {
var tmpFile = IOUtils.createTempFile(); var tmpFile = IOUtils.createTempFile();
try (var input = messageReceiver.retrieveProfileAvatar(avatarPath, try (var input = dependencies.getMessageReceiver()
tmpFile, .retrieveProfileAvatar(avatarPath,
profileKey, tmpFile,
ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { profileKey,
ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
// Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ... // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ...
IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
} finally { } finally {
@ -2678,7 +2598,8 @@ public class Manager implements Closeable {
private InputStream retrieveAttachmentAsStream( private InputStream retrieveAttachmentAsStream(
SignalServiceAttachmentPointer pointer, File tmpFile SignalServiceAttachmentPointer pointer, File tmpFile
) throws IOException, InvalidMessageException, MissingConfigurationException { ) throws IOException, InvalidMessageException, MissingConfigurationException {
return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); return dependencies.getMessageReceiver()
.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
} }
void sendGroups() throws IOException, UntrustedIdentityException { void sendGroups() throws IOException, UntrustedIdentityException {
@ -2979,7 +2900,10 @@ public class Manager implements Closeable {
} }
private void enqueueJob(Job job) { private void enqueueJob(Job job) {
var context = new Context(account, accountManager, messageReceiver, stickerPackStore); var context = new Context(account,
dependencies.getAccountManager(),
dependencies.getMessageReceiver(),
stickerPackStore);
job.run(context); job.run(context);
} }
@ -2991,15 +2915,7 @@ public class Manager implements Closeable {
void close(boolean closeAccount) throws IOException { void close(boolean closeAccount) throws IOException {
executor.shutdown(); executor.shutdown();
if (messagePipe != null) { dependencies.getSignalWebSocket().disconnect();
messagePipe.shutdown();
messagePipe = null;
}
if (unidentifiedMessagePipe != null) {
unidentifiedMessagePipe.shutdown();
unidentifiedMessagePipe = null;
}
if (closeAccount && account != null) { if (closeAccount && account != null) {
account.close(); account.close();

View file

@ -31,8 +31,6 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import java.io.File; import java.io.File;
@ -61,7 +59,6 @@ public class ProvisioningManager {
tempIdentityKey = KeyUtils.generateIdentityKeyPair(); tempIdentityKey = KeyUtils.generateIdentityKeyPair();
registrationId = KeyHelper.generateRegistrationId(false); registrationId = KeyHelper.generateRegistrationId(false);
password = KeyUtils.createPassword(); password = KeyUtils.createPassword();
final SleepTimer timer = new UptimeSleepTimer();
GroupsV2Operations groupsV2Operations; GroupsV2Operations groupsV2Operations;
try { try {
groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration())); groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()));
@ -72,8 +69,7 @@ public class ProvisioningManager {
new DynamicCredentialsProvider(null, null, password, SignalServiceAddress.DEFAULT_DEVICE_ID), new DynamicCredentialsProvider(null, null, password, SignalServiceAddress.DEFAULT_DEVICE_ID),
userAgent, userAgent,
groupsV2Operations, groupsV2Operations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY, ServiceConfig.AUTOMATIC_NETWORK_RETRY);
timer);
} }
public static ProvisioningManager init( public static ProvisioningManager init(
@ -162,7 +158,7 @@ public class ProvisioningManager {
} }
} }
private boolean canRelinkExistingAccount(final String number) throws UserAlreadyExists, IOException { private boolean canRelinkExistingAccount(final String number) throws IOException {
final SignalAccount signalAccount; final SignalAccount signalAccount;
try { try {
signalAccount = SignalAccount.load(pathConfig.getDataPath(), number, false); signalAccount = SignalAccount.load(pathConfig.getDataPath(), number, false);

View file

@ -33,8 +33,6 @@ import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.LockedException; import org.whispersystems.signalservice.internal.push.LockedException;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
@ -68,7 +66,6 @@ public class RegistrationManager implements Closeable {
this.serviceEnvironmentConfig = serviceEnvironmentConfig; this.serviceEnvironmentConfig = serviceEnvironmentConfig;
this.userAgent = userAgent; this.userAgent = userAgent;
final SleepTimer timer = new UptimeSleepTimer();
GroupsV2Operations groupsV2Operations; GroupsV2Operations groupsV2Operations;
try { try {
groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration())); groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()));
@ -81,8 +78,7 @@ public class RegistrationManager implements Closeable {
null, account.getUsername(), account.getPassword(), SignalServiceAddress.DEFAULT_DEVICE_ID), null, account.getUsername(), account.getPassword(), SignalServiceAddress.DEFAULT_DEVICE_ID),
userAgent, userAgent,
groupsV2Operations, groupsV2Operations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY, ServiceConfig.AUTOMATIC_NETWORK_RETRY);
timer);
final var keyBackupService = accountManager.getKeyBackupService(ServiceConfig.getIasKeyStore(), final var keyBackupService = accountManager.getKeyBackupService(ServiceConfig.getIasKeyStore(),
serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(), serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(),
serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(), serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(),

View file

@ -0,0 +1,150 @@
package org.asamk.signal.manager;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
import org.whispersystems.signalservice.api.websocket.WebSocketFactory;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import java.util.concurrent.ExecutorService;
import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
public class SignalDependencies {
private final SignalServiceAccountManager accountManager;
private final GroupsV2Api groupsV2Api;
private final GroupsV2Operations groupsV2Operations;
private final SignalWebSocket signalWebSocket;
private final SignalServiceMessageReceiver messageReceiver;
private final SignalServiceMessageSender messageSender;
private final KeyBackupService keyBackupService;
private final ProfileService profileService;
private final SignalServiceCipher cipher;
public SignalDependencies(
final SignalServiceAddress selfAddress,
final ServiceEnvironmentConfig serviceEnvironmentConfig,
final String userAgent,
final DynamicCredentialsProvider credentialsProvider,
final SignalServiceDataStore dataStore,
final ExecutorService executor,
final SignalSessionLock sessionLock
) {
this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create(
serviceEnvironmentConfig.getSignalServiceConfiguration())) : null;
final SleepTimer timer = new UptimeSleepTimer();
this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
credentialsProvider,
userAgent,
groupsV2Operations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
this.groupsV2Api = accountManager.getGroupsV2Api();
this.keyBackupService = accountManager.getKeyBackupService(ServiceConfig.getIasKeyStore(),
serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(),
serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(),
serviceEnvironmentConfig.getKeyBackupConfig().getMrenclave(),
10);
final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create(
serviceEnvironmentConfig.getSignalServiceConfiguration()).getProfileOperations() : null;
this.messageReceiver = new SignalServiceMessageReceiver(serviceEnvironmentConfig.getSignalServiceConfiguration(),
credentialsProvider,
userAgent,
clientZkProfileOperations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
final WebSocketFactory webSocketFactory = new WebSocketFactory() {
@Override
public WebSocketConnection createWebSocket() {
return new WebSocketConnection("normal",
serviceEnvironmentConfig.getSignalServiceConfiguration(),
Optional.of(credentialsProvider),
userAgent,
healthMonitor);
}
@Override
public WebSocketConnection createUnidentifiedWebSocket() {
return new WebSocketConnection("unidentified",
serviceEnvironmentConfig.getSignalServiceConfiguration(),
Optional.absent(),
userAgent,
healthMonitor);
}
};
this.signalWebSocket = new SignalWebSocket(webSocketFactory);
healthMonitor.monitor(signalWebSocket);
this.profileService = new ProfileService(clientZkProfileOperations, messageReceiver, signalWebSocket);
final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.getUnidentifiedSenderTrustRoot());
this.cipher = new SignalServiceCipher(selfAddress, dataStore, sessionLock, certificateValidator);
this.messageSender = new SignalServiceMessageSender(serviceEnvironmentConfig.getSignalServiceConfiguration(),
credentialsProvider,
dataStore,
sessionLock,
userAgent,
signalWebSocket,
Optional.absent(),
clientZkProfileOperations,
executor,
ServiceConfig.MAX_ENVELOPE_SIZE,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
}
public SignalServiceAccountManager getAccountManager() {
return accountManager;
}
public GroupsV2Api getGroupsV2Api() {
return groupsV2Api;
}
public GroupsV2Operations getGroupsV2Operations() {
return groupsV2Operations;
}
public SignalWebSocket getSignalWebSocket() {
return signalWebSocket;
}
public SignalServiceMessageReceiver getMessageReceiver() {
return messageReceiver;
}
public SignalServiceMessageSender getMessageSender() {
return messageSender;
}
public KeyBackupService getKeyBackupService() {
return keyBackupService;
}
public ProfileService getProfileService() {
return profileService;
}
public SignalServiceCipher getCipher() {
return cipher;
}
}

View file

@ -0,0 +1,200 @@
package org.asamk.signal.manager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.guava.Preconditions;
import org.whispersystems.signalservice.api.SignalWebSocket;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.websocket.HealthMonitor;
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.schedulers.Schedulers;
/**
* Monitors the health of the identified and unidentified WebSockets. If either one appears to be
* unhealthy, will trigger restarting both.
* <p>
* The monitor is also responsible for sending heartbeats/keep-alive messages to prevent
* timeouts.
*/
public final class SignalWebSocketHealthMonitor implements HealthMonitor {
private final static Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class);
private static final long KEEP_ALIVE_SEND_CADENCE = TimeUnit.SECONDS.toMillis(WebSocketConnection.KEEPALIVE_TIMEOUT_SECONDS);
private static final long MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE = KEEP_ALIVE_SEND_CADENCE * 3;
private SignalWebSocket signalWebSocket;
private final SleepTimer sleepTimer;
private volatile KeepAliveSender keepAliveSender;
private final HealthState identified = new HealthState();
private final HealthState unidentified = new HealthState();
public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) {
this.sleepTimer = sleepTimer;
}
public void monitor(SignalWebSocket signalWebSocket) {
Preconditions.checkNotNull(signalWebSocket);
Preconditions.checkArgument(this.signalWebSocket == null, "monitor can only be called once");
this.signalWebSocket = signalWebSocket;
//noinspection ResultOfMethodCallIgnored
signalWebSocket.getWebSocketState()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe(s -> onStateChange(s, identified));
//noinspection ResultOfMethodCallIgnored
signalWebSocket.getUnidentifiedWebSocketState()
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe(s -> onStateChange(s, unidentified));
}
private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) {
switch (connectionState) {
case CONNECTED:
logger.debug("WebSocket is now connected");
break;
case AUTHENTICATION_FAILED:
logger.debug("WebSocket authentication failed");
break;
case FAILED:
logger.debug("WebSocket connection failed");
break;
}
healthState.needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED;
if (keepAliveSender == null && isKeepAliveNecessary()) {
keepAliveSender = new KeepAliveSender();
keepAliveSender.start();
} else if (keepAliveSender != null && !isKeepAliveNecessary()) {
keepAliveSender.shutdown();
keepAliveSender = null;
}
}
@Override
public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) {
if (isIdentifiedWebSocket) {
identified.lastKeepAliveReceived = System.currentTimeMillis();
} else {
unidentified.lastKeepAliveReceived = System.currentTimeMillis();
}
}
@Override
public void onMessageError(int status, boolean isIdentifiedWebSocket) {
if (status == 409) {
HealthState healthState = (isIdentifiedWebSocket ? identified : unidentified);
if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) {
logger.warn("Received too many mismatch device errors, forcing new websockets.");
signalWebSocket.forceNewWebSockets();
}
}
}
private boolean isKeepAliveNecessary() {
return identified.needsKeepAlive || unidentified.needsKeepAlive;
}
private static class HealthState {
private final HttpErrorTracker mismatchErrorTracker = new HttpErrorTracker(5, TimeUnit.MINUTES.toMillis(1));
private volatile boolean needsKeepAlive;
private volatile long lastKeepAliveReceived;
}
/**
* Sends periodic heartbeats/keep-alives over both WebSockets to prevent connection timeouts. If
* either WebSocket fails 3 times to get a return heartbeat both are forced to be recreated.
*/
private class KeepAliveSender extends Thread {
private volatile boolean shouldKeepRunning = true;
public void run() {
identified.lastKeepAliveReceived = System.currentTimeMillis();
unidentified.lastKeepAliveReceived = System.currentTimeMillis();
while (shouldKeepRunning && isKeepAliveNecessary()) {
try {
sleepTimer.sleep(KEEP_ALIVE_SEND_CADENCE);
if (shouldKeepRunning && isKeepAliveNecessary()) {
long keepAliveRequiredSinceTime = System.currentTimeMillis()
- MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE;
if (identified.lastKeepAliveReceived < keepAliveRequiredSinceTime
|| unidentified.lastKeepAliveReceived < keepAliveRequiredSinceTime) {
logger.warn("Missed keep alives, identified last: "
+ identified.lastKeepAliveReceived
+ " unidentified last: "
+ unidentified.lastKeepAliveReceived
+ " needed by: "
+ keepAliveRequiredSinceTime);
signalWebSocket.forceNewWebSockets();
} else {
signalWebSocket.sendKeepAlive();
}
}
} catch (Throwable e) {
logger.warn("Error occured in KeepAliveSender, ignoring ...", e);
}
}
}
public void shutdown() {
shouldKeepRunning = false;
}
}
private final static class HttpErrorTracker {
private final long[] timestamps;
private final long errorTimeRange;
public HttpErrorTracker(int samples, long errorTimeRange) {
this.timestamps = new long[samples];
this.errorTimeRange = errorTimeRange;
}
public synchronized boolean addSample(long now) {
long errorsMustBeAfter = now - errorTimeRange;
int count = 1;
int minIndex = 0;
for (int i = 0; i < timestamps.length; i++) {
if (timestamps[i] < errorsMustBeAfter) {
timestamps[i] = 0;
} else if (timestamps[i] != 0) {
count++;
}
if (timestamps[i] < timestamps[minIndex]) {
minIndex = i;
}
}
timestamps[minIndex] = now;
if (count >= timestamps.length) {
Arrays.fill(timestamps, 0);
return true;
}
return false;
}
}
}

View file

@ -34,7 +34,12 @@ public class ServiceConfig {
} catch (Throwable ignored) { } catch (Throwable ignored) {
zkGroupAvailable = false; zkGroupAvailable = false;
} }
capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable, false); capabilities = new AccountAttributes.Capabilities(false,
zkGroupAvailable,
false,
zkGroupAvailable,
false,
false);
try { try {
TrustStore contactTrustStore = new IasTrustStore(); TrustStore contactTrustStore = new IasTrustStore();

View file

@ -207,7 +207,7 @@ public class GroupV2Helper {
var change = name != null ? groupOperations.createModifyGroupTitle(name) : GroupChange.Actions.newBuilder(); var change = name != null ? groupOperations.createModifyGroupTitle(name) : GroupChange.Actions.newBuilder();
if (description != null) { if (description != null) {
change.setModifyDescription(groupOperations.createModifyGroupDescription(description)); change.setModifyDescription(groupOperations.createModifyGroupDescriptionAction(description));
} }
if (avatarFile != null) { if (avatarFile != null) {

View file

@ -1,8 +0,0 @@
package org.asamk.signal.manager.helper;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
public interface MessagePipeProvider {
SignalServiceMessagePipe getMessagePipe(boolean unidentified);
}

View file

@ -9,14 +9,12 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.internal.util.concurrent.CascadingFuture; import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture; import org.whispersystems.signalservice.internal.ServiceResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutionException; import io.reactivex.rxjava3.core.Single;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public final class ProfileHelper { public final class ProfileHelper {
@ -24,7 +22,7 @@ public final class ProfileHelper {
private final UnidentifiedAccessProvider unidentifiedAccessProvider; private final UnidentifiedAccessProvider unidentifiedAccessProvider;
private final MessagePipeProvider messagePipeProvider; private final ProfileServiceProvider profileServiceProvider;
private final MessageReceiverProvider messageReceiverProvider; private final MessageReceiverProvider messageReceiverProvider;
@ -33,13 +31,13 @@ public final class ProfileHelper {
public ProfileHelper( public ProfileHelper(
final ProfileKeyProvider profileKeyProvider, final ProfileKeyProvider profileKeyProvider,
final UnidentifiedAccessProvider unidentifiedAccessProvider, final UnidentifiedAccessProvider unidentifiedAccessProvider,
final MessagePipeProvider messagePipeProvider, final ProfileServiceProvider profileServiceProvider,
final MessageReceiverProvider messageReceiverProvider, final MessageReceiverProvider messageReceiverProvider,
final SignalServiceAddressResolver addressResolver final SignalServiceAddressResolver addressResolver
) { ) {
this.profileKeyProvider = profileKeyProvider; this.profileKeyProvider = profileKeyProvider;
this.unidentifiedAccessProvider = unidentifiedAccessProvider; this.unidentifiedAccessProvider = unidentifiedAccessProvider;
this.messagePipeProvider = messagePipeProvider; this.profileServiceProvider = profileServiceProvider;
this.messageReceiverProvider = messageReceiverProvider; this.messageReceiverProvider = messageReceiverProvider;
this.addressResolver = addressResolver; this.addressResolver = addressResolver;
} }
@ -48,8 +46,8 @@ public final class ProfileHelper {
RecipientId recipientId, SignalServiceProfile.RequestType requestType RecipientId recipientId, SignalServiceProfile.RequestType requestType
) throws IOException { ) throws IOException {
try { try {
return retrieveProfile(recipientId, requestType).get(10, TimeUnit.SECONDS); return retrieveProfile(recipientId, requestType).blockingGet();
} catch (ExecutionException e) { } catch (RuntimeException e) {
if (e.getCause() instanceof PushNetworkException) { if (e.getCause() instanceof PushNetworkException) {
throw (PushNetworkException) e.getCause(); throw (PushNetworkException) e.getCause();
} else if (e.getCause() instanceof NotFoundException) { } else if (e.getCause() instanceof NotFoundException) {
@ -57,79 +55,55 @@ public final class ProfileHelper {
} else { } else {
throw new IOException(e); throw new IOException(e);
} }
} catch (InterruptedException | TimeoutException e) {
throw new PushNetworkException(e);
} }
} }
public ListenableFuture<ProfileAndCredential> retrieveProfile( public SignalServiceProfile retrieveProfileSync(String username) throws IOException {
return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent());
}
public Single<ProfileAndCredential> retrieveProfile(
RecipientId recipientId, SignalServiceProfile.RequestType requestType RecipientId recipientId, SignalServiceProfile.RequestType requestType
) { ) throws IOException {
var unidentifiedAccess = getUnidentifiedAccess(recipientId); var unidentifiedAccess = getUnidentifiedAccess(recipientId);
var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId)); var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId));
final var address = addressResolver.resolveSignalServiceAddress(recipientId); final var address = addressResolver.resolveSignalServiceAddress(recipientId);
if (unidentifiedAccess.isPresent()) { return retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address,
profileKey,
unidentifiedAccess,
requestType),
() -> getSocketRetrievalFuture(address, profileKey, unidentifiedAccess, requestType),
() -> getPipeRetrievalFuture(address, profileKey, Optional.absent(), requestType),
() -> getSocketRetrievalFuture(address, profileKey, Optional.absent(), requestType)),
e -> !(e instanceof NotFoundException));
} else {
return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address,
profileKey,
Optional.absent(),
requestType), () -> getSocketRetrievalFuture(address, profileKey, Optional.absent(), requestType)),
e -> !(e instanceof NotFoundException));
}
} }
private ListenableFuture<ProfileAndCredential> getPipeRetrievalFuture( private Single<ProfileAndCredential> retrieveProfile(
SignalServiceAddress address, SignalServiceAddress address,
Optional<ProfileKey> profileKey, Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess, Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType SignalServiceProfile.RequestType requestType
) throws IOException { ) throws IOException {
var unidentifiedPipe = messagePipeProvider.getMessagePipe(true); var profileService = profileServiceProvider.getProfileService();
var pipe = unidentifiedPipe != null && unidentifiedAccess.isPresent()
? unidentifiedPipe
: messagePipeProvider.getMessagePipe(false);
if (pipe != null) {
try {
return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
} catch (NoClassDefFoundError e) {
// Native zkgroup lib not available for ProfileKey
if (!address.getNumber().isPresent()) {
throw new NotFoundException("Can't request profile without number");
}
var addressWithoutUuid = new SignalServiceAddress(Optional.absent(), address.getNumber());
return pipe.getProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType);
}
}
throw new IOException("No pipe available!"); Single<ServiceResponse<ProfileAndCredential>> responseSingle;
}
private ListenableFuture<ProfileAndCredential> getSocketRetrievalFuture(
SignalServiceAddress address,
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType
) throws NotFoundException {
var receiver = messageReceiverProvider.getMessageReceiver();
try { try {
return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType); responseSingle = profileService.getProfile(address, profileKey, unidentifiedAccess, requestType);
} catch (NoClassDefFoundError e) { } catch (NoClassDefFoundError e) {
// Native zkgroup lib not available for ProfileKey // Native zkgroup lib not available for ProfileKey
if (!address.getNumber().isPresent()) { if (!address.getNumber().isPresent()) {
throw new NotFoundException("Can't request profile without number"); throw new NotFoundException("Can't request profile without number");
} }
var addressWithoutUuid = new SignalServiceAddress(Optional.absent(), address.getNumber()); var addressWithoutUuid = new SignalServiceAddress(Optional.absent(), address.getNumber());
return receiver.retrieveProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType); responseSingle = profileService.getProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType);
} }
return responseSingle.map(pair -> {
var processor = new ProfileService.ProfileResponseProcessor(pair);
if (processor.hasResult()) {
return processor.getResult();
} else if (processor.notFound()) {
throw new NotFoundException("Profile not found");
} else {
throw pair.getExecutionError()
.or(pair.getApplicationError())
.or(new IOException("Unknown error while retrieving profile"));
}
});
} }
private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) { private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) {

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.helper;
import org.whispersystems.signalservice.api.services.ProfileService;
public interface ProfileServiceProvider {
ProfileService getProfileService();
}

View file

@ -168,7 +168,11 @@ public class SignalAccount implements Closeable {
recipientStore::resolveRecipient, recipientStore::resolveRecipient,
identityKey, identityKey,
registrationId); registrationId);
signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore); signalProtocolStore = new SignalProtocolStore(preKeyStore,
signedPreKeyStore,
sessionStore,
identityKeyStore,
this::isMultiDevice);
messageCache = new MessageCache(getMessageCachePath(dataPath, username)); messageCache = new MessageCache(getMessageCachePath(dataPath, username));
} }

View file

@ -12,7 +12,7 @@ import org.whispersystems.libsignal.state.PreKeyStore;
import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore; import org.whispersystems.libsignal.state.SignedPreKeyStore;
import org.whispersystems.signalservice.api.SignalServiceProtocolStore; import org.whispersystems.signalservice.api.SignalServiceDataStore;
import org.whispersystems.signalservice.api.SignalServiceSessionStore; import org.whispersystems.signalservice.api.SignalServiceSessionStore;
import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.DistributionId;
@ -20,24 +20,28 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.function.Supplier;
public class SignalProtocolStore implements SignalServiceProtocolStore { public class SignalProtocolStore implements SignalServiceDataStore {
private final PreKeyStore preKeyStore; private final PreKeyStore preKeyStore;
private final SignedPreKeyStore signedPreKeyStore; private final SignedPreKeyStore signedPreKeyStore;
private final SignalServiceSessionStore sessionStore; private final SignalServiceSessionStore sessionStore;
private final IdentityKeyStore identityKeyStore; private final IdentityKeyStore identityKeyStore;
private final Supplier<Boolean> isMultiDevice;
public SignalProtocolStore( public SignalProtocolStore(
final PreKeyStore preKeyStore, final PreKeyStore preKeyStore,
final SignedPreKeyStore signedPreKeyStore, final SignedPreKeyStore signedPreKeyStore,
final SignalServiceSessionStore sessionStore, final SignalServiceSessionStore sessionStore,
final IdentityKeyStore identityKeyStore final IdentityKeyStore identityKeyStore,
final Supplier<Boolean> isMultiDevice
) { ) {
this.preKeyStore = preKeyStore; this.preKeyStore = preKeyStore;
this.signedPreKeyStore = signedPreKeyStore; this.signedPreKeyStore = signedPreKeyStore;
this.sessionStore = sessionStore; this.sessionStore = sessionStore;
this.identityKeyStore = identityKeyStore; this.identityKeyStore = identityKeyStore;
this.isMultiDevice = isMultiDevice;
} }
@Override @Override
@ -177,9 +181,12 @@ public class SignalProtocolStore implements SignalServiceProtocolStore {
} }
@Override @Override
public void clearSenderKeySharedWith( public void clearSenderKeySharedWith(final Collection<SignalProtocolAddress> addresses) {
final DistributionId distributionId, final Collection<SignalProtocolAddress> addresses
) {
// TODO // TODO
} }
@Override
public boolean isMultiDevice() {
return isMultiDevice.get();
}
} }