1 package org
.asamk
.signal
.manager
.util
;
3 import org
.asamk
.signal
.manager
.api
.CaptchaRequiredException
;
4 import org
.asamk
.signal
.manager
.api
.IncorrectPinException
;
5 import org
.asamk
.signal
.manager
.api
.NonNormalizedPhoneNumberException
;
6 import org
.asamk
.signal
.manager
.api
.Pair
;
7 import org
.asamk
.signal
.manager
.api
.PinLockedException
;
8 import org
.asamk
.signal
.manager
.api
.RateLimitException
;
9 import org
.asamk
.signal
.manager
.api
.VerificationMethodNotAvailableException
;
10 import org
.asamk
.signal
.manager
.helper
.PinHelper
;
11 import org
.slf4j
.Logger
;
12 import org
.slf4j
.LoggerFactory
;
13 import org
.whispersystems
.signalservice
.api
.NetworkResult
;
14 import org
.whispersystems
.signalservice
.api
.kbs
.MasterKey
;
15 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NoSuchSessionException
;
16 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NonSuccessfulResponseCodeException
;
17 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.PushChallengeRequiredException
;
18 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.TokenNotAcceptedException
;
19 import org
.whispersystems
.signalservice
.api
.registration
.RegistrationApi
;
20 import org
.whispersystems
.signalservice
.internal
.push
.LockedException
;
21 import org
.whispersystems
.signalservice
.internal
.push
.PushServiceSocket
.VerificationCodeTransport
;
22 import org
.whispersystems
.signalservice
.internal
.push
.RegistrationSessionMetadataResponse
;
23 import org
.whispersystems
.signalservice
.internal
.push
.VerifyAccountResponse
;
25 import java
.io
.IOException
;
26 import java
.util
.Locale
;
27 import java
.util
.function
.Consumer
;
29 public class NumberVerificationUtils
{
31 private static final Logger logger
= LoggerFactory
.getLogger(NumberVerificationUtils
.class);
33 public static String
handleVerificationSession(
34 RegistrationApi registrationApi
,
36 Consumer
<String
> sessionIdSaver
,
37 boolean voiceVerification
,
39 ) throws CaptchaRequiredException
, IOException
, RateLimitException
, VerificationMethodNotAvailableException
{
40 RegistrationSessionMetadataResponse sessionResponse
;
42 sessionResponse
= getValidSession(registrationApi
, sessionId
);
43 } catch (PushChallengeRequiredException
|
44 org
.whispersystems
.signalservice
.api
.push
.exceptions
.CaptchaRequiredException e
) {
45 if (captcha
!= null) {
46 sessionResponse
= submitCaptcha(registrationApi
, sessionId
, captcha
);
48 throw new CaptchaRequiredException("Captcha Required");
52 sessionId
= sessionResponse
.getBody().getId();
53 sessionIdSaver
.accept(sessionId
);
55 if (sessionResponse
.getBody().getVerified()) {
59 if (sessionResponse
.getBody().getAllowedToRequestCode()) {
63 final var nextAttempt
= voiceVerification
64 ? sessionResponse
.getBody().getNextCall()
65 : sessionResponse
.getBody().getNextSms();
66 if (nextAttempt
== null) {
67 throw new VerificationMethodNotAvailableException();
68 } else if (nextAttempt
> 0) {
69 final var timestamp
= sessionResponse
.getHeaders().getTimestamp() + nextAttempt
* 1000;
70 throw new RateLimitException(timestamp
);
73 final var nextVerificationAttempt
= sessionResponse
.getBody().getNextVerificationAttempt();
74 if (nextVerificationAttempt
!= null && nextVerificationAttempt
> 0) {
75 final var timestamp
= sessionResponse
.getHeaders().getTimestamp() + nextVerificationAttempt
* 1000;
76 throw new CaptchaRequiredException(timestamp
);
79 if (sessionResponse
.getBody().getRequestedInformation().contains("captcha")) {
80 if (captcha
!= null) {
81 sessionResponse
= submitCaptcha(registrationApi
, sessionId
, captcha
);
83 if (!sessionResponse
.getBody().getAllowedToRequestCode()) {
84 throw new CaptchaRequiredException("Captcha Required");
91 public static void requestVerificationCode(
92 RegistrationApi registrationApi
, String sessionId
, boolean voiceVerification
93 ) throws IOException
, CaptchaRequiredException
, NonNormalizedPhoneNumberException
{
94 final NetworkResult
<RegistrationSessionMetadataResponse
> response
;
95 final var locale
= Utils
.getDefaultLocale(Locale
.US
);
96 if (voiceVerification
) {
97 response
= registrationApi
.requestSmsVerificationCode(sessionId
,
100 VerificationCodeTransport
.VOICE
);
102 response
= registrationApi
.requestSmsVerificationCode(sessionId
,
105 VerificationCodeTransport
.SMS
);
108 Utils
.handleResponseException(response
);
109 } catch (org
.whispersystems
.signalservice
.api
.push
.exceptions
.CaptchaRequiredException e
) {
110 throw new CaptchaRequiredException(e
.getMessage(), e
);
111 } catch (org
.whispersystems
.signalservice
.api
.push
.exceptions
.NonNormalizedPhoneNumberException e
) {
112 throw new NonNormalizedPhoneNumberException("Phone number is not normalized ("
114 + "). Expected normalized: "
115 + e
.getNormalizedNumber(), e
);
119 public static Pair
<VerifyAccountResponse
, MasterKey
> verifyNumber(
120 String sessionId
, String verificationCode
, String pin
, PinHelper pinHelper
, Verifier verifier
121 ) throws IOException
, PinLockedException
, IncorrectPinException
{
122 verificationCode
= verificationCode
.replace("-", "");
124 final var response
= verifier
.verify(sessionId
, verificationCode
, null);
126 return new Pair
<>(response
, null);
127 } catch (LockedException e
) {
129 throw new PinLockedException(e
.getTimeRemaining());
132 final var registrationLockData
= pinHelper
.getRegistrationLockData(pin
, e
);
133 if (registrationLockData
== null) {
137 var registrationLock
= registrationLockData
.getMasterKey().deriveRegistrationLock();
138 VerifyAccountResponse response
;
140 response
= verifier
.verify(sessionId
, verificationCode
, registrationLock
);
141 } catch (LockedException _e
) {
142 throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
145 return new Pair
<>(response
, registrationLockData
.getMasterKey());
149 private static RegistrationSessionMetadataResponse
validateSession(
150 final RegistrationApi registrationApi
, final String sessionId
151 ) throws IOException
{
152 if (sessionId
== null || sessionId
.isEmpty()) {
153 throw new NoSuchSessionException();
155 return Utils
.handleResponseException(registrationApi
.getRegistrationSessionStatus(sessionId
));
158 private static RegistrationSessionMetadataResponse
requestValidSession(
159 final RegistrationApi registrationApi
160 ) throws IOException
{
161 return Utils
.handleResponseException(registrationApi
.createRegistrationSession(null, "", ""));
164 private static RegistrationSessionMetadataResponse
getValidSession(
165 final RegistrationApi registrationApi
, final String sessionId
166 ) throws IOException
{
168 return validateSession(registrationApi
, sessionId
);
169 } catch (NoSuchSessionException e
) {
170 logger
.debug("No registration session, creating new one.");
171 return requestValidSession(registrationApi
);
175 private static RegistrationSessionMetadataResponse
submitCaptcha(
176 RegistrationApi registrationApi
, String sessionId
, String captcha
177 ) throws IOException
, CaptchaRequiredException
{
178 captcha
= captcha
== null ?
null : captcha
.replace("signalcaptcha://", "");
180 return Utils
.handleResponseException(registrationApi
.submitCaptchaToken(sessionId
, captcha
));
181 } catch (PushChallengeRequiredException
|
182 org
.whispersystems
.signalservice
.api
.push
.exceptions
.CaptchaRequiredException
|
183 TokenNotAcceptedException _e
) {
184 throw new CaptchaRequiredException("Captcha not accepted");
185 } catch (NonSuccessfulResponseCodeException e
) {
186 if (e
.getCode() == 400) {
187 throw new CaptchaRequiredException("Captcha has invalid format");
193 public interface Verifier
{
195 VerifyAccountResponse
verify(
196 String sessionId
, String verificationCode
, String registrationLock
197 ) throws IOException
;