]> nmode's Git Repositories - signal-cli/commitdiff
Update libsignal-service-java
authorAsamK <asamk@gmx.de>
Fri, 31 Mar 2023 15:16:59 +0000 (17:16 +0200)
committerAsamK <asamk@gmx.de>
Sat, 1 Apr 2023 10:19:53 +0000 (12:19 +0200)
- Use session based number verification and registration

19 files changed:
graalvm-config-dir/jni-config.json
graalvm-config-dir/reflect-config.json
lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java
lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java
lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java
lib/src/main/java/org/asamk/signal/manager/api/CaptchaRequiredException.java
lib/src/main/java/org/asamk/signal/manager/api/RateLimitException.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java
lib/src/main/java/org/asamk/signal/manager/helper/ReceiveHelper.java
lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
lib/src/main/java/org/asamk/signal/manager/storage/messageCache/CachedMessage.java
lib/src/main/java/org/asamk/signal/manager/storage/messageCache/MessageCache.java
lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java
lib/src/main/java/org/asamk/signal/manager/util/Utils.java
settings.gradle.kts
src/main/java/org/asamk/signal/commands/RegisterCommand.java
src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java

index b168120e0dd8d6e536fbc080c7d3c9344cfd0062..09c4a05a69c4e2e603620a99199b2360c8e31add 100644 (file)
     {"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"] }]
index b9c3c2809cc2ed513e704af980d468ef92f8b31c..d34b2243596f3dfba14f63ee4f32e29b577e30bc 100644 (file)
   "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
   "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":"<init>","parameterTypes":[] }]
 {
   "name":"javax.smartcardio.CardPermission"
 },
-{
-  "name":"libcore.io.Memory"
-},
 {
   "name":"long",
   "allDeclaredMethods":true,
     {"name":"startColor","parameterTypes":[] }
   ]
 },
-{
-  "name":"org.asamk.signal.json.JsonStreamSerializer",
-  "methods":[{"name":"<init>","parameterTypes":[] }]
-},
 {
   "name":"org.asamk.signal.json.JsonSyncDataMessage",
   "allDeclaredFields":true,
   "name":"org.freedesktop.dbus.interfaces.Properties$PropertiesChanged",
   "allPublicConstructors":true
 },
-{
-  "name":"org.robolectric.Robolectric"
-},
 {
   "name":"org.signal.cdsi.proto.ClientRequest",
   "fields":[
   "queryAllDeclaredMethods":true,
   "queryAllDeclaredConstructors":true,
   "methods":[
-    {"name":"getCode","parameterTypes":[] }, 
     {"name":"getNumber","parameterTypes":[] }, 
     {"name":"getRegistrationLock","parameterTypes":[] }
   ]
   "allDeclaredMethods":true,
   "allDeclaredConstructors":true
 },
+{
+  "name":"org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson",
+  "allDeclaredFields":true,
+  "queryAllDeclaredMethods":true,
+  "queryAllDeclaredConstructors":true,
+  "methods":[{"name":"<init>","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,
     {"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":[
   "queryAllDeclaredConstructors":true,
   "methods":[{"name":"<init>","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,
   "name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord",
   "fields":[
     {"name":"identifiers_"}, 
+    {"name":"sourceDevice_"}, 
     {"name":"version_"}
   ]
 },
index a92792c71d78a62568d51e70ce9e9f476886fbee..8ba2bf70db0b288d4dbcca94180aeddd742f4d1b 100644 (file)
@@ -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
index 4b7c836201525532c482e605bba11133a66de36d..f5bff4d278e406fb1381028044045c29acb3f21a 100644 (file)
@@ -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<VerifyAccountResponse> 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
index 3b3b375a7d6ddd99d2f4872d14310b31d042e3fe..f4489ea7142729f332ee1af8ce3d7e2d886acbf2 100644 (file)
@@ -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);
     }
index 9504d2406ac4b9630ccc97c7c9237865e091e7a2..86fcdc93d6443bc55a3f479e9088be3bc39245e4 100644 (file)
@@ -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 (file)
index 0000000..54fcbbd
--- /dev/null
@@ -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;
+    }
+}
index 36cd76a363956ca1041491aba15f4ca5a7cc6720..2b6c812bc431db5a5c78f8aeb8b5ec58c74e39cf 100644 (file)
@@ -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<OutgoingPushMessage> deviceMessages = null;
         final Map<String, SignedPreKeyEntity> devicePniSignedPreKeys = null;
         final Map<String, Integer> 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 {
index 0efe90a48c2a75681e3136dba4298dbd7ce7b494..b908823ae8236a2587001ab770f69c0a469db66a 100644 (file)
@@ -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)
index bd42ae5f90637a048263cba97b8f04a1c1e60c9c..3a834a7019a30869a35352afd675fd77b56917ca 100644 (file)
@@ -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()) {
index c15f4f94cf89d6993a282a52fc89ad70f45199b7..2c5dbe69b1ebac51a10b2438c5e36075bfc1f192 100644 (file)
@@ -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");
index e80f79017d971e5285fc9caa5d75f8262e9fbf2d..25847c6cb61592d6986c73d61d9126182f8ebd43 100644 (file)
@@ -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);
     }
index c1f310e848cc61d2bc7996185e032870afe4f858..e053b91fb4e23276f1f19a578f04bf5293510f7e 100644 (file)
@@ -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() {
index 4853b2231de9909682c162e88dd1e57c611521ce..083dba06cea3cbfc69d4a3738317361d60f7c301 100644 (file)
@@ -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;
index a4c2ba2450fb91738687cf5643a4229aa4a283b5..3d733c163e3e0de2e47a7df20d13e4996d24328a 100644 (file)
@@ -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<String> 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<RequestVerificationCodeResponse> response;
+        final ServiceResponse<RegistrationSessionMetadataResponse> 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<VerifyAccountResponse, MasterKey> 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<VerifyAccountResponse> verify(
-                String verificationCode, String registrationLock
-        );
+        VerifyAccountResponse verify(
+                String sessionId, String verificationCode, String registrationLock
+        ) throws IOException;
     }
 }
index 792a107d667b3c86d0c42b55d3a24d96d5ef2dca..efa72bf2abb9b350836126f7b202566d30ba6b97 100644 (file)
@@ -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> T handleResponseException(final ServiceResponse<T> 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);
+    }
 }
index 4dd9432729bd17b247c6602690be329a6a0d75e9..70dc23c1a3cf37572d7cd14e87782c2fbb7cd091 100644 (file)
@@ -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")
index 842878776a700eddd12c4aff390a3e6b0116bc8c..5ba66d7a6c98991a0410bbfbc9624edfcc529a0f 100644 (file)
@@ -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);
index 59afebf8932dc6476d45dbb2e11cba1c438a9b88..5c4d807ecca04e95e2abc17900436a7d5b24ab1a 100644 (file)
@@ -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);