Implement new CDS changes.

master
Greyson Parrelli 2020-07-02 10:38:52 -07:00
parent 1752972be9
commit 2791790bf5
22 changed files with 908 additions and 561 deletions

View File

@ -197,7 +197,7 @@ android {
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "CDS_MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
buildConfigField "String", "CDS_MRENCLAVE", "\"b657cad56d518827b0938949bb1e5727a9a4db358dd6a88e55e710a89ffa50bd\""
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""

View File

@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.contacts.sync;
import androidx.annotation.NonNull;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper.DirectoryResult;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.SetUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.UUID;
class ContactDiscoveryV1 {
private static final String TAG = ContactDiscoveryV1.class.getSimpleName();
static @NonNull DirectoryResult getDirectoryResult(@NonNull Set<String> databaseNumbers,
@NonNull Set<String> systemNumbers)
throws IOException
{
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers);
List<ContactTokenDetails> activeTokens = getTokens(inputResult.getNumbers());
Set<String> activeNumbers = Stream.of(activeTokens).map(ContactTokenDetails::getNumber).collect(Collectors.toSet());
FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(activeNumbers, inputResult);
HashMap<String, UUID> uuids = new HashMap<>();
for (String number : outputResult.getNumbers()) {
uuids.put(number, null);
}
return new DirectoryResult(uuids, outputResult.getRewrites());
}
static @NonNull DirectoryResult getDirectoryResult(@NonNull String number) throws IOException {
return getDirectoryResult(Collections.singleton(number), Collections.singleton(number));
}
private static @NonNull List<ContactTokenDetails> getTokens(@NonNull Set<String> numbers) throws IOException {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
if (numbers.size() == 1) {
Optional<ContactTokenDetails> details = accountManager.getContact(numbers.iterator().next());
return details.isPresent() ? Collections.singletonList(details.get()) : Collections.emptyList();
} else {
return accountManager.getContacts(numbers);
}
}
}

View File

