]> nmode's Git Repositories - signal-cli/blobdiff - lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java
Improve behavior when pin data doesn't exist on the server
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / util / NumberVerificationUtils.java
index 9dcee1fb863a11f4385a2cb3961d369ef0af34c4..659a8c676addafe6b0d08a7f362f7429d3832214 100644 (file)
@@ -4,41 +4,102 @@ 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.Pair;
+import org.asamk.signal.manager.api.PinLockMissingException;
 import org.asamk.signal.manager.api.PinLockedException;
+import org.asamk.signal.manager.api.RateLimitException;
+import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
 import org.asamk.signal.manager.helper.PinHelper;
-import org.whispersystems.signalservice.api.KbsPinData;
-import org.whispersystems.signalservice.api.SignalServiceAccountManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.whispersystems.signalservice.api.kbs.MasterKey;
-import org.whispersystems.signalservice.internal.ServiceResponse;
+import org.whispersystems.signalservice.api.push.exceptions.ChallengeRequiredException;
+import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException;
+import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
+import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException;
+import org.whispersystems.signalservice.api.registration.RegistrationApi;
 import org.whispersystems.signalservice.internal.push.LockedException;
-import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse;
+import org.whispersystems.signalservice.internal.push.PushServiceSocket.VerificationCodeTransport;
+import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse;
 import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
 
 import java.io.IOException;
-import java.util.Optional;
+import java.util.Locale;
+import java.util.function.Consumer;
 
 public class NumberVerificationUtils {
 
-    public static void requestVerificationCode(
-            SignalServiceAccountManager accountManager, String captcha, boolean voiceVerification
-    ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException {
-        captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
+    private static final Logger logger = LoggerFactory.getLogger(NumberVerificationUtils.class);
+
+    public static String handleVerificationSession(
+            RegistrationApi registrationApi,
+            String sessionId,
+            Consumer<String> sessionIdSaver,
+            boolean voiceVerification,
+            String captcha
+    ) throws CaptchaRequiredException, IOException, RateLimitException, VerificationMethodNotAvailableException {
+        RegistrationSessionMetadataResponse sessionResponse;
+        try {
+            sessionResponse = getValidSession(registrationApi, sessionId);
+        } catch (ChallengeRequiredException e) {
+            if (captcha != null) {
+                sessionResponse = submitCaptcha(registrationApi, sessionId, captcha);
+            } else {
+                throw new CaptchaRequiredException("Captcha Required");
+            }
+        }
+
+        sessionId = sessionResponse.getMetadata().getId();
+        sessionIdSaver.accept(sessionId);
 
-        final ServiceResponse<RequestVerificationCodeResponse> response;
-        if (voiceVerification) {
-            response = accountManager.requestVoiceVerificationCode(Utils.getDefaultLocale(null),
-                    Optional.ofNullable(captcha),
-                    Optional.empty(),
-                    Optional.empty());
-        } else {
-            response = accountManager.requestSmsVerificationCode(false,
-                    Optional.ofNullable(captcha),
-                    Optional.empty(),
-                    Optional.empty());
+        if (sessionResponse.getMetadata().getVerified()) {
+            return sessionId;
+        }
+
+        if (sessionResponse.getMetadata().getAllowedToRequestCode()) {
+            return sessionId;
+        }
+
+        final var nextAttempt = voiceVerification
+                ? sessionResponse.getMetadata().getNextCall()
+                : sessionResponse.getMetadata().getNextSms();
+        if (nextAttempt == null) {
+            throw new VerificationMethodNotAvailableException();
+        } else if (nextAttempt > 0) {
+            final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextAttempt * 1000;
+            throw new RateLimitException(timestamp);
+        }
+
+        final var nextVerificationAttempt = sessionResponse.getMetadata().getNextVerificationAttempt();
+        if (nextVerificationAttempt != null && nextVerificationAttempt > 0) {
+            final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextVerificationAttempt * 1000;
+            throw new CaptchaRequiredException(timestamp);
+        }
+
+        if (sessionResponse.getMetadata().getRequestedInformation().contains("captcha")) {
+            if (captcha != null) {
+                sessionResponse = submitCaptcha(registrationApi, sessionId, captcha);
+            }
+            if (!sessionResponse.getMetadata().getAllowedToRequestCode()) {
+                throw new CaptchaRequiredException("Captcha Required");
+            }
         }
+
+        return sessionId;
+    }
+
+    public static void requestVerificationCode(
+            RegistrationApi registrationApi,
+            String sessionId,
+            boolean voiceVerification
+    ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException {
+        final var locale = Utils.getDefaultLocale(Locale.US);
+        final var response = registrationApi.requestSmsVerificationCode(sessionId,
+                locale,
+                false,
+                voiceVerification ? VerificationCodeTransport.VOICE : VerificationCodeTransport.SMS);
         try {
-            handleResponseException(response);
-        } catch (org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) {
+            Utils.handleResponseException(response);
+        } catch (ChallengeRequiredException e) {
             throw new CaptchaRequiredException(e.getMessage(), e);
         } catch (org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException e) {
             throw new NonNormalizedPhoneNumberException("Phone number is not normalized ("
@@ -49,11 +110,15 @@ public class NumberVerificationUtils {
     }
 
     public static Pair<VerifyAccountResponse, MasterKey> verifyNumber(
-            String verificationCode, String pin, PinHelper pinHelper, Verifier verifier
-    ) throws IOException, PinLockedException, IncorrectPinException {
+            String sessionId,
+            String verificationCode,
+            String pin,
+            PinHelper pinHelper,
+            Verifier verifier
+    ) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
         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) {
@@ -61,16 +126,15 @@ public class NumberVerificationUtils {
                 throw new PinLockedException(e.getTimeRemaining());
             }
 
-            KbsPinData registrationLockData;
-            registrationLockData = pinHelper.getRegistrationLockData(pin, e);
+            final var registrationLockData = pinHelper.getRegistrationLockData(pin, e);
             if (registrationLockData == null) {
-                throw e;
+                throw new PinLockMissingException();
             }
 
             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!");
             }
@@ -79,29 +143,58 @@ public class NumberVerificationUtils {
         }
     }
 
-    private static VerifyAccountResponse verifyAccountWithCode(
-            final String verificationCode, final String registrationLock, final Verifier verifier
+    private static RegistrationSessionMetadataResponse validateSession(
+            final RegistrationApi registrationApi,
+            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(registrationApi.getRegistrationSessionStatus(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 RegistrationApi registrationApi
+    ) throws IOException {
+        return Utils.handleResponseException(registrationApi.createRegistrationSession(null, "", ""));
+    }
+
+    private static RegistrationSessionMetadataResponse getValidSession(
+            final RegistrationApi registrationApi,
+            final String sessionId
+    ) throws IOException {
+        try {
+            return validateSession(registrationApi, sessionId);
+        } catch (NoSuchSessionException e) {
+            logger.debug("No registration session, creating new one.");
+            return requestValidSession(registrationApi);
+        }
+    }
+
+    private static RegistrationSessionMetadataResponse submitCaptcha(
+            RegistrationApi registrationApi,
+            String sessionId,
+            String captcha
+    ) throws IOException, CaptchaRequiredException {
+        captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
+        try {
+            return Utils.handleResponseException(registrationApi.submitCaptchaToken(sessionId, captcha));
+        } catch (ChallengeRequiredException | TokenNotAcceptedException _e) {
+            throw new CaptchaRequiredException("Captcha not accepted");
+        } catch (NonSuccessfulResponseCodeException e) {
+            if (e.code == 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;
     }
 }