]> nmode's Git Repositories - signal-cli/blob - 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
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.PinLockMissingException;
8 import org.asamk.signal.manager.api.PinLockedException;
9 import org.asamk.signal.manager.api.RateLimitException;
10 import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
11 import org.asamk.signal.manager.helper.PinHelper;
12 import org.slf4j.Logger;
13 import org.slf4j.LoggerFactory;
14 import org.whispersystems.signalservice.api.kbs.MasterKey;
15 import org.whispersystems.signalservice.api.push.exceptions.ChallengeRequiredException;
16 import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException;
17 import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
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 (ChallengeRequiredException e) {
44 if (captcha != null) {
45 sessionResponse = submitCaptcha(registrationApi, sessionId, captcha);
46 } else {
47 throw new CaptchaRequiredException("Captcha Required");
48 }
49 }
50
51 sessionId = sessionResponse.getMetadata().getId();
52 sessionIdSaver.accept(sessionId);
53
54 if (sessionResponse.getMetadata().getVerified()) {
55 return sessionId;
56 }
57
58 if (sessionResponse.getMetadata().getAllowedToRequestCode()) {
59 return sessionId;
60 }
61
62 final var nextAttempt = voiceVerification
63 ? sessionResponse.getMetadata().getNextCall()
64 : sessionResponse.getMetadata().getNextSms();
65 if (nextAttempt == null) {
66 throw new VerificationMethodNotAvailableException();
67 } else if (nextAttempt > 0) {
68 final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextAttempt * 1000;
69 throw new RateLimitException(timestamp);
70 }
71
72 final var nextVerificationAttempt = sessionResponse.getMetadata().getNextVerificationAttempt();
73 if (nextVerificationAttempt != null && nextVerificationAttempt > 0) {
74 final var timestamp = sessionResponse.getClientReceivedAtMilliseconds() + nextVerificationAttempt * 1000;
75 throw new CaptchaRequiredException(timestamp);
76 }
77
78 if (sessionResponse.getMetadata().getRequestedInformation().contains("captcha")) {
79 if (captcha != null) {
80 sessionResponse = submitCaptcha(registrationApi, sessionId, captcha);
81 }
82 if (!sessionResponse.getMetadata().getAllowedToRequestCode()) {
83 throw new CaptchaRequiredException("Captcha Required");
84 }
85 }
86
87 return sessionId;
88 }
89
90 public static void requestVerificationCode(
91 RegistrationApi registrationApi,
92 String sessionId,
93 boolean voiceVerification
94 ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException {
95 final var locale = Utils.getDefaultLocale(Locale.US);
96 final var response = registrationApi.requestSmsVerificationCode(sessionId,
97 locale,
98 false,
99 voiceVerification ? VerificationCodeTransport.VOICE : VerificationCodeTransport.SMS);
100 try {
101 Utils.handleResponseException(response);
102 } catch (ChallengeRequiredException e) {
103 throw new CaptchaRequiredException(e.getMessage(), e);
104 } catch (org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException e) {
105 throw new NonNormalizedPhoneNumberException("Phone number is not normalized ("
106 + e.getMessage()
107 + "). Expected normalized: "
108 + e.getNormalizedNumber(), e);
109 }
110 }
111
112 public static Pair<VerifyAccountResponse, MasterKey> verifyNumber(
113 String sessionId,
114 String verificationCode,
115 String pin,
116 PinHelper pinHelper,
117 Verifier verifier
118 ) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
119 verificationCode = verificationCode.replace("-", "");
120 try {
121 final var response = verifier.verify(sessionId, verificationCode, null);
122
123 return new Pair<>(response, null);
124 } catch (LockedException e) {
125 if (pin == null) {
126 throw new PinLockedException(e.getTimeRemaining());
127 }
128
129 final var registrationLockData = pinHelper.getRegistrationLockData(pin, e);
130 if (registrationLockData == null) {
131 throw new PinLockMissingException();
132 }
133
134 var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
135 VerifyAccountResponse response;
136 try {
137 response = verifier.verify(sessionId, verificationCode, registrationLock);
138 } catch (LockedException _e) {
139 throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
140 }
141
142 return new Pair<>(response, registrationLockData.getMasterKey());
143 }
144 }
145
146 private static RegistrationSessionMetadataResponse validateSession(
147 final RegistrationApi registrationApi,
148 final String sessionId
149 ) throws IOException {
150 if (sessionId == null || sessionId.isEmpty()) {
151 throw new NoSuchSessionException();
152 }
153 return Utils.handleResponseException(registrationApi.getRegistrationSessionStatus(sessionId));
154 }
155
156 private static RegistrationSessionMetadataResponse requestValidSession(
157 final RegistrationApi registrationApi
158 ) throws IOException {
159 return Utils.handleResponseException(registrationApi.createRegistrationSession(null, "", ""));
160 }
161
162 private static RegistrationSessionMetadataResponse getValidSession(
163 final RegistrationApi registrationApi,
164 final String sessionId
165 ) throws IOException {
166 try {
167 return validateSession(registrationApi, sessionId);
168 } catch (NoSuchSessionException e) {
169 logger.debug("No registration session, creating new one.");
170 return requestValidSession(registrationApi);
171 }
172 }
173
174 private static RegistrationSessionMetadataResponse submitCaptcha(
175 RegistrationApi registrationApi,
176 String sessionId,
177 String captcha
178 ) throws IOException, CaptchaRequiredException {
179 captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
180 try {
181 return Utils.handleResponseException(registrationApi.submitCaptchaToken(sessionId, captcha));
182 } catch (ChallengeRequiredException | TokenNotAcceptedException _e) {
183 throw new CaptchaRequiredException("Captcha not accepted");
184 } catch (NonSuccessfulResponseCodeException e) {
185 if (e.code == 400) {
186 throw new CaptchaRequiredException("Captcha has invalid format");
187 }
188 throw e;
189 }
190 }
191
192 public interface Verifier {
193
194 VerifyAccountResponse verify(
195 String sessionId,
196 String verificationCode,
197 String registrationLock
198 ) throws IOException;
199 }
200 }