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