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