@ -0,0 +1,89 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper.DirectoryResult;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.push.IasTrustStore;
import org.thoughtcrime.securesms.util.SetUtil;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
class ContactDiscoveryV2 {
private static final String TAG = Log.tag(ContactDiscoveryV2.class);
@WorkerThread
static DirectoryResult getDirectoryResult(@NonNull Context context,
@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());
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
KeyStore iasKeyStore = getIasKeyStore(context);
try {
Map<String, UUID> results = accountManager.getRegisteredUsers(iasKeyStore, sanitizedNumbers, BuildConfig.CDS_MRENCLAVE);
FuzzyPhoneNumberHelper.OutputResultV2 outputResult = FuzzyPhoneNumberHelper.generateOutputV2(results, inputResult);
return new DirectoryResult(outputResult.getNumbers(), outputResult.getRewrites());
} catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException e) {
Log.w(TAG, "Attestation error.", e);
throw new IOException(e);
}
}
static @NonNull DirectoryResult getDirectoryResult(@NonNull Context context, @NonNull String number) throws IOException {
return getDirectoryResult(context, Collections.singleton(number), Collections.singleton(number));
}
private static Set<String> sanitizeNumbers(@NonNull Set<String> numbers) {
return Stream.of(numbers).filter(number -> {
try {
return number.startsWith("+") && number.length() > 1 && number.charAt(1) != '0' && Long.parseLong(number.substring(1)) > 0;
} catch (NumberFormatException e) {
return false;
}
}).collect(Collectors.toSet());
}
private static KeyStore getIasKeyStore(@NonNull Context context) {
try {
TrustStore contactTrustStore = new IasTrustStore(context);
KeyStore keyStore = KeyStore.getInstance("BKS");
keyStore.load(contactTrustStore.getKeyStoreInputStream(), contactTrustStore.getKeyStorePassword().toCharArray());
return keyStore;
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@ -1,32 +1,153 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.BulkOperationsHandle;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Manages all the stuff around determining if a user is registered or not.
*/
public class DirectoryHelper {
private static final String TAG = Log.tag(DirectoryHelper.class);
@WorkerThread
public static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) throws IOException {
if (FeatureFlags.uuids()) {
// TODO [greyson] Create a DirectoryHelperV2 when appropriate.
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) {
Log.i(TAG, "Have not yet set our own local number. Skipping.");
return;
}
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
Log.i(TAG, "No contact permissions. Skipping.");
return;
}
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Set<String> databaseNumbers = sanitizeNumbers(recipientDatabase.getAllPhoneNumbers());
Set<String> systemNumbers = sanitizeNumbers(ContactAccessor.getInstance().getAllContactsWithNumbers(context));
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
DirectoryResult result;
if (FeatureFlags.cds()) {
result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
} else {
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
}
if (result.getNumberRewrites().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
}
HashMap<RecipientId, String> uuidMap = new HashMap<>();
// TODO [greyson] [cds] Probably want to do this in a DB transaction to prevent concurrent operations
for (Map.Entry<String, UUID> entry : result.getRegisteredNumbers().entrySet()) {
// TODO [greyson] [cds] This is where we'll have to do record merging
String e164 = entry.getKey();
UUID uuid = entry.getValue();
Optional<RecipientId> uuidEntry = uuid != null ? recipientDatabase.getByUuid(uuid) : Optional.absent();
// TODO [greyson] [cds] Handle phone numbers changing, possibly conflicting
if (uuidEntry.isPresent()) {
recipientDatabase.setPhoneNumber(uuidEntry.get(), e164);
}
RecipientId id = uuidEntry.isPresent() ? uuidEntry.get() : recipientDatabase.getOrInsertFromE164(e164);
uuidMap.put(id, uuid != null ? uuid.toString() : null);
}
Set<String> activeNumbers = result.getRegisteredNumbers().keySet();
Set<RecipientId> activeIds = uuidMap.keySet();
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
.filterNot(activeNumbers::contains)
.filterNot(n -> result.getNumberRewrites().containsKey(n))
.map(recipientDatabase::getOrInsertFromE164)
.collect(Collectors.toSet());
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
updateContactsDatabase(context, activeIds, true, result.getNumberRewrites());
if (TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
}
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
Set<RecipientId> existingSignalIds = new HashSet<>(recipientDatabase.getRegistered());
Set<RecipientId> existingSystemIds = new HashSet<>(recipientDatabase.getSystemContacts());
Set<RecipientId> newlyActiveIds = new HashSet<>(activeIds);
newlyActiveIds.removeAll(existingSignalIds);
newlyActiveIds.retainAll(existingSystemIds);
notifyNewUsers(context, newlyActiveIds);
} else {
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
}
StorageSyncHelper.scheduleSyncForDataChange();
@ -34,20 +155,249 @@ public class DirectoryHelper {
@WorkerThread
public static RegisteredState refreshDirectoryFor(@NonNull Context context, @NonNull Recipient recipient, boolean notifyOfNewUsers) throws IOException {
RegisteredState originalRegisteredState = recipient.resolve().getRegistered();
RegisteredState newRegisteredState = null;
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
RegisteredState originalRegisteredState = recipient.resolve().getRegistered();
RegisteredState newRegisteredState = null;
if (FeatureFlags.uuids()) {
// TODO [greyson] Create a DirectoryHelperV2 when appropriate.
newRegisteredState = DirectoryHelperV1.refreshDirectoryFor(context, recipient, notifyOfNewUsers);
} else {
newRegisteredState = DirectoryHelperV1.refreshDirectoryFor(context, recipient, notifyOfNewUsers);
if (recipient.getUuid().isPresent()) {
boolean isRegistered = isUuidRegistered(context, recipient);
if (isRegistered) {
recipientDatabase.markRegistered(recipient.getId(), recipient.getUuid().get());
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
return isRegistered ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED;
}
if (!recipient.getE164().isPresent()) {
Log.w(TAG, "No UUID or E164?");
return RegisteredState.NOT_REGISTERED;
}
DirectoryResult result;
if (FeatureFlags.cds()) {
result = ContactDiscoveryV2.getDirectoryResult(context, recipient.getE164().get());
} else {
result = ContactDiscoveryV1.getDirectoryResult(recipient.getE164().get());
}
if (result.getNumberRewrites().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
}
if (result.getRegisteredNumbers().size() > 0) {
recipientDatabase.markRegistered(recipient.getId(), result.getRegisteredNumbers().values().iterator().next());
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
updateContactsDatabase(context, Collections.singletonList(recipient.getId()), false, result.getNumberRewrites());
}
newRegisteredState = result.getRegisteredNumbers().size() > 0 ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED;
if (newRegisteredState != originalRegisteredState) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
if (notifyOfNewUsers && newRegisteredState == RegisteredState.REGISTERED && recipient.resolve().isSystemContact()) {
notifyNewUsers(context, Collections.singletonList(recipient.getId()));
}
StorageSyncHelper.scheduleSyncForDataChange();
}
return newRegisteredState;
}
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
try {
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS);
return true;
} catch (ExecutionException e) {
if (e.getCause() instanceof NotFoundException) {
return false;
} else {
throw new IOException(e);
}
} catch (InterruptedException | TimeoutException e) {
throw new IOException(e);
}
}
private static void updateContactsDatabase(@NonNull Context context,
@NonNull Collection<RecipientId> activeIds,
boolean removeMissing,
@NonNull Map<String, String> rewrites)
{
AccountHolder account = getOrCreateSystemAccount(context);
if (account == null) {
Log.w(TAG, "Failed to create an account!");
return;
}
try {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
ContactsDatabase contactsDatabase = DatabaseFactory.getContactsDatabase(context);
List<String> activeAddresses = Stream.of(activeIds)
.map(Recipient::resolved)
.filter(Recipient::hasE164)
.map(Recipient::requireE164)
.toList();
contactsDatabase.removeDeletedRawContacts(account.getAccount());
contactsDatabase.setRegisteredUsers(account.getAccount(), activeAddresses, removeMissing);
Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context);
BulkOperationsHandle handle = recipientDatabase.beginBulkSystemContactUpdate();
try {
while (cursor != null && cursor.moveToNext()) {
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
if (isValidContactNumber(number)) {
String formattedNumber = PhoneNumberFormatter.get(context).format(number);
String realNumber = Util.getFirstNonEmpty(rewrites.get(formattedNumber), formattedNumber);
RecipientId recipientId = Recipient.externalContact(context, realNumber).getId();
String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String contactPhotoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI));
String contactLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL));
int phoneType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE));
Uri contactUri = ContactsContract.Contacts.getLookupUri(cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone._ID)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)));
handle.setSystemContactInfo(recipientId, displayName, contactPhotoUri, contactLabel, phoneType, contactUri.toString());
}
}
} finally {
handle.finish();
}
if (NotificationChannels.supported()) {
try (RecipientDatabase.RecipientReader recipients = DatabaseFactory.getRecipientDatabase(context).getRecipientsWithNotificationChannels()) {
Recipient recipient;
while ((recipient = recipients.getNext()) != null) {
NotificationChannels.updateContactChannelName(context, recipient);
}
}
}
} catch (RemoteException | OperationApplicationException e) {
Log.w(TAG, "Failed to update contacts.", e);
}
}
private static boolean isValidContactNumber(@Nullable String number) {
return !TextUtils.isEmpty(number) && !UuidUtil.isUuid(number);
}
private static @Nullable AccountHolder getOrCreateSystemAccount(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account[] accounts = accountManager.getAccountsByType(BuildConfig.APPLICATION_ID);
AccountHolder account;
if (accounts.length == 0) {
account = createAccount(context);
} else {
account = new AccountHolder(accounts[0], false);
}
if (account != null && !ContentResolver.getSyncAutomatically(account.getAccount(), ContactsContract.AUTHORITY)) {
ContentResolver.setSyncAutomatically(account.getAccount(), ContactsContract.AUTHORITY, true);
}
return account;
}
private static @Nullable AccountHolder createAccount(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account account = new Account(context.getString(R.string.app_name), BuildConfig.APPLICATION_ID);
if (accountManager.addAccountExplicitly(account, null, null)) {
Log.i(TAG, "Created new account...");
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
return new AccountHolder(account, true);
} else {
Log.w(TAG, "Failed to create account!");
return null;
}
}
private static void notifyNewUsers(@NonNull Context context,
@NonNull Collection<RecipientId> newUsers)
{
if (!TextSecurePreferences.isNewContactsNotificationEnabled(context)) return;
for (RecipientId newUser: newUsers) {
Recipient recipient = Recipient.resolved(newUser);
if (!SessionUtil.hasSession(context, recipient.getId()) && !recipient.isLocalNumber()) {
IncomingJoinedMessage message = new IncomingJoinedMessage(newUser);
Optional<InsertResult> insertResult = DatabaseFactory.getSmsDatabase(context).insertMessageInbox(message);
if (insertResult.isPresent()) {
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
if (hour >= 9 && hour < 23) {
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), true);
} else {
Log.i(TAG, "Not notifying of a new user due to the time of day. (Hour: " + hour + ")");
}
}
}
}
}
private static Set<String> sanitizeNumbers(@NonNull Set<String> numbers) {
return Stream.of(numbers).filter(number -> {
try {
return number.startsWith("+") && number.length() > 1 && number.charAt(1) != '0' && Long.parseLong(number.substring(1)) > 0;
} catch (NumberFormatException e) {
return false;
}
}).collect(Collectors.toSet());
}
static class DirectoryResult {
private final Map<String, UUID> registeredNumbers;
private final Map<String, String> numberRewrites;
DirectoryResult(@NonNull Map<String, UUID> registeredNumbers,
@NonNull Map<String, String> numberRewrites)
{
this.registeredNumbers = registeredNumbers;
this.numberRewrites = numberRewrites;
}
@NonNull Map<String, UUID> getRegisteredNumbers() {
return registeredNumbers;
}
@NonNull Map<String, String> getNumberRewrites() {
return numberRewrites;
}
}
private static class AccountHolder {
private final boolean fresh;
private final Account account;
private AccountHolder(Account account, boolean fresh) {
this.fresh = fresh;
this.account = account;
}
@SuppressWarnings("unused")
public boolean isFresh() {
return fresh;
}
public Account getAccount() {
return account;
}
}
}

