From: AsamK Date: Fri, 31 Mar 2023 15:16:59 +0000 (+0200) Subject: Update libsignal-service-java X-Git-Tag: v0.11.8~13 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/276ecef3009baecb740f23d53ddaa81f487d57a2 Update libsignal-service-java - Use session based number verification and registration --- diff --git a/graalvm-config-dir/jni-config.json b/graalvm-config-dir/jni-config.json index b168120e..09c4a05a 100644 --- a/graalvm-config-dir/jni-config.json +++ b/graalvm-config-dir/jni-config.json @@ -76,6 +76,13 @@ {"name":"storeSession","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.state.SessionRecord"] } ] }, +{ + "name":"org.asamk.signal.manager.storage.senderKeys.SenderKeyStore", + "methods":[ + {"name":"loadSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID"] }, + {"name":"storeSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID","org.signal.libsignal.protocol.groups.state.SenderKeyRecord"] } + ] +}, { "name":"org.graalvm.jniutils.JNIExceptionWrapperEntryPoints", "methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }] diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index b9c3c280..d34b2243 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -78,22 +78,10 @@ "allDeclaredFields":true, "allDeclaredMethods":true }, -{ - "name":"com.google.protobuf.DescriptorMessageInfoFactory" -}, -{ - "name":"com.google.protobuf.ExtensionRegistry" -}, -{ - "name":"com.google.protobuf.ExtensionSchemaFull" -}, { "name":"com.google.protobuf.GeneratedMessageLite", "fields":[{"name":"unknownFields"}] }, -{ - "name":"com.google.protobuf.GeneratedMessageV3" -}, { "name":"com.google.protobuf.Internal$LongList", "allDeclaredMethods":true @@ -108,19 +96,10 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, -{ - "name":"com.google.protobuf.MapFieldSchemaFull" -}, -{ - "name":"com.google.protobuf.NewInstanceSchemaFull" -}, { "name":"com.google.protobuf.PrimitiveNonBoxingCollection", "allDeclaredMethods":true }, -{ - "name":"com.google.protobuf.UnknownFieldSetSchema" -}, { "name":"com.sun.crypto.provider.AESCipher$General", "methods":[{"name":"","parameterTypes":[] }] @@ -465,9 +444,6 @@ { "name":"javax.smartcardio.CardPermission" }, -{ - "name":"libcore.io.Memory" -}, { "name":"long", "allDeclaredMethods":true, @@ -982,10 +958,6 @@ {"name":"startColor","parameterTypes":[] } ] }, -{ - "name":"org.asamk.signal.json.JsonStreamSerializer", - "methods":[{"name":"","parameterTypes":[] }] -}, { "name":"org.asamk.signal.json.JsonSyncDataMessage", "allDeclaredFields":true, @@ -1819,9 +1791,6 @@ "name":"org.freedesktop.dbus.interfaces.Properties$PropertiesChanged", "allPublicConstructors":true }, -{ - "name":"org.robolectric.Robolectric" -}, { "name":"org.signal.cdsi.proto.ClientRequest", "fields":[ @@ -2288,7 +2257,6 @@ "queryAllDeclaredMethods":true, "queryAllDeclaredConstructors":true, "methods":[ - {"name":"getCode","parameterTypes":[] }, {"name":"getNumber","parameterTypes":[] }, {"name":"getRegistrationLock","parameterTypes":[] } ] @@ -2712,6 +2680,25 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, +{ + "name":"org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.Integer","java.lang.Integer","java.lang.Integer","boolean","java.util.List","boolean"] }] +}, +{ + "name":"org.whispersystems.signalservice.internal.push.RegistrationSessionRequestBody", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[ + {"name":"getAccountAttributes","parameterTypes":[] }, + {"name":"getRecoveryPassword","parameterTypes":[] }, + {"name":"getSessionId","parameterTypes":[] }, + {"name":"getSkipDeviceTransfer","parameterTypes":[] } + ] +}, { "name":"org.whispersystems.signalservice.internal.push.SendGroupMessageResponse", "allDeclaredFields":true, @@ -2896,16 +2883,6 @@ {"name":"timestamp_"} ] }, -{ - "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$BodyRange", - "fields":[ - {"name":"associatedValueCase_"}, - {"name":"associatedValue_"}, - {"name":"bitField0_"}, - {"name":"length_"}, - {"name":"start_"} - ] -}, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact", "fields":[ @@ -3445,6 +3422,33 @@ "queryAllDeclaredConstructors":true, "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"org.whispersystems.signalservice.internal.push.UpdateVerificationSessionRequestBody", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[ + {"name":"getCaptcha","parameterTypes":[] }, + {"name":"getMcc","parameterTypes":[] }, + {"name":"getMnc","parameterTypes":[] }, + {"name":"getPushChallenge","parameterTypes":[] }, + {"name":"getPushToken","parameterTypes":[] }, + {"name":"getPushTokenType","parameterTypes":[] } + ] +}, +{ + "name":"org.whispersystems.signalservice.internal.push.VerificationSessionMetadataRequestBody", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[ + {"name":"getMcc","parameterTypes":[] }, + {"name":"getMnc","parameterTypes":[] }, + {"name":"getNumber","parameterTypes":[] }, + {"name":"getPushToken","parameterTypes":[] }, + {"name":"getPushTokenType","parameterTypes":[] } + ] +}, { "name":"org.whispersystems.signalservice.internal.push.VerifyAccountResponse", "allDeclaredFields":true, @@ -3544,6 +3548,7 @@ "name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord", "fields":[ {"name":"identifiers_"}, + {"name":"sourceDevice_"}, {"name":"version_"} ] }, diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index a92792c7..8ba2bf70 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -4,6 +4,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.PinLockedException; +import org.asamk.signal.manager.api.RateLimitException; import java.io.Closeable; import java.io.IOException; @@ -12,7 +13,7 @@ public interface RegistrationManager extends Closeable { void register( boolean voiceVerification, String captcha - ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException; + ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException; void verifyAccount( String verificationCode, String pin diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java index 4b7c8362..f5bff4d2 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java @@ -20,6 +20,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.PinLockedException; +import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; @@ -27,6 +28,7 @@ import org.asamk.signal.manager.helper.AccountFileUpdater; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.util.NumberVerificationUtils; +import org.asamk.signal.manager.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.SignalServiceAccountManager; @@ -35,15 +37,13 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.internal.ServiceResponse; +import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import java.io.IOException; import java.util.function.Consumer; -import static org.asamk.signal.manager.config.ServiceConfig.capabilities; - class RegistrationManagerImpl implements RegistrationManager { private final static Logger logger = LoggerFactory.getLogger(RegistrationManagerImpl.class); @@ -106,7 +106,7 @@ class RegistrationManagerImpl implements RegistrationManager { @Override public void register( boolean voiceVerification, String captcha - ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException { + ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException { if (account.isRegistered() && account.getServiceEnvironment() != null && account.getServiceEnvironment() != serviceEnvironmentConfig.getType()) { @@ -117,14 +117,21 @@ class RegistrationManagerImpl implements RegistrationManager { return; } - NumberVerificationUtils.requestVerificationCode(accountManager, captcha, voiceVerification); + String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager, + account.getSessionId(account.getNumber()), + id -> account.setSessionId(account.getNumber(), id), + voiceVerification, + captcha); + NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification); } @Override public void verifyAccount( String verificationCode, String pin ) throws IOException, PinLockedException, IncorrectPinException { - final var result = NumberVerificationUtils.verifyNumber(verificationCode, + var sessionId = account.getSessionId(account.getNumber()); + final var result = NumberVerificationUtils.verifyNumber(sessionId, + verificationCode, pin, pinHelper, this::verifyAccountWithCode); @@ -186,17 +193,7 @@ class RegistrationManagerImpl implements RegistrationManager { userAgent, null, ServiceConfig.AUTOMATIC_NETWORK_RETRY); - accountManager.setAccountAttributes(null, - account.getLocalRegistrationId(), - true, - null, - account.getRegistrationLock(), - account.getSelfUnidentifiedAccessKey(), - account.isUnrestrictedUnidentifiedAccess(), - capabilities, - account.isDiscoverableByPhoneNumber(), - account.getEncryptedDeviceName(), - account.getLocalPniRegistrationId()); + accountManager.setAccountAttributes(account.getAccountAttributes(null)); account.setRegistered(true); logger.info("Reactivated existing account, verify is not necessary."); if (newManagerListener != null) { @@ -215,29 +212,18 @@ class RegistrationManagerImpl implements RegistrationManager { return false; } - private ServiceResponse verifyAccountWithCode( - final String verificationCode, final String registrationLock - ) { - if (registrationLock == null) { - return accountManager.verifyAccount(verificationCode, - account.getLocalRegistrationId(), - true, - account.getSelfUnidentifiedAccessKey(), - account.isUnrestrictedUnidentifiedAccess(), - ServiceConfig.capabilities, - account.isDiscoverableByPhoneNumber(), - account.getLocalPniRegistrationId()); - } else { - return accountManager.verifyAccountWithRegistrationLockPin(verificationCode, - account.getLocalRegistrationId(), - true, - registrationLock, - account.getSelfUnidentifiedAccessKey(), - account.isUnrestrictedUnidentifiedAccess(), - ServiceConfig.capabilities, - account.isDiscoverableByPhoneNumber(), - account.getLocalPniRegistrationId()); + private VerifyAccountResponse verifyAccountWithCode( + final String sessionId, final String verificationCode, final String registrationLock + ) throws IOException { + try { + Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId)); + } catch (AlreadyVerifiedException e) { + // Already verified so can continue registering } + return Utils.handleResponseException(accountManager.registerAccount(sessionId, + null, + account.getAccountAttributes(registrationLock), + true)); } @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java index 3b3b375a..f4489ea7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java @@ -88,6 +88,10 @@ public class SignalDependencies { return serviceEnvironmentConfig; } + public SignalSessionLock getSessionLock() { + return sessionLock; + } + public SignalServiceAccountManager getAccountManager() { return getOrCreate(() -> accountManager, () -> accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(), @@ -115,7 +119,7 @@ public class SignalDependencies { public GroupsV2Operations getGroupsV2Operations() { return getOrCreate(() -> groupsV2Operations, - () -> groupsV2Operations = capabilities.isGv2() + () -> groupsV2Operations = capabilities.getGv2() ? new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()), ServiceConfig.GROUP_MAX_SIZE) : null); @@ -123,7 +127,7 @@ public class SignalDependencies { private ClientZkOperations getClientZkOperations() { return getOrCreate(() -> clientZkOperations, - () -> clientZkOperations = capabilities.isGv2() + () -> clientZkOperations = capabilities.getGv2() ? ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()) : null); } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/CaptchaRequiredException.java b/lib/src/main/java/org/asamk/signal/manager/api/CaptchaRequiredException.java index 9504d240..86fcdc93 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/CaptchaRequiredException.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/CaptchaRequiredException.java @@ -2,6 +2,13 @@ package org.asamk.signal.manager.api; public class CaptchaRequiredException extends Exception { + private long nextAttemptTimestamp; + + public CaptchaRequiredException(final long nextAttemptTimestamp) { + super("Captcha required"); + this.nextAttemptTimestamp = nextAttemptTimestamp; + } + public CaptchaRequiredException(final String message) { super(message); } @@ -9,4 +16,8 @@ public class CaptchaRequiredException extends Exception { public CaptchaRequiredException(final String message, final Throwable cause) { super(message, cause); } + + public long getNextAttemptTimestamp() { + return nextAttemptTimestamp; + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RateLimitException.java b/lib/src/main/java/org/asamk/signal/manager/api/RateLimitException.java new file mode 100644 index 00000000..54fcbbd1 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/RateLimitException.java @@ -0,0 +1,15 @@ +package org.asamk.signal.manager.api; + +public class RateLimitException extends Exception { + + private final long nextAttemptTimestamp; + + public RateLimitException(final long nextAttemptTimestamp) { + super("Rate limit"); + this.nextAttemptTimestamp = nextAttemptTimestamp; + } + + public long getNextAttemptTimestamp() { + return nextAttemptTimestamp; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java index 36cd76a3..2b6c812b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java @@ -7,10 +7,11 @@ import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.InvalidDeviceLinkException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.PinLockedException; -import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.NumberVerificationUtils; +import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; @@ -21,6 +22,7 @@ import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; +import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; @@ -128,9 +130,14 @@ public class AccountHelper { public void startChangeNumber( String newNumber, String captcha, boolean voiceVerification - ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException { + ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException { final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword()); - NumberVerificationUtils.requestVerificationCode(accountManager, captcha, voiceVerification); + String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager, + account.getSessionId(newNumber), + id -> account.setSessionId(newNumber, id), + voiceVerification, + captcha); + NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification); } public void finishChangeNumber( @@ -140,17 +147,28 @@ public class AccountHelper { final List deviceMessages = null; final Map devicePniSignedPreKeys = null; final Map pniRegistrationIds = null; - final var result = NumberVerificationUtils.verifyNumber(verificationCode, + var sessionId = account.getSessionId(account.getNumber()); + final var result = NumberVerificationUtils.verifyNumber(sessionId, + verificationCode, pin, context.getPinHelper(), - (verificationCode1, registrationLock) -> dependencies.getAccountManager() - .changeNumber(new ChangePhoneNumberRequest(newNumber, - verificationCode1, - registrationLock, - account.getPniIdentityKeyPair().getPublicKey(), - deviceMessages, - devicePniSignedPreKeys, - pniRegistrationIds))); + (sessionId1, verificationCode1, registrationLock) -> { + final var accountManager = dependencies.getAccountManager(); + try { + Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId1)); + } catch (AlreadyVerifiedException e) { + // Already verified so can continue changing number + } + return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest( + sessionId1, + null, + newNumber, + registrationLock, + account.getPniIdentityKeyPair().getPublicKey(), + deviceMessages, + devicePniSignedPreKeys, + pniRegistrationIds))); + }); // TODO handle response updateSelfIdentifiers(newNumber, account.getAci(), PNI.parseOrThrow(result.first().getPni())); } @@ -162,18 +180,7 @@ public class AccountHelper { } public void updateAccountAttributes() throws IOException { - dependencies.getAccountManager() - .setAccountAttributes(null, - account.getLocalRegistrationId(), - true, - null, - account.getRegistrationLock(), - account.getSelfUnidentifiedAccessKey(), - account.isUnrestrictedUnidentifiedAccess(), - ServiceConfig.capabilities, - account.isDiscoverableByPhoneNumber(), - account.getEncryptedDeviceName(), - account.getLocalPniRegistrationId()); + dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null)); } public void addDevice(DeviceLinkInfo deviceLinkInfo) throws IOException, InvalidDeviceLinkException { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java index 0efe90a4..b908823a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java @@ -80,7 +80,7 @@ public class IdentityHelper { final var address = account.getRecipientAddressResolver() .resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(serviceId)); - return Utils.computeSafetyNumber(capabilities.isUuid(), + return Utils.computeSafetyNumber(capabilities.getUuid(), account.getSelfRecipientAddress(), account.getAciIdentityKeyPair().getPublicKey(), address.getServiceId().equals(serviceId) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index bd42ae5f..3a834a70 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -44,12 +44,14 @@ import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.SelfSendException; import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.InvalidMessageException; +import org.signal.libsignal.protocol.groups.GroupSessionBuilder; import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; @@ -275,7 +277,8 @@ public final class IncomingMessageHandler { logger.debug("Received a sender key distribution message for distributionId {} from {}", message.getDistributionId(), protocolAddress); - dependencies.getMessageSender().processSenderKeyDistributionMessage(protocolAddress, message); + new SignalGroupSessionBuilder(dependencies.getSessionLock(), + new GroupSessionBuilder(account.getSenderKeyStore())).process(protocolAddress, message); } if (content.getDecryptionErrorMessage().isPresent()) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java index c15f4f94..2c5dbe69 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java @@ -144,22 +144,28 @@ public class ReceiveHelper { logger.debug("Checking for new message from server"); try { isWaitingForMessage = true; - var result = signalWebSocket.readOrEmpty(timeout.toMillis(), envelope1 -> { + var queueNotEmpty = signalWebSocket.readMessageBatch(timeout.toMillis(), 1, batch -> { + logger.debug("Retrieved {} envelopes!", batch.size()); isWaitingForMessage = false; - final var recipientId = envelope1.hasSourceUuid() ? account.getRecipientResolver() - .resolveRecipient(envelope1.getSourceAddress()) : null; - logger.trace("Storing new message from {}", recipientId); - // store message on disk, before acknowledging receipt to the server - cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); + for (final var it : batch) { + SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(), + it.getServerDeliveredTimestamp()); + final var recipientId = envelope1.hasSourceUuid() ? account.getRecipientResolver() + .resolveRecipient(envelope1.getSourceAddress()) : null; + logger.trace("Storing new message from {}", recipientId); + // store message on disk, before acknowledging receipt to the server + cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); + } + return true; }); isWaitingForMessage = false; backOffCounter = 0; - if (result.isPresent()) { + if (queueNotEmpty) { if (remainingMessages > 0) { remainingMessages -= 1; } - envelope = result.get(); + envelope = cachedMessage[0].loadEnvelope(); logger.debug("New message received from server"); } else { logger.debug("Received indicator that server queue is empty"); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index e80f7901..25847c6c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -62,6 +62,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.SignalServiceAccountDataStore; import org.whispersystems.signalservice.api.SignalServiceDataStore; +import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.ACI; @@ -97,6 +98,8 @@ import java.util.List; import java.util.Optional; import java.util.function.Supplier; +import static org.asamk.signal.manager.config.ServiceConfig.capabilities; + public class SignalAccount implements Closeable { private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class); @@ -119,6 +122,8 @@ public class SignalAccount implements Closeable { private String number; private ACI aci; private PNI pni; + private String sessionId; + private String sessionNumber; private String encryptedDeviceName; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private boolean isMultiDevice = false; @@ -551,6 +556,12 @@ public class SignalAccount implements Closeable { throw new IOException("Config file contains an invalid pni, needs to be a valid UUID", e); } } + if (rootNode.hasNonNull("sessionId")) { + sessionId = rootNode.get("sessionId").asText(); + } + if (rootNode.hasNonNull("sessionNumber")) { + sessionNumber = rootNode.get("sessionNumber").asText(); + } if (rootNode.hasNonNull("deviceName")) { encryptedDeviceName = rootNode.get("deviceName").asText(); } @@ -926,6 +937,8 @@ public class SignalAccount implements Closeable { .put("serviceEnvironment", serviceEnvironment == null ? null : serviceEnvironment.name()) .put("uuid", aci == null ? null : aci.toString()) .put("pni", pni == null ? null : pni.toString()) + .put("sessionId", sessionId) + .put("sessionNumber", sessionNumber) .put("deviceName", encryptedDeviceName) .put("deviceId", deviceId) .put("isMultiDevice", isMultiDevice) @@ -1293,6 +1306,21 @@ public class SignalAccount implements Closeable { save(); } + public AccountAttributes getAccountAttributes(String registrationLock) { + return new AccountAttributes(null, + getLocalRegistrationId(), + true, + null, + registrationLock != null ? registrationLock : getRegistrationLock(), + getSelfUnidentifiedAccessKey(), + isUnrestrictedUnidentifiedAccess(), + capabilities, + isDiscoverableByPhoneNumber(), + encryptedDeviceName, + getLocalPniRegistrationId(), + null); // TODO recoveryPassword? + } + public ServiceId getAccountId(ServiceIdType serviceIdType) { return serviceIdType.equals(ServiceIdType.ACI) ? aci : pni; } @@ -1347,6 +1375,19 @@ public class SignalAccount implements Closeable { return getRecipientResolver().resolveRecipient(getSelfRecipientAddress()); } + public String getSessionId(final String forNumber) { + if (!forNumber.equals(sessionNumber)) { + return null; + } + return sessionId; + } + + public void setSessionId(final String sessionNumber, final String sessionId) { + this.sessionNumber = sessionNumber; + this.sessionId = sessionId; + save(); + } + public byte[] getEncryptedDeviceName() { return encryptedDeviceName == null ? null : Base64.getDecoder().decode(encryptedDeviceName); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java b/lib/src/main/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java index c1f310e8..e053b91f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java @@ -15,21 +15,30 @@ public final class CachedMessage { private final File file; + private SignalServiceEnvelope envelope; + CachedMessage(final File file) { this.file = file; } + CachedMessage(final File file, SignalServiceEnvelope envelope) { + this.file = file; + this.envelope = envelope; + } + File getFile() { return file; } public SignalServiceEnvelope loadEnvelope() { - try { - return MessageCacheUtils.loadEnvelope(file); - } catch (Exception e) { - logger.error("Failed to load cached message envelope “{}”: {}", file, e.getMessage(), e); - return null; + if (envelope == null) { + try { + envelope = MessageCacheUtils.loadEnvelope(file); + } catch (Exception e) { + logger.error("Failed to load cached message envelope “{}”: {}", file, e.getMessage(), e); + } } + return envelope; } public void delete() { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java b/lib/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java index 4853b223..083dba06 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java @@ -54,7 +54,7 @@ public class MessageCache { try { var cacheFile = getMessageCacheFile(recipientId, now, envelope.getTimestamp()); MessageCacheUtils.storeEnvelope(envelope, cacheFile); - return new CachedMessage(cacheFile); + return new CachedMessage(cacheFile, envelope); } catch (IOException e) { logger.warn("Failed to store encrypted message in disk cache, ignoring: {}", e.getMessage()); return null; diff --git a/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java index a4c2ba24..3d733c16 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java @@ -5,41 +5,94 @@ import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.PinLockedException; +import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.helper.PinHelper; import org.whispersystems.signalservice.api.KbsPinData; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.kbs.MasterKey; +import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException; +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; +import org.whispersystems.signalservice.api.push.exceptions.PushChallengeRequiredException; +import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException; import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.push.LockedException; -import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse; +import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import java.io.IOException; import java.util.Locale; -import java.util.Optional; +import java.util.function.Consumer; public class NumberVerificationUtils { + public static String handleVerificationSession( + SignalServiceAccountManager accountManager, + String sessionId, + Consumer sessionIdSaver, + boolean voiceVerification, + String captcha + ) throws CaptchaRequiredException, IOException, RateLimitException { + RegistrationSessionMetadataResponse sessionResponse; + try { + sessionResponse = getValidSession(accountManager, sessionId); + } catch (PushChallengeRequiredException | + org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) { + if (captcha != null) { + sessionResponse = submitCaptcha(accountManager, sessionId, captcha); + } else { + throw new CaptchaRequiredException("Captcha Required"); + } + } + + sessionId = sessionResponse.getBody().getId(); + sessionIdSaver.accept(sessionId); + + if (sessionResponse.getBody().getVerified()) { + return sessionId; + } + + if (sessionResponse.getBody().getAllowedToRequestCode()) { + return sessionId; + } + + final var nextAttempt = voiceVerification + ? sessionResponse.getBody().getNextCall() + : sessionResponse.getBody().getNextSms(); + if (nextAttempt != null && nextAttempt > 0) { + final var timestamp = sessionResponse.getHeaders().getTimestamp() + nextAttempt * 1000; + throw new RateLimitException(timestamp); + } + + final var nextVerificationAttempt = sessionResponse.getBody().getNextVerificationAttempt(); + if (nextVerificationAttempt != null && nextVerificationAttempt > 0) { + final var timestamp = sessionResponse.getHeaders().getTimestamp() + nextVerificationAttempt * 1000; + throw new CaptchaRequiredException(timestamp); + } + + if (sessionResponse.getBody().getRequestedInformation().contains("captcha")) { + if (captcha != null) { + sessionResponse = submitCaptcha(accountManager, sessionId, captcha); + } + if (!sessionResponse.getBody().getAllowedToRequestCode()) { + throw new CaptchaRequiredException("Captcha Required"); + } + } + + return sessionId; + } + public static void requestVerificationCode( - SignalServiceAccountManager accountManager, String captcha, boolean voiceVerification + SignalServiceAccountManager accountManager, String sessionId, boolean voiceVerification ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException { - captcha = captcha == null ? null : captcha.replace("signalcaptcha://", ""); - final ServiceResponse response; + final ServiceResponse response; final var locale = Utils.getDefaultLocale(Locale.US); if (voiceVerification) { - response = accountManager.requestVoiceVerificationCode(locale, - Optional.ofNullable(captcha), - Optional.empty(), - Optional.empty()); + response = accountManager.requestVoiceVerificationCode(sessionId, locale, false); } else { - response = accountManager.requestSmsVerificationCode(locale, - false, - Optional.ofNullable(captcha), - Optional.empty(), - Optional.empty()); + response = accountManager.requestSmsVerificationCode(sessionId, locale, false); } try { - handleResponseException(response); + Utils.handleResponseException(response); } catch (org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) { throw new CaptchaRequiredException(e.getMessage(), e); } catch (org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException e) { @@ -51,11 +104,11 @@ public class NumberVerificationUtils { } public static Pair verifyNumber( - String verificationCode, String pin, PinHelper pinHelper, Verifier verifier + String sessionId, String verificationCode, String pin, PinHelper pinHelper, Verifier verifier ) throws IOException, PinLockedException, IncorrectPinException { verificationCode = verificationCode.replace("-", ""); try { - final var response = verifyAccountWithCode(verificationCode, null, verifier); + final var response = verifier.verify(sessionId, verificationCode, null); return new Pair<>(response, null); } catch (LockedException e) { @@ -72,7 +125,7 @@ public class NumberVerificationUtils { var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock(); VerifyAccountResponse response; try { - response = verifyAccountWithCode(verificationCode, registrationLock, verifier); + response = verifier.verify(sessionId, verificationCode, registrationLock); } catch (LockedException _e) { throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!"); } @@ -81,29 +134,53 @@ public class NumberVerificationUtils { } } - private static VerifyAccountResponse verifyAccountWithCode( - final String verificationCode, final String registrationLock, final Verifier verifier + private static RegistrationSessionMetadataResponse validateSession( + final SignalServiceAccountManager accountManager, final String sessionId ) throws IOException { - final var response = verifier.verify(verificationCode, registrationLock); - handleResponseException(response); - return response.getResult().get(); + if (sessionId == null || sessionId.isEmpty()) { + throw new NoSuchSessionException(); + } + return Utils.handleResponseException(accountManager.getRegistrationSession(sessionId)); } - private static void handleResponseException(final ServiceResponse response) throws IOException { - final var throwableOptional = response.getExecutionError().or(response::getApplicationError); - if (throwableOptional.isPresent()) { - if (throwableOptional.get() instanceof IOException) { - throw (IOException) throwableOptional.get(); - } else { - throw new IOException(throwableOptional.get()); + private static RegistrationSessionMetadataResponse requestValidSession( + final SignalServiceAccountManager accountManager + ) throws NoSuchSessionException, IOException { + return Utils.handleResponseException(accountManager.createRegistrationSession(null, "", "")); + } + + private static RegistrationSessionMetadataResponse getValidSession( + final SignalServiceAccountManager accountManager, final String sessionId + ) throws IOException { + try { + return validateSession(accountManager, sessionId); + } catch (NoSuchSessionException e) { + return requestValidSession(accountManager); + } + } + + private static RegistrationSessionMetadataResponse submitCaptcha( + SignalServiceAccountManager accountManager, String sessionId, String captcha + ) throws IOException, CaptchaRequiredException { + captcha = captcha == null ? null : captcha.replace("signalcaptcha://", ""); + try { + return Utils.handleResponseException(accountManager.submitCaptchaToken(sessionId, captcha)); + } catch (PushChallengeRequiredException | + org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException | + TokenNotAcceptedException _e) { + throw new CaptchaRequiredException("Captcha not accepted"); + } catch (NonSuccessfulResponseCodeException e) { + if (e.getCode() == 400) { + throw new CaptchaRequiredException("Captcha has invalid format"); } + throw e; } } public interface Verifier { - ServiceResponse verify( - String verificationCode, String registrationLock - ); + VerifyAccountResponse verify( + String sessionId, String verificationCode, String registrationLock + ) throws IOException; } } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index 792a107d..efa72bf2 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -8,6 +8,7 @@ import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.util.StreamDetails; +import org.whispersystems.signalservice.internal.ServiceResponse; import java.io.ByteArrayInputStream; import java.io.File; @@ -128,4 +129,16 @@ public class Utils { } return map; } + + public static T handleResponseException(final ServiceResponse response) throws IOException { + final var throwableOptional = response.getExecutionError().or(response::getApplicationError); + if (throwableOptional.isPresent()) { + if (throwableOptional.get() instanceof IOException) { + throw (IOException) throwableOptional.get(); + } else { + throw new IOException(throwableOptional.get()); + } + } + return response.getResult().orElse(null); + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 4dd94327..70dc23c1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,7 +16,7 @@ dependencyResolutionManagement { library("logback", "ch.qos.logback", "logback-classic").version("1.4.5") - library("signalservice", "com.github.turasa", "signal-service-java").version("2.15.3_unofficial_67") + library("signalservice", "com.github.turasa", "signal-service-java").version("2.15.3_unofficial_68") library("protobuf", "com.google.protobuf", "protobuf-javalite").version("3.22.0") library("sqlite", "org.xerial", "sqlite-jdbc").version("3.40.1.0") library("hikari", "com.zaxxer", "HikariCP").version("5.0.1") diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java index 84287877..5ba66d7a 100644 --- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java @@ -13,7 +13,9 @@ import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; +import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.output.JsonWriter; +import org.asamk.signal.util.DateUtils; import java.io.IOException; import java.util.List; @@ -65,6 +67,12 @@ public class RegisterCommand implements RegistrationCommand, JsonRpcRegistration ) throws UserErrorException, IOErrorException { try { m.register(voiceVerification, captcha); + } catch (RateLimitException e) { + String message = "Rate limit reached"; + if (e.getNextAttemptTimestamp() > 0) { + message += "\nNext attempt may be tried at " + DateUtils.formatTimestamp(e.getNextAttemptTimestamp()); + } + throw new UserErrorException(message); } catch (CaptchaRequiredException e) { String message; if (captcha == null) { @@ -76,6 +84,10 @@ public class RegisterCommand implements RegistrationCommand, JsonRpcRegistration } else { message = "Invalid captcha given."; } + if (e.getNextAttemptTimestamp() > 0) { + message += "\nNext Captcha may be provided at " + + DateUtils.formatTimestamp(e.getNextAttemptTimestamp()); + } throw new UserErrorException(message); } catch (NonNormalizedPhoneNumberException e) { throw new UserErrorException("Failed to register: " + e.getMessage(), e); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java index 59afebf8..5c4d807e 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java @@ -11,6 +11,7 @@ import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; import org.asamk.signal.manager.api.PinLockedException; +import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.UserAlreadyExistsException; import org.freedesktop.dbus.DBusPath; @@ -59,6 +60,9 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { } try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) { registrationManager.register(voiceVerification, captcha); + } catch (RateLimitException e) { + String message = "Rate limit reached"; + throw new SignalControl.Error.Failure(message); } catch (CaptchaRequiredException e) { String message = captcha == null ? "Captcha required for verification." : "Invalid captcha given."; throw new SignalControl.Error.RequiresCaptcha(message);