Versioned Profiles support (disabled).

This commit is contained in:
Alan Evans
2020-02-10 18:40:22 -05:00
committed by Greyson Parrelli
parent f10d1eac61
commit 7ecb50a3fe
67 changed files with 1200 additions and 321 deletions

View File

@@ -233,6 +233,7 @@ android {
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
@@ -305,6 +306,7 @@ android {
buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"a1e9c1d3f352b5c4f0fc7a421b98119e60e5ff703c28fbea85c66bfa7306deab\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"\""
}
release {
minifyEnabled true

View File

@@ -23,7 +23,6 @@ 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.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase;
@@ -37,19 +36,16 @@ 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.recipients.RecipientUtil;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
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.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.Calendar;
@@ -343,25 +339,8 @@ class DirectoryHelperV1 {
}
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
SignalServiceMessagePipe authPipe = IncomingMessageObserver.getPipe();
SignalServiceMessagePipe unidentifiedPipe = IncomingMessageObserver.getUnidentifiedPipe();
SignalServiceMessagePipe pipe = unidentifiedPipe != null && unidentifiedAccess.isPresent() ? unidentifiedPipe : authPipe;
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
if (pipe != null) {
try {
pipe.getProfile(address, unidentifiedAccess.get().getTargetUnidentifiedAccess());
return true;
} catch (NotFoundException e) {
return false;
} catch (IOException e) {
Log.w(TAG, "Websocket request failed. Falling back to REST.");
}
}
try {
ApplicationDependencies.getSignalServiceMessageReceiver().retrieveProfile(address, unidentifiedAccess.get().getTargetUnidentifiedAccess());
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE);
return true;
} catch (NotFoundException e) {
return false;

View File

@@ -18,6 +18,7 @@ import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.api.storage.SignalContactRecord.IdentityState;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -362,7 +363,7 @@ public final class StorageSyncHelper {
private final SignalContactRecord oldContact;
private final SignalContactRecord newContact;
public ContactUpdate(@NonNull SignalContactRecord oldContact, @NonNull SignalContactRecord newContact) {
ContactUpdate(@NonNull SignalContactRecord oldContact, @NonNull SignalContactRecord newContact) {
this.oldContact = oldContact;
this.newContact = newContact;
}
@@ -377,6 +378,10 @@ public final class StorageSyncHelper {
return newContact;
}
public boolean profileKeyChanged() {
return !OptionalUtil.byteArrayEquals(oldContact.getProfileKey(), newContact.getProfileKey());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -494,7 +499,7 @@ public final class StorageSyncHelper {
private final WriteOperationResult writeResult;
private final Map<RecipientId, byte[]> storageKeyUpdates;
public LocalWriteResult(WriteOperationResult writeResult, Map<RecipientId, byte[]> storageKeyUpdates) {
private LocalWriteResult(WriteOperationResult writeResult, Map<RecipientId, byte[]> storageKeyUpdates) {
this.writeResult = writeResult;
this.storageKeyUpdates = storageKeyUpdates;
}
@@ -510,17 +515,17 @@ public final class StorageSyncHelper {
private static final class ContactRecordMergeResult {
final Set<SignalContactRecord> localInserts;
final Set<ContactUpdate> localUpdates;
final Set<ContactUpdate> localUpdates;
final Set<SignalContactRecord> remoteInserts;
final Set<ContactUpdate> remoteUpdates;
final Set<ContactUpdate> remoteUpdates;
ContactRecordMergeResult(@NonNull Set<SignalContactRecord> localInserts,
@NonNull Set<ContactUpdate> localUpdates,
@NonNull Set<SignalContactRecord> remoteInserts,
@NonNull Set<ContactUpdate> remoteUpdates)
{
this.localInserts = localInserts;
this.localUpdates = localUpdates;
this.localInserts = localInserts;
this.localUpdates = localUpdates;
this.remoteInserts = remoteInserts;
this.remoteUpdates = remoteUpdates;
}

View File

@@ -3,17 +3,27 @@ package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale;
public final class ProfileKeyUtil {
private static final String TAG = Log.tag(ProfileKeyUtil.class);
private ProfileKeyUtil() {
}
/**
* @deprecated Will inline later as part of Versioned profiles.
*/
/** @deprecated Use strongly typed {@link org.signal.zkgroup.profiles.ProfileKey}
* from {@link #getSelfProfileKey()}
* or {@code getSelfProfileKey().serialize()} if you need the bytes. */
@Deprecated
public static @NonNull byte[] getProfileKey(@NonNull Context context) {
byte[] profileKey = Recipient.self().getProfileKey();
@@ -22,4 +32,48 @@ public final class ProfileKeyUtil {
}
return profileKey;
}
public static synchronized @NonNull ProfileKey getSelfProfileKey() {
try {
return new ProfileKey(Recipient.self().getProfileKey());
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
public static @Nullable ProfileKey profileKeyOrNull(@Nullable byte[] profileKey) {
if (profileKey != null) {
try {
return new ProfileKey(profileKey);
} catch (InvalidInputException e) {
Log.w(TAG, String.format(Locale.US, "Seen non-null profile key of wrong length %d", profileKey.length), e);
}
}
return null;
}
public static @NonNull ProfileKey profileKeyOrThrow(@NonNull byte[] profileKey) {
try {
return new ProfileKey(profileKey);
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
public static @NonNull Optional<ProfileKey> profileKeyOptional(@Nullable byte[] profileKey) {
return Optional.fromNullable(profileKeyOrNull(profileKey));
}
public static @NonNull Optional<ProfileKey> profileKeyOptionalOrThrow(@NonNull byte[] profileKey) {
return Optional.of(profileKeyOrThrow(profileKey));
}
public static @NonNull ProfileKey createNew() {
try {
return new ProfileKey(Util.getSecretBytes(32));
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -8,6 +8,7 @@ import androidx.annotation.WorkerThread;
import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -42,7 +43,7 @@ public class UnidentifiedAccessUtil {
{
try {
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(ProfileKeyUtil.getProfileKey(context));
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
byte[] ourUnidentifiedAccessCertificate = recipient.resolve().isUuidSupported() && Recipient.self().isUuidSupported()
? TextSecurePreferences.getUnidentifiedAccessCertificate(context)
: TextSecurePreferences.getUnidentifiedAccessCertificateLegacy(context);
@@ -75,7 +76,7 @@ public class UnidentifiedAccessUtil {
public static Optional<UnidentifiedAccessPair> getAccessForSync(@NonNull Context context) {
try {
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(ProfileKeyUtil.getProfileKey(context));
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
byte[] ourUnidentifiedAccessCertificate = Recipient.self().isUuidSupported() ? TextSecurePreferences.getUnidentifiedAccessCertificate(context)
: TextSecurePreferences.getUnidentifiedAccessCertificateLegacy(context);
@@ -97,12 +98,8 @@ public class UnidentifiedAccessUtil {
}
}
public static @NonNull byte[] getSelfUnidentifiedAccessKey(@NonNull byte[] selfProfileKey) {
return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKey);
}
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
byte[] theirProfileKey = recipient.resolve().getProfileKey();
ProfileKey theirProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.resolve().getProfileKey());
switch (recipient.resolve().getUnidentifiedAccessMode()) {
case UNKNOWN:

View File

@@ -14,6 +14,8 @@ import com.google.android.gms.common.util.ArrayUtils;
import net.sqlcipher.database.SQLiteDatabase;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
@@ -31,7 +33,6 @@ import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
@@ -80,6 +81,7 @@ public class RecipientDatabase extends Database {
private static final String SYSTEM_CONTACT_URI = "system_contact_uri";
private static final String SYSTEM_INFO_PENDING = "system_info_pending";
private static final String PROFILE_KEY = "profile_key";
private static final String PROFILE_KEY_CREDENTIAL = "profile_key_credential";
private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar";
private static final String PROFILE_SHARING = "profile_sharing";
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
@@ -100,7 +102,8 @@ public class RecipientDatabase extends Database {
private static final String[] RECIPIENT_PROJECTION = new String[] {
UUID, USERNAME, PHONE, EMAIL, GROUP_ID,
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
PROFILE_KEY, PROFILE_KEY_CREDENTIAL,
SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION, UUID_SUPPORTED, STORAGE_SERVICE_KEY, DIRTY
@@ -242,6 +245,7 @@ public class RecipientDatabase extends Database {
SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " +
SYSTEM_INFO_PENDING + " INTEGER DEFAULT 0, " +
PROFILE_KEY + " TEXT DEFAULT NULL, " +
PROFILE_KEY_CREDENTIAL + " TEXT DEFAULT NULL, " +
PROFILE_GIVEN_NAME + " TEXT DEFAULT NULL, " +
PROFILE_FAMILY_NAME + " TEXT DEFAULT NULL, " +
PROFILE_JOINED_NAME + " TEXT DEFAULT NULL, " +
@@ -428,6 +432,10 @@ public class RecipientDatabase extends Database {
RecipientId recipientId = getByStorageKeyOrThrow(update.getNewContact().getKey());
if (update.profileKeyChanged()) {
clearProfileKeyCredential(recipientId);
}
try {
Optional<IdentityRecord> oldIdentityRecord = identityDatabase.getIdentity(recipientId);
IdentityKey identityKey = update.getNewContact().getIdentityKey().isPresent() ? new IdentityKey(update.getNewContact().getIdentityKey().get(), 0) : null;
@@ -562,42 +570,44 @@ public class RecipientDatabase extends Database {
}
@NonNull RecipientSettings getRecipientSettings(@NonNull Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
UUID uuid = UuidUtil.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(UUID)));
String username = cursor.getString(cursor.getColumnIndexOrThrow(USERNAME));
String e164 = cursor.getString(cursor.getColumnIndexOrThrow(PHONE));
String email = cursor.getString(cursor.getColumnIndexOrThrow(EMAIL));
String groupId = cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID));
boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKED)) == 1;
String messageRingtone = cursor.getString(cursor.getColumnIndexOrThrow(MESSAGE_RINGTONE));
String callRingtone = cursor.getString(cursor.getColumnIndexOrThrow(CALL_RINGTONE));
int messageVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_VIBRATE));
int callVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(CALL_VIBRATE));
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR));
int insightsBannerTier = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER));
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_EXPIRATION_TIME));
int registeredState = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED));
String profileKeyString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY));
String systemDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME));
String systemContactPhoto = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHOTO_URI));
String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
String profileGivenName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_GIVEN_NAME));
String profileFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_FAMILY_NAME));
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
boolean uuidSupported = cursor.getInt(cursor.getColumnIndexOrThrow(UUID_SUPPORTED)) == 1;
String storageKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_KEY));
String identityKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
int identityStatusRaw = cursor.getInt(cursor.getColumnIndexOrThrow(IDENTITY_STATUS));
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
UUID uuid = UuidUtil.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(UUID)));
String username = cursor.getString(cursor.getColumnIndexOrThrow(USERNAME));
String e164 = cursor.getString(cursor.getColumnIndexOrThrow(PHONE));
String email = cursor.getString(cursor.getColumnIndexOrThrow(EMAIL));
String groupId = cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID));
boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKED)) == 1;
String messageRingtone = cursor.getString(cursor.getColumnIndexOrThrow(MESSAGE_RINGTONE));
String callRingtone = cursor.getString(cursor.getColumnIndexOrThrow(CALL_RINGTONE));
int messageVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_VIBRATE));
int callVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(CALL_VIBRATE));
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR));
int insightsBannerTier = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER));
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_EXPIRATION_TIME));
int registeredState = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED));
String profileKeyString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY));
String profileKeyCredentialString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY_CREDENTIAL));
String systemDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME));
String systemContactPhoto = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHOTO_URI));
String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
String profileGivenName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_GIVEN_NAME));
String profileFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_FAMILY_NAME));
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
boolean uuidSupported = cursor.getInt(cursor.getColumnIndexOrThrow(UUID_SUPPORTED)) == 1;
String storageKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_KEY));
String identityKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
int identityStatusRaw = cursor.getInt(cursor.getColumnIndexOrThrow(IDENTITY_STATUS));
MaterialColor color;
byte[] profileKey = null;
byte[] profileKey = null;
byte[] profileKeyCredential = null;
try {
color = serializedColor == null ? null : MaterialColor.fromSerialized(serializedColor);
@@ -613,6 +623,15 @@ public class RecipientDatabase extends Database {
Log.w(TAG, e);
profileKey = null;
}
if (profileKeyCredentialString != null) {
try {
profileKeyCredential = Base64.decode(profileKeyCredentialString);
} catch (IOException e) {
Log.w(TAG, e);
profileKeyCredential = null;
}
}
}
byte[] storageKey = null;
@@ -637,7 +656,8 @@ public class RecipientDatabase extends Database {
Util.uri(messageRingtone), Util.uri(callRingtone),
color, defaultSubscriptionId, expireMessages,
RegisteredState.fromId(registeredState),
profileKey, systemDisplayName, systemContactPhoto,
profileKey, profileKeyCredential,
systemDisplayName, systemContactPhoto,
systemPhoneLabel, systemContactUri,
ProfileName.fromParts(profileGivenName, profileFamilyName), signalProfileAvatar, profileSharing,
notificationChannel, UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
@@ -776,9 +796,56 @@ public class RecipientDatabase extends Database {
Recipient.live(id).refresh();
}
public void setProfileKey(@NonNull RecipientId id, @Nullable byte[] profileKey) {
/**
* Updates the profile key.
* <p>
* If it changes, it clears out the profile key credential.
*/
public void setProfileKey(@NonNull RecipientId id, @Nullable ProfileKey profileKey) {
String selection = ID + " = ?";
String[] args = new String[]{id.serialize()};
ContentValues valuesToCompare = new ContentValues(1);
ContentValues valuesToSet = new ContentValues(2);
String encodedProfileKey = profileKey == null ? null : Base64.encodeBytes(profileKey.serialize());
valuesToCompare.put(PROFILE_KEY, encodedProfileKey);
valuesToSet.put(PROFILE_KEY, encodedProfileKey);
valuesToSet.putNull(PROFILE_KEY_CREDENTIAL);
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare);
if (update(updateQuery, valuesToSet)) {
markDirty(id, DirtyState.UPDATE);
Recipient.live(id).refresh();
}
}
/**
* Updates the profile key credential as long as the profile key matches.
*/
public void setProfileKeyCredential(@NonNull RecipientId id,
@NonNull ProfileKey profileKey,
@NonNull ProfileKeyCredential profileKeyCredential)
{
String selection = ID + " = ? AND " + PROFILE_KEY + " = ?";
String[] args = new String[]{id.serialize(), Base64.encodeBytes(profileKey.serialize())};
ContentValues values = new ContentValues(1);
values.put(PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(profileKeyCredential.serialize()));
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
if (update(updateQuery, values)) {
// TODO [greyson] If we sync this in future, mark dirty
//markDirty(id, DirtyState.UPDATE);
Recipient.live(id).refresh();
}
}
private void clearProfileKeyCredential(@NonNull RecipientId id) {
ContentValues values = new ContentValues(1);
values.put(PROFILE_KEY, profileKey == null ? null : Base64.encodeBytes(profileKey));
values.putNull(PROFILE_KEY_CREDENTIAL);
if (update(id, values)) {
markDirty(id, DirtyState.UPDATE);
Recipient.live(id).refresh();
@@ -1224,14 +1291,23 @@ public class RecipientDatabase extends Database {
* Will update the database with the content values you specified. It will make an intelligent
* query such that this will only return true if a row was *actually* updated.
*/
private boolean update(@NonNull RecipientId id, ContentValues contentValues) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
String selection = ID + " = ?";
String[] args = new String[]{id.serialize()};
private boolean update(@NonNull RecipientId id, @NonNull ContentValues contentValues) {
String selection = ID + " = ?";
String[] args = new String[]{id.serialize()};
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, contentValues);
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, contentValues);
return update(updateQuery, contentValues);
}
return database.update(TABLE_NAME, contentValues, result.first(), result.second()) > 0;
/**
* Will update the database with the {@param contentValues} you specified.
* <p>
* This will only return true if a row was *actually* updated with respect to the where clause of the {@param updateQuery}.
*/
private boolean update(@NonNull SqlUtil.UpdateQuery updateQuery, @NonNull ContentValues contentValues) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
return database.update(TABLE_NAME, contentValues, updateQuery.getWhere(), updateQuery.getWhereArgs()) > 0;
}
private @NonNull Optional<RecipientId> getByColumn(@NonNull String column, String value) {
@@ -1374,6 +1450,7 @@ public class RecipientDatabase extends Database {
private final int expireMessages;
private final RegisteredState registered;
private final byte[] profileKey;
private final byte[] profileKeyCredential;
private final String systemDisplayName;
private final String systemContactPhoto;
private final String systemPhoneLabel;
@@ -1406,6 +1483,7 @@ public class RecipientDatabase extends Database {
int expireMessages,
@NonNull RegisteredState registered,
@Nullable byte[] profileKey,
@Nullable byte[] profileKeyCredential,
@Nullable String systemDisplayName,
@Nullable String systemContactPhoto,
@Nullable String systemPhoneLabel,
@@ -1439,6 +1517,7 @@ public class RecipientDatabase extends Database {
this.expireMessages = expireMessages;
this.registered = registered;
this.profileKey = profileKey;
this.profileKeyCredential = profileKeyCredential;
this.systemDisplayName = systemDisplayName;
this.systemContactPhoto = systemContactPhoto;
this.systemPhoneLabel = systemPhoneLabel;
@@ -1528,6 +1607,10 @@ public class RecipientDatabase extends Database {
return profileKey;
}
public @Nullable byte[] getProfileKeyCredential() {
return profileKeyCredential;
}
public @Nullable String getSystemDisplayName() {
return systemDisplayName;
}

View File

@@ -109,8 +109,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int MEGAPHONES = 45;
private static final int MEGAPHONE_FIRST_APPEARANCE = 46;
private static final int PROFILE_KEY_TO_DB = 47;
private static final int PROFILE_KEY_CREDENTIALS = 48;
private static final int DATABASE_VERSION = 47;
private static final int DATABASE_VERSION = 48;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -743,6 +744,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
}
}
if (oldVersion < PROFILE_KEY_CREDENTIALS) {
db.execSQL("ALTER TABLE recipient ADD COLUMN profile_key_credential TEXT DEFAULT NULL");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@@ -8,7 +8,9 @@ import android.provider.ContactsContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
@@ -134,7 +136,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
getSystemAvatar(recipient.getContactUri()),
Optional.fromNullable(recipient.getColor().serialize()),
verifiedMessage,
Optional.fromNullable(recipient.getProfileKey()),
ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()),
recipient.isBlocked(),
recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages())
: Optional.absent(),
@@ -181,7 +183,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
Optional<VerifiedMessage> verified = getVerifiedMessage(recipient, identity);
Optional<String> name = Optional.fromNullable(recipient.getName(context));
Optional<String> color = Optional.of(recipient.getColor().serialize());
Optional<byte[]> profileKey = Optional.fromNullable(recipient.getProfileKey());
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
boolean blocked = recipient.isBlocked();
Optional<Integer> expireTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent();
Optional<Integer> inboxPosition = Optional.fromNullable(inboxPositions.get(recipient.getId()));
@@ -208,7 +210,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
Optional.absent(),
Optional.of(self.getColor().serialize()),
Optional.absent(),
Optional.of(profileKey),
ProfileKeyUtil.profileKeyOptionalOrThrow(self.getProfileKey()),
false,
self.getExpireMessages() > 0 ? Optional.of(self.getExpireMessages()) : Optional.absent(),
Optional.fromNullable(inboxPositions.get(self.getId())),

View File

@@ -3,14 +3,14 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -66,7 +66,7 @@ public class MultiDeviceProfileKeyUpdateJob extends BaseJob {
return;
}
Optional<byte[]> profileKey = Optional.of(ProfileKeyUtil.getProfileKey(context));
Optional<ProfileKey> profileKey = Optional.of(ProfileKeyUtil.getSelfProfileKey());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DeviceContactsOutputStream out = new DeviceContactsOutputStream(baos);

View File

@@ -4,6 +4,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
@@ -11,16 +12,11 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.ByteArrayInputStream;
public final class ProfileUploadJob extends BaseJob {
public static final String KEY = "ProfileUploadJob";
@@ -47,8 +43,17 @@ public final class ProfileUploadJob extends BaseJob {
@Override
protected void onRun() throws Exception {
uploadProfileName();
uploadAvatar();
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
ProfileName profileName = TextSecurePreferences.getProfileName(context);
try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
if (FeatureFlags.VERSIONED_PROFILES) {
accountManager.setVersionedProfile(profileKey, profileName.serialize(), avatar);
} else {
accountManager.setProfileName(profileKey, profileName.serialize());
accountManager.setProfileAvatar(profileKey, avatar);
}
}
}
@Override
@@ -70,33 +75,6 @@ public final class ProfileUploadJob extends BaseJob {
public void onFailure() {
}
private void uploadProfileName() throws Exception {
ProfileName profileName = TextSecurePreferences.getProfileName(context);
accountManager.setProfileName(ProfileKeyUtil.getProfileKey(context), profileName.serialize());
}
private void uploadAvatar() throws Exception {
final RecipientId selfId = Recipient.self().getId();
final byte[] avatar;
if (AvatarHelper.getAvatarFile(context, selfId).exists() && AvatarHelper.getAvatarFile(context, selfId).length() > 0) {
avatar = Util.readFully(AvatarHelper.getInputStreamFor(context, Recipient.self().getId()));
} else {
avatar = null;
}
final StreamDetails avatarDetails;
if (avatar == null || avatar.length == 0) {
avatarDetails = null;
} else {
avatarDetails = new StreamDetails(new ByteArrayInputStream(avatar),
MediaUtil.IMAGE_JPEG,
avatar.length);
}
accountManager.setProfileAvatar(ProfileKeyUtil.getProfileKey(context), avatarDetails);
}
public static class Factory implements Job.Factory {
@NonNull

View File

@@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
@@ -20,6 +21,7 @@ import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
@@ -102,7 +104,6 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
@@ -271,8 +272,8 @@ public final class PushProcessMessageJob extends BaseJob {
handleUnknownGroupMessage(content, message.getGroupInfo().get());
}
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
handleProfileKey(content, message);
if (message.getProfileKey().isPresent()) {
handleProfileKey(content, message.getProfileKey().get());
}
if (content.isNeedsReceipt()) {
@@ -1175,13 +1176,15 @@ public final class PushProcessMessageJob extends BaseJob {
}
private void handleProfileKey(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message)
@NonNull byte[] messageProfileKeyBytes)
{
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
Recipient recipient = Recipient.externalPush(context, content.getSender());
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
Recipient recipient = Recipient.externalPush(context, content.getSender());
ProfileKey currentProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
ProfileKey messageProfileKey = ProfileKeyUtil.profileKeyOrNull(messageProfileKeyBytes);
if (recipient.getProfileKey() == null || !MessageDigest.isEqual(recipient.getProfileKey(), message.getProfileKey().get())) {
database.setProfileKey(recipient.getId(), message.getProfileKey().get());
if (messageProfileKey != null && !messageProfileKey.equals(currentProfileKey)) {
database.setProfileKey(recipient.getId(), messageProfileKey);
database.setUnidentifiedAccessMode(recipient.getId(), RecipientDatabase.UnidentifiedAccessMode.UNKNOWN);
ApplicationDependencies.getJobManager().add(new RetrieveProfileJob(recipient));
}

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
@@ -13,6 +12,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
import java.io.IOException;
@@ -48,7 +48,7 @@ public class RefreshAttributesJob extends BaseJob {
public void onRun() throws IOException {
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
boolean fetchesMessages = TextSecurePreferences.isFcmDisabled(context);
byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(ProfileKeyUtil.getProfileKey(context));
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
String pin = null;
String registrationLockToken = null;

View File

@@ -3,8 +3,11 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
@@ -12,9 +15,12 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
@@ -58,11 +64,31 @@ public class RefreshOwnProfileJob extends BaseJob {
@Override
protected void onRun() throws Exception {
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, Recipient.self());
Recipient self = Recipient.self();
ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfile(context, self, getRequestType(self));
SignalServiceProfile profile = profileAndCredential.getProfile();
setProfileName(profile.getName());
setProfileAvatar(profile.getAvatar());
setProfileCapabilities(profile.getCapabilities());
Optional<ProfileKeyCredential> profileKeyCredential = profileAndCredential.getProfileKeyCredential();
if (profileKeyCredential.isPresent()) {
setProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), profileKeyCredential.get());
}
}
private void setProfileKeyCredential(@NonNull Recipient recipient,
@NonNull ProfileKey recipientProfileKey,
@NonNull ProfileKeyCredential credential)
{
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
recipientDatabase.setProfileKeyCredential(recipient.getId(), recipientProfileKey, credential);
}
private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) {
return FeatureFlags.VERSIONED_PROFILES && !recipient.hasProfileKeyCredential()
? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL
: SignalServiceProfile.RequestType.PROFILE;
}
@Override
@@ -75,7 +101,7 @@ public class RefreshOwnProfileJob extends BaseJob {
private void setProfileName(@Nullable String encryptedName) {
try {
byte[] profileKey = ProfileKeyUtil.getProfileKey(context);
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
String plaintextName = ProfileUtil.decryptName(profileKey, encryptedName);
ProfileName profileName = ProfileName.fromSerialized(plaintextName);
@@ -86,7 +112,7 @@ public class RefreshOwnProfileJob extends BaseJob {
}
}
private void setProfileAvatar(@Nullable String avatar) {
private static void setProfileAvatar(@Nullable String avatar) {
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), avatar));
}

View File

@@ -1,9 +1,12 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -37,8 +40,8 @@ public class RetrieveProfileAvatarJob extends BaseJob {
private static final String KEY_PROFILE_AVATAR = "profile_avatar";
private static final String KEY_RECIPIENT = "recipient";
private String profileAvatar;
private Recipient recipient;
private final String profileAvatar;
private final Recipient recipient;
public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) {
this(new Job.Parameters.Builder()
@@ -73,7 +76,7 @@ public class RetrieveProfileAvatarJob extends BaseJob {
@Override
public void onRun() throws IOException {
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
byte[] profileKey = recipient.resolve().getProfileKey();
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.resolve().getProfileKey());
if (profileKey == null) {
Log.w(TAG, "Recipient profile key is gone!");

View File

@@ -1,11 +1,14 @@
package org.thoughtcrime.securesms.jobs;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
@@ -24,8 +27,10 @@ import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import java.io.IOException;
@@ -92,9 +97,11 @@ public class RetrieveProfileJob extends BaseJob {
}
private void handlePhoneNumberRecipient(Recipient recipient) throws IOException {
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, recipient);
ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfile(context, recipient, getRequestType(recipient));
SignalServiceProfile profile = profileAndCredential.getProfile();
ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
if (recipient.getProfileKey() == null) {
if (recipientProfileKey == null) {
Log.i(TAG, "No profile key available for " + recipient.getId());
} else {
Log.i(TAG, "Profile key available for " + recipient.getId());
@@ -106,6 +113,27 @@ public class RetrieveProfileJob extends BaseJob {
setProfileCapabilities(recipient, profile.getCapabilities());
setIdentityKey(recipient, profile.getIdentityKey());
setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());
if (recipientProfileKey != null) {
Optional<ProfileKeyCredential> profileKeyCredential = profileAndCredential.getProfileKeyCredential();
if (profileKeyCredential.isPresent()) {
setProfileKeyCredential(recipient, recipientProfileKey, profileKeyCredential.get());
}
}
}
private void setProfileKeyCredential(@NonNull Recipient recipient,
@NonNull ProfileKey recipientProfileKey,
@NonNull ProfileKeyCredential credential)
{
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
recipientDatabase.setProfileKeyCredential(recipient.getId(), recipientProfileKey, credential);
}
private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) {
return FeatureFlags.VERSIONED_PROFILES && !recipient.hasProfileKeyCredential()
? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL
: SignalServiceProfile.RequestType.PROFILE;
}
private void handleGroupRecipient(Recipient group) throws IOException {
@@ -141,7 +169,7 @@ public class RetrieveProfileJob extends BaseJob {
private void setUnidentifiedAccessMode(Recipient recipient, String unidentifiedAccessVerifier, boolean unrestrictedUnidentifiedAccess) {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
byte[] profileKey = recipient.getProfileKey();
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
if (unrestrictedUnidentifiedAccess && unidentifiedAccessVerifier != null) {
if (recipient.getUnidentifiedAccessMode() != UnidentifiedAccessMode.UNRESTRICTED) {
@@ -175,7 +203,7 @@ public class RetrieveProfileJob extends BaseJob {
private void setProfileName(Recipient recipient, String profileName) {
try {
byte[] profileKey = recipient.getProfileKey();
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
if (profileKey == null) return;
String plaintextProfileName = ProfileUtil.decryptName(profileKey, profileName);

View File

@@ -1,8 +1,9 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -11,15 +12,11 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class RotateProfileKeyJob extends BaseJob {
@@ -52,12 +49,20 @@ public class RotateProfileKeyJob extends BaseJob {
public void onRun() throws Exception {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
byte[] profileKey = Util.getSecretBytes(32);
ProfileKey profileKey = ProfileKeyUtil.createNew();
Recipient self = Recipient.self();
recipientDatabase.setProfileKey(self.getId(), profileKey);
accountManager.setProfileName(profileKey, TextSecurePreferences.getProfileName(context).serialize());
accountManager.setProfileAvatar(profileKey, getProfileAvatar());
try (StreamDetails avatarStream = AvatarHelper.getSelfProfileAvatarStream(context)) {
if (FeatureFlags.VERSIONED_PROFILES) {
accountManager.setVersionedProfile(profileKey,
TextSecurePreferences.getProfileName(context).serialize(),
avatarStream);
} else {
accountManager.setProfileName(profileKey, TextSecurePreferences.getProfileName(context).serialize());
accountManager.setProfileAvatar(profileKey, avatarStream);
}
}
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
}
@@ -72,19 +77,6 @@ public class RotateProfileKeyJob extends BaseJob {
return exception instanceof PushNetworkException;
}
private @Nullable StreamDetails getProfileAvatar() {
try {
File avatarFile = AvatarHelper.getAvatarFile(context, Recipient.self().getId());
if (avatarFile.exists()) {
return new StreamDetails(new FileInputStream(avatarFile), "image/jpeg", avatarFile.length());
}
} catch (IOException e) {
return null;
}
return null;
}
public static final class Factory implements Job.Factory<RotateProfileKeyJob> {
@Override
public @NonNull RotateProfileKeyJob create(@NonNull Parameters parameters, @NonNull Data data) {

View File

@@ -2,15 +2,21 @@ package org.thoughtcrime.securesms.profiles;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -57,4 +63,24 @@ public class AvatarHelper {
out.close();
}
}
public static @NonNull StreamDetails avatarStream(@NonNull byte[] data) {
return new StreamDetails(new ByteArrayInputStream(data), MediaUtil.IMAGE_JPEG, data.length);
}
public static @Nullable StreamDetails getSelfProfileAvatarStream(@NonNull Context context) {
File avatarFile = getAvatarFile(context, Recipient.self().getId());
if (avatarFile.exists() && avatarFile.length() > 0) {
try {
FileInputStream stream = new FileInputStream(avatarFile);
return new StreamDetails(stream, MediaUtil.IMAGE_JPEG, avatarFile.length());
} catch (FileNotFoundException e) {
throw new AssertionError(e);
}
} else {
return null;
}
}
}

View File

@@ -20,17 +20,14 @@ import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.IOException;
import java.security.SecureRandom;
@@ -132,7 +129,7 @@ class EditProfileRepository {
@WorkerThread
private @NonNull Optional<String> getUsernameInternal() {
try {
SignalServiceProfile profile = retrieveOwnProfile();
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, Recipient.self(), SignalServiceProfile.RequestType.PROFILE).getProfile();
TextSecurePreferences.setLocalUsername(context, profile.getUsername());
DatabaseFactory.getRecipientDatabase(context).setUsername(Recipient.self().getId(), profile.getUsername());
} catch (IOException e) {
@@ -141,22 +138,6 @@ class EditProfileRepository {
return Optional.fromNullable(TextSecurePreferences.getLocalUsername(context));
}
private SignalServiceProfile retrieveOwnProfile() throws IOException {
SignalServiceAddress address = new SignalServiceAddress(TextSecurePreferences.getLocalUuid(context), TextSecurePreferences.getLocalNumber(context));
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
SignalServiceMessagePipe pipe = IncomingMessageObserver.getPipe();
if (pipe != null) {
try {
return pipe.getProfile(address, Optional.absent());
} catch (IOException e) {
Log.w(TAG, e);
}
}
return receiver.retrieveProfile(address, Optional.absent());
}
public enum UploadResult {
SUCCESS,
ERROR_FILE_IO

View File

@@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.net.UserAgentInterceptor;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
@@ -16,6 +17,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -144,7 +146,13 @@ public class SignalServiceNetworkAccess {
final SignalStorageUrl qatarGoogleStorage = new SignalStorageUrl("https://www.google.com.qa/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
final List<Interceptor> interceptors = Collections.singletonList(new UserAgentInterceptor());
final byte[] zkGroupServerPublicParams;
try {
zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS);
} catch (IOException e) {
throw new AssertionError(e);
}
this.censorshipConfiguration = new HashMap<String, SignalServiceConfiguration>() {{
put(COUNTRY_CODE_EGYPT, new SignalServiceConfiguration(new SignalServiceUrl[] {egyptGoogleService, baseGoogleService, baseAndroidService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
@@ -152,21 +160,24 @@ public class SignalServiceNetworkAccess {
new SignalContactDiscoveryUrl[] {egyptGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
new SignalKeyBackupServiceUrl[] {egyptGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
new SignalStorageUrl[] {egyptGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
interceptors));
interceptors,
zkGroupServerPublicParams));
put(COUNTRY_CODE_UAE, new SignalServiceConfiguration(new SignalServiceUrl[] {uaeGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
new SignalCdnUrl[] {uaeGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn},
new SignalContactDiscoveryUrl[] {uaeGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
new SignalKeyBackupServiceUrl[] {uaeGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
new SignalStorageUrl[] {uaeGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
interceptors));
interceptors,
zkGroupServerPublicParams));
put(COUNTRY_CODE_OMAN, new SignalServiceConfiguration(new SignalServiceUrl[] {omanGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
new SignalCdnUrl[] {omanGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn},
new SignalContactDiscoveryUrl[] {omanGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
new SignalKeyBackupServiceUrl[] {omanGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
new SignalStorageUrl[] {omanGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
interceptors));
interceptors,
zkGroupServerPublicParams));
put(COUNTRY_CODE_QATAR, new SignalServiceConfiguration(new SignalServiceUrl[] {qatarGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
@@ -174,7 +185,8 @@ public class SignalServiceNetworkAccess {
new SignalContactDiscoveryUrl[] {qatarGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
new SignalKeyBackupServiceUrl[] {qatarGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
new SignalStorageUrl[] {qatarGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
interceptors));
interceptors,
zkGroupServerPublicParams));
}};
this.uncensoredConfiguration = new SignalServiceConfiguration(new SignalServiceUrl[] {new SignalServiceUrl(BuildConfig.SIGNAL_URL, new SignalServiceTrustStore(context))},
@@ -182,7 +194,8 @@ public class SignalServiceNetworkAccess {
new SignalContactDiscoveryUrl[] {new SignalContactDiscoveryUrl(BuildConfig.SIGNAL_CONTACT_DISCOVERY_URL, new SignalServiceTrustStore(context))},
new SignalKeyBackupServiceUrl[] { new SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, new SignalServiceTrustStore(context)) },
new SignalStorageUrl[] {new SignalStorageUrl(BuildConfig.STORAGE_URL, new SignalServiceTrustStore(context))},
interceptors);
interceptors,
zkGroupServerPublicParams);
this.censoredCountries = this.censorshipConfiguration.keySet().toArray(new String[0]);
}

View File

@@ -78,6 +78,7 @@ public class Recipient {
private final int expireMessages;
private final RegisteredState registered;
private final byte[] profileKey;
private final byte[] profileKeyCredential;
private final String name;
private final Uri systemContactPhoto;
private final String customLabel;
@@ -297,6 +298,7 @@ public class Recipient {
this.expireMessages = 0;
this.registered = RegisteredState.UNKNOWN;
this.profileKey = null;
this.profileKeyCredential = null;
this.name = null;
this.systemContactPhoto = null;
this.customLabel = null;
@@ -336,6 +338,7 @@ public class Recipient {
this.expireMessages = details.expireMessages;
this.registered = details.registered;
this.profileKey = details.profileKey;
this.profileKeyCredential = details.profileKeyCredential;
this.name = details.name;
this.systemContactPhoto = details.systemContactPhoto;
this.customLabel = details.customLabel;
@@ -666,6 +669,14 @@ public class Recipient {
return profileKey;
}
public @Nullable byte[] getProfileKeyCredential() {
return profileKeyCredential;
}
public boolean hasProfileKeyCredential() {
return profileKeyCredential != null;
}
public @Nullable byte[] getStorageServiceKey() {
return storageKey;
}

View File

@@ -47,6 +47,7 @@ public class RecipientDetails {
final Optional<Integer> defaultSubscriptionId;
final RegisteredState registered;
final byte[] profileKey;
final byte[] profileKeyCredential;
final String profileAvatar;
final boolean profileSharing;
final boolean systemContact;
@@ -90,6 +91,7 @@ public class RecipientDetails {
this.defaultSubscriptionId = settings.getDefaultSubscriptionId();
this.registered = settings.getRegistered();
this.profileKey = settings.getProfileKey();
this.profileKeyCredential = settings.getProfileKeyCredential();
this.profileAvatar = settings.getProfileAvatar();
this.profileSharing = settings.isProfileSharing();
this.systemContact = systemContact;
@@ -134,6 +136,7 @@ public class RecipientDetails {
this.defaultSubscriptionId = Optional.absent();
this.registered = RegisteredState.UNKNOWN;
this.profileKey = null;
this.profileKeyCredential = null;
this.profileAvatar = null;
this.profileSharing = false;
this.systemContact = true;

View File

@@ -6,10 +6,11 @@ import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
@@ -28,7 +29,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
@@ -39,6 +39,7 @@ import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.api.RegistrationLockData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.kbs.HashedPin;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
@@ -191,17 +192,17 @@ public final class CodeVerificationRequest {
@Nullable String fcmToken)
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
{
boolean isV2KbsPin = kbsTokenResponse != null;
int registrationId = KeyHelper.generateRegistrationId(false);
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
byte[] profileKey = findExistingProfileKey(context, credentials.getE164number());
boolean isV2KbsPin = kbsTokenResponse != null;
int registrationId = KeyHelper.generateRegistrationId(false);
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
ProfileKey profileKey = findExistingProfileKey(context, credentials.getE164number());
if (profileKey == null) {
profileKey = Util.getSecretBytes(32);
profileKey = ProfileKeyUtil.createNew();
Log.i(TAG, "No profile key found, created a new one");
}
byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(profileKey);
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(profileKey);
TextSecurePreferences.setLocalRegistrationId(context, registrationId);
SessionUtil.archiveAllSessions(context);
@@ -269,12 +270,12 @@ public final class CodeVerificationRequest {
}
}
private static @Nullable byte[] findExistingProfileKey(@NonNull Context context, @NonNull String e164number) {
private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Optional<RecipientId> recipient = recipientDatabase.getByE164(e164number);
if (recipient.isPresent()) {
return Recipient.resolved(recipient.get()).getProfileKey();
return ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).getProfileKey());
}
return null;

View File

@@ -312,4 +312,7 @@ public final class FeatureFlags {
return disk;
}
}
/** Read and write versioned profile information. */
public static final boolean VERSIONED_PROFILES = org.whispersystems.signalservice.FeatureFlags.VERSIONED_PROFILES;
}

View File

@@ -6,6 +6,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
@@ -19,6 +22,7 @@ import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
@@ -28,22 +32,30 @@ import java.io.IOException;
/**
* Aids in the retrieval and decryption of profiles.
*/
public class ProfileUtil {
public final class ProfileUtil {
private ProfileUtil() {
}
private static final String TAG = Log.tag(ProfileUtil.class);
@WorkerThread
public static SignalServiceProfile retrieveProfile(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
public static @NonNull ProfileAndCredential retrieveProfile(@NonNull Context context,
@NonNull Recipient recipient,
@NonNull SignalServiceProfile.RequestType requestType)
throws IOException
{
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
Optional<UnidentifiedAccess> unidentifiedAccess = getUnidentifiedAccess(context, recipient);
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
SignalServiceProfile profile;
ProfileAndCredential profile;
try {
profile = retrieveProfileInternal(address, unidentifiedAccess);
profile = retrieveProfileInternal(address, profileKey, unidentifiedAccess, requestType);
} catch (NonSuccessfulResponseCodeException e) {
if (unidentifiedAccess.isPresent()) {
profile = retrieveProfileInternal(address, Optional.absent());
profile = retrieveProfileInternal(address, profileKey, Optional.absent(), requestType);
} else {
throw e;
}
@@ -52,7 +64,7 @@ public class ProfileUtil {
return profile;
}
public static @Nullable String decryptName(@NonNull byte[] profileKey, @Nullable String encryptedName)
public static @Nullable String decryptName(@NonNull ProfileKey profileKey, @Nullable String encryptedName)
throws InvalidCiphertextException, IOException
{
if (encryptedName == null) {
@@ -64,8 +76,11 @@ public class ProfileUtil {
}
@WorkerThread
private static SignalServiceProfile retrieveProfileInternal(@NonNull SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess)
throws IOException
private static @NonNull ProfileAndCredential retrieveProfileInternal(@NonNull SignalServiceAddress address,
@NonNull Optional<ProfileKey> profileKey,
@NonNull Optional<UnidentifiedAccess> unidentifiedAccess,
@NonNull SignalServiceProfile.RequestType requestType)
throws IOException
{
SignalServiceMessagePipe authPipe = IncomingMessageObserver.getPipe();
SignalServiceMessagePipe unidentifiedPipe = IncomingMessageObserver.getUnidentifiedPipe();
@@ -74,14 +89,18 @@ public class ProfileUtil {
if (pipe != null) {
try {
return pipe.getProfile(address, unidentifiedAccess);
return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
} catch (IOException e) {
Log.w(TAG, e);
Log.w(TAG, "Websocket request failed. Falling back to REST.", e);
}
}
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
return receiver.retrieveProfile(address, unidentifiedAccess);
try {
return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
} catch (VerificationFailedException e) {
throw new IOException("Verification Problem", e);
}
}
private static Optional<UnidentifiedAccess> getUnidentifiedAccess(@NonNull Context context, @NonNull Recipient recipient) {

View File

@@ -7,8 +7,6 @@ import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import org.whispersystems.libsignal.util.Pair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -46,9 +44,9 @@ public final class SqlUtil {
* change. In other words, if {@link SQLiteDatabase#update(String, ContentValues, String, String[])}
* returns > 0, then you know something *actually* changed.
*/
public static @NonNull Pair<String, String[]> buildTrueUpdateQuery(@NonNull String selection,
@NonNull String[] args,
@NonNull ContentValues contentValues)
public static @NonNull UpdateQuery buildTrueUpdateQuery(@NonNull String selection,
@NonNull String[] args,
@NonNull ContentValues contentValues)
{
StringBuilder qualifier = new StringBuilder();
Set<Map.Entry<String, Object>> valueSet = contentValues.valueSet();
@@ -73,6 +71,24 @@ public final class SqlUtil {
i++;
}
return new Pair<>("(" + selection + ") AND (" + qualifier + ")", fullArgs.toArray(new String[0]));
return new UpdateQuery("(" + selection + ") AND (" + qualifier + ")", fullArgs.toArray(new String[0]));
}
public static class UpdateQuery {
private final String where;
private final String[] whereArgs;
private UpdateQuery(@NonNull String where, @NonNull String[] whereArgs) {
this.where = where;
this.whereArgs = whereArgs;
}
public String getWhere() {
return where;
}
public String[] getWhereArgs() {
return whereArgs;
}
}
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.contacts.sync;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import com.google.common.collect.Sets;
import org.junit.Before;
import org.junit.Test;
@@ -21,13 +22,13 @@ import java.util.List;
import java.util.Set;
import java.util.UUID;
import edu.emory.mathcs.backport.java.util.Arrays;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
public class StorageSyncHelperTest {
public final class StorageSyncHelperTest {
private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7");
private static final UUID UUID_B = UuidUtil.parseOrThrow("32119989-77fb-4e18-af70-81d55185c6b1");
@@ -253,8 +254,63 @@ public class StorageSyncHelperTest {
assertByteListEquals(byteListOf(1), result.getDeletes());
}
private static <E> Set<E> setOf(E... vals) {
return new LinkedHashSet<E>(Arrays.asList(vals));
@Test
public void contacts_with_same_profile_key_contents_are_equal() {
byte[] profileKey = new byte[32];
byte[] profileKeyCopy = profileKey.clone();
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setProfileKey(profileKey).build();
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setProfileKey(profileKeyCopy).build();
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
assertFalse(contactUpdate(a, b).profileKeyChanged());
}
@Test
public void contacts_with_different_profile_key_contents_are_not_equal() {
byte[] profileKey = new byte[32];
byte[] profileKeyCopy = profileKey.clone();
profileKeyCopy[0] = 1;
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setProfileKey(profileKey).build();
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setProfileKey(profileKeyCopy).build();
assertNotEquals(a, b);
assertNotEquals(a.hashCode(), b.hashCode());
assertTrue(contactUpdate(a, b).profileKeyChanged());
}
@Test
public void contacts_with_same_identity_key_contents_are_equal() {
byte[] profileKey = new byte[32];
byte[] profileKeyCopy = profileKey.clone();
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKey).build();
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
}
@Test
public void contacts_with_different_identity_key_contents_are_not_equal() {
byte[] profileKey = new byte[32];
byte[] profileKeyCopy = profileKey.clone();
profileKeyCopy[0] = 1;
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKey).build();
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
assertNotEquals(a, b);
assertNotEquals(a.hashCode(), b.hashCode());
}
@SafeVarargs
private static <E> Set<E> setOf(E... values) {
return Sets.newHashSet(values);
}
private static Set<SignalStorageRecord> recordSetOf(SignalContactRecord... contactRecords) {
@@ -267,14 +323,21 @@ public class StorageSyncHelperTest {
return storageRecords;
}
private static SignalContactRecord.Builder contactBuilder(int key,
UUID uuid,
String e164,
String profileName)
{
return new SignalContactRecord.Builder(byteArray(key), new SignalServiceAddress(uuid, e164))
.setProfileName(profileName);
}
private static SignalContactRecord contact(int key,
UUID uuid,
String e164,
String profileName)
{
return new SignalContactRecord.Builder(byteArray(key), new SignalServiceAddress(uuid, e164))
.setProfileName(profileName)
.build();
return contactBuilder(key, uuid, e164, profileName).build();
}
private static StorageSyncHelper.ContactUpdate contactUpdate(SignalContactRecord oldContact, SignalContactRecord newContact) {

View File

@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.database;
package org.thoughtcrime.securesms.util;
import android.app.Application;
import android.content.ContentValues;
@@ -7,15 +7,13 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.util.Pair;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, application = Application.class)
public class SqlUtilTest {
public final class SqlUtilTest {
@Test
public void buildTrueUpdateQuery_simple() {
@@ -25,10 +23,10 @@ public class SqlUtilTest {
ContentValues values = new ContentValues();
values.put("a", 2);
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, values);
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
assertEquals("(_id = ?) AND (a != ? OR a IS NULL)", result.first());
assertArrayEquals(new String[] { "1", "2" }, result.second());
assertEquals("(_id = ?) AND (a != ? OR a IS NULL)", updateQuery.getWhere());
assertArrayEquals(new String[] { "1", "2" }, updateQuery.getWhereArgs());
}
@Test
@@ -39,10 +37,10 @@ public class SqlUtilTest {
ContentValues values = new ContentValues();
values.put("a", 4);
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, values);
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
assertEquals("(_id = ? AND (foo = ? OR bar != ?)) AND (a != ? OR a IS NULL)", result.first());
assertArrayEquals(new String[] { "1", "2", "3", "4" }, result.second());
assertEquals("(_id = ? AND (foo = ? OR bar != ?)) AND (a != ? OR a IS NULL)", updateQuery.getWhere());
assertArrayEquals(new String[] { "1", "2", "3", "4" }, updateQuery.getWhereArgs());
}
@Test
@@ -55,10 +53,10 @@ public class SqlUtilTest {
values.put("b", 3);
values.put("c", 4);
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, values);
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
assertEquals("(_id = ?) AND (a != ? OR a IS NULL OR b != ? OR b IS NULL OR c != ? OR c IS NULL)", result.first());
assertArrayEquals(new String[] { "1", "2", "3", "4"}, result.second());
assertEquals("(_id = ?) AND (a != ? OR a IS NULL OR b != ? OR b IS NULL OR c != ? OR c IS NULL)", updateQuery.getWhere());
assertArrayEquals(new String[] { "1", "2", "3", "4"}, updateQuery.getWhereArgs());
}
@Test
@@ -69,10 +67,10 @@ public class SqlUtilTest {
ContentValues values = new ContentValues();
values.put("a", (String) null);
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, values);
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
assertEquals("(_id = ?) AND (a NOT NULL)", result.first());
assertArrayEquals(new String[] { "1" }, result.second());
assertEquals("(_id = ?) AND (a NOT NULL)", updateQuery.getWhere());
assertArrayEquals(new String[] { "1" }, updateQuery.getWhereArgs());
}
@Test
@@ -87,9 +85,9 @@ public class SqlUtilTest {
values.put("d", (String) null);
values.put("e", (String) null);
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, values);
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
assertEquals("(_id = ?) AND (a NOT NULL OR b != ? OR b IS NULL OR c != ? OR c IS NULL OR d NOT NULL OR e NOT NULL)", result.first());
assertArrayEquals(new String[] { "1", "2", "3" }, result.second());
assertEquals("(_id = ?) AND (a NOT NULL OR b != ? OR b IS NULL OR c != ? OR c IS NULL OR d NOT NULL OR e NOT NULL)", updateQuery.getWhere());
assertArrayEquals(new String[] { "1", "2", "3" }, updateQuery.getWhereArgs());
}
}