]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/util/NumberVerificationUtils.java
Update dependencies
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / util / NumberVerificationUtils.java
1 package org.asamk.signal.manager.util;
2
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;
24
25 import java.io.IOException;
26 import java.util.Locale;
27 import java.util.function.Consumer;
28
29 public class NumberVerificationUtils {
30
31 private static final Logger logger = LoggerFactory.getLogger(NumberVerificationUtils.class);
32
33 public static String handleVerificationSession(
34 RegistrationApi registrationApi,
35 String sessionId,
36 Consumer<String> sessionIdSaver,
37 boolean voiceVerification,
38 String captcha
39 ) throws CaptchaRequiredException, IOException, RateLimitException, VerificationMethodNotAvailableException {
40 RegistrationSessionMetadataResponse sessionResponse;
41 try {
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);
47 } else {
48 throw new CaptchaRequiredException("Captcha Required");
49 }
50 }
51
52 sessionId = sessionResponse.getBody().getId();
53 sessionIdSaver.accept(sessionId);
54
55 if (sessionResponse.getBody().getVerified()) {
56 return sessionId;
57 }
58
59 if (sessionResponse.getBody().getAllowedToRequestCode()) {
60 return sessionId;
61 }
62
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);
71 }
72
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);
77 }
78
79 if (sessionResponse.getBody().getRequestedInformation().contains("captcha")) {
80 if (captcha != null) {
81 sessionResponse = submitCaptcha(registrationApi, sessionId, captcha);
82 }
83 if (!sessionResponse.getBody().getAllowedToRequestCode()) {
84 throw new CaptchaRequiredException("Captcha Required");
85 }
86 }
87
88 return sessionId;
89 }
90
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,
98 locale,
99 false,
100 VerificationCodeTransport.VOICE);
101 } else {
102 response = registrationApi.requestSmsVerificationCode(sessionId,
103 locale,
104 false,
105 VerificationCodeTransport.SMS);
106 }
107 try {
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 ("
113 + e.getMessage()
114 + "). Expected normalized: "
115 + e.getNormalizedNumber(), e);
116 }
117 }
118
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("-", "");
123 try {
124 final var response = verifier.verify(sessionId, verificationCode, null);
125
126 return new Pair<>(response, null);
127 } catch (LockedException e) {
128 if (pin == null) {
129 throw new PinLockedException(e.getTimeRemaining());
130 }
131
132 final var registrationLockData = pinHelper.getRegistrationLockData(pin, e);
133 if (registrationLockData == null) {
134 throw e;
135 }
136
137 var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
138 VerifyAccountResponse response;
139 try {
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!");
143 }
144
145 return new Pair<>(response, registrationLockData.getMasterKey());
146 }
147 }
148
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();
154 }
155 return Utils.handleResponseException(registrationApi.getRegistrationSessionStatus(sessionId));
156 }
157
158 private static RegistrationSessionMetadataResponse requestValidSession(
159 final RegistrationApi registrationApi
160 ) throws IOException {
161 return Utils.handleResponseException(registrationApi.createRegistrationSession(null, "", ""));
162 }
163
164 private static RegistrationSessionMetadataResponse getValidSession(
165 final RegistrationApi registrationApi, final String sessionId
166 ) throws IOException {
167 try {
168 return validateSession(registrationApi, sessionId);
169 } catch (NoSuchSessionException e) {
170 logger.debug("No registration session, creating new one.");
171 return requestValidSession(registrationApi);
172 }
173 }
174
175 private static RegistrationSessionMetadataResponse submitCaptcha(
176 RegistrationApi registrationApi, String sessionId, String captcha
177 ) throws IOException, CaptchaRequiredException {
178 captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
179 try {
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");
188 }
189 throw e;
190 }
191 }
192
193 public interface Verifier {
194
195 VerifyAccountResponse verify(
196 String sessionId, String verificationCode, String registrationLock
197 ) throws IOException;
198 }
199 }