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