View File

@ -1,401 +0,0 @@
package org.thoughtcrime.securesms.contacts.sync;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
class DirectoryHelperV1 {
private static final String TAG = DirectoryHelperV1.class.getSimpleName();
@WorkerThread
static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) throws IOException {
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) return;
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) return;
List<RecipientId> newlyActiveUsers = refreshDirectory(context, ApplicationDependencies.getSignalServiceAccountManager());
if (TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
}
if (notifyOfNewUsers) notifyNewUsers(context, newlyActiveUsers);
}
@SuppressLint("CheckResult")
private static @NonNull List<RecipientId> refreshDirectory(@NonNull Context context, @NonNull SignalServiceAccountManager accountManager) throws IOException {
if (TextUtils.isEmpty(TextSecurePreferences.getLocalNumber(context))) {
return Collections.emptyList();
}
if (!Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
return Collections.emptyList();
}
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Set<String> allRecipientNumbers = recipientDatabase.getAllPhoneNumbers();
Stream<String> eligibleRecipientDatabaseContactNumbers = Stream.of(allRecipientNumbers);
Stream<String> eligibleSystemDatabaseContactNumbers = Stream.of(ContactAccessor.getInstance().getAllContactsWithNumbers(context));
Set<String> eligibleContactNumbers = Stream.concat(eligibleRecipientDatabaseContactNumbers, eligibleSystemDatabaseContactNumbers).collect(Collectors.toSet());
Set<String> storedNumbers = Stream.of(allRecipientNumbers).collect(Collectors.toSet());
DirectoryResult directoryResult = getDirectoryResult(context, accountManager, recipientDatabase, storedNumbers, eligibleContactNumbers);
return directoryResult.getNewlyActiveRecipients();
}
@WorkerThread
static RegisteredState refreshDirectoryFor(@NonNull Context context, @NonNull Recipient recipient, boolean notifyOfNewUsers) throws IOException {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
if (recipient.getUuid().isPresent() && !recipient.getE164().isPresent()) {
boolean isRegistered = isUuidRegistered(context, recipient);
if (isRegistered) {
recipientDatabase.markRegistered(recipient.getId(), recipient.getUuid().get());
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
return isRegistered ? RegisteredState.REGISTERED : RegisteredState.NOT_REGISTERED;
}
return getRegisteredState(context, ApplicationDependencies.getSignalServiceAccountManager(), recipientDatabase, recipient);
}
private static void updateContactsDatabase(@NonNull Context context, @NonNull List<RecipientId> activeIds, boolean removeMissing, Map<String, String> rewrites) {
Optional<AccountHolder> account = getOrCreateAccount(context);
if (account.isPresent()) {
try {
List<String> activeAddresses = Stream.of(activeIds).map(Recipient::resolved).filter(Recipient::hasE164).map(Recipient::requireE164).toList();
DatabaseFactory.getContactsDatabase(context).removeDeletedRawContacts(account.get().getAccount());
DatabaseFactory.getContactsDatabase(context).setRegisteredUsers(account.get().getAccount(), activeAddresses, removeMissing);
Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context);
RecipientDatabase.BulkOperationsHandle handle = DatabaseFactory.getRecipientDatabase(context).beginBulkSystemContactUpdate();
try {
while (cursor != null && cursor.moveToNext()) {
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));
if (isValidContactNumber(number)) {
String formattedNumber = PhoneNumberFormatter.get(context).format(number);
String realNumber = Util.getFirstNonEmpty(rewrites.get(formattedNumber), formattedNumber);
RecipientId recipientId = Recipient.externalContact(context, realNumber).getId();
String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String contactPhotoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_URI));
String contactLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL));
int phoneType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE));
Uri contactUri = ContactsContract.Contacts.getLookupUri(cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone._ID)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)));
handle.setSystemContactInfo(recipientId, displayName, contactPhotoUri, contactLabel, phoneType, contactUri.toString());
}
}
} finally {
handle.finish();
}
if (NotificationChannels.supported()) {
try (RecipientDatabase.RecipientReader recipients = DatabaseFactory.getRecipientDatabase(context).getRecipientsWithNotificationChannels()) {
Recipient recipient;
while ((recipient = recipients.getNext()) != null) {
NotificationChannels.updateContactChannelName(context, recipient);
}
}
}
} catch (RemoteException | OperationApplicationException e) {
Log.w(TAG, "Failed to update contacts.", e);
}
}
}
private static void notifyNewUsers(@NonNull Context context,
@NonNull List<RecipientId> newUsers)
{
if (!TextSecurePreferences.isNewContactsNotificationEnabled(context)) return;
for (RecipientId newUser: newUsers) {
Recipient recipient = Recipient.resolved(newUser);
if (!SessionUtil.hasSession(context, recipient.getId()) && !recipient.isLocalNumber()) {
IncomingJoinedMessage message = new IncomingJoinedMessage(newUser);
Optional<InsertResult> insertResult = DatabaseFactory.getSmsDatabase(context).insertMessageInbox(message);
if (insertResult.isPresent()) {
int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
if (hour >= 9 && hour < 23) {
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), true);
} else {
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), false);
}
}
}
}
}
private static Optional<AccountHolder> getOrCreateAccount(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account[] accounts = accountManager.getAccountsByType("org.thoughtcrime.securesms");
Optional<AccountHolder> account;
if (accounts.length == 0) account = createAccount(context);
else account = Optional.of(new AccountHolder(accounts[0], false));
if (account.isPresent() && !ContentResolver.getSyncAutomatically(account.get().getAccount(), ContactsContract.AUTHORITY)) {
ContentResolver.setSyncAutomatically(account.get().getAccount(), ContactsContract.AUTHORITY, true);
}
return account;
}
private static Optional<AccountHolder> createAccount(Context context) {
AccountManager accountManager = AccountManager.get(context);
Account account = new Account(context.getString(R.string.app_name), "org.thoughtcrime.securesms");
if (accountManager.addAccountExplicitly(account, null, null)) {
Log.i(TAG, "Created new account...");
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1);
return Optional.of(new AccountHolder(account, true));
} else {
Log.w(TAG, "Failed to create account!");
return Optional.absent();
}
}
private static DirectoryResult getDirectoryResult(@NonNull Context context,
@NonNull SignalServiceAccountManager accountManager,
@NonNull RecipientDatabase recipientDatabase,
@NonNull Set<String> locallyStoredNumbers,
@NonNull Set<String> eligibleContactNumbers)
throws IOException
{
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(eligibleContactNumbers, locallyStoredNumbers);
List<ContactTokenDetails> activeTokens = accountManager.getContacts(inputResult.getNumbers());
Set<String> activeNumbers = Stream.of(activeTokens).map(ContactTokenDetails::getNumber).collect(Collectors.toSet());
FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(activeNumbers, inputResult);
if (inputResult.getFuzzies().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Got a fuzzy number result.");
}
if (outputResult.getRewrites().size() > 0) {
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
}
recipientDatabase.updatePhoneNumbers(outputResult.getRewrites());
List<RecipientId> activeIds = new LinkedList<>();
List<RecipientId> inactiveIds = new LinkedList<>();
Set<String> inactiveContactNumbers = new HashSet<>(inputResult.getNumbers());
inactiveContactNumbers.removeAll(outputResult.getRewrites().keySet());
for (String number : outputResult.getNumbers()) {
activeIds.add(recipientDatabase.getOrInsertFromE164(number));
inactiveContactNumbers.remove(number);
}
for (String inactiveContactNumber : inactiveContactNumbers) {
inactiveIds.add(recipientDatabase.getOrInsertFromE164(inactiveContactNumber));
}
Set<RecipientId> currentActiveIds = new HashSet<>(recipientDatabase.getRegistered());
Set<RecipientId> contactIds = new HashSet<>(recipientDatabase.getSystemContacts());
List<RecipientId> newlyActiveIds = Stream.of(activeIds)
.filter(id -> !currentActiveIds.contains(id))
.filter(contactIds::contains)
.toList();
recipientDatabase.setRegistered(activeIds, inactiveIds);
updateContactsDatabase(context, activeIds, true, outputResult.getRewrites());
Set<String> activeContactNumbers = Stream.of(activeIds).map(Recipient::resolved).filter(Recipient::hasSmsAddress).map(Recipient::requireSmsAddress).collect(Collectors.toSet());
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context)) {
return new DirectoryResult(activeContactNumbers, newlyActiveIds);
} else {
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
return new DirectoryResult(activeContactNumbers);
}
}
private static RegisteredState getRegisteredState(@NonNull Context context,
@NonNull SignalServiceAccountManager accountManager,
@NonNull RecipientDatabase recipientDatabase,
@NonNull Recipient recipient)
throws IOException
{
boolean activeUser = recipient.resolve().getRegistered() == RegisteredState.REGISTERED;
boolean systemContact = recipient.isSystemContact();
Optional<ContactTokenDetails> details = Optional.absent();
Map<String, String> rewrites = new HashMap<>();
if (recipient.hasE164()) {
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(Collections.singletonList(recipient.requireE164()), recipientDatabase.getAllPhoneNumbers());
if (inputResult.getNumbers().size() > 1) {
Log.i(TAG, "[getRegisteredState] Got a fuzzy number result.");
List<ContactTokenDetails> detailList = accountManager.getContacts(inputResult.getNumbers());
Collection<String> registered = Stream.of(detailList).map(ContactTokenDetails::getNumber).collect(Collectors.toSet());
FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(registered, inputResult);
String finalNumber = recipient.requireE164();
ContactTokenDetails detail = new ContactTokenDetails();
if (outputResult.getRewrites().size() > 0 && outputResult.getRewrites().containsKey(finalNumber)) {
Log.i(TAG, "[getRegisteredState] Need to rewrite a number.");
finalNumber = outputResult.getRewrites().get(finalNumber);
rewrites = outputResult.getRewrites();
}
detail.setNumber(finalNumber);
details = Optional.of(detail);
recipientDatabase.updatePhoneNumbers(outputResult.getRewrites());
} else {
details = accountManager.getContact(recipient.requireE164());
}
}
if (details.isPresent()) {
recipientDatabase.setRegistered(recipient.getId(), RegisteredState.REGISTERED);
if (Permissions.hasAll(context, Manifest.permission.WRITE_CONTACTS)) {
updateContactsDatabase(context, Util.asList(recipient.getId()), false, rewrites);
}
if (!activeUser && TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
}
if (!activeUser && systemContact && !TextSecurePreferences.getNeedsSqlCipherMigration(context)) {
notifyNewUsers(context, Collections.singletonList(recipient.getId()));
}
return RegisteredState.REGISTERED;
} else {
recipientDatabase.setRegistered(recipient.getId(), RegisteredState.NOT_REGISTERED);
return RegisteredState.NOT_REGISTERED;
}
}
private static boolean isValidContactNumber(@Nullable String number) {
return !TextUtils.isEmpty(number) && !UuidUtil.isUuid(number);
}
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
try {
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS);
return true;
} catch (ExecutionException e) {
if (e.getCause() instanceof NotFoundException) {
return false;
} else {
throw new IOException(e);
}
} catch (InterruptedException | TimeoutException e) {
throw new IOException(e);
}
}
private static class DirectoryResult {
private final Set<String> numbers;
private final List<RecipientId> newlyActiveRecipients;
DirectoryResult(@NonNull Set<String> numbers) {
this(numbers, Collections.emptyList());
}
DirectoryResult(@NonNull Set<String> numbers, @NonNull List<RecipientId> newlyActiveRecipients) {
this.numbers = numbers;
this.newlyActiveRecipients = newlyActiveRecipients;
}
Set<String> getNumbers() {
return numbers;
}
List<RecipientId> getNewlyActiveRecipients() {
return newlyActiveRecipients;
}
}
private static class AccountHolder {
private final boolean fresh;
private final Account account;
private AccountHolder(Account account, boolean fresh) {
this.fresh = fresh;
this.account = account;
}
@SuppressWarnings("unused")
public boolean isFresh() {
return fresh;
}
public Account getAccount() {
return account;
}
}
}

