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