mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-01 22:25:46 +01:00
Remove all unused KBS/SVR1 code.
This commit is contained in:
committed by
Cody Henthorne
parent
5b0e71b680
commit
609e9fcdb0
@@ -1,303 +0,0 @@
|
||||
package org.whispersystems.signalservice.api;
|
||||
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.signal.libsignal.svr2.PinHash;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.kbs.KbsData;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.KeyBackupCipher;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse;
|
||||
import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse;
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Locale;
|
||||
|
||||
public class KeyBackupService {
|
||||
|
||||
private static final String TAG = KeyBackupService.class.getSimpleName();
|
||||
|
||||
private final KeyStore iasKeyStore;
|
||||
private final String enclaveName;
|
||||
private final byte[] serviceId;
|
||||
private final String mrenclave;
|
||||
private final PushServiceSocket pushServiceSocket;
|
||||
private final int maxTries;
|
||||
|
||||
KeyBackupService(KeyStore iasKeyStore,
|
||||
String enclaveName,
|
||||
byte[] serviceId,
|
||||
String mrenclave,
|
||||
PushServiceSocket pushServiceSocket,
|
||||
int maxTries)
|
||||
{
|
||||
this.iasKeyStore = iasKeyStore;
|
||||
this.enclaveName = enclaveName;
|
||||
this.serviceId = serviceId;
|
||||
this.mrenclave = mrenclave;
|
||||
this.pushServiceSocket = pushServiceSocket;
|
||||
this.maxTries = maxTries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this if you don't want to validate that the server has not changed since you last set the pin.
|
||||
*/
|
||||
public PinChangeSession newPinChangeSession()
|
||||
throws IOException
|
||||
{
|
||||
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization().asBasic(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this if you want to validate that the server has not changed since you last set the pin.
|
||||
* The supplied token will have to match for the change to be successful.
|
||||
*/
|
||||
public PinChangeSession newPinChangeSession(TokenResponse currentToken)
|
||||
throws IOException
|
||||
{
|
||||
return newSession(pushServiceSocket.getKeyBackupServiceAuthorization().asBasic(), currentToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only call before registration, to see how many tries are left.
|
||||
* <p>
|
||||
* Pass the token to {@link #newRegistrationSession(String, TokenResponse)}.
|
||||
*/
|
||||
public TokenResponse getToken(String authAuthorization) throws IOException {
|
||||
return pushServiceSocket.getKeyBackupServiceToken(authAuthorization, enclaveName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the authorization token to be used with other requests.
|
||||
*/
|
||||
public AuthCredentials getAuthorization() throws IOException {
|
||||
return pushServiceSocket.getKeyBackupServiceAuthorization();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this during registration, good for one try, on subsequent attempts, pass the token from the previous attempt.
|
||||
*
|
||||
* @param tokenResponse Supplying a token response from a failed previous attempt prevents certain attacks.
|
||||
*/
|
||||
public RestoreSession newRegistrationSession(String authAuthorization, TokenResponse tokenResponse)
|
||||
throws IOException
|
||||
{
|
||||
return newSession(authAuthorization, tokenResponse);
|
||||
}
|
||||
|
||||
public String getEnclaveName() {
|
||||
return enclaveName;
|
||||
}
|
||||
|
||||
public String getMrenclave() {
|
||||
return mrenclave;
|
||||
}
|
||||
|
||||
private Session newSession(String authorization, TokenResponse currentToken)
|
||||
throws IOException
|
||||
{
|
||||
TokenResponse token = currentToken != null ? currentToken : pushServiceSocket.getKeyBackupServiceToken(authorization, enclaveName);
|
||||
|
||||
return new Session(authorization, token);
|
||||
}
|
||||
|
||||
private class Session implements RestoreSession, PinChangeSession {
|
||||
|
||||
private final String authorization;
|
||||
private final TokenResponse currentToken;
|
||||
|
||||
Session(String authorization, TokenResponse currentToken) {
|
||||
this.authorization = authorization;
|
||||
this.currentToken = currentToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] hashSalt() {
|
||||
return currentToken.getBackupId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SvrPinData restorePin(PinHash hashedPin)
|
||||
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, SvrNoDataException, InvalidKeyException
|
||||
{
|
||||
int attempt = 0;
|
||||
SecureRandom random = new SecureRandom();
|
||||
TokenResponse token = currentToken;
|
||||
|
||||
while (true) {
|
||||
|
||||
attempt++;
|
||||
|
||||
try {
|
||||
return restorePin(hashedPin, token);
|
||||
} catch (TokenException tokenException) {
|
||||
|
||||
token = tokenException.getToken();
|
||||
|
||||
if (tokenException instanceof KeyBackupServicePinException) {
|
||||
throw (KeyBackupServicePinException) tokenException;
|
||||
}
|
||||
|
||||
if (tokenException.isCanAutomaticallyRetry() && attempt < 5) {
|
||||
// back off randomly, between 250 and 8000 ms
|
||||
int backoffMs = 250 * (1 << (attempt - 1));
|
||||
|
||||
Util.sleep(backoffMs + random.nextInt(backoffMs));
|
||||
} else {
|
||||
throw new UnauthenticatedResponseException("Token mismatch, expended all automatic retries");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SvrPinData restorePin(PinHash hashedPin, TokenResponse token)
|
||||
throws UnauthenticatedResponseException, IOException, TokenException, SvrNoDataException, InvalidKeyException
|
||||
{
|
||||
try {
|
||||
final int remainingTries = token.getTries();
|
||||
final RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
|
||||
final KeyBackupRequest request = KeyBackupCipher.createKeyRestoreRequest(hashedPin.accessKey(), token, remoteAttestation, serviceId);
|
||||
final KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
|
||||
final RestoreResponse status = KeyBackupCipher.getKeyRestoreResponse(response, remoteAttestation);
|
||||
|
||||
TokenResponse nextToken = status.token != null ? new TokenResponse(token.getBackupId(), status.token.toByteArray(), status.tries)
|
||||
: token;
|
||||
|
||||
Log.i(TAG, "Restore " + status.status);
|
||||
switch (status.status) {
|
||||
case OK:
|
||||
KbsData kbsData = PinHashUtil.decryptSvrDataIVCipherText(hashedPin, status.data_.toByteArray());
|
||||
MasterKey masterKey = kbsData.getMasterKey();
|
||||
return new SvrPinData(masterKey, nextToken);
|
||||
case PIN_MISMATCH:
|
||||
Log.i(TAG, "Restore PIN_MISMATCH");
|
||||
throw new KeyBackupServicePinException(nextToken);
|
||||
case TOKEN_MISMATCH:
|
||||
Log.i(TAG, "Restore TOKEN_MISMATCH");
|
||||
// if the number of tries has not fallen, the pin is correct we're just using an out of date token
|
||||
boolean canRetry = remainingTries == status.tries;
|
||||
Log.i(TAG, String.format(Locale.US, "Token MISMATCH remainingTries: %d, status.getTries(): %d", remainingTries, status.tries));
|
||||
throw new TokenException(nextToken, canRetry);
|
||||
case MISSING:
|
||||
Log.i(TAG, "Restore OK! No data though");
|
||||
throw new SvrNoDataException();
|
||||
case NOT_YET_VALID:
|
||||
throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
|
||||
default:
|
||||
throw new AssertionError("Unexpected case");
|
||||
}
|
||||
} catch (InvalidCiphertextException e) {
|
||||
throw new UnauthenticatedResponseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private RemoteAttestation getAndVerifyRemoteAttestation() throws UnauthenticatedResponseException, IOException, InvalidKeyException {
|
||||
try {
|
||||
return RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.KeyBackup, iasKeyStore, enclaveName, mrenclave, authorization);
|
||||
} catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | InvalidCiphertextException | SignatureException e) {
|
||||
throw new UnauthenticatedResponseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SvrPinData setPin(PinHash pinHash, MasterKey masterKey) throws IOException, UnauthenticatedResponseException {
|
||||
KbsData newKbsData = PinHashUtil.createNewKbsData(pinHash, masterKey);
|
||||
TokenResponse tokenResponse = putKbsData(newKbsData.getKbsAccessKey(),
|
||||
newKbsData.getCipherText(),
|
||||
enclaveName,
|
||||
currentToken);
|
||||
|
||||
return new SvrPinData(masterKey, tokenResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePin()
|
||||
throws IOException, UnauthenticatedResponseException
|
||||
{
|
||||
try {
|
||||
RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
|
||||
KeyBackupRequest request = KeyBackupCipher.createKeyDeleteRequest(currentToken, remoteAttestation, serviceId);
|
||||
KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
|
||||
|
||||
KeyBackupCipher.getKeyDeleteResponseStatus(response, remoteAttestation);
|
||||
} catch (InvalidCiphertextException | InvalidKeyException e) {
|
||||
throw new UnauthenticatedResponseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableRegistrationLock(MasterKey masterKey) throws IOException {
|
||||
pushServiceSocket.setRegistrationLockV2(masterKey.deriveRegistrationLock());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableRegistrationLock() throws IOException {
|
||||
pushServiceSocket.disableRegistrationLockV2();
|
||||
}
|
||||
|
||||
private TokenResponse putKbsData(byte[] kbsAccessKey, byte[] kbsData, String enclaveName, TokenResponse token)
|
||||
throws IOException, UnauthenticatedResponseException
|
||||
{
|
||||
try {
|
||||
RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation();
|
||||
KeyBackupRequest request = KeyBackupCipher.createKeyBackupRequest(kbsAccessKey, kbsData, token, remoteAttestation, serviceId, maxTries);
|
||||
KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName);
|
||||
BackupResponse backupResponse = KeyBackupCipher.getKeyBackupResponse(response, remoteAttestation);
|
||||
BackupResponse.Status status = backupResponse.status;
|
||||
|
||||
switch (status) {
|
||||
case OK:
|
||||
return backupResponse.token != null ? new TokenResponse(token.getBackupId(), backupResponse.token.toByteArray(), maxTries) : token;
|
||||
case ALREADY_EXISTS:
|
||||
throw new UnauthenticatedResponseException("Already exists");
|
||||
case NOT_YET_VALID:
|
||||
throw new UnauthenticatedResponseException("Key is not valid yet, clock mismatch");
|
||||
default:
|
||||
throw new AssertionError("Unknown response status " + status);
|
||||
}
|
||||
} catch (InvalidCiphertextException | InvalidKeyException e) {
|
||||
throw new UnauthenticatedResponseException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface HashSession {
|
||||
|
||||
byte[] hashSalt();
|
||||
}
|
||||
|
||||
public interface RestoreSession extends HashSession {
|
||||
|
||||
SvrPinData restorePin(PinHash hashedPin)
|
||||
throws UnauthenticatedResponseException, IOException, KeyBackupServicePinException, SvrNoDataException, InvalidKeyException;
|
||||
}
|
||||
|
||||
public interface PinChangeSession extends HashSession {
|
||||
/** Creates a PIN. Does nothing to registration lock. */
|
||||
SvrPinData setPin(PinHash hashedPin, MasterKey masterKey) throws IOException, UnauthenticatedResponseException;
|
||||
|
||||
/** Removes the PIN data from KBS. */
|
||||
void removePin() throws IOException, UnauthenticatedResponseException;
|
||||
|
||||
/** Enables registration lock. This assumes a PIN is set. */
|
||||
void enableRegistrationLock(MasterKey masterKey) throws IOException;
|
||||
|
||||
/** Disables registration lock. The user keeps their PIN. */
|
||||
void disableRegistrationLock() throws IOException;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package org.whispersystems.signalservice.api;
|
||||
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
public final class KeyBackupServicePinException extends TokenException {
|
||||
|
||||
private final int triesRemaining;
|
||||
|
||||
public KeyBackupServicePinException(TokenResponse nextToken) {
|
||||
super(nextToken, false);
|
||||
this.triesRemaining = nextToken.getTries();
|
||||
}
|
||||
|
||||
public int getTriesRemaining() {
|
||||
return triesRemaining;
|
||||
}
|
||||
}
|
||||
@@ -179,30 +179,10 @@ public class SignalServiceAccountManager {
|
||||
return new SecureValueRecoveryV2(configuration, mrEnclave, pushServiceSocket);
|
||||
}
|
||||
|
||||
/**
|
||||
* V1 PINs are no longer used in favor of V2 PINs stored on KBS.
|
||||
*
|
||||
* You can remove a V1 PIN, but typically this is unnecessary, as setting a V2 PIN via
|
||||
* {@link KeyBackupService.PinChangeSession#enableRegistrationLock(MasterKey)}} will automatically clear the
|
||||
* V1 PIN on the service.
|
||||
*/
|
||||
public void removeRegistrationLockV1() throws IOException {
|
||||
this.pushServiceSocket.removeRegistrationLockV1();
|
||||
}
|
||||
|
||||
public WhoAmIResponse getWhoAmI() throws IOException {
|
||||
return this.pushServiceSocket.getWhoAmI();
|
||||
}
|
||||
|
||||
public KeyBackupService getKeyBackupService(KeyStore iasKeyStore,
|
||||
String enclaveName,
|
||||
byte[] serviceId,
|
||||
String mrenclave,
|
||||
int tries)
|
||||
{
|
||||
return new KeyBackupService(iasKeyStore, enclaveName, serviceId, mrenclave, pushServiceSocket, tries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register/Unregister a Google Cloud Messaging registration ID.
|
||||
*
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.svr
|
||||
|
||||
import org.signal.libsignal.svr2.PinHash
|
||||
import org.whispersystems.signalservice.api.KeyBackupService
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException
|
||||
import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.SvrPinData
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupResponse
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.DeleteResponse
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSession
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.RestoreResponse
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import java.io.IOException
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
/**
|
||||
* An implementation of the [SecureValueRecovery] interface backed by the [KeyBackupService].
|
||||
*/
|
||||
class SecureValueRecoveryV1(private val kbs: KeyBackupService) : SecureValueRecovery {
|
||||
|
||||
companion object {
|
||||
const val TAG = "SVR1"
|
||||
}
|
||||
|
||||
override fun setPin(userPin: String, masterKey: MasterKey): PinChangeSession {
|
||||
return Svr1PinChangeSession(userPin, masterKey)
|
||||
}
|
||||
|
||||
override fun resumePinChangeSession(userPin: String, masterKey: MasterKey, serializedChangeSession: String): PinChangeSession {
|
||||
return setPin(userPin, masterKey)
|
||||
}
|
||||
|
||||
override fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): RestoreResponse {
|
||||
return restoreData({ authorization }, userPin)
|
||||
}
|
||||
|
||||
override fun restoreDataPostRegistration(userPin: String): RestoreResponse {
|
||||
return restoreData({ kbs.authorization }, userPin)
|
||||
}
|
||||
|
||||
override fun deleteData(): DeleteResponse {
|
||||
return try {
|
||||
kbs.newPinChangeSession().removePin()
|
||||
DeleteResponse.Success
|
||||
} catch (e: UnauthenticatedResponseException) {
|
||||
DeleteResponse.ApplicationError(e)
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
when (e.code) {
|
||||
404 -> DeleteResponse.EnclaveNotFound
|
||||
508 -> DeleteResponse.ServerRejected
|
||||
else -> DeleteResponse.NetworkError(e)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
DeleteResponse.NetworkError(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun authorization(): AuthCredentials {
|
||||
return kbs.authorization
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "SVR1::${kbs.enclaveName}::${kbs.mrenclave}"
|
||||
}
|
||||
|
||||
private fun restoreData(fetchAuthorization: () -> AuthCredentials, userPin: String): RestoreResponse {
|
||||
return try {
|
||||
val authorization: AuthCredentials = fetchAuthorization()
|
||||
val session = kbs.newRegistrationSession(authorization.asBasic(), null)
|
||||
val pinHash: PinHash = PinHashUtil.hashPin(userPin, session.hashSalt())
|
||||
|
||||
val data: SvrPinData = session.restorePin(pinHash)
|
||||
RestoreResponse.Success(data.masterKey, authorization)
|
||||
} catch (e: SvrNoDataException) {
|
||||
RestoreResponse.Missing
|
||||
} catch (e: KeyBackupServicePinException) {
|
||||
RestoreResponse.PinMismatch(e.triesRemaining)
|
||||
} catch (e: IOException) {
|
||||
RestoreResponse.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
RestoreResponse.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
inner class Svr1PinChangeSession(
|
||||
private val userPin: String,
|
||||
private val masterKey: MasterKey
|
||||
) : PinChangeSession {
|
||||
override fun execute(): BackupResponse {
|
||||
return try {
|
||||
val session = kbs.newPinChangeSession()
|
||||
val pinHash: PinHash = PinHashUtil.hashPin(userPin, session.hashSalt())
|
||||
|
||||
val data: SvrPinData = session.setPin(pinHash, masterKey)
|
||||
BackupResponse.Success(data.masterKey, kbs.authorization)
|
||||
} catch (e: UnauthenticatedResponseException) {
|
||||
BackupResponse.ApplicationError(e)
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
when (e.code) {
|
||||
404 -> BackupResponse.EnclaveNotFound
|
||||
508 -> BackupResponse.ServerRejected
|
||||
else -> BackupResponse.NetworkError(e)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
BackupResponse.NetworkError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/** No real need to serialize */
|
||||
override fun serialize(): String {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import java.util.Optional
|
||||
class SignalServiceConfiguration(
|
||||
val signalServiceUrls: Array<SignalServiceUrl>,
|
||||
val signalCdnUrlMap: Map<Int, Array<SignalCdnUrl>>,
|
||||
val signalKeyBackupServiceUrls: Array<SignalKeyBackupServiceUrl>,
|
||||
val signalStorageUrls: Array<SignalStorageUrl>,
|
||||
val signalCdsiUrls: Array<SignalCdsiUrl>,
|
||||
val signalSvr2Urls: Array<SignalSvr2Url>,
|
||||
|
||||
@@ -316,7 +316,6 @@ public class PushServiceSocket {
|
||||
|
||||
private final ServiceConnectionHolder[] serviceClients;
|
||||
private final Map<Integer, ConnectionHolder[]> cdnClientsMap;
|
||||
private final ConnectionHolder[] keyBackupServiceClients;
|
||||
private final ConnectionHolder[] storageClients;
|
||||
|
||||
private final CredentialsProvider credentialsProvider;
|
||||
@@ -336,7 +335,6 @@ public class PushServiceSocket {
|
||||
this.automaticNetworkRetry = automaticNetworkRetry;
|
||||
this.serviceClients = createServiceConnectionHolders(configuration.getSignalServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns(), configuration.getSignalProxy());
|
||||
this.cdnClientsMap = createCdnClientsMap(configuration.getSignalCdnUrlMap(), configuration.getNetworkInterceptors(), configuration.getDns(), configuration.getSignalProxy());
|
||||
this.keyBackupServiceClients = createConnectionHolders(configuration.getSignalKeyBackupServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns(), configuration.getSignalProxy());
|
||||
this.storageClients = createConnectionHolders(configuration.getSignalStorageUrls(), configuration.getNetworkInterceptors(), configuration.getDns(), configuration.getSignalProxy());
|
||||
this.random = new SecureRandom();
|
||||
this.clientZkProfileOperations = clientZkProfileOperations;
|
||||
@@ -1281,34 +1279,10 @@ public class PushServiceSocket {
|
||||
return getAuthCredentials(authPath).asBasic();
|
||||
}
|
||||
|
||||
public String getContactDiscoveryAuthorization() throws IOException {
|
||||
return getCredentials(DIRECTORY_AUTH_PATH);
|
||||
}
|
||||
|
||||
public AuthCredentials getKeyBackupServiceAuthorization() throws IOException {
|
||||
return getAuthCredentials(KBS_AUTH_PATH);
|
||||
}
|
||||
|
||||
public AuthCredentials getPaymentsAuthorization() throws IOException {
|
||||
return getAuthCredentials(PAYMENTS_AUTH_PATH);
|
||||
}
|
||||
|
||||
public TokenResponse getKeyBackupServiceToken(String authorizationToken, String enclaveName)
|
||||
throws IOException
|
||||
{
|
||||
try (Response response = makeRequest(ClientSet.KeyBackup, authorizationToken, null, "/v1/token/" + enclaveName, "GET", null)) {
|
||||
return readBodyJson(response, TokenResponse.class);
|
||||
}
|
||||
}
|
||||
|
||||
public KeyBackupResponse putKbsData(String authorizationToken, KeyBackupRequest request, List<String> cookies, String mrenclave)
|
||||
throws IOException
|
||||
{
|
||||
try (Response response = makeRequest(ClientSet.KeyBackup, authorizationToken, cookies, "/v1/backup/" + mrenclave, "PUT", JsonUtil.toJson(request))) {
|
||||
return readBodyJson(response, KeyBackupResponse.class);
|
||||
}
|
||||
}
|
||||
|
||||
public TurnServerInfo getTurnServerInfo() throws IOException {
|
||||
String response = makeServiceRequest(TURN_SERVER_INFO, "GET", null);
|
||||
return JsonUtil.fromJson(response, TurnServerInfo.class);
|
||||
@@ -2113,82 +2087,6 @@ public class PushServiceSocket {
|
||||
return request.build();
|
||||
}
|
||||
|
||||
|
||||
private ConnectionHolder[] clientsFor(ClientSet clientSet) {
|
||||
switch (clientSet) {
|
||||
case KeyBackup:
|
||||
return keyBackupServiceClients;
|
||||
default:
|
||||
throw new AssertionError("Unknown attestation purpose");
|
||||
}
|
||||
}
|
||||
|
||||
Response makeRequest(ClientSet clientSet, String authorization, List<String> cookies, String path, String method, String body)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
ConnectionHolder connectionHolder = getRandom(clientsFor(clientSet), random);
|
||||
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
|
||||
|
||||
if (body != null) {
|
||||
request.method(method, RequestBody.create(MediaType.parse("application/json"), body));
|
||||
} else {
|
||||
request.method(method, null);
|
||||
}
|
||||
|
||||
if (connectionHolder.getHostHeader().isPresent()) {
|
||||
request.addHeader("Host", connectionHolder.getHostHeader().get());
|
||||
}
|
||||
|
||||
if (authorization != null) {
|
||||
request.addHeader("Authorization", authorization);
|
||||
}
|
||||
|
||||
if (cookies != null && !cookies.isEmpty()) {
|
||||
request.addHeader("Cookie", Util.join(cookies, "; "));
|
||||
}
|
||||
|
||||
Call call = okHttpClient.newCall(request.build());
|
||||
|
||||
synchronized (connections) {
|
||||
connections.add(call);
|
||||
}
|
||||
|
||||
Response response;
|
||||
|
||||
try {
|
||||
response = call.execute();
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
return response;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new PushNetworkException(e);
|
||||
} finally {
|
||||
synchronized (connections) {
|
||||
connections.remove(call);
|
||||
}
|
||||
}
|
||||
|
||||
switch (response.code()) {
|
||||
case 401:
|
||||
case 403:
|
||||
throw new AuthorizationFailedException(response.code(), "Authorization failed!");
|
||||
case 409:
|
||||
throw new RemoteAttestationResponseExpiredException("Remote attestation response expired");
|
||||
case 429:
|
||||
throw new RateLimitException(response.code(), "Rate limit exceeded: " + response.code());
|
||||
}
|
||||
|
||||
throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response);
|
||||
}
|
||||
|
||||
private Response makeStorageRequest(String authorization, String path, String method, RequestBody body, ResponseCodeHandler responseCodeHandler)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestation;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationCipher;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestationKeys;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.MultiRemoteAttestationResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
public final class RemoteAttestationUtil {
|
||||
|
||||
private RemoteAttestationUtil() {
|
||||
}
|
||||
|
||||
public static RemoteAttestation getAndVerifyRemoteAttestation(PushServiceSocket socket,
|
||||
PushServiceSocket.ClientSet clientSet,
|
||||
KeyStore iasKeyStore,
|
||||
String enclaveName,
|
||||
String mrenclave,
|
||||
String authorization)
|
||||
throws IOException, Quote.InvalidQuoteFormatException, InvalidCiphertextException, UnauthenticatedQuoteException, SignatureException, InvalidKeyException
|
||||
{
|
||||
ECKeyPair keyPair = buildKeyPair();
|
||||
ResponsePair result = makeAttestationRequest(socket, clientSet, authorization, enclaveName, keyPair);
|
||||
RemoteAttestationResponse response = JsonUtil.fromJson(result.body, RemoteAttestationResponse.class);
|
||||
|
||||
return validateAndBuildRemoteAttestation(response, result.cookies, iasKeyStore, keyPair, mrenclave);
|
||||
}
|
||||
|
||||
public static Map<String, RemoteAttestation> getAndVerifyMultiRemoteAttestation(PushServiceSocket socket,
|
||||
PushServiceSocket.ClientSet clientSet,
|
||||
KeyStore iasKeyStore,
|
||||
String enclaveName,
|
||||
String mrenclave,
|
||||
String authorization)
|
||||
throws IOException, Quote.InvalidQuoteFormatException, InvalidCiphertextException, UnauthenticatedQuoteException, SignatureException, InvalidKeyException
|
||||
{
|
||||
ECKeyPair keyPair = buildKeyPair();
|
||||
ResponsePair result = makeAttestationRequest(socket, clientSet, authorization, enclaveName, keyPair);
|
||||
MultiRemoteAttestationResponse response = JsonUtil.fromJson(result.body, MultiRemoteAttestationResponse.class);
|
||||
Map<String, RemoteAttestation> attestations = new HashMap<>();
|
||||
|
||||
if (response.getAttestations().isEmpty() || response.getAttestations().size() > 3) {
|
||||
throw new MalformedResponseException("Incorrect number of attestations: " + response.getAttestations().size());
|
||||
}
|
||||
|
||||
for (Map.Entry<String, RemoteAttestationResponse> entry : response.getAttestations().entrySet()) {
|
||||
attestations.put(entry.getKey(),
|
||||
validateAndBuildRemoteAttestation(entry.getValue(),
|
||||
result.cookies,
|
||||
iasKeyStore,
|
||||
keyPair,
|
||||
mrenclave));
|
||||
}
|
||||
|
||||
return attestations;
|
||||
}
|
||||
|
||||
private static ECKeyPair buildKeyPair() {
|
||||
return Curve.generateKeyPair();
|
||||
}
|
||||
|
||||
private static ResponsePair makeAttestationRequest(PushServiceSocket socket,
|
||||
PushServiceSocket.ClientSet clientSet,
|
||||
String authorization,
|
||||
String enclaveName,
|
||||
ECKeyPair keyPair)
|
||||
throws IOException
|
||||
{
|
||||
RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey().getPublicKeyBytes());
|
||||
|
||||
try (Response response = socket.makeRequest(clientSet, authorization, Collections.emptyList(), "/v1/attestation/" +enclaveName, "PUT", JsonUtil.toJson(attestationRequest))) {
|
||||
return new ResponsePair(PushServiceSocket.readBodyString(response), parseCookies(response));
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> parseCookies(Response response) {
|
||||
List<String> rawCookies = response.headers("Set-Cookie");
|
||||
List<String> cookies = new LinkedList<>();
|
||||
|
||||
for (String cookie : rawCookies) {
|
||||
cookies.add(cookie.split(";")[0]);
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
private static RemoteAttestation validateAndBuildRemoteAttestation(RemoteAttestationResponse response,
|
||||
List<String> cookies,
|
||||
KeyStore iasKeyStore,
|
||||
ECKeyPair keyPair,
|
||||
String mrenclave)
|
||||
throws Quote.InvalidQuoteFormatException, InvalidCiphertextException, UnauthenticatedQuoteException, SignatureException, InvalidKeyException
|
||||
{
|
||||
RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, response.getServerEphemeralPublic(), response.getServerStaticPublic());
|
||||
Quote quote = new Quote(response.getQuote());
|
||||
byte[] requestId = RemoteAttestationCipher.getRequestId(keys, response);
|
||||
|
||||
RemoteAttestationCipher.verifyServerQuote(quote, response.getServerStaticPublic(), mrenclave);
|
||||
|
||||
RemoteAttestationCipher.verifyIasSignature(iasKeyStore, response.getCertificates(), response.getSignatureBody(), response.getSignature(), quote);
|
||||
|
||||
return new RemoteAttestation(requestId, keys, cookies);
|
||||
}
|
||||
|
||||
private static class ResponsePair {
|
||||
final String body;
|
||||
final List<String> cookies;
|
||||
|
||||
private ResponsePair(String body, List<String> cookies) {
|
||||
this.body = body;
|
||||
this.cookies = cookies;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user