View File

@ -8,6 +8,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* A helper class to match a single number with multiple possible registered numbers. An example is
@ -67,6 +68,32 @@ class FuzzyPhoneNumberHelper {
return new OutputResult(allNumbers, rewrites);
}
/**
* This should be run on the list of numbers we find out are registered with the server. Based on
* these results and our initial input set, we can decide if we need to rewrite which number we
* have stored locally.
*/
static @NonNull OutputResultV2 generateOutputV2(@NonNull Map<String, UUID> registeredNumbers, @NonNull InputResult inputResult) {
Map<String, UUID> allNumbers = new HashMap<>(registeredNumbers);
Map<String, String> rewrites = new HashMap<>();
for (Map.Entry<String, String> entry : inputResult.getFuzzies().entrySet()) {
if (registeredNumbers.containsKey(entry.getKey()) && registeredNumbers.containsKey(entry.getValue())) {
if (mxHas1(entry.getKey())) {
rewrites.put(entry.getKey(), entry.getValue());
allNumbers.remove(entry.getKey());
} else {
allNumbers.remove(entry.getValue());
}
} else if (registeredNumbers.containsKey(entry.getValue())) {
rewrites.put(entry.getKey(), entry.getValue());
allNumbers.remove(entry.getKey());
}
}
return new OutputResultV2(allNumbers, rewrites);
}
private static boolean mx(@NonNull String number) {
return number.startsWith("+52") && (number.length() == 13 || number.length() == 14);
@ -127,4 +154,22 @@ class FuzzyPhoneNumberHelper {
return rewrites;
}
}
public static class OutputResultV2 {
private final Map<String, UUID> numbers;
private final Map<String, String> rewrites;
private OutputResultV2(@NonNull Map<String, UUID> numbers, @NonNull Map<String, String> rewrites) {
this.numbers = numbers;
this.rewrites = rewrites;
}
public @NonNull Map<String, UUID> getNumbers() {
return numbers;
}
public @NonNull Map<String, String> getRewrites() {
return rewrites;
}
}
}

View File

@ -1321,10 +1321,14 @@ public class RecipientDatabase extends Database {
return results;
}
public void markRegistered(@NonNull RecipientId id, @NonNull UUID uuid) {
ContentValues contentValues = new ContentValues(3);
public void markRegistered(@NonNull RecipientId id, @Nullable UUID uuid) {
ContentValues contentValues = new ContentValues(2);
contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
contentValues.put(UUID, uuid.toString().toLowerCase());
if (uuid != null) {
contentValues.put(UUID, uuid.toString().toLowerCase());
}
if (update(id, contentValues)) {
markDirty(id, DirtyState.INSERT);
Recipient.live(id).refresh();
@ -1337,7 +1341,7 @@ public class RecipientDatabase extends Database {
* preferred.
*/
public void markRegistered(@NonNull RecipientId id) {
ContentValues contentValues = new ContentValues(2);
ContentValues contentValues = new ContentValues(1);
contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
if (update(id, contentValues)) {
markDirty(id, DirtyState.INSERT);
@ -1348,7 +1352,7 @@ public class RecipientDatabase extends Database {
public void markUnregistered(@NonNull RecipientId id) {
ContentValues contentValues = new ContentValues(2);
contentValues.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
contentValues.put(UUID, (String) null);
contentValues.putNull(UUID);
if (update(id, contentValues)) {
markDirty(id, DirtyState.DELETE);
Recipient.live(id).refresh();
@ -1363,14 +1367,18 @@ public class RecipientDatabase extends Database {
for (Map.Entry<RecipientId, String> entry : registered.entrySet()) {
ContentValues values = new ContentValues(2);
values.put(REGISTERED, RegisteredState.REGISTERED.getId());
values.put(UUID, entry.getValue().toLowerCase());
if (entry.getValue() != null) {
values.put(UUID, entry.getValue().toLowerCase());
}
if (update(entry.getKey(), values)) {
markDirty(entry.getKey(), DirtyState.INSERT);
}
}
for (RecipientId id : unregistered) {
ContentValues values = new ContentValues(1);
ContentValues values = new ContentValues(2);
values.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
values.put(UUID, (String) null);
if (update(id, values)) {

View File

@ -63,6 +63,7 @@ public final class FeatureFlags {
private static final String GROUPS_V2_CREATE = "android.groupsv2.create";
private static final String GROUPS_V2_CAPACITY = "android.groupsv2.capacity";
private static final String GROUPS_V2_INTERNAL_TEST = "android.groupsv2.internalTest";
private static final String CDS = "android.cds";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -256,6 +257,11 @@ public final class FeatureFlags {
return groupsV2() && getBoolean(GROUPS_V2_INTERNAL_TEST, false);
}
/** Whether or not to use the new contact discovery service endpoint. */
public static boolean cds() {
return getBoolean(CDS, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View File

@ -19,13 +19,9 @@ public final class SetUtil {
return difference;
}
public static <E> Set<E> union(Set<E>... sets) {
Set<E> result = new LinkedHashSet<>();
for (Set<E> set : sets) {
result.addAll(set);
}
public static <E> Set<E> union(Set<E> a, Set<E> b) {
Set<E> result = new LinkedHashSet<>(a);
result.addAll(b);
return result;
}
}

View File

@ -3,15 +3,21 @@ package org.thoughtcrime.securesms.contacts.sync;
import org.junit.Test;
import org.thoughtcrime.securesms.contacts.sync.FuzzyPhoneNumberHelper.InputResult;
import org.thoughtcrime.securesms.contacts.sync.FuzzyPhoneNumberHelper.OutputResult;
import org.thoughtcrime.securesms.contacts.sync.FuzzyPhoneNumberHelper.OutputResultV2;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import edu.emory.mathcs.backport.java.util.Arrays;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.thoughtcrime.securesms.testutil.TestHelpers.mapOf;
public class FuzzyPhoneNumberHelperTest {
@ -21,6 +27,9 @@ public class FuzzyPhoneNumberHelperTest {
private static final String MX_A = "+525512345678";
private static final String MX_A_1 = "+5215512345678";
private static final UUID UUID_A = UuidUtil.parseOrThrow("db980097-1e02-452f-9937-899630508705");
private static final UUID UUID_B = UuidUtil.parseOrThrow("11ccd6de-8fcc-49d6-bb9e-df21ff88bd6f");
@Test
public void generateInput_noMxNumbers() {
InputResult result = FuzzyPhoneNumberHelper.generateInput(setOf(US_A, US_B), setOf(US_A, US_B));
@ -156,6 +165,69 @@ public class FuzzyPhoneNumberHelperTest {
assertEquals(MX_A_1, result.getRewrites().get(MX_A));
}
@Test
public void generateOutputV2_noMxNumbers() {
OutputResultV2 result = FuzzyPhoneNumberHelper.generateOutputV2(mapOf(US_A, UUID_A, US_B, UUID_B), new InputResult(setOf(US_A, US_B), Collections.emptyMap()));
assertEquals(2, result.getNumbers().size());
assertEquals(UUID_A, result.getNumbers().get(US_A));
assertEquals(UUID_B, result.getNumbers().get(US_B));
assertTrue(result.getRewrites().isEmpty());
}
@Test
public void generateOutputV2_bothMatch_no1To1() {
OutputResultV2 result = FuzzyPhoneNumberHelper.generateOutputV2(mapOf(MX_A, UUID_A, MX_A_1, UUID_B), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A, MX_A_1)));
assertEquals(1, result.getNumbers().size());
assertEquals(UUID_A, result.getNumbers().get(MX_A));
assertTrue(result.getRewrites().isEmpty());
}
@Test
public void generateOutputV2_bothMatch_1toNo1() {
OutputResultV2 result = FuzzyPhoneNumberHelper.generateOutputV2(mapOf(MX_A, UUID_A, MX_A_1, UUID_B), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A_1, MX_A)));
assertEquals(1, result.getNumbers().size());
assertEquals(UUID_A, result.getNumbers().get(MX_A));
assertEquals(MX_A, result.getRewrites().get(MX_A_1));
}
@Test
public void generateOutputV2_no1Match_no1To1() {
OutputResultV2 result = FuzzyPhoneNumberHelper.generateOutputV2(mapOf(MX_A, UUID_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A, MX_A_1)));
assertEquals(1, result.getNumbers().size());
assertEquals(UUID_A, result.getNumbers().get(MX_A));
assertTrue(result.getRewrites().isEmpty());
}
@Test
public void generateOutputV2_no1Match_1ToNo1() {
OutputResultV2 result = FuzzyPhoneNumberHelper.generateOutputV2(mapOf(MX_A, UUID_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A_1, MX_A)));
assertEquals(1, result.getNumbers().size());
assertEquals(UUID_A, result.getNumbers().get(MX_A));
assertEquals(MX_A, result.getRewrites().get(MX_A_1));
}
@Test
public void generateOutputV2_1Match_1ToNo1() {
OutputResultV2 result = FuzzyPhoneNumberHelper.generateOutputV2(mapOf(MX_A_1, UUID_A), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A_1, MX_A)));
assertEquals(1, result.getNumbers().size());
assertEquals(UUID_A, result.getNumbers().get(MX_A_1));
assertTrue(result.getRewrites().isEmpty());
}
@Test
public void generateOutputV2_1Match_no1To1() {
OutputResult result = FuzzyPhoneNumberHelper.generateOutput(setOf(MX_A_1), new InputResult(setOf(MX_A, MX_A_1), Collections.singletonMap(MX_A, MX_A_1)));
assertEquals(1, result.getNumbers().size());
assertTrue(result.getNumbers().containsAll(setOf(MX_A_1)));
assertEquals(MX_A_1, result.getRewrites().get(MX_A));
}
private static <E> Set<E> setOf(E... values) {
//noinspection unchecked

View File

@ -9,7 +9,9 @@ import org.whispersystems.libsignal.util.ByteUtil;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static junit.framework.TestCase.assertTrue;
@ -58,4 +60,21 @@ public final class TestHelpers {
assertEquals(a.size(), b.size());
assertTrue(a.containsAll(b));
}
public static <K, V> Map<K, V> mapOf() {
return new HashMap<>();
}
public static <K, V> Map<K, V> mapOf(K k, V v) {
return new HashMap<K, V>() {{
put(k, v);
}};
}
public static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2) {
return new HashMap<K, V>() {{
put(k1, v1);
put(k2, v2);
}};
}
}

View File

@ -14,6 +14,7 @@ import java.util.Set;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.thoughtcrime.securesms.testutil.TestHelpers.mapOf;
public class FeatureFlagsTest extends BaseUnitTest {
@ -367,21 +368,4 @@ public class FeatureFlagsTest extends BaseUnitTest {
private static <V> Set<V> setOf(V... values) {
return new HashSet<>(Arrays.asList(values));
}
private static <K, V> Map<K, V> mapOf() {
return new HashMap<>();
}
private static <K, V> Map<K, V> mapOf(K k, V v) {
return new HashMap<K, V>() {{
put(k, v);
}};
}
private static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2) {
return new HashMap<K, V>() {{
put(k1, v1);
put(k2, v2);
}};
}
}

View File

@ -73,6 +73,8 @@ import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
@ -363,29 +365,38 @@ public class SignalServiceAccountManager {
return activeTokens;
}
public List<String> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String enclaveId)
public Map<String, UUID> getRegisteredUsers(KeyStore iasKeyStore, Set<String> e164numbers, String mrenclave)
throws IOException, Quote.InvalidQuoteFormatException, UnauthenticatedQuoteException, SignatureException, UnauthenticatedResponseException
{
try {
String authorization = pushServiceSocket.getContactDiscoveryAuthorization();
RemoteAttestation remoteAttestation = RemoteAttestationUtil.getAndVerifyRemoteAttestation(pushServiceSocket, PushServiceSocket.ClientSet.ContactDiscovery, iasKeyStore, enclaveId, enclaveId, authorization);
List<String> addressBook = new LinkedList<>();
String authorization = this.pushServiceSocket.getContactDiscoveryAuthorization();
Map<String, RemoteAttestation> attestations = RemoteAttestationUtil.getAndVerifyMultiRemoteAttestation(pushServiceSocket,
PushServiceSocket.ClientSet.ContactDiscovery,
iasKeyStore,
mrenclave,
mrenclave,
authorization);
List<String> addressBook = new ArrayList<>(e164numbers.size());
for (String e164number : e164numbers) {
addressBook.add(e164number.substring(1));
}
DiscoveryRequest request = ContactDiscoveryCipher.createDiscoveryRequest(addressBook, remoteAttestation);
DiscoveryResponse response = pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, remoteAttestation.getCookies(), enclaveId);
byte[] data = ContactDiscoveryCipher.getDiscoveryResponseData(response, remoteAttestation);
List<String> cookies = attestations.values().iterator().next().getCookies();
DiscoveryRequest request = ContactDiscoveryCipher.createDiscoveryRequest(addressBook, attestations);
DiscoveryResponse response = this.pushServiceSocket.getContactDiscoveryRegisteredUsers(authorization, request, cookies, mrenclave);
byte[] data = ContactDiscoveryCipher.getDiscoveryResponseData(response, attestations.values());
Iterator<String> addressBookIterator = addressBook.iterator();
List<String> results = new LinkedList<>();
HashMap<String, UUID> results = new HashMap<>(addressBook.size());
DataInputStream uuidInputStream = new DataInputStream(new ByteArrayInputStream(data));
for (byte aData : data) {
String candidate = addressBookIterator.next();
if (aData != 0) results.add('+' + candidate);
for (String candidate : addressBook) {
long candidateUuidHigh = uuidInputStream.readLong();
long candidateUuidLow = uuidInputStream.readLong();
if (candidateUuidHigh != 0 || candidateUuidLow != 0) {
results.put('+' + candidate, new UUID(candidateUuidHigh, candidateUuidLow));
}
}
return results;
@ -394,38 +405,6 @@ public class SignalServiceAccountManager {
}
}
public void reportContactDiscoveryServiceMatch() {
try {
this.pushServiceSocket.reportContactDiscoveryServiceMatch();
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery result match failed. Ignoring.", e);
}
}
public void reportContactDiscoveryServiceMismatch() {
try {
this.pushServiceSocket.reportContactDiscoveryServiceMismatch();
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery result mismatch failed. Ignoring.", e);
}
}
public void reportContactDiscoveryServiceAttestationError(String reason) {
try {
this.pushServiceSocket.reportContactDiscoveryServiceAttestationError(reason);
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery attestation error failed. Ignoring.", e);
}
}
public void reportContactDiscoveryServiceUnexpectedError(String reason) {
try {
this.pushServiceSocket.reportContactDiscoveryServiceUnexpectedError(reason);
} catch (IOException e) {
Log.w(TAG, "Request to indicate a contact discovery unexpected error failed. Ignoring.", e);
}
}
public Optional<SignalStorageManifest> getStorageManifest(StorageKey storageKey) throws IOException {
try {
String authToken = this.pushServiceSocket.getStorageAuth();

View File

@ -1,6 +1,7 @@
package org.whispersystems.signalservice.api.crypto;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
@ -22,4 +23,13 @@ public final class CryptoUtil {
throw new AssertionError(e);
}
}
public static byte[] sha256(byte[] data) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(data);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@ -39,7 +39,9 @@ final class AESCipher {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(TAG_LENGTH_BITS, iv));
cipher.updateAAD(aad);
if (aad != null) {
cipher.updateAAD(aad);
}
byte[] cipherText = cipher.doFinal(requestData);
byte[][] parts = ByteUtil.split(cipherText, cipherText.length - TAG_LENGTH_BYTES, TAG_LENGTH_BYTES);

View File

@ -1,40 +1,81 @@
package org.whispersystems.signalservice.internal.contacts.crypto;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.api.crypto.CryptoUtil;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.internal.contacts.crypto.AESCipher.AESEncryptedResult;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
import org.whispersystems.signalservice.internal.contacts.entities.QueryEnvelope;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class ContactDiscoveryCipher {
private ContactDiscoveryCipher() {
}
public static DiscoveryRequest createDiscoveryRequest(List<String> addressBook, RemoteAttestation remoteAttestation) {
public static DiscoveryRequest createDiscoveryRequest(List<String> addressBook, Map<String, RemoteAttestation> remoteAttestations) {
byte[] queryDataKey = Util.getSecretBytes(32);
byte[] queryData = buildQueryData(addressBook);
AESEncryptedResult encryptedQueryData = AESCipher.encrypt(queryDataKey, null, queryData);
byte[] commitment = CryptoUtil.sha256(queryData);
Map<String, QueryEnvelope> envelopes = new HashMap<>(remoteAttestations.size());
for (Map.Entry<String, RemoteAttestation> entry : remoteAttestations.entrySet()) {
envelopes.put(entry.getKey(),
buildQueryEnvelope(entry.getValue().getRequestId(),
entry.getValue().getKeys().getClientKey(),
queryDataKey));
}
return new DiscoveryRequest(addressBook.size(),
commitment,
encryptedQueryData.iv,
encryptedQueryData.data,
encryptedQueryData.mac,
envelopes);
}
public static byte[] getDiscoveryResponseData(DiscoveryResponse response, Collection<RemoteAttestation> attestations) throws InvalidCiphertextException, IOException {
for (RemoteAttestation attestation : attestations) {
if (Arrays.equals(response.getRequestId(), attestation.getRequestId())) {
return AESCipher.decrypt(attestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
}
}
throw new NoMatchingRequestIdException();
}
private static byte[] buildQueryData(List<String> addresses) {
try {
byte[] nonce = Util.getSecretBytes(32);
ByteArrayOutputStream requestDataStream = new ByteArrayOutputStream();
for (String address : addressBook) {
requestDataStream.write(nonce);
for (String address : addresses) {
requestDataStream.write(ByteUtil.longToByteArray(Long.parseLong(address)));
}
byte[] clientKey = remoteAttestation.getKeys().getClientKey();
byte[] requestData = requestDataStream.toByteArray();
byte[] aad = remoteAttestation.getRequestId();
AESCipher.AESEncryptedResult aesEncryptedResult = AESCipher.encrypt(clientKey, aad, requestData);
return new DiscoveryRequest(addressBook.size(), aesEncryptedResult.aad, aesEncryptedResult.iv, aesEncryptedResult.data, aesEncryptedResult.mac);
return requestDataStream.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static byte[] getDiscoveryResponseData(DiscoveryResponse response, RemoteAttestation remoteAttestation) throws InvalidCiphertextException {
return AESCipher.decrypt(remoteAttestation.getKeys().getServerKey(), response.getIv(), response.getData(), response.getMac());
private static QueryEnvelope buildQueryEnvelope(byte[] requestId, byte[] clientKey, byte[] queryDataKey) {
AESEncryptedResult result = AESCipher.encrypt(clientKey, requestId, queryDataKey);
return new QueryEnvelope(requestId, result.iv, result.data, result.mac);
}
static class NoMatchingRequestIdException extends IOException {
}
}

View File

@ -18,8 +18,8 @@ package org.whispersystems.signalservice.internal.contacts.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.internal.util.Hex;
import java.util.List;
import java.util.Map;
public class DiscoveryRequest {
@ -27,7 +27,7 @@ public class DiscoveryRequest {
private int addressCount;
@JsonProperty
private byte[] requestId;
private byte[] commitment;
@JsonProperty
private byte[] iv;
@ -38,20 +38,22 @@ public class DiscoveryRequest {
@JsonProperty
private byte[] mac;
public DiscoveryRequest() {
@JsonProperty
private Map<String, QueryEnvelope> envelopes;
}
public DiscoveryRequest() { }
public DiscoveryRequest(int addressCount, byte[] requestId, byte[] iv, byte[] data, byte[] mac) {
public DiscoveryRequest(int addressCount, byte[] commitment, byte[] iv, byte[] data, byte[] mac, Map<String, QueryEnvelope> envelopes) {
this.addressCount = addressCount;
this.requestId = requestId;
this.commitment = commitment;
this.iv = iv;
this.data = data;
this.mac = mac;
this.envelopes = envelopes;
}
public byte[] getRequestId() {
return requestId;
public byte[] getCommitment() {
return commitment;
}
public byte[] getIv() {
@ -70,8 +72,8 @@ public class DiscoveryRequest {
return addressCount;
}
@Override
public String toString() {
return "{ addressCount: " + addressCount + ", ticket: " + Hex.toString(requestId) + ", iv: " + Hex.toString(iv) + ", data: " + Hex.toString(data) + ", mac: " + Hex.toString(mac) + "}";
return "{ addressCount: " + addressCount + ", envelopes: " + envelopes.size() + " }";
}
}

View File

@ -22,6 +22,9 @@ import org.whispersystems.signalservice.internal.util.Hex;
public class DiscoveryResponse {
@JsonProperty
private byte[] requestId;
@JsonProperty
private byte[] iv;
@ -33,10 +36,8 @@ public class DiscoveryResponse {
public DiscoveryResponse() {}
public DiscoveryResponse(byte[] iv, byte[] data, byte[] mac) {
this.iv = iv;
this.data = data;
this.mac = mac;
public byte[] getRequestId() {
return requestId;
}
public byte[] getIv() {

View File

@ -0,0 +1,16 @@
package org.whispersystems.signalservice.internal.contacts.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
public class MultiRemoteAttestationResponse {
@JsonProperty
private Map<String, RemoteAttestationResponse> attestations;
public Map<String, RemoteAttestationResponse> getAttestations() {
return attestations;
}
}

View File

@ -0,0 +1,27 @@
package org.whispersystems.signalservice.internal.contacts.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class QueryEnvelope {
@JsonProperty
private byte[] requestId;
@JsonProperty
private byte[] iv;
@JsonProperty
private byte[] data;
@JsonProperty
private byte[] mac;
public QueryEnvelope() { }
public QueryEnvelope(byte[] requestId, byte[] iv, byte[] data, byte[] mac) {
this.requestId = requestId;
this.iv = iv;
this.data = data;
this.mac = mac;
}
}

View File

@ -852,24 +852,6 @@ public class PushServiceSocket {
}
}
public void reportContactDiscoveryServiceMatch() throws IOException {
makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "ok"), "PUT", "");
}
public void reportContactDiscoveryServiceMismatch() throws IOException {
makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "mismatch"), "PUT", "");
}
public void reportContactDiscoveryServiceAttestationError(String reason) throws IOException {
ContactDiscoveryFailureReason failureReason = new ContactDiscoveryFailureReason(reason);
makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "attestation-error"), "PUT", JsonUtil.toJson(failureReason));
}
public void reportContactDiscoveryServiceUnexpectedError(String reason) throws IOException {
ContactDiscoveryFailureReason failureReason = new ContactDiscoveryFailureReason(reason);
makeServiceRequest(String.format(DIRECTORY_FEEDBACK_PATH, "unexpected-error"), "PUT", JsonUtil.toJson(failureReason));
}
public TurnServerInfo getTurnServerInfo() throws IOException {
String response = makeServiceRequest(TURN_SERVER_INFO, "GET", null);
return JsonUtil.fromJson(response, TurnServerInfo.class);

View File

@ -10,6 +10,7 @@ import org.whispersystems.signalservice.internal.contacts.crypto.RemoteAttestati
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;
@ -17,8 +18,11 @@ import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.io.IOException;
import java.security.KeyStore;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import okhttp3.Response;
import okhttp3.ResponseBody;
@ -36,33 +40,66 @@ public final class RemoteAttestationUtil {
String authorization)
throws IOException, Quote.InvalidQuoteFormatException, InvalidCiphertextException, UnauthenticatedQuoteException, SignatureException
{
Curve25519 curve = Curve25519.getInstance(Curve25519.BEST);
Curve25519KeyPair keyPair = curve.generateKeyPair();
RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey());
Pair<RemoteAttestationResponse, List<String>> attestationResponsePair = getRemoteAttestation(socket, clientSet, authorization, attestationRequest, enclaveName);
RemoteAttestationResponse attestationResponse = attestationResponsePair.first();
List<String> attestationCookies = attestationResponsePair.second();
Curve25519KeyPair keyPair = buildKeyPair();
ResponsePair result = makeAttestationRequest(socket, clientSet, authorization, enclaveName, keyPair);
RemoteAttestationResponse response = JsonUtil.fromJson(result.body, RemoteAttestationResponse.class);
RemoteAttestationKeys keys = new RemoteAttestationKeys(keyPair, attestationResponse.getServerEphemeralPublic(), attestationResponse.getServerStaticPublic());
Quote quote = new Quote(attestationResponse.getQuote());
byte[] requestId = RemoteAttestationCipher.getRequestId(keys, attestationResponse);
RemoteAttestationCipher.verifyServerQuote(quote, attestationResponse.getServerStaticPublic(), mrenclave);
RemoteAttestationCipher.verifyIasSignature(iasKeyStore, attestationResponse.getCertificates(), attestationResponse.getSignatureBody(), attestationResponse.getSignature(), quote);
return new RemoteAttestation(requestId, keys, attestationCookies);
return validateAndBuildRemoteAttestation(response, result.cookies, iasKeyStore, keyPair, mrenclave);
}
private static Pair<RemoteAttestationResponse, List<String>> getRemoteAttestation(PushServiceSocket socket,
PushServiceSocket.ClientSet clientSet,
String authorization,
RemoteAttestationRequest request,
String enclaveName)
throws IOException
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
{
Response response = socket.makeRequest(clientSet, authorization, new LinkedList<String>(), "/v1/attestation/" + enclaveName, "PUT", JsonUtil.toJson(request));
ResponseBody body = response.body();
Curve25519KeyPair 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 NonSuccessfulResponseCodeException("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 Curve25519KeyPair buildKeyPair() {
Curve25519 curve = Curve25519.getInstance(Curve25519.BEST);
return curve.generateKeyPair();
}
private static ResponsePair makeAttestationRequest(PushServiceSocket socket,
PushServiceSocket.ClientSet clientSet,
String authorization,
String enclaveName,
Curve25519KeyPair keyPair)
throws IOException
{
RemoteAttestationRequest attestationRequest = new RemoteAttestationRequest(keyPair.getPublicKey());
Response response = socket.makeRequest(clientSet, authorization, new LinkedList<String>(), "/v1/attestation/" + enclaveName, "PUT", JsonUtil.toJson(attestationRequest));
ResponseBody body = response.body();
if (body == null) {
throw new NonSuccessfulResponseCodeException("Empty response!");
}
return new ResponsePair(body.string(), parseCookies(response));
}
private static List<String> parseCookies(Response response) {
List<String> rawCookies = response.headers("Set-Cookie");
List<String> cookies = new LinkedList<>();
@ -70,10 +107,34 @@ public final class RemoteAttestationUtil {
cookies.add(cookie.split(";")[0]);
}
if (body != null) {
return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies);
} else {
throw new NonSuccessfulResponseCodeException("Empty response!");
return cookies;
}
private static RemoteAttestation validateAndBuildRemoteAttestation(RemoteAttestationResponse response,
List<String> cookies,
KeyStore iasKeyStore,
Curve25519KeyPair keyPair,
String mrenclave)
throws Quote.InvalidQuoteFormatException, InvalidCiphertextException, UnauthenticatedQuoteException, SignatureException
{
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;
}
}
}