Add support for CDSI.
This commit is contained in:
parent
8407f2ff69
commit
9ab275195f
16 changed files with 350 additions and 462 deletions
|
@ -179,7 +179,7 @@ android {
|
|||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDSH_URL", "\"https://cdsh.staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
|
||||
|
@ -196,9 +196,8 @@ android {
|
|||
buildConfigField "String[]", "SIGNAL_SFU_IPS", sfu_ips
|
||||
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
|
||||
buildConfigField "String", "CDSH_CODE_HASH", "\"2f79dc6c1599b71c70fc2d14f3ea2e3bc65134436eb87011c88845b137af673a\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "String", "CDSI_MRENCLAVE", "\"42e36b74794abe612d698308b148ff8a7dc5fdc6ad28d99bc5024ed6ece18dfe\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
|
||||
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
|
||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
|
||||
|
|
|
@ -111,12 +111,7 @@ class ContactDiscoveryRefreshV1 {
|
|||
|
||||
Stopwatch stopwatch = new Stopwatch("refresh");
|
||||
|
||||
ContactIntersection result;
|
||||
if (FeatureFlags.cdsh()) {
|
||||
result = getIntersectionWithHsm(databaseNumbers, systemNumbers);
|
||||
} else {
|
||||
result = getIntersection(context, databaseNumbers, systemNumbers);
|
||||
}
|
||||
ContactIntersection result = getIntersection(context, databaseNumbers, systemNumbers);
|
||||
|
||||
stopwatch.split("network");
|
||||
|
||||
|
@ -250,38 +245,6 @@ class ContactDiscoveryRefreshV1 {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the contact intersection using an HSM-backed implementation of CDS that is being tested.
|
||||
*/
|
||||
private static ContactIntersection getIntersectionWithHsm(@NonNull Set<String> databaseNumbers,
|
||||
@NonNull Set<String> systemNumbers)
|
||||
throws IOException
|
||||
{
|
||||
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
|
||||
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers);
|
||||
Set<String> sanitizedNumbers = sanitizeNumbers(inputResult.getNumbers());
|
||||
Set<String> ignoredNumbers = new HashSet<>();
|
||||
|
||||
if (sanitizedNumbers.size() > MAX_NUMBERS) {
|
||||
Set<String> randomlySelected = randomlySelect(sanitizedNumbers, MAX_NUMBERS);
|
||||
|
||||
ignoredNumbers = SetUtil.difference(sanitizedNumbers, randomlySelected);
|
||||
sanitizedNumbers = randomlySelected;
|
||||
}
|
||||
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
|
||||
try {
|
||||
Map<String, ACI> results = accountManager.getRegisteredUsersWithCdshV1(sanitizedNumbers, BuildConfig.CDSH_PUBLIC_KEY, BuildConfig.CDSH_CODE_HASH);
|
||||
FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(results, inputResult);
|
||||
|
||||
return new ContactIntersection(outputResult.getNumbers(), outputResult.getRewrites(), ignoredNumbers);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Attestation error.", e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull Set<String> randomlySelect(@NonNull Set<String> numbers, int max) {
|
||||
List<String> list = new ArrayList<>(numbers);
|
||||
Collections.shuffle(list);
|
||||
|
|
|
@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.Stopwatch
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.services.CdshV2Service
|
||||
import org.whispersystems.signalservice.api.services.CdsiV2Service
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
|
||||
|
@ -41,7 +41,12 @@ object ContactDiscoveryRefreshV2 {
|
|||
fun refreshAll(context: Context): ContactDiscovery.RefreshResult {
|
||||
val stopwatch = Stopwatch("refresh-all")
|
||||
|
||||
val previousE164s: Set<String> = SignalDatabase.cds.getAllE164s()
|
||||
val previousE164s: Set<String> = if (SignalStore.misc().cdsToken != null) {
|
||||
SignalDatabase.cds.getAllE164s()
|
||||
} else {
|
||||
Log.w(TAG, "No token set! Cannot provide previousE164s.")
|
||||
emptySet()
|
||||
}
|
||||
stopwatch.split("previous")
|
||||
|
||||
val recipientE164s: Set<String> = SignalDatabase.recipients.getAllE164s().sanitize()
|
||||
|
@ -54,14 +59,15 @@ object ContactDiscoveryRefreshV2 {
|
|||
|
||||
val newE164s: Set<String> = newRecipientE164s + newSystemE164s
|
||||
|
||||
val response: CdshV2Service.Response = makeRequest(
|
||||
val response: CdsiV2Service.Response = makeRequest(
|
||||
previousE164s = previousE164s,
|
||||
newE164s = newE164s,
|
||||
serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(),
|
||||
token = SignalStore.misc().cdsToken,
|
||||
saveToken = true
|
||||
)
|
||||
stopwatch.split("network")
|
||||
|
||||
SignalStore.misc().cdsToken = response.token
|
||||
SignalDatabase.cds.updateAfterCdsQuery(newE164s, recipientE164s + systemE164s)
|
||||
stopwatch.split("cds-db")
|
||||
|
||||
|
@ -106,10 +112,12 @@ object ContactDiscoveryRefreshV2 {
|
|||
Log.i(TAG, "Doing a one-off request for ${inputE164s.size} recipients.")
|
||||
}
|
||||
|
||||
val response: CdshV2Service.Response = makeRequest(
|
||||
val response: CdsiV2Service.Response = makeRequest(
|
||||
previousE164s = emptySet(),
|
||||
newE164s = inputE164s,
|
||||
serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs()
|
||||
serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(),
|
||||
token = null,
|
||||
saveToken = false
|
||||
)
|
||||
stopwatch.split("network")
|
||||
|
||||
|
@ -125,15 +133,18 @@ object ContactDiscoveryRefreshV2 {
|
|||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun makeRequest(previousE164s: Set<String>, newE164s: Set<String>, serviceIds: Map<ServiceId, ProfileKey>): CdshV2Service.Response {
|
||||
return ApplicationDependencies.getSignalServiceAccountManager().getRegisteredUsersWithCdshV2(
|
||||
private fun makeRequest(previousE164s: Set<String>, newE164s: Set<String>, serviceIds: Map<ServiceId, ProfileKey>, token: ByteArray?, saveToken: Boolean): CdsiV2Service.Response {
|
||||
return ApplicationDependencies.getSignalServiceAccountManager().getRegisteredUsersWithCdsi(
|
||||
previousE164s,
|
||||
newE164s,
|
||||
serviceIds,
|
||||
Optional.ofNullable(SignalStore.misc().cdsToken),
|
||||
BuildConfig.CDSH_PUBLIC_KEY,
|
||||
BuildConfig.CDSH_CODE_HASH
|
||||
)
|
||||
Optional.ofNullable(token),
|
||||
BuildConfig.CDSI_MRENCLAVE
|
||||
) { token ->
|
||||
if (saveToken) {
|
||||
SignalStore.misc().cdsToken = token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Set<String>.toE164s(context: Context): Set<String> {
|
||||
|
|
|
@ -79,6 +79,7 @@ public final class PartProvider extends BaseContentProvider {
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (uriMatcher.match(uri) == SINGLE_ROW) {
|
||||
Log.i(TAG, "Parting out a single row...");
|
||||
try {
|
||||
|
|
|
@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
|||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.whispersystems.signalservice.api.push.TrustStore
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
|
@ -169,7 +169,7 @@ class SignalServiceNetworkAccess(context: Context) {
|
|||
fUrls.map { SignalContactDiscoveryUrl(it, F_DIRECTORY_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(),
|
||||
fUrls.map { SignalKeyBackupServiceUrl(it, F_KBS_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(),
|
||||
fUrls.map { SignalStorageUrl(it, F_STORAGE_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(),
|
||||
arrayOf(SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, serviceTrustStore)),
|
||||
arrayOf(SignalCdsiUrl(BuildConfig.SIGNAL_CDSI_URL, serviceTrustStore)),
|
||||
interceptors,
|
||||
Optional.of(DNS),
|
||||
Optional.empty(),
|
||||
|
@ -220,7 +220,7 @@ class SignalServiceNetworkAccess(context: Context) {
|
|||
arrayOf(SignalContactDiscoveryUrl(BuildConfig.SIGNAL_CONTACT_DISCOVERY_URL, serviceTrustStore)),
|
||||
arrayOf(SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, serviceTrustStore)),
|
||||
arrayOf(SignalStorageUrl(BuildConfig.STORAGE_URL, serviceTrustStore)),
|
||||
arrayOf(SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, serviceTrustStore)),
|
||||
arrayOf(SignalCdsiUrl(BuildConfig.SIGNAL_CDSI_URL, serviceTrustStore)),
|
||||
interceptors,
|
||||
Optional.of(DNS),
|
||||
if (SignalStore.proxy().isProxyEnabled) Optional.ofNullable(SignalStore.proxy().proxy) else Optional.empty(),
|
||||
|
@ -276,7 +276,7 @@ class SignalServiceNetworkAccess(context: Context) {
|
|||
val cdsUrls: Array<SignalContactDiscoveryUrl> = hostConfigs.map { SignalContactDiscoveryUrl("${it.baseUrl}/directory", it.host, gTrustStore, it.connectionSpec) }.toTypedArray()
|
||||
val kbsUrls: Array<SignalKeyBackupServiceUrl> = hostConfigs.map { SignalKeyBackupServiceUrl("${it.baseUrl}/backup", it.host, gTrustStore, it.connectionSpec) }.toTypedArray()
|
||||
val storageUrls: Array<SignalStorageUrl> = hostConfigs.map { SignalStorageUrl("${it.baseUrl}/storage", it.host, gTrustStore, it.connectionSpec) }.toTypedArray()
|
||||
val cdshUrls: Array<SignalCdshUrl> = listOf(SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, serviceTrustStore)).toTypedArray()
|
||||
val cdsiUrls: Array<SignalCdsiUrl> = listOf(SignalCdsiUrl(BuildConfig.SIGNAL_CDSI_URL, serviceTrustStore)).toTypedArray()
|
||||
|
||||
return SignalServiceConfiguration(
|
||||
serviceUrls,
|
||||
|
@ -287,7 +287,7 @@ class SignalServiceNetworkAccess(context: Context) {
|
|||
cdsUrls,
|
||||
kbsUrls,
|
||||
storageUrls,
|
||||
cdshUrls,
|
||||
cdsiUrls,
|
||||
interceptors,
|
||||
Optional.of(DNS),
|
||||
Optional.empty(),
|
||||
|
|
|
@ -8,4 +8,8 @@ fun <E> Optional<E>.or(other: Optional<E>): Optional<E> {
|
|||
} else {
|
||||
other
|
||||
}
|
||||
}
|
||||
|
||||
fun <E> Optional<E>.isAbsent(): Boolean {
|
||||
return !isPresent
|
||||
}
|
|
@ -42,8 +42,7 @@ import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
|
|||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.services.CdshV1Service;
|
||||
import org.whispersystems.signalservice.api.services.CdshV2Service;
|
||||
import org.whispersystems.signalservice.api.services.CdsiV2Service;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
|
||||
|
@ -64,7 +63,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequ
|
|||
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
|
||||
import org.whispersystems.signalservice.internal.crypto.PrimaryProvisioningCipher;
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials;
|
||||
import org.whispersystems.signalservice.internal.push.CdshAuthResponse;
|
||||
import org.whispersystems.signalservice.internal.push.CdsiAuthResponse;
|
||||
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||
|
@ -105,6 +104,7 @@ import java.util.UUID;
|
|||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
|
@ -507,45 +507,20 @@ public class SignalServiceAccountManager {
|
|||
}
|
||||
}
|
||||
|
||||
public Map<String, ACI> getRegisteredUsersWithCdshV1(Set<String> e164numbers, String hexPublicKey, String hexCodeHash)
|
||||
public CdsiV2Service.Response getRegisteredUsersWithCdsi(Set<String> previousE164s,
|
||||
Set<String> newE164s,
|
||||
Map<ServiceId, ProfileKey> serviceIds,
|
||||
Optional<byte[]> token,
|
||||
String mrEnclave,
|
||||
Consumer<byte[]> tokenSaver)
|
||||
throws IOException
|
||||
{
|
||||
CdshAuthResponse auth = pushServiceSocket.getCdshAuth();
|
||||
CdshV1Service service = new CdshV1Service(configuration, hexPublicKey, hexCodeHash);
|
||||
Single<ServiceResponse<Map<String, ACI>>> result = service.getRegisteredUsers(auth.getUsername(), auth.getPassword(), e164numbers);
|
||||
CdsiAuthResponse auth = pushServiceSocket.getCdsiAuth();
|
||||
CdsiV2Service service = new CdsiV2Service(configuration, mrEnclave);
|
||||
CdsiV2Service.Request request = new CdsiV2Service.Request(previousE164s, newE164s, serviceIds, token);
|
||||
Single<ServiceResponse<CdsiV2Service.Response>> single = service.getRegisteredUsers(auth.getUsername(), auth.getPassword(), request, tokenSaver);
|
||||
|
||||
ServiceResponse<Map<String, ACI>> response;
|
||||
try {
|
||||
response = result.blockingGet();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unexpected exception when retrieving registered users!", e);
|
||||
}
|
||||
|
||||
if (response.getResult().isPresent()) {
|
||||
return response.getResult().get();
|
||||
} else if (response.getApplicationError().isPresent()) {
|
||||
throw new IOException(response.getApplicationError().get());
|
||||
} else if (response.getExecutionError().isPresent()) {
|
||||
throw new IOException(response.getExecutionError().get());
|
||||
} else {
|
||||
throw new IOException("Missing result!");
|
||||
}
|
||||
}
|
||||
|
||||
public CdshV2Service.Response getRegisteredUsersWithCdshV2(Set<String> previousE164s,
|
||||
Set<String> newE164s,
|
||||
Map<ServiceId, ProfileKey> serviceIds,
|
||||
Optional<byte[]> token,
|
||||
String hexPublicKey,
|
||||
String hexCodeHash)
|
||||
throws IOException
|
||||
{
|
||||
CdshAuthResponse auth = pushServiceSocket.getCdshAuth();
|
||||
CdshV2Service service = new CdshV2Service(configuration, hexPublicKey, hexCodeHash);
|
||||
CdshV2Service.Request request = new CdshV2Service.Request(previousE164s, newE164s, serviceIds, token);
|
||||
Single<ServiceResponse<CdshV2Service.Response>> single = service.getRegisteredUsers(auth.getUsername(), auth.getPassword(), request);
|
||||
|
||||
ServiceResponse<CdshV2Service.Response> serviceResponse;
|
||||
ServiceResponse<CdsiV2Service.Response> serviceResponse;
|
||||
try {
|
||||
serviceResponse = single.blockingGet();
|
||||
} catch (Exception e) {
|
||||
|
|
|
@ -1,190 +0,0 @@
|
|||
package org.whispersystems.signalservice.api.services;
|
||||
|
||||
import org.signal.cds.ClientRequest;
|
||||
import org.signal.cds.ClientResponse;
|
||||
import org.signal.libsignal.hsmenclave.HsmEnclaveClient;
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import okhttp3.ConnectionSpec;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
|
||||
/**
|
||||
* Handles the websocket and general lifecycle of a CDSH request.
|
||||
*/
|
||||
final class CdshSocket {
|
||||
|
||||
private static final String TAG = CdshSocket.class.getSimpleName();
|
||||
|
||||
private final OkHttpClient client;
|
||||
private final HsmEnclaveClient enclave;
|
||||
private final String baseUrl;
|
||||
private final String hexPublicKey;
|
||||
private final String hexCodeHash;
|
||||
private final Version version;
|
||||
|
||||
CdshSocket(SignalServiceConfiguration configuration, String hexPublicKey, String hexCodeHash, Version version) {
|
||||
this.baseUrl = configuration.getSignalCdshUrls()[0].getUrl();
|
||||
this.hexPublicKey = hexPublicKey;
|
||||
this.hexCodeHash = hexCodeHash;
|
||||
this.version = version;
|
||||
|
||||
Pair<SSLSocketFactory, X509TrustManager> socketFactory = createTlsSocketFactory(configuration.getSignalCdshUrls()[0].getTrustStore());
|
||||
|
||||
this.client = new OkHttpClient.Builder().sslSocketFactory(new Tls12SocketFactory(socketFactory.first()),
|
||||
socketFactory.second())
|
||||
.connectionSpecs(Util.immutableList(ConnectionSpec.RESTRICTED_TLS))
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
|
||||
try {
|
||||
this.enclave = new HsmEnclaveClient(Hex.fromStringCondensed(hexPublicKey),
|
||||
Collections.singletonList(Hex.fromStringCondensed(hexCodeHash)));
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Badly-formatted public key or code hash!", e);
|
||||
}
|
||||
}
|
||||
|
||||
Observable<ClientResponse> connect(String username, String password, List<ClientRequest> requests) {
|
||||
return Observable.create(emitter -> {
|
||||
AtomicReference<Stage> stage = new AtomicReference<>(Stage.WAITING_TO_INITIALIZE);
|
||||
|
||||
String url = String.format("%s/discovery/%s/%s", baseUrl, hexPublicKey, hexCodeHash);
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authorization", basicAuth(username, password))
|
||||
.build();
|
||||
|
||||
WebSocket webSocket = client.newWebSocket(request, new WebSocketListener() {
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, okio.ByteString bytes) {
|
||||
switch (stage.get()) {
|
||||
case WAITING_TO_INITIALIZE:
|
||||
enclave.completeHandshake(bytes.toByteArray());
|
||||
|
||||
stage.set(Stage.WAITING_FOR_RESPONSE);
|
||||
for (ClientRequest request : requests) {
|
||||
byte[] plaintextBytes = requestToBytes(request, version);
|
||||
byte[] ciphertextBytes = enclave.establishedSend(plaintextBytes);
|
||||
webSocket.send(okio.ByteString.of(ciphertextBytes));
|
||||
}
|
||||
|
||||
break;
|
||||
case WAITING_FOR_RESPONSE:
|
||||
byte[] rawResponse = enclave.establishedRecv(bytes.toByteArray());
|
||||
|
||||
try {
|
||||
ClientResponse clientResponse = ClientResponse.parseFrom(rawResponse);
|
||||
emitter.onNext(clientResponse);
|
||||
} catch (IOException e) {
|
||||
emitter.onError(e);
|
||||
}
|
||||
|
||||
break;
|
||||
case FAILURE:
|
||||
Log.w(TAG, "Received a message after we entered the failure state! Ignoring.");
|
||||
webSocket.close(1000, "OK");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosing(WebSocket webSocket, int code, String reason) {
|
||||
if (code == 1000) {
|
||||
emitter.onComplete();
|
||||
} else {
|
||||
Log.w(TAG, "Remote side is closing with non-normal code " + code);
|
||||
webSocket.close(1000, "Remote closed with code " + code);
|
||||
stage.set(Stage.FAILURE);
|
||||
emitter.onError(new NonSuccessfulResponseCodeException(code));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
||||
emitter.onError(t);
|
||||
stage.set(Stage.FAILURE);
|
||||
webSocket.close(1000, "OK");
|
||||
}
|
||||
});
|
||||
|
||||
webSocket.send(okio.ByteString.of(enclave.initialRequest()));
|
||||
emitter.setCancellable(() -> webSocket.close(1000, "OK"));
|
||||
});
|
||||
}
|
||||
|
||||
private static byte[] requestToBytes(ClientRequest request, Version version) {
|
||||
ByteArrayOutputStream requestStream = new ByteArrayOutputStream();
|
||||
try {
|
||||
requestStream.write(version.getValue());
|
||||
requestStream.write(request.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Failed to write bytes!");
|
||||
}
|
||||
return requestStream.toByteArray();
|
||||
}
|
||||
|
||||
private static String basicAuth(String username, String password) {
|
||||
return "Basic " + Base64.encodeBytes((username + ":" + password).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static Pair<SSLSocketFactory, X509TrustManager> createTlsSocketFactory(TrustStore trustStore) {
|
||||
try {
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
TrustManager[] trustManagers = BlacklistingTrustManager.createFor(trustStore);
|
||||
context.init(null, trustManagers, null);
|
||||
|
||||
return new Pair<>(context.getSocketFactory(), (X509TrustManager) trustManagers[0]);
|
||||
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private enum Stage {
|
||||
WAITING_TO_INITIALIZE, WAITING_FOR_RESPONSE, FAILURE
|
||||
}
|
||||
|
||||
enum Version {
|
||||
V1(1), V2(2);
|
||||
|
||||
private final int value;
|
||||
|
||||
Version(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
package org.whispersystems.signalservice.api.services;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.cds.ClientRequest;
|
||||
import org.signal.cds.ClientResponse;
|
||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
/**
|
||||
* Handles network interactions with CDSHv1, the HSM-backed CDS service.
|
||||
*/
|
||||
public final class CdshV1Service {
|
||||
|
||||
private static final String TAG = CdshV1Service.class.getSimpleName();
|
||||
|
||||
private static final int MAX_E164S_PER_REQUEST = 5000;
|
||||
private static final UUID EMPTY_ACI = new UUID(0, 0);
|
||||
private static final int RESPONSE_ITEM_SIZE = 8 + 16 + 16; // 1 uint64 + 2 UUIDs
|
||||
|
||||
private final CdshSocket cdshSocket;
|
||||
|
||||
public CdshV1Service(SignalServiceConfiguration configuration, String hexPublicKey, String hexCodeHash) {
|
||||
this.cdshSocket = new CdshSocket(configuration, hexPublicKey, hexCodeHash, CdshSocket.Version.V1);
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<Map<String, ACI>>> getRegisteredUsers(String username, String password, Set<String> e164Numbers) {
|
||||
List<String> addressBook = e164Numbers.stream().map(e -> e.substring(1)).collect(Collectors.toList());
|
||||
|
||||
return cdshSocket
|
||||
.connect(username, password, buildClientRequests(addressBook))
|
||||
.map(CdshV1Service::parseEntries)
|
||||
.collect(Collectors.toList())
|
||||
.flatMap(pages -> {
|
||||
Map<String, ACI> all = new HashMap<>();
|
||||
for (Map<String, ACI> page : pages) {
|
||||
all.putAll(page);
|
||||
}
|
||||
return Single.just(all);
|
||||
})
|
||||
.map(result -> ServiceResponse.forResult(result, 200, null))
|
||||
.onErrorReturn(error -> {
|
||||
if (error instanceof NonSuccessfulResponseCodeException) {
|
||||
int status = ((NonSuccessfulResponseCodeException) error).getCode();
|
||||
return ServiceResponse.forApplicationError(error, status, null);
|
||||
} else {
|
||||
return ServiceResponse.forUnknownError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Map<String, ACI> parseEntries(ClientResponse clientResponse) {
|
||||
Map<String, ACI> out = new HashMap<>();
|
||||
ByteBuffer parser = clientResponse.getE164PniAciTriples().asReadOnlyByteBuffer();
|
||||
|
||||
while (parser.remaining() >= RESPONSE_ITEM_SIZE) {
|
||||
String e164 = "+" + parser.getLong();
|
||||
UUID unusedPni = new UUID(parser.getLong(), parser.getLong());
|
||||
UUID aci = new UUID(parser.getLong(), parser.getLong());
|
||||
|
||||
if (!aci.equals(EMPTY_ACI)) {
|
||||
out.put(e164, ACI.from(aci));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private static List<ClientRequest> buildClientRequests(List<String> addressBook) {
|
||||
List<ClientRequest> out = new ArrayList<>((addressBook.size() / MAX_E164S_PER_REQUEST) + 1);
|
||||
ByteString.Output e164Page = ByteString.newOutput();
|
||||
int pageSize = 0;
|
||||
|
||||
for (String address : addressBook) {
|
||||
if (pageSize >= MAX_E164S_PER_REQUEST) {
|
||||
pageSize = 0;
|
||||
out.add(e164sToRequest(e164Page.toByteString(), true));
|
||||
e164Page = ByteString.newOutput();
|
||||
}
|
||||
|
||||
try {
|
||||
e164Page.write(ByteUtil.longToByteArray(Long.parseLong(address)));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Failed to write long to ByteString", e);
|
||||
}
|
||||
|
||||
pageSize++;
|
||||
}
|
||||
|
||||
if (pageSize > 0) {
|
||||
out.add(e164sToRequest(e164Page.toByteString(), false));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private static ClientRequest e164sToRequest(ByteString e164s, boolean more) {
|
||||
return ClientRequest.newBuilder()
|
||||
.setNewE164S(e164s)
|
||||
.setHasMore(more)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
package org.whispersystems.signalservice.api.services;
|
||||
|
||||
import org.signal.cdsi.proto.ClientRequest;
|
||||
import org.signal.cdsi.proto.ClientResponse;
|
||||
import org.signal.libsignal.cds2.AttestationDataException;
|
||||
import org.signal.libsignal.cds2.Cds2Client;
|
||||
import org.signal.libsignal.cds2.Cds2CommunicationFailureException;
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import okhttp3.ConnectionSpec;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
|
||||
/**
|
||||
* Handles the websocket and general lifecycle of a CDSI request.
|
||||
*/
|
||||
final class CdsiSocket {
|
||||
|
||||
private static final String TAG = CdsiSocket.class.getSimpleName();
|
||||
|
||||
private final OkHttpClient okhttp;
|
||||
private final String baseUrl;
|
||||
private final String mrEnclave;
|
||||
|
||||
private Cds2Client client;
|
||||
|
||||
private static final byte[] CERTIFICATE = ("-----BEGIN CERTIFICATE-----\n"
|
||||
+ " MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\n"
|
||||
+ " aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\n"
|
||||
+ " cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\n"
|
||||
+ " BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\n"
|
||||
+ " A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\n"
|
||||
+ " aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\n"
|
||||
+ " AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n"
|
||||
+ " 1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\n"
|
||||
+ " uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\n"
|
||||
+ " MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\n"
|
||||
+ " ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\n"
|
||||
+ " Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\n"
|
||||
+ " KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\n"
|
||||
+ " AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n"
|
||||
+ " -----END CERTIFICATE-----").getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
CdsiSocket(SignalServiceConfiguration configuration, String mrEnclave) {
|
||||
this.baseUrl = configuration.getSignalCdsiUrls()[0].getUrl();
|
||||
this.mrEnclave = mrEnclave;
|
||||
|
||||
Pair<SSLSocketFactory, X509TrustManager> socketFactory = createTlsSocketFactory(configuration.getSignalCdsiUrls()[0].getTrustStore());
|
||||
|
||||
this.okhttp = new OkHttpClient.Builder().sslSocketFactory(new Tls12SocketFactory(socketFactory.first()),
|
||||
socketFactory.second())
|
||||
.connectionSpecs(Util.immutableList(ConnectionSpec.RESTRICTED_TLS))
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
Observable<ClientResponse> connect(String username, String password, ClientRequest clientRequest, Consumer<byte[]> tokenSaver) {
|
||||
return Observable.create(emitter -> {
|
||||
AtomicReference<Stage> stage = new AtomicReference<>(Stage.WAITING_TO_INITIALIZE);
|
||||
|
||||
String url = String.format("%s/v1/%s/discovery", baseUrl, mrEnclave);
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authorization", basicAuth(username, password))
|
||||
.build();
|
||||
|
||||
WebSocket webSocket = okhttp.newWebSocket(request, new WebSocketListener() {
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
Log.d(TAG, "onOpen");
|
||||
stage.set(Stage.WAITING_FOR_CONNECTION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, okio.ByteString bytes) {
|
||||
Log.d(TAG, "[onMessage] stage: " + stage.get());
|
||||
|
||||
try {
|
||||
switch (stage.get()) {
|
||||
case INIT:
|
||||
throw new IOException("Received a message before we were open!");
|
||||
|
||||
case WAITING_FOR_CONNECTION:
|
||||
client = Cds2Client.create_NOT_FOR_PRODUCTION(Hex.fromStringCondensed(mrEnclave),
|
||||
CERTIFICATE,
|
||||
bytes.toByteArray(),
|
||||
Instant.now().minus(Duration.ofHours(24)));
|
||||
|
||||
Log.d(TAG, "[onMessage] Sending initial handshake...");
|
||||
webSocket.send(okio.ByteString.of(client.initialRequest()));
|
||||
stage.set(Stage.WAITING_FOR_HANDSHAKE);
|
||||
break;
|
||||
|
||||
case WAITING_FOR_HANDSHAKE:
|
||||
client.completeHandshake(bytes.toByteArray());
|
||||
Log.d(TAG, "[onMessage] Handshake read success.");
|
||||
|
||||
Log.d(TAG, "[onMessage] Sending data...");
|
||||
byte[] ciphertextBytes = client.establishedSend(clientRequest.toByteArray());
|
||||
webSocket.send(okio.ByteString.of(ciphertextBytes));
|
||||
Log.d(TAG, "[onMessage] Data sent.");
|
||||
|
||||
stage.set(Stage.WAITING_FOR_TOKEN);
|
||||
break;
|
||||
|
||||
case WAITING_FOR_TOKEN:
|
||||
ClientResponse tokenResponse = ClientResponse.parseFrom(client.establishedRecv(bytes.toByteArray()));
|
||||
if (tokenResponse.getToken().isEmpty()) {
|
||||
throw new IOException("No token! Cannot continue!");
|
||||
}
|
||||
|
||||
tokenSaver.accept(tokenResponse.getToken().toByteArray());
|
||||
|
||||
Log.d(TAG, "[onMessage] Sending token ack...");
|
||||
webSocket.send(okio.ByteString.of(client.establishedSend(ClientRequest.newBuilder()
|
||||
.setTokenAck(true)
|
||||
.build()
|
||||
.toByteArray())));
|
||||
stage.set(Stage.WAITING_FOR_RESPONSE);
|
||||
break;
|
||||
|
||||
case WAITING_FOR_RESPONSE:
|
||||
emitter.onNext(ClientResponse.parseFrom(client.establishedRecv(bytes.toByteArray())));
|
||||
break;
|
||||
|
||||
case CLOSED:
|
||||
Log.w(TAG, "[onMessage] Received a message after the websocket closed! Ignoring.");
|
||||
break;
|
||||
|
||||
case FAILED:
|
||||
Log.w(TAG, "[onMessage] Received a message after we entered the failure state! Ignoring.");
|
||||
webSocket.close(1000, "OK");
|
||||
break;
|
||||
}
|
||||
} catch (IOException | AttestationDataException | Cds2CommunicationFailureException e) {
|
||||
Log.w(TAG, e);
|
||||
webSocket.close(1000, "OK");
|
||||
emitter.onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosing(WebSocket webSocket, int code, String reason) {
|
||||
if (code == 1000) {
|
||||
emitter.onComplete();
|
||||
stage.set(Stage.CLOSED);
|
||||
} else {
|
||||
Log.w(TAG, "Remote side is closing with non-normal code " + code);
|
||||
webSocket.close(1000, "Remote closed with code " + code);
|
||||
stage.set(Stage.FAILED);
|
||||
emitter.onError(new NonSuccessfulResponseCodeException(code));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
||||
emitter.onError(t);
|
||||
stage.set(Stage.FAILED);
|
||||
webSocket.close(1000, "OK");
|
||||
}
|
||||
});
|
||||
|
||||
emitter.setCancellable(() -> webSocket.close(1000, "OK"));
|
||||
});
|
||||
}
|
||||
|
||||
private static String basicAuth(String username, String password) {
|
||||
return "Basic " + Base64.encodeBytes((username + ":" + password).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static Pair<SSLSocketFactory, X509TrustManager> createTlsSocketFactory(TrustStore trustStore) {
|
||||
try {
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
TrustManager[] trustManagers = BlacklistingTrustManager.createFor(trustStore);
|
||||
context.init(null, trustManagers, null);
|
||||
|
||||
return new Pair<>(context.getSocketFactory(), (X509TrustManager) trustManagers[0]);
|
||||
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private enum Stage {
|
||||
INIT,
|
||||
WAITING_FOR_CONNECTION,
|
||||
WAITING_FOR_HANDSHAKE,
|
||||
WAITING_FOR_TOKEN,
|
||||
WAITING_TO_INITIALIZE,
|
||||
WAITING_FOR_RESPONSE,
|
||||
CLOSED,
|
||||
FAILED
|
||||
}
|
||||
}
|
|
@ -2,8 +2,8 @@ package org.whispersystems.signalservice.api.services;
|
|||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.cds.ClientRequest;
|
||||
import org.signal.cds.ClientResponse;
|
||||
import org.signal.cdsi.proto.ClientRequest;
|
||||
import org.signal.cdsi.proto.ClientResponse;
|
||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
|
@ -25,45 +25,40 @@ import java.util.Map;
|
|||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
/**
|
||||
* Handles network interactions with CDSHv2, the HSM-backed CDS service.
|
||||
* Handles network interactions with CDSI, the SGX-backed version of the CDSv2 API.
|
||||
*/
|
||||
public final class CdshV2Service {
|
||||
public final class CdsiV2Service {
|
||||
|
||||
private static final String TAG = CdshV2Service.class.getSimpleName();
|
||||
private static final String TAG = CdsiV2Service.class.getSimpleName();
|
||||
|
||||
private static final UUID EMPTY_UUID = new UUID(0, 0);
|
||||
private static final int RESPONSE_ITEM_SIZE = 8 + 16 + 16; // 1 uint64 + 2 UUIDs
|
||||
|
||||
private final CdshSocket cdshSocket;
|
||||
private final CdsiSocket cdshSocket;
|
||||
|
||||
public CdshV2Service(SignalServiceConfiguration configuration, String hexPublicKey, String hexCodeHash) {
|
||||
this.cdshSocket = new CdshSocket(configuration, hexPublicKey, hexCodeHash, CdshSocket.Version.V2);
|
||||
public CdsiV2Service(SignalServiceConfiguration configuration, String mrEnclave) {
|
||||
this.cdshSocket = new CdsiSocket(configuration, mrEnclave);
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<Response>> getRegisteredUsers(String username, String password, Request request) {
|
||||
public Single<ServiceResponse<Response>> getRegisteredUsers(String username, String password, Request request, Consumer<byte[]> tokenSaver) {
|
||||
return cdshSocket
|
||||
.connect(username, password, buildClientRequests(request))
|
||||
.map(CdshV2Service::parseEntries)
|
||||
.connect(username, password, buildClientRequest(request), tokenSaver)
|
||||
.map(CdsiV2Service::parseEntries)
|
||||
.collect(Collectors.toList())
|
||||
.flatMap(pages -> {
|
||||
byte[] token = null;
|
||||
Map<String, ResponseItem> all = new HashMap<>();
|
||||
|
||||
for (Response page : pages) {
|
||||
all.putAll(page.getResults());
|
||||
token = token == null ? page.getToken() : token;
|
||||
}
|
||||
|
||||
if (token == null) {
|
||||
throw new IOException("No token found in response!");
|
||||
}
|
||||
|
||||
return Single.just(new Response(all, token));
|
||||
return Single.just(new Response(all));
|
||||
})
|
||||
.map(result -> ServiceResponse.forResult(result, 200, null))
|
||||
.onErrorReturn(error -> {
|
||||
|
@ -77,7 +72,6 @@ public final class CdshV2Service {
|
|||
}
|
||||
|
||||
private static Response parseEntries(ClientResponse clientResponse) {
|
||||
byte[] token = !clientResponse.getToken().isEmpty() ? clientResponse.getToken().toByteArray() : null;
|
||||
Map<String, ResponseItem> results = new HashMap<>();
|
||||
ByteBuffer parser = clientResponse.getE164PniAciTriples().asReadOnlyByteBuffer();
|
||||
|
||||
|
@ -93,22 +87,25 @@ public final class CdshV2Service {
|
|||
}
|
||||
}
|
||||
|
||||
return new Response(results, token);
|
||||
return new Response(results);
|
||||
}
|
||||
|
||||
private static List<ClientRequest> buildClientRequests(Request request) {
|
||||
private static ClientRequest buildClientRequest(Request request) {
|
||||
List<Long> previousE164s = parseAndSortE164Strings(request.previousE164s);
|
||||
List<Long> newE164s = parseAndSortE164Strings(request.newE164s);
|
||||
List<Long> removedE164s = parseAndSortE164Strings(request.removedE164s);
|
||||
|
||||
return Collections.singletonList(ClientRequest.newBuilder()
|
||||
.setPrevE164S(toByteString(previousE164s))
|
||||
.setNewE164S(toByteString(newE164s))
|
||||
.setDiscardE164S(toByteString(removedE164s))
|
||||
.setAciUakPairs(toByteString(request.serviceIds))
|
||||
.setToken(ByteString.copyFrom(request.token))
|
||||
.setHasMore(false)
|
||||
.build());
|
||||
ClientRequest.Builder builder = ClientRequest.newBuilder()
|
||||
.setPrevE164S(toByteString(previousE164s))
|
||||
.setNewE164S(toByteString(newE164s))
|
||||
.setDiscardE164S(toByteString(removedE164s))
|
||||
.setAciUakPairs(toByteString(request.serviceIds));
|
||||
|
||||
if (request.token != null) {
|
||||
builder.setToken(ByteString.copyFrom(request.token));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static ByteString toByteString(List<Long> numbers) {
|
||||
|
@ -158,11 +155,15 @@ public final class CdshV2Service {
|
|||
private final byte[] token;
|
||||
|
||||
public Request(Set<String> previousE164s, Set<String> newE164s, Map<ServiceId, ProfileKey> serviceIds, Optional<byte[]> token) {
|
||||
if (previousE164s.size() > 0 && !token.isPresent()) {
|
||||
throw new IllegalArgumentException("You must have a token if you have previousE164s!");
|
||||
}
|
||||
|
||||
this.previousE164s = previousE164s;
|
||||
this.newE164s = newE164s;
|
||||
this.removedE164s = Collections.emptySet();
|
||||
this.serviceIds = serviceIds;
|
||||
this.token = token.isPresent() ? token.get() : new byte[32];
|
||||
this.token = token.orElse(null);
|
||||
}
|
||||
|
||||
public int totalE164s() {
|
||||
|
@ -176,20 +177,14 @@ public final class CdshV2Service {
|
|||
|
||||
public static final class Response {
|
||||
private final Map<String, ResponseItem> results;
|
||||
private final byte[] token;
|
||||
|
||||
public Response(Map<String, ResponseItem> results, byte[] token) {
|
||||
public Response(Map<String, ResponseItem> results) {
|
||||
this.results = results;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public Map<String, ResponseItem> getResults() {
|
||||
return results;
|
||||
}
|
||||
|
||||
public byte[] getToken() {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ResponseItem {
|
|
@ -5,13 +5,13 @@ import org.whispersystems.signalservice.api.push.TrustStore;
|
|||
|
||||
import okhttp3.ConnectionSpec;
|
||||
|
||||
public class SignalCdshUrl extends SignalUrl {
|
||||
public class SignalCdsiUrl extends SignalUrl {
|
||||
|
||||
public SignalCdshUrl(String url, TrustStore trustStore) {
|
||||
public SignalCdsiUrl(String url, TrustStore trustStore) {
|
||||
super(url, trustStore);
|
||||
}
|
||||
|
||||
public SignalCdshUrl(String url, String hostHeader, TrustStore trustStore, ConnectionSpec connectionSpec) {
|
||||
public SignalCdsiUrl(String url, String hostHeader, TrustStore trustStore, ConnectionSpec connectionSpec) {
|
||||
super(url, hostHeader, trustStore, connectionSpec);
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ public final class SignalServiceConfiguration {
|
|||
private final SignalServiceUrl[] signalServiceUrls;
|
||||
private final Map<Integer, SignalCdnUrl[]> signalCdnUrlMap;
|
||||
private final SignalContactDiscoveryUrl[] signalContactDiscoveryUrls;
|
||||
private final SignalCdshUrl[] signalCdshUrls;
|
||||
private final SignalCdsiUrl[] signalCdsiUrls;
|
||||
private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls;
|
||||
private final SignalStorageUrl[] signalStorageUrls;
|
||||
private final List<Interceptor> networkInterceptors;
|
||||
|
@ -27,7 +27,7 @@ public final class SignalServiceConfiguration {
|
|||
SignalContactDiscoveryUrl[] signalContactDiscoveryUrls,
|
||||
SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls,
|
||||
SignalStorageUrl[] signalStorageUrls,
|
||||
SignalCdshUrl[] signalCdshUrls,
|
||||
SignalCdsiUrl[] signalCdsiUrls,
|
||||
List<Interceptor> networkInterceptors,
|
||||
Optional<Dns> dns,
|
||||
Optional<SignalProxy> proxy,
|
||||
|
@ -36,7 +36,7 @@ public final class SignalServiceConfiguration {
|
|||
this.signalServiceUrls = signalServiceUrls;
|
||||
this.signalCdnUrlMap = signalCdnUrlMap;
|
||||
this.signalContactDiscoveryUrls = signalContactDiscoveryUrls;
|
||||
this.signalCdshUrls = signalCdshUrls;
|
||||
this.signalCdsiUrls = signalCdsiUrls;
|
||||
this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls;
|
||||
this.signalStorageUrls = signalStorageUrls;
|
||||
this.networkInterceptors = networkInterceptors;
|
||||
|
@ -57,8 +57,8 @@ public final class SignalServiceConfiguration {
|
|||
return signalContactDiscoveryUrls;
|
||||
}
|
||||
|
||||
public SignalCdshUrl[] getSignalCdshUrls() {
|
||||
return signalCdshUrls;
|
||||
public SignalCdsiUrl[] getSignalCdsiUrls() {
|
||||
return signalCdsiUrls;
|
||||
}
|
||||
|
||||
public SignalKeyBackupServiceUrl[] getSignalKeyBackupServiceUrls() {
|
||||
|
|
|
@ -2,7 +2,7 @@ package org.whispersystems.signalservice.internal.push;
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class CdshAuthResponse {
|
||||
public class CdsiAuthResponse {
|
||||
|
||||
@JsonProperty
|
||||
private String username;
|
|
@ -22,7 +22,6 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
|||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.libsignal.zkgroup.profiles.PniCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||
|
@ -259,7 +258,7 @@ public class PushServiceSocket {
|
|||
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials";
|
||||
private static final String BOOST_BADGES = "/v1/subscription/boost/badges";
|
||||
|
||||
private static final String CDSH_AUTH = "/v2/directory/auth";
|
||||
private static final String CDSI_AUTH = "/v2/directory/auth";
|
||||
|
||||
private static final String REPORT_SPAM = "/v1/messages/report/%s/%s";
|
||||
|
||||
|
@ -343,9 +342,9 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
public CdshAuthResponse getCdshAuth() throws IOException {
|
||||
String body = makeServiceRequest(CDSH_AUTH, "GET", null);
|
||||
return JsonUtil.fromJsonResponse(body, CdshAuthResponse.class);
|
||||
public CdsiAuthResponse getCdsiAuth() throws IOException {
|
||||
String body = makeServiceRequest(CDSI_AUTH, "GET", null);
|
||||
return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class);
|
||||
}
|
||||
|
||||
public VerifyAccountResponse verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages,
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
syntax = "proto3";
|
||||
|
||||
option java_multiple_files = true;
|
||||
option java_package = "org.signal.cds";
|
||||
option java_outer_classname = "Cds";
|
||||
option java_package = "org.signal.cdsi.proto";
|
||||
|
||||
package org.signal.cds;
|
||||
package org.signal.cdsi;
|
||||
|
||||
message ClientRequest {
|
||||
// Each ACI/UAK pair is a 32-byte buffer, containing the 16-byte ACI followed
|
||||
|
@ -16,14 +15,16 @@ message ClientRequest {
|
|||
bytes new_e164s = 3;
|
||||
bytes discard_e164s = 4;
|
||||
|
||||
// If true, the client has more pairs or e164s to send. If false or unset,
|
||||
// this is the client's last request, and processing should commence.
|
||||
bool has_more = 5;
|
||||
reserved /*bool has_more*/ 5;
|
||||
|
||||
// If set, a token which allows rate limiting to discount the e164s in
|
||||
// the request's prev_e164s, only counting new_e164s. If not set, then
|
||||
// rate limiting considers both prev_e164s' and new_e164s' size.
|
||||
bytes token = 6;
|
||||
|
||||
// After receiving a new token from the server, send back a message just
|
||||
// containing a token_ack.
|
||||
bool token_ack = 7;
|
||||
}
|
||||
|
||||
message ClientResponse {
|
||||
|
@ -53,3 +54,29 @@ message ClientResponse {
|
|||
// request's new_e164s.
|
||||
bytes token = 3;
|
||||
}
|
||||
|
||||
message EnclaveLoad {
|
||||
// If set, before loading any tuples entirely clear the current map,
|
||||
// zero'ing out all current data.
|
||||
bool clear_all = 1;
|
||||
|
||||
// Each tuple is an 8-byte e164, a 16-byte PNI, a 16-byte ACI, and a
|
||||
// 16-byte UAK. These should be loaded as a 48-byte value (PNI,ACI,UAK)
|
||||
// associated with an 8-byte key (e164).
|
||||
// ACI/PNI/UAK may all be zeros, in which case this is a delete of the e164.
|
||||
bytes e164_aci_pni_uak_tuples = 2;
|
||||
|
||||
// If non-empty, overwrite the shared token secret with this value.
|
||||
bytes shared_token_secret = 3;
|
||||
}
|
||||
|
||||
message ClientHandshakeStart {
|
||||
// Public key associated with this server's enclave
|
||||
bytes pubkey = 1;
|
||||
|
||||
// Remote-attestation evidence associated with the public key
|
||||
bytes evidence = 2;
|
||||
|
||||
// Endorsements of remote-attestation evidence.
|
||||
bytes endorsement = 3;
|
||||
}
|
Loading…
Add table
Reference in a new issue