mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-02 06:33:38 +01:00
Add initial registration v5 prototype.
This commit is contained in:
committed by
jeffrey-signal
parent
1a5163fc47
commit
5ea5279fbb
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.push.exceptions
|
||||
|
||||
class InvalidRegistrationSessionIdException : NonSuccessfulResponseCodeException(400)
|
||||
@@ -44,6 +44,7 @@ import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedExc
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ExternalServiceFailureException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.HttpConflictException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.IncorrectRegistrationRecoveryPasswordException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.InvalidRegistrationSessionIdException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.InvalidTransportModeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException;
|
||||
@@ -340,6 +341,184 @@ public class PushServiceSocket {
|
||||
return JsonUtil.fromJson(response, VerifyAccountResponse.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Creates a verification session and returns the raw Response for manual handling.
|
||||
* Caller is responsible for closing the response.
|
||||
*/
|
||||
public Response createVerificationSessionV2(@Nonnull String e164, @Nullable String pushToken, @Nullable String mcc, @Nullable String mnc) throws IOException {
|
||||
final String jsonBody = JsonUtil.toJson(new VerificationSessionMetadataRequestBody(e164, pushToken, mcc, mnc));
|
||||
return makeServiceRequestWithoutValidation(VERIFICATION_SESSION_PATH, "POST", jsonRequestBody(jsonBody), NO_HEADERS, SealedSenderAccess.NONE, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Gets session status and returns the raw Response for manual handling.
|
||||
* Caller is responsible for closing the response.
|
||||
*/
|
||||
public Response getSessionStatusV2(String sessionId) throws IOException {
|
||||
String path = VERIFICATION_SESSION_PATH + "/" + sessionId;
|
||||
return makeServiceRequestWithoutValidation(path, "GET", jsonRequestBody(null), NO_HEADERS, SealedSenderAccess.NONE, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Patches verification session and returns the raw Response for manual handling.
|
||||
* Caller is responsible for closing the response.
|
||||
*/
|
||||
public Response patchVerificationSessionV2(String sessionId, @Nullable String pushToken, @Nullable String mcc, @Nullable String mnc, @Nullable String captchaToken, @Nullable String pushChallengeToken) throws IOException {
|
||||
String path = VERIFICATION_SESSION_PATH + "/" + sessionId;
|
||||
final UpdateVerificationSessionRequestBody requestBody = new UpdateVerificationSessionRequestBody(captchaToken, pushToken, pushChallengeToken, mcc, mnc);
|
||||
return makeServiceRequestWithoutValidation(path, "PATCH", jsonRequestBody(JsonUtil.toJson(requestBody)), NO_HEADERS, SealedSenderAccess.NONE, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Requests verification code and returns the raw Response for manual handling.
|
||||
* Caller is responsible for closing the response.
|
||||
*/
|
||||
public Response requestVerificationCodeV2(String sessionId, Locale locale, boolean androidSmsRetriever, VerificationCodeTransport transport) throws IOException {
|
||||
String path = String.format(VERIFICATION_CODE_PATH, sessionId);
|
||||
Map<String, String> headers = locale != null ? Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry()) : NO_HEADERS;
|
||||
Map<String, String> body = new HashMap<>();
|
||||
|
||||
switch (transport) {
|
||||
case SMS:
|
||||
body.put("transport", "sms");
|
||||
break;
|
||||
case VOICE:
|
||||
body.put("transport", "voice");
|
||||
break;
|
||||
}
|
||||
|
||||
body.put("client", androidSmsRetriever ? "android-2021-03" : "android");
|
||||
|
||||
return makeServiceRequestWithoutValidation(path, "POST", jsonRequestBody(JsonUtil.toJson(body)), headers, SealedSenderAccess.NONE, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Submits verification code and returns the raw Response for manual handling.
|
||||
* Caller is responsible for closing the response.
|
||||
*/
|
||||
public Response submitVerificationCodeV2(String sessionId, String verificationCode) throws IOException {
|
||||
String path = String.format(VERIFICATION_CODE_PATH, sessionId);
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("code", verificationCode);
|
||||
return makeServiceRequestWithoutValidation(path, "PUT", jsonRequestBody(JsonUtil.toJson(body)), NO_HEADERS, SealedSenderAccess.NONE, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Submits registration request and returns the raw Response for manual handling.
|
||||
* Caller is responsible for closing the response.
|
||||
*/
|
||||
public Response submitRegistrationRequestV2(@Nullable String sessionId, @Nullable String recoveryPassword, AccountAttributes attributes, PreKeyCollection aciPreKeys, PreKeyCollection pniPreKeys, @Nullable String fcmToken, boolean skipDeviceTransfer) throws IOException {
|
||||
String path = REGISTRATION_PATH;
|
||||
if (sessionId == null && recoveryPassword == null) {
|
||||
throw new IllegalArgumentException("Neither Session ID nor Recovery Password provided.");
|
||||
}
|
||||
|
||||
if (sessionId != null && recoveryPassword != null) {
|
||||
throw new IllegalArgumentException("You must supply one and only one of either: Session ID, or Recovery Password.");
|
||||
}
|
||||
|
||||
GcmRegistrationId gcmRegistrationId;
|
||||
if (attributes.getFetchesMessages()) {
|
||||
gcmRegistrationId = null;
|
||||
} else {
|
||||
gcmRegistrationId = new GcmRegistrationId(fcmToken, true);
|
||||
}
|
||||
|
||||
RegistrationSessionRequestBody body;
|
||||
try {
|
||||
final SignedPreKeyEntity aciSignedPreKey = new SignedPreKeyEntity(Objects.requireNonNull(aciPreKeys.getSignedPreKey()).getId(),
|
||||
aciPreKeys.getSignedPreKey().getKeyPair().getPublicKey(),
|
||||
aciPreKeys.getSignedPreKey().getSignature());
|
||||
final SignedPreKeyEntity pniSignedPreKey = new SignedPreKeyEntity(Objects.requireNonNull(pniPreKeys.getSignedPreKey()).getId(),
|
||||
pniPreKeys.getSignedPreKey().getKeyPair().getPublicKey(),
|
||||
pniPreKeys.getSignedPreKey().getSignature());
|
||||
final KyberPreKeyEntity aciLastResortKyberPreKey = new KyberPreKeyEntity(Objects.requireNonNull(aciPreKeys.getLastResortKyberPreKey()).getId(),
|
||||
aciPreKeys.getLastResortKyberPreKey().getKeyPair().getPublicKey(),
|
||||
aciPreKeys.getLastResortKyberPreKey().getSignature());
|
||||
final KyberPreKeyEntity pniLastResortKyberPreKey = new KyberPreKeyEntity(Objects.requireNonNull(pniPreKeys.getLastResortKyberPreKey()).getId(),
|
||||
pniPreKeys.getLastResortKyberPreKey().getKeyPair().getPublicKey(),
|
||||
pniPreKeys.getLastResortKyberPreKey().getSignature());
|
||||
|
||||
body = new RegistrationSessionRequestBody(sessionId,
|
||||
recoveryPassword,
|
||||
attributes,
|
||||
Base64.encodeWithoutPadding(aciPreKeys.getIdentityKey().serialize()),
|
||||
Base64.encodeWithoutPadding(pniPreKeys.getIdentityKey().serialize()),
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciLastResortKyberPreKey,
|
||||
pniLastResortKyberPreKey,
|
||||
gcmRegistrationId,
|
||||
skipDeviceTransfer,
|
||||
true);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError("unexpected invalid key", e);
|
||||
}
|
||||
|
||||
return makeServiceRequestWithoutValidation(path, "POST", jsonRequestBody(JsonUtil.toJson(body)), NO_HEADERS, SealedSenderAccess.NONE, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 API: Submits registration request with explicit credentials and returns the raw Response for manual handling.
|
||||
* Caller is responsible for closing the response.
|
||||
*
|
||||
* @param e164 The phone number in E.164 format (used as username for basic auth)
|
||||
* @param password The password for basic auth
|
||||
*/
|
||||
public Response submitRegistrationRequestV2(@Nonnull String e164, @Nonnull String password, @Nullable String sessionId, @Nullable String recoveryPassword, AccountAttributes attributes, PreKeyCollection aciPreKeys, PreKeyCollection pniPreKeys, @Nullable String fcmToken, boolean skipDeviceTransfer) throws IOException {
|
||||
String path = REGISTRATION_PATH;
|
||||
if (sessionId == null && recoveryPassword == null) {
|
||||
throw new IllegalArgumentException("Neither Session ID nor Recovery Password provided.");
|
||||
}
|
||||
|
||||
if (sessionId != null && recoveryPassword != null) {
|
||||
throw new IllegalArgumentException("You must supply one and only one of either: Session ID, or Recovery Password.");
|
||||
}
|
||||
|
||||
GcmRegistrationId gcmRegistrationId;
|
||||
if (attributes.getFetchesMessages()) {
|
||||
gcmRegistrationId = null;
|
||||
} else {
|
||||
gcmRegistrationId = new GcmRegistrationId(fcmToken, true);
|
||||
}
|
||||
|
||||
RegistrationSessionRequestBody body;
|
||||
try {
|
||||
final SignedPreKeyEntity aciSignedPreKey = new SignedPreKeyEntity(Objects.requireNonNull(aciPreKeys.getSignedPreKey()).getId(),
|
||||
aciPreKeys.getSignedPreKey().getKeyPair().getPublicKey(),
|
||||
aciPreKeys.getSignedPreKey().getSignature());
|
||||
final SignedPreKeyEntity pniSignedPreKey = new SignedPreKeyEntity(Objects.requireNonNull(pniPreKeys.getSignedPreKey()).getId(),
|
||||
pniPreKeys.getSignedPreKey().getKeyPair().getPublicKey(),
|
||||
pniPreKeys.getSignedPreKey().getSignature());
|
||||
final KyberPreKeyEntity aciLastResortKyberPreKey = new KyberPreKeyEntity(Objects.requireNonNull(aciPreKeys.getLastResortKyberPreKey()).getId(),
|
||||
aciPreKeys.getLastResortKyberPreKey().getKeyPair().getPublicKey(),
|
||||
aciPreKeys.getLastResortKyberPreKey().getSignature());
|
||||
final KyberPreKeyEntity pniLastResortKyberPreKey = new KyberPreKeyEntity(Objects.requireNonNull(pniPreKeys.getLastResortKyberPreKey()).getId(),
|
||||
pniPreKeys.getLastResortKyberPreKey().getKeyPair().getPublicKey(),
|
||||
pniPreKeys.getLastResortKyberPreKey().getSignature());
|
||||
|
||||
body = new RegistrationSessionRequestBody(sessionId,
|
||||
recoveryPassword,
|
||||
attributes,
|
||||
Base64.encodeWithoutPadding(aciPreKeys.getIdentityKey().serialize()),
|
||||
Base64.encodeWithoutPadding(pniPreKeys.getIdentityKey().serialize()),
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciLastResortKyberPreKey,
|
||||
pniLastResortKyberPreKey,
|
||||
gcmRegistrationId,
|
||||
skipDeviceTransfer,
|
||||
true);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError("unexpected invalid key", e);
|
||||
}
|
||||
|
||||
String authHeader = "Basic " + Base64.encodeWithPadding((e164 + ":" + password).getBytes("UTF-8"));
|
||||
Map<String, String> headers = Collections.singletonMap("Authorization", authHeader);
|
||||
|
||||
return makeServiceRequestWithoutValidation(path, "POST", jsonRequestBody(JsonUtil.toJson(body)), headers, SealedSenderAccess.NONE, false);
|
||||
}
|
||||
|
||||
public void setRestoreMethodChosen(@Nonnull String token, @Nonnull RestoreMethodBody request) throws IOException {
|
||||
String body = JsonUtil.toJson(request);
|
||||
makeServiceRequest(String.format(Locale.US, SET_RESTORE_METHOD_PATH, urlEncode(token)), "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
|
||||
@@ -1167,6 +1346,26 @@ public class PushServiceSocket {
|
||||
}
|
||||
}
|
||||
|
||||
private Response makeServiceRequestWithoutValidation(String urlFragment,
|
||||
String method,
|
||||
RequestBody body,
|
||||
Map<String, String> headers,
|
||||
@Nullable SealedSenderAccess sealedSenderAccess,
|
||||
boolean doNotAddAuthenticationOrUnidentifiedAccessKey)
|
||||
throws PushNetworkException
|
||||
{
|
||||
Response response = null;
|
||||
try {
|
||||
response = getServiceConnection(urlFragment, method, body, headers, sealedSenderAccess, doNotAddAuthenticationOrUnidentifiedAccessKey);
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
if (response != null && response.body() != null) {
|
||||
response.body().close();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private Response validateServiceResponse(Response response)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException {
|
||||
int responseCode = response.code();
|
||||
@@ -1903,7 +2102,9 @@ public class PushServiceSocket {
|
||||
@Override
|
||||
public void handle(int responseCode, ResponseBody body, Function<String, String> getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
|
||||
if (responseCode == 403) {
|
||||
if (responseCode == 400) {
|
||||
throw new InvalidRegistrationSessionIdException();
|
||||
} if (responseCode == 403) {
|
||||
throw new IncorrectRegistrationRecoveryPasswordException();
|
||||
} else if (responseCode == 404) {
|
||||
throw new NoSuchSessionException();
|
||||
|
||||
Reference in New Issue
Block a user