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