Versioned Profiles support (disabled).

master
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());
}
}

View File

@ -35,6 +35,8 @@ dependencies {
api 'com.squareup.okhttp3:okhttp:3.12.1'
implementation 'org.threeten:threetenbp:1.3.6'
api project(':zkgroups')
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:1.7.1'
testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.0.0'

View File

@ -0,0 +1,13 @@
package org.whispersystems.signalservice;
/**
* A location for constants that allows us to turn features on and off at the service level during development.
* After a feature has been launched, the flag should be removed.
*/
public final class FeatureFlags {
/** Zero Knowledge Group functions */
public static final boolean ZK_GROUPS = false;
/** Read and write versioned profile information. */
public static final boolean VERSIONED_PROFILES = ZK_GROUPS && false;
}

View File

@ -9,6 +9,7 @@ package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
@ -17,18 +18,20 @@ import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
@ -56,6 +59,7 @@ import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -559,19 +563,27 @@ public class SignalServiceAccountManager {
return this.pushServiceSocket.getTurnServerInfo();
}
public void setProfileName(byte[] key, String name)
public void setProfileName(ProfileKey key, String name)
throws IOException
{
if (FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
if (name == null) name = "";
String ciphertextName = Base64.encodeBytesWithoutPadding(new ProfileCipher(key).encryptName(name.getBytes("UTF-8"), ProfileCipher.NAME_PADDED_LENGTH));
String ciphertextName = Base64.encodeBytesWithoutPadding(new ProfileCipher(key).encryptName(name.getBytes(StandardCharsets.UTF_8), ProfileCipher.NAME_PADDED_LENGTH));
this.pushServiceSocket.setProfileName(ciphertextName);
}
public void setProfileAvatar(byte[] key, StreamDetails avatar)
public void setProfileAvatar(ProfileKey key, StreamDetails avatar)
throws IOException
{
if (FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
ProfileAvatarData profileAvatarData = null;
if (avatar != null) {
@ -584,6 +596,33 @@ public class SignalServiceAccountManager {
this.pushServiceSocket.setProfileAvatar(profileAvatarData);
}
public void setVersionedProfile(ProfileKey profileKey, String name, StreamDetails avatar)
throws IOException
{
if (!FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
if (name == null) name = "";
byte[] ciphertextName = new ProfileCipher(profileKey).encryptName(name.getBytes(StandardCharsets.UTF_8), ProfileCipher.NAME_PADDED_LENGTH);
boolean hasAvatar = avatar != null;
ProfileAvatarData profileAvatarData = null;
if (hasAvatar) {
profileAvatarData = new ProfileAvatarData(avatar.getStream(),
ProfileCipherOutputStream.getCiphertextLength(avatar.getLength()),
avatar.getContentType(),
new ProfileCipherOutputStreamFactory(profileKey));
}
this.pushServiceSocket.writeProfile(new SignalServiceProfileWrite(profileKey.getProfileKeyVersion().serialize(),
ciphertextName,
hasAvatar,
profileKey.getCommitment().serialize()),
profileAvatarData);
}
public void setUsername(String username) throws IOException {
this.pushServiceSocket.setUsername(username);
}

View File

@ -8,11 +8,21 @@ package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
import org.signal.zkgroup.profiles.ProfileKeyVersion;
import org.whispersystems.libsignal.InvalidVersionException;
import org.whispersystems.libsignal.util.Hex;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
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.util.CredentialsProvider;
@ -25,10 +35,10 @@ import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ -47,10 +57,15 @@ public class SignalServiceMessagePipe {
private final WebSocketConnection websocket;
private final Optional<CredentialsProvider> credentialsProvider;
private final ClientZkProfileOperations clientZkProfile;
SignalServiceMessagePipe(WebSocketConnection websocket, Optional<CredentialsProvider> credentialsProvider) {
SignalServiceMessagePipe(WebSocketConnection websocket,
Optional<CredentialsProvider> credentialsProvider,
ClientZkProfileOperations clientZkProfile)
{
this.websocket = websocket;
this.credentialsProvider = credentialsProvider;
this.clientZkProfile = clientZkProfile;
this.websocket.connect();
}
@ -149,7 +164,12 @@ public class SignalServiceMessagePipe {
}
}
public SignalServiceProfile getProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
public ProfileAndCredential getProfile(SignalServiceAddress address,
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType)
throws IOException
{
try {
List<String> headers = new LinkedList<>();
@ -157,12 +177,30 @@ public class SignalServiceMessagePipe {
headers.add("Unidentified-Access-Key:" + Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
}
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
.setId(new SecureRandom().nextLong())
.setVerb("GET")
.setPath(String.format("/v1/profile/%s", address.getIdentifier()))
.addAllHeaders(headers)
.build();
Optional<UUID> uuid = address.getUuid();
SecureRandom random = new SecureRandom();
ProfileKeyCredentialRequestContext requestContext = null;
WebSocketRequestMessage.Builder builder = WebSocketRequestMessage.newBuilder()
.setId(random.nextLong())
.setVerb("GET")
.addAllHeaders(headers);
if (FeatureFlags.VERSIONED_PROFILES && requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL && uuid.isPresent() && profileKey.isPresent()) {
ProfileKeyVersion profileKeyIdentifier = profileKey.get().getProfileKeyVersion();
UUID target = uuid.get();
requestContext = clientZkProfile.createProfileKeyCredentialRequestContext(random, target, profileKey.get());
ProfileKeyCredentialRequest request = requestContext.getRequest();
String version = profileKeyIdentifier.serialize();
String credentialRequest = Hex.toStringCondensed(request.serialize());
builder.setPath(String.format("/v1/profile/%s/%s/%s", target, version, credentialRequest));
} else {
builder.setPath(String.format("/v1/profile/%s", address.getIdentifier()));
}
WebSocketRequestMessage requestMessage = builder.build();
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
@ -170,8 +208,13 @@ public class SignalServiceMessagePipe {
throw new IOException("Non-successful response: " + response.first());
}
return JsonUtil.fromJson(response.second(), SignalServiceProfile.class);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
SignalServiceProfile signalServiceProfile = JsonUtil.fromJson(response.second(), SignalServiceProfile.class);
ProfileKeyCredential profileKeyCredential = requestContext != null && signalServiceProfile.getProfileKeyCredentialResponse() != null
? clientZkProfile.receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse())
: null;
return new ProfileAndCredential(signalServiceProfile, requestType, Optional.fromNullable(profileKeyCredential));
} catch (InterruptedException | ExecutionException | TimeoutException | VerificationFailedException e) {
throw new IOException(e);
}
}

View File

@ -6,8 +6,14 @@
package org.whispersystems.signalservice.api;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
@ -16,6 +22,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
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.util.CredentialsProvider;
@ -54,6 +61,7 @@ public class SignalServiceMessageReceiver {
private final String signalAgent;
private final ConnectivityListener connectivityListener;
private final SleepTimer sleepTimer;
private final ClientZkProfileOperations clientZkProfile;
/**
* Construct a SignalServiceMessageReceiver.
@ -91,6 +99,7 @@ public class SignalServiceMessageReceiver {
this.signalAgent = signalAgent;
this.connectivityListener = listener;
this.sleepTimer = timer;
this.clientZkProfile = new ClientZkProfileOperations(new ServerPublicParams(urls.getZkGroupServerPublicParams()));
}
/**
@ -110,10 +119,21 @@ public class SignalServiceMessageReceiver {
return retrieveAttachment(pointer, destination, maxSizeBytes, null);
}
public SignalServiceProfile retrieveProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess)
throws IOException
public ProfileAndCredential retrieveProfile(SignalServiceAddress address,
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType)
throws IOException, VerificationFailedException
{
return socket.retrieveProfile(address, unidentifiedAccess);
Optional<UUID> uuid = address.getUuid();
if (FeatureFlags.VERSIONED_PROFILES && requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL && uuid.isPresent() && profileKey.isPresent()) {
return socket.retrieveProfile(uuid.get(), profileKey.get(), unidentifiedAccess);
} else {
return new ProfileAndCredential(socket.retrieveProfile(address, unidentifiedAccess),
SignalServiceProfile.RequestType.PROFILE,
Optional.<ProfileKeyCredential>absent());
}
}
public SignalServiceProfile retrieveProfileByUsername(String username, Optional<UnidentifiedAccess> unidentifiedAccess)
@ -122,7 +142,7 @@ public class SignalServiceMessageReceiver {
return socket.retrieveProfileByUsername(username, unidentifiedAccess);
}
public InputStream retrieveProfileAvatar(String path, File destination, byte[] profileKey, int maxSizeBytes)
public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, int maxSizeBytes)
throws IOException
{
socket.retrieveProfileAvatar(path, destination, maxSizeBytes);
@ -203,7 +223,7 @@ public class SignalServiceMessageReceiver {
sleepTimer,
urls.getNetworkInterceptors());
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider));
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile);
}
public SignalServiceMessagePipe createUnidentifiedMessagePipe() {
@ -213,7 +233,7 @@ public class SignalServiceMessageReceiver {
sleepTimer,
urls.getNetworkInterceptors());
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider));
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile);
}
public List<SignalServiceEnvelope> retrieveMessages() throws IOException {

View File

@ -1,6 +1,7 @@
package org.whispersystems.signalservice.api.crypto;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.internal.util.Util;
@ -21,9 +22,9 @@ public class ProfileCipher {
public static final int NAME_PADDED_LENGTH = 53;
private final byte[] key;
private final ProfileKey key;
public ProfileCipher(byte[] key) {
public ProfileCipher(ProfileKey key) {
this.key = key;
}
@ -40,7 +41,7 @@ public class ProfileCipher {
byte[] nonce = Util.getSecretBytes(12);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
return ByteUtil.combine(nonce, cipher.doFinal(inputPadded));
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | BadPaddingException | NoSuchPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
@ -58,7 +59,7 @@ public class ProfileCipher {
System.arraycopy(input, 0, nonce, 0, nonce.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
byte[] paddedPlaintext = cipher.doFinal(input, nonce.length, input.length - nonce.length);
int plaintextLength = 0;

View File

@ -1,6 +1,7 @@
package org.whispersystems.signalservice.api.crypto;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.FilterInputStream;
@ -24,7 +25,7 @@ public class ProfileCipherInputStream extends FilterInputStream {
private boolean finished = false;
public ProfileCipherInputStream(InputStream in, byte[] key) throws IOException {
public ProfileCipherInputStream(InputStream in, ProfileKey key) throws IOException {
super(in);
try {
@ -33,7 +34,7 @@ public class ProfileCipherInputStream extends FilterInputStream {
byte[] nonce = new byte[12];
Util.readFully(in, nonce);
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
} catch (InvalidKeyException e) {

View File

@ -1,5 +1,7 @@
package org.whispersystems.signalservice.api.crypto;
import org.signal.zkgroup.profiles.ProfileKey;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
@ -18,13 +20,13 @@ public class ProfileCipherOutputStream extends DigestingOutputStream {
private final Cipher cipher;
public ProfileCipherOutputStream(OutputStream out, byte[] key) throws IOException {
public ProfileCipherOutputStream(OutputStream out, ProfileKey key) throws IOException {
super(out);
try {
this.cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] nonce = generateNonce();
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
this.cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
super.write(nonce, 0, nonce.length);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {

View File

@ -3,6 +3,7 @@ package org.whispersystems.signalservice.api.crypto;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.libsignal.metadata.certificate.SenderCertificate;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.ByteUtil;
import java.security.InvalidAlgorithmParameterException;
@ -36,13 +37,13 @@ public class UnidentifiedAccess {
return unidentifiedCertificate;
}
public static byte[] deriveAccessKeyFrom(byte[] profileKey) {
public static byte[] deriveAccessKeyFrom(ProfileKey profileKey) {
try {
byte[] nonce = new byte[12];
byte[] input = new byte[16];
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(profileKey, "AES"), new GCMParameterSpec(128, nonce));
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(profileKey.serialize(), "AES"), new GCMParameterSpec(128, nonce));
byte[] ciphertext = cipher.doFinal(input);

View File

@ -6,6 +6,7 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -17,10 +18,10 @@ public class DeviceContact {
private final Optional<SignalServiceAttachmentStream> avatar;
private final Optional<String> color;
private final Optional<VerifiedMessage> verified;
private final Optional<byte[]> profileKey;
private final Optional<ProfileKey> profileKey;
private final boolean blocked;
private final Optional<Integer> expirationTimer;
private final Optional<Integer> inboxPosition;
private final Optional<Integer> inboxPosition;
private final boolean archived;
public DeviceContact(SignalServiceAddress address,
@ -28,7 +29,7 @@ public class DeviceContact {
Optional<SignalServiceAttachmentStream> avatar,
Optional<String> color,
Optional<VerifiedMessage> verified,
Optional<byte[]> profileKey,
Optional<ProfileKey> profileKey,
boolean blocked,
Optional<Integer> expirationTimer,
Optional<Integer> inboxPosition,
@ -66,7 +67,7 @@ public class DeviceContact {
return verified;
}
public Optional<byte[]> getProfileKey() {
public Optional<ProfileKey> getProfileKey() {
return profileKey;
}

View File

@ -6,6 +6,8 @@
package org.whispersystems.signalservice.api.messages.multidevice;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidMessageException;
@ -44,7 +46,7 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
Optional<SignalServiceAttachmentStream> avatar = Optional.absent();
Optional<String> color = details.hasColor() ? Optional.of(details.getColor()) : Optional.<String>absent();
Optional<VerifiedMessage> verified = Optional.absent();
Optional<byte[]> profileKey = Optional.absent();
Optional<ProfileKey> profileKey = Optional.absent();
boolean blocked = false;
Optional<Integer> expireTimer = Optional.absent();
Optional<Integer> inboxPosition = Optional.absent();
@ -84,7 +86,11 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
}
if (details.hasProfileKey()) {
profileKey = Optional.fromNullable(details.getProfileKey().toByteArray());
try {
profileKey = Optional.fromNullable(new ProfileKey(details.getProfileKey().toByteArray()));
} catch (InvalidInputException e) {
Log.w(TAG, "Invalid profile key ignored", e);
}
}
if (details.hasExpireTimer() && details.getExpireTimer() > 0) {

View File

@ -85,7 +85,7 @@ public class DeviceContactsOutputStream extends ChunkedOutputStream {
}
if (contact.getProfileKey().isPresent()) {
contactDetails.setProfileKey(ByteString.copyFrom(contact.getProfileKey().get()));
contactDetails.setProfileKey(ByteString.copyFrom(contact.getProfileKey().get().serialize()));
}
if (contact.getExpirationTimer().isPresent()) {

View File

@ -0,0 +1,32 @@
package org.whispersystems.signalservice.api.profiles;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.libsignal.util.guava.Optional;
public final class ProfileAndCredential {
private final SignalServiceProfile profile;
private final SignalServiceProfile.RequestType requestType;
private final Optional<ProfileKeyCredential> profileKeyCredential;
public ProfileAndCredential(SignalServiceProfile profile,
SignalServiceProfile.RequestType requestType,
Optional<ProfileKeyCredential> profileKeyCredential)
{
this.profile = profile;
this.requestType = requestType;
this.profileKeyCredential = profileKeyCredential;
}
public SignalServiceProfile getProfile() {
return profile;
}
public SignalServiceProfile.RequestType getRequestType() {
return requestType;
}
public Optional<ProfileKeyCredential> getProfileKeyCredential() {
return profileKeyCredential;
}
}

View File

@ -1,16 +1,28 @@
package org.whispersystems.signalservice.api.profiles;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.util.UUID;
public class SignalServiceProfile {
public enum RequestType {
PROFILE,
PROFILE_AND_CREDENTIAL
}
private static final String TAG = SignalServiceProfile.class.getSimpleName();
@JsonProperty
private String identityKey;
@ -37,6 +49,12 @@ public class SignalServiceProfile {
@JsonDeserialize(using = JsonUtil.UuidDeserializer.class)
private UUID uuid;
@JsonProperty
private byte[] credential;
@JsonIgnore
private RequestType requestType;
public SignalServiceProfile() {}
public String getIdentityKey() {
@ -71,6 +89,14 @@ public class SignalServiceProfile {
return uuid;
}
public RequestType getRequestType() {
return requestType;
}
public void setRequestType(RequestType requestType) {
this.requestType = requestType;
}
public static class Capabilities {
@JsonProperty
private boolean uuid;
@ -81,4 +107,17 @@ public class SignalServiceProfile {
return uuid;
}
}
public ProfileKeyCredentialResponse getProfileKeyCredentialResponse() {
if (!FeatureFlags.VERSIONED_PROFILES) return null;
if (credential == null) return null;
try {
return new ProfileKeyCredentialResponse(credential);
} catch (InvalidInputException e) {
Log.w(TAG, e);
return null;
}
}
}

View File

@ -0,0 +1,34 @@
package org.whispersystems.signalservice.api.profiles;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class SignalServiceProfileWrite {
@JsonProperty
private String version;
@JsonProperty
private byte[] name;
@JsonProperty
private boolean avatar;
@JsonProperty
private byte[] commitment;
@JsonCreator
public SignalServiceProfileWrite(){
}
public SignalServiceProfileWrite(String version, byte[] name, boolean avatar, byte[] commitment) {
this.version = version;
this.name = name;
this.avatar = avatar;
this.commitment = commitment;
}
public boolean hasAvatar() {
return avatar;
}
}

View File

@ -2,11 +2,12 @@ package org.whispersystems.signalservice.api.storage;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import java.util.Arrays;
import java.util.Objects;
public class SignalContactRecord {
public final class SignalContactRecord {
private final byte[] key;
private final SignalServiceAddress address;
@ -18,7 +19,7 @@ public class SignalContactRecord {
private final boolean blocked;
private final boolean profileSharingEnabled;
private final Optional<String> nickname;
private final int protoVersion;
private final int protoVersion;
private SignalContactRecord(byte[] key,
SignalServiceAddress address,
@ -42,7 +43,7 @@ public class SignalContactRecord {
this.blocked = blocked;
this.profileSharingEnabled = profileSharingEnabled;
this.nickname = Optional.fromNullable(nickname);
this.protoVersion = protoVersion;
this.protoVersion = protoVersion;
}
public byte[] getKey() {
@ -98,18 +99,20 @@ public class SignalContactRecord {
profileSharingEnabled == contact.profileSharingEnabled &&
Arrays.equals(key, contact.key) &&
Objects.equals(address, contact.address) &&
Objects.equals(profileName, contact.profileName) &&
Objects.equals(profileKey, contact.profileKey) &&
Objects.equals(username, contact.username) &&
Objects.equals(identityKey, contact.identityKey) &&
profileName.equals(contact.profileName) &&
OptionalUtil.byteArrayEquals(profileKey, contact.profileKey) &&
username.equals(contact.username) &&
OptionalUtil.byteArrayEquals(identityKey, contact.identityKey) &&
identityState == contact.identityState &&
Objects.equals(nickname, contact.nickname);
}
@Override
public int hashCode() {
int result = Objects.hash(address, profileName, profileKey, username, identityKey, identityState, blocked, profileSharingEnabled, nickname);
int result = Objects.hash(address, profileName, username, identityState, blocked, profileSharingEnabled, nickname);
result = 31 * result + Arrays.hashCode(key);
result = 31 * result + OptionalUtil.byteArrayHashCode(profileKey);
result = 31 * result + OptionalUtil.byteArrayHashCode(identityKey);
return result;
}
@ -138,7 +141,7 @@ public class SignalContactRecord {
}
public Builder setProfileKey(byte[] profileKey) {
this.profileKey= profileKey;
this.profileKey = profileKey;
return this;
}

View File

@ -46,8 +46,8 @@ public final class SignalStorageModels {
}
public static SignalContactRecord remoteToLocalContactRecord(byte[] storageKey, ContactRecord contact) throws IOException {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164());
SignalContactRecord.Builder builder = new SignalContactRecord.Builder(storageKey, address);
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164());
SignalContactRecord.Builder builder = new SignalContactRecord.Builder(storageKey, address);
if (contact.hasBlocked()) {
builder.setBlocked(contact.getBlocked());

View File

@ -0,0 +1,27 @@
package org.whispersystems.signalservice.api.util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Arrays;
public final class OptionalUtil {
private OptionalUtil() {
}
public static boolean byteArrayEquals(Optional<byte[]> a, Optional<byte[]> b) {
if (a.isPresent() != b.isPresent()) {
return false;
}
if (a.isPresent()) {
return Arrays.equals(a.get(), b.get());
}
return true;
}
public static int byteArrayHashCode(Optional<byte[]> bytes) {
return bytes.isPresent() ? Arrays.hashCode(bytes.get()) : 0;
}
}

View File

@ -1,9 +1,14 @@
package org.whispersystems.signalservice.api.util;
import org.whispersystems.libsignal.logging.Log;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
public class StreamDetails {
public final class StreamDetails implements Closeable {
private static final String TAG = StreamDetails.class.getSimpleName();
private final InputStream stream;
private final String contentType;
@ -26,4 +31,13 @@ public class StreamDetails {
public long getLength() {
return length;
}
@Override
public void close() {
try {
stream.close();
} catch (IOException e) {
Log.w(TAG, e);
}
}
}

View File

@ -1,11 +1,10 @@
package org.whispersystems.signalservice.internal.configuration;
import java.util.List;
import okhttp3.Interceptor;
public class SignalServiceConfiguration {
public final class SignalServiceConfiguration {
private final SignalServiceUrl[] signalServiceUrls;
private final SignalCdnUrl[] signalCdnUrls;
@ -13,13 +12,15 @@ public class SignalServiceConfiguration {
private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls;
private final SignalStorageUrl[] signalStorageUrls;
private final List<Interceptor> networkInterceptors;
private final byte[] zkGroupServerPublicParams;
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
SignalCdnUrl[] signalCdnUrls,
SignalContactDiscoveryUrl[] signalContactDiscoveryUrls,
SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls,
SignalStorageUrl[] signalStorageUrls,
List<Interceptor> networkInterceptors)
List<Interceptor> networkInterceptors,
byte[] zkGroupServerPublicParams)
{
this.signalServiceUrls = signalServiceUrls;
this.signalCdnUrls = signalCdnUrls;
@ -27,6 +28,7 @@ public class SignalServiceConfiguration {
this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls;
this.signalStorageUrls = signalStorageUrls;
this.networkInterceptors = networkInterceptors;
this.zkGroupServerPublicParams = zkGroupServerPublicParams;
}
public SignalServiceUrl[] getSignalServiceUrls() {
@ -52,4 +54,8 @@ public class SignalServiceConfiguration {
public List<Interceptor> getNetworkInterceptors() {
return networkInterceptors;
}
public byte[] getZkGroupServerPublicParams() {
return zkGroupServerPublicParams;
}
}

View File

@ -0,0 +1,17 @@
package org.whispersystems.signalservice.internal.groupsv2;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
public final class ClientZkOperations {
private final ClientZkProfileOperations clientZkProfileOperations;
public ClientZkOperations(ServerPublicParams serverPublicParams) {
clientZkProfileOperations = new ClientZkProfileOperations(serverPublicParams);
}
public ClientZkProfileOperations getProfileOperations() {
return clientZkProfileOperations;
}
}

View File

@ -4,8 +4,6 @@ package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ProfileAvatarUploadAttributes {
@JsonProperty
private String url;
@JsonProperty
private String key;
@ -30,10 +28,6 @@ public class ProfileAvatarUploadAttributes {
public ProfileAvatarUploadAttributes() {}
public String getUrl() {
return url;
}
public String getKey() {
return key;
}

View File

@ -9,6 +9,13 @@ package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
import org.signal.zkgroup.profiles.ProfileKeyVersion;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.logging.Log;
@ -17,11 +24,14 @@ import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
@ -48,6 +58,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResp
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
@ -170,6 +181,7 @@ public class PushServiceSocket {
private final CredentialsProvider credentialsProvider;
private final String signalAgent;
private final SecureRandom random;
private final ClientZkOperations clientZkOperations;
public PushServiceSocket(SignalServiceConfiguration signalServiceConfiguration, CredentialsProvider credentialsProvider, String signalAgent) {
this.credentialsProvider = credentialsProvider;
@ -180,6 +192,7 @@ public class PushServiceSocket {
this.keyBackupServiceClients = createConnectionHolders(signalServiceConfiguration.getSignalKeyBackupServiceUrls(), signalServiceConfiguration.getNetworkInterceptors());
this.storageClients = createConnectionHolders(signalServiceConfiguration.getSignalStorageUrls(), signalServiceConfiguration.getNetworkInterceptors());
this.random = new SecureRandom();
this.clientZkOperations = FeatureFlags.ZK_GROUPS ? new ClientZkOperations(new ServerPublicParams(signalServiceConfiguration.getZkGroupServerPublicParams())) : null;
}
public void requestSmsVerificationCode(boolean androidSmsRetriever, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
@ -543,6 +556,37 @@ public class PushServiceSocket {
}
}
public ProfileAndCredential retrieveProfile(UUID target, ProfileKey profileKey, Optional<UnidentifiedAccess> unidentifiedAccess)
throws NonSuccessfulResponseCodeException, VerificationFailedException
{
if (!FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
try {
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion();
ProfileKeyCredentialRequestContext requestContext = clientZkOperations.getProfileOperations().createProfileKeyCredentialRequestContext(random, target, profileKey);
ProfileKeyCredentialRequest request = requestContext.getRequest();
String version = profileKeyIdentifier.serialize();
String credentialRequest = Hex.toStringCondensed(request.serialize());
String subPath = String.format("%s/%s/%s", target, version, credentialRequest);
String response = makeServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, NO_HEADERS, unidentifiedAccess);
SignalServiceProfile signalServiceProfile = JsonUtil.fromJson(response, SignalServiceProfile.class);
ProfileKeyCredential profileKeyCredential = signalServiceProfile.getProfileKeyCredentialResponse() != null
? clientZkOperations.getProfileOperations().receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse())
: null;
return new ProfileAndCredential(signalServiceProfile, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, Optional.fromNullable(profileKeyCredential));
} catch (IOException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
}
}
public void retrieveProfileAvatar(String path, File destination, int maxSizeBytes)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
@ -550,12 +594,20 @@ public class PushServiceSocket {
}
public void setProfileName(String name) throws NonSuccessfulResponseCodeException, PushNetworkException {
if (FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
makeServiceRequest(String.format(PROFILE_PATH, "name/" + (name == null ? "" : URLEncoder.encode(name))), "PUT", "");
}
public void setProfileAvatar(ProfileAvatarData profileAvatar)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
if (FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
String response = makeServiceRequest(String.format(PROFILE_PATH, "form/avatar"), "GET", null);
ProfileAvatarUploadAttributes formAttributes;
@ -576,6 +628,35 @@ public class PushServiceSocket {
}
}
public void writeProfile(SignalServiceProfileWrite signalServiceProfileWrite, ProfileAvatarData profileAvatar)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
if (!FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
String requestBody = JsonUtil.toJson(signalServiceProfileWrite);
ProfileAvatarUploadAttributes formAttributes;
String response = makeServiceRequest(String.format(PROFILE_PATH, ""), "PUT", requestBody);
if (signalServiceProfileWrite.hasAvatar() && profileAvatar != null) {
try {
formAttributes = JsonUtil.fromJson(response, ProfileAvatarUploadAttributes.class);
} catch (IOException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
}
uploadToCdn("", formAttributes.getAcl(), formAttributes.getKey(),
formAttributes.getPolicy(), formAttributes.getAlgorithm(),
formAttributes.getCredential(), formAttributes.getDate(),
formAttributes.getSignature(), profileAvatar.getData(),
profileAvatar.getContentType(), profileAvatar.getDataLength(),
profileAvatar.getOutputStreamFactory(), null, null);
}
}
public void setUsername(String username) throws IOException {
makeServiceRequest(String.format(SET_USERNAME_PATH, username), "PUT", "", NO_HEADERS, new ResponseCodeHandler() {
@Override

View File

@ -1,6 +1,7 @@
package org.whispersystems.signalservice.internal.push.http;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.crypto.DigestingOutputStream;
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
@ -9,9 +10,9 @@ import java.io.OutputStream;
public class ProfileCipherOutputStreamFactory implements OutputStreamFactory {
private final byte[] key;
private final ProfileKey key;
public ProfileCipherOutputStreamFactory(byte[] key) {
public ProfileCipherOutputStreamFactory(ProfileKey key) {
this.key = key;
}

View File

@ -4,6 +4,8 @@ package org.whispersystems.signalservice.api.crypto;
import junit.framework.TestCase;
import org.conscrypt.Conscrypt;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayInputStream;
@ -16,8 +18,8 @@ public class ProfileCipherTest extends TestCase {
Security.insertProviderAt(Conscrypt.newProvider(), 1);
}
public void testEncryptDecrypt() throws InvalidCiphertextException {
byte[] key = Util.getSecretBytes(32);
public void testEncryptDecrypt() throws InvalidCiphertextException, InvalidInputException {
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
byte[] name = cipher.encryptName("Clement\0Duval".getBytes(), ProfileCipher.NAME_PADDED_LENGTH);
byte[] plaintext = cipher.decryptName(name);
@ -25,7 +27,7 @@ public class ProfileCipherTest extends TestCase {
}
public void testEmpty() throws Exception {
byte[] key = Util.getSecretBytes(32);
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
byte[] name = cipher.encryptName("".getBytes(), 26);
byte[] plaintext = cipher.decryptName(name);
@ -34,7 +36,7 @@ public class ProfileCipherTest extends TestCase {
}
public void testStreams() throws Exception {
byte[] key = Util.getSecretBytes(32);
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ProfileCipherOutputStream out = new ProfileCipherOutputStream(baos, key);

View File

@ -3,6 +3,8 @@ package org.whispersystems.signalservice.api.crypto;
import junit.framework.TestCase;
import org.conscrypt.OpenSSLProvider;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import java.security.Security;
import java.util.Arrays;
@ -15,11 +17,11 @@ public class UnidentifiedAccessTest extends TestCase {
private final byte[] EXPECTED_RESULT = {(byte)0x5a, (byte)0x72, (byte)0x3a, (byte)0xce, (byte)0xe5, (byte)0x2c, (byte)0x5e, (byte)0xa0, (byte)0x2b, (byte)0x92, (byte)0xa3, (byte)0xa3, (byte)0x60, (byte)0xc0, (byte)0x95, (byte)0x95};
public void testKeyDerivation() {
public void testKeyDerivation() throws InvalidInputException {
byte[] key = new byte[32];
Arrays.fill(key, (byte)0x02);
byte[] result = UnidentifiedAccess.deriveAccessKeyFrom(key);
byte[] result = UnidentifiedAccess.deriveAccessKeyFrom(new ProfileKey(key));
assertTrue(Arrays.equals(result, EXPECTED_RESULT));
}

View File

@ -0,0 +1,53 @@
package org.whispersystems.signalservice.api.util;
import org.junit.Test;
import org.whispersystems.libsignal.util.guava.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public final class OptionalUtilTest {
@Test
public void absent_are_equal() {
assertTrue(OptionalUtil.byteArrayEquals(Optional.<byte[]>absent(), Optional.<byte[]>absent()));
}
@Test
public void first_non_absent_not_equal() {
assertFalse(OptionalUtil.byteArrayEquals(Optional.of(new byte[1]), Optional.<byte[]>absent()));
}
@Test
public void second_non_absent_not_equal() {
assertFalse(OptionalUtil.byteArrayEquals(Optional.<byte[]>absent(), Optional.of(new byte[1])));
}
@Test
public void equal_contents() {
byte[] contentsA = new byte[]{1, 2, 3};
byte[] contentsB = contentsA.clone();
Optional<byte[]> a = Optional.of(contentsA);
Optional<byte[]> b = Optional.of(contentsB);
assertTrue(OptionalUtil.byteArrayEquals(a, b));
assertEquals(OptionalUtil.byteArrayHashCode(a), OptionalUtil.byteArrayHashCode(b));
}
@Test
public void in_equal_contents() {
byte[] contentsA = new byte[]{1, 2, 3};
byte[] contentsB = new byte[]{4, 5, 6};
Optional<byte[]> a = Optional.of(contentsA);
Optional<byte[]> b = Optional.of(contentsB);
assertFalse(OptionalUtil.byteArrayEquals(a, b));
assertNotEquals(OptionalUtil.byteArrayHashCode(a), OptionalUtil.byteArrayHashCode(b));
}
@Test
public void hash_code_absent() {
assertEquals(0, OptionalUtil.byteArrayHashCode(Optional.<byte[]>absent()));
}
}

View File

@ -0,0 +1,3 @@
apply plugin: 'java-library'
sourceCompatibility = 1.7

View File

@ -0,0 +1,6 @@
package org.signal.zkgroup;
public final class InvalidInputException extends Exception {
public InvalidInputException() {
}
}

View File

@ -0,0 +1,6 @@
package org.signal.zkgroup;
public final class ServerPublicParams {
public ServerPublicParams(byte[] zkGroupServerPublicParams) {
}
}

View File

@ -0,0 +1,4 @@
package org.signal.zkgroup;
public final class VerificationFailedException extends Exception {
}

View File

@ -0,0 +1,20 @@
package org.signal.zkgroup.profiles;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.VerificationFailedException;
import java.security.SecureRandom;
import java.util.UUID;
public final class ClientZkProfileOperations {
public ClientZkProfileOperations(ServerPublicParams serverPublicParams) {
}
public ProfileKeyCredentialRequestContext createProfileKeyCredentialRequestContext(SecureRandom random, UUID target, ProfileKey profileKey) {
throw new AssertionError();
}
public ProfileKeyCredential receiveProfileKeyCredential(ProfileKeyCredentialRequestContext requestContext, ProfileKeyCredentialResponse profileKeyCredentialResponse) throws VerificationFailedException {
throw new AssertionError();
}
}

View File

@ -0,0 +1,50 @@
package org.signal.zkgroup.profiles;
import org.signal.zkgroup.InvalidInputException;
import java.util.Arrays;
/**
* Unlike the rest of this place-holder library, this does function as a wrapper around the
* traditional byte array used for profile keys.
*/
public final class ProfileKey {
public static final int SIZE = 32;
private final byte[] profileKey;
public ProfileKey(byte[] profileKey) throws InvalidInputException {
if (profileKey == null || profileKey.length != SIZE) {
throw new InvalidInputException();
}
this.profileKey = profileKey.clone();
}
public ProfileKeyVersion getProfileKeyVersion() {
throw new AssertionError();
}
public ProfileKeyCommitment getCommitment() {
throw new AssertionError();
}
public byte[] serialize() {
return this.profileKey.clone();
}
@Override
public boolean equals(Object o) {
if(o == null || o.getClass() != getClass()) return false;
ProfileKey other = (ProfileKey) o;
return Arrays.equals(profileKey, other.profileKey);
}
@Override
public int hashCode() {
return Arrays.hashCode(profileKey);
}
}

View File

@ -0,0 +1,10 @@
package org.signal.zkgroup.profiles;
public final class ProfileKeyCommitment {
private ProfileKeyCommitment() {
}
public byte[] serialize() {
throw new AssertionError();
}
}

View File

@ -0,0 +1,13 @@
package org.signal.zkgroup.profiles;
import org.signal.zkgroup.InvalidInputException;
public final class ProfileKeyCredential {
public ProfileKeyCredential(byte[] bytes) throws InvalidInputException {
throw new AssertionError();
}
public byte[] serialize() {
throw new AssertionError();
}
}

View File

@ -0,0 +1,10 @@
package org.signal.zkgroup.profiles;
public final class ProfileKeyCredentialRequest {
private ProfileKeyCredentialRequest() {
}
public byte[] serialize() {
throw new AssertionError();
}
}

View File

@ -0,0 +1,10 @@
package org.signal.zkgroup.profiles;
public final class ProfileKeyCredentialRequestContext {
private ProfileKeyCredentialRequestContext() {
}
public ProfileKeyCredentialRequest getRequest() {
throw new AssertionError();
}
}

View File

@ -0,0 +1,9 @@
package org.signal.zkgroup.profiles;
import org.signal.zkgroup.InvalidInputException;
public final class ProfileKeyCredentialResponse {
public ProfileKeyCredentialResponse(byte[] bytes) throws InvalidInputException {
throw new AssertionError();
}
}

View File

@ -0,0 +1,10 @@
package org.signal.zkgroup.profiles;
public final class ProfileKeyVersion {
private ProfileKeyVersion() {
}
public String serialize() {
throw new AssertionError();
}
}

View File

@ -7,3 +7,6 @@ project(':libsignal-service').projectDir = file('libsignal/service')
project(':').buildFileName = 'main.gradle'
rootProject.name='Signal'
include ':zkgroups'
project(':zkgroups').projectDir = file('libsignal/zkgroups-api')