From 7ecb50a3fee53842879a2f4119181842238fdb4e Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Mon, 10 Feb 2020 18:40:22 -0500 Subject: [PATCH] Versioned Profiles support (disabled). --- app/build.gradle | 2 + .../contacts/sync/DirectoryHelperV1.java | 29 +-- .../contacts/sync/StorageSyncHelper.java | 17 +- .../securesms/crypto/ProfileKeyUtil.java | 60 +++++- .../crypto/UnidentifiedAccessUtil.java | 11 +- .../securesms/database/RecipientDatabase.java | 173 +++++++++++++----- .../database/helpers/SQLCipherOpenHelper.java | 7 +- .../jobs/MultiDeviceContactUpdateJob.java | 8 +- .../jobs/MultiDeviceProfileKeyUpdateJob.java | 8 +- .../securesms/jobs/ProfileUploadJob.java | 48 ++--- .../securesms/jobs/PushProcessMessageJob.java | 19 +- .../securesms/jobs/RefreshAttributesJob.java | 4 +- .../securesms/jobs/RefreshOwnProfileJob.java | 32 +++- .../jobs/RetrieveProfileAvatarJob.java | 11 +- .../securesms/jobs/RetrieveProfileJob.java | 40 +++- .../securesms/jobs/RotateProfileKeyJob.java | 36 ++-- .../securesms/profiles/AvatarHelper.java | 26 +++ .../profiles/edit/EditProfileRepository.java | 23 +-- .../push/SignalServiceNetworkAccess.java | 23 ++- .../securesms/recipients/Recipient.java | 11 ++ .../recipients/RecipientDetails.java | 3 + .../service/CodeVerificationRequest.java | 21 ++- .../securesms/util/FeatureFlags.java | 3 + .../securesms/util/ProfileUtil.java | 41 +++-- .../thoughtcrime/securesms/util/SqlUtil.java | 28 ++- .../contacts/sync/StorageSyncHelperTest.java | 79 +++++++- .../{database => util}/SqlUtilTest.java | 36 ++-- libsignal/service/build.gradle | 2 + .../signalservice/FeatureFlags.java | 13 ++ .../api/SignalServiceAccountManager.java | 47 ++++- .../api/SignalServiceMessagePipe.java | 65 +++++-- .../api/SignalServiceMessageReceiver.java | 32 +++- .../api/crypto/ProfileCipher.java | 9 +- .../api/crypto/ProfileCipherInputStream.java | 5 +- .../api/crypto/ProfileCipherOutputStream.java | 6 +- .../api/crypto/UnidentifiedAccess.java | 5 +- .../messages/multidevice/DeviceContact.java | 9 +- .../DeviceContactsInputStream.java | 10 +- .../DeviceContactsOutputStream.java | 2 +- .../api/profiles/ProfileAndCredential.java | 32 ++++ .../api/profiles/SignalServiceProfile.java | 39 ++++ .../profiles/SignalServiceProfileWrite.java | 34 ++++ .../api/storage/SignalContactRecord.java | 21 ++- .../api/storage/SignalStorageModels.java | 4 +- .../signalservice/api/util/OptionalUtil.java | 27 +++ .../signalservice/api/util/StreamDetails.java | 16 +- .../SignalServiceConfiguration.java | 12 +- .../internal/groupsv2/ClientZkOperations.java | 17 ++ .../push/ProfileAvatarUploadAttributes.java | 6 - .../internal/push/PushServiceSocket.java | 81 ++++++++ .../ProfileCipherOutputStreamFactory.java | 5 +- .../api/crypto/ProfileCipherTest.java | 10 +- .../api/crypto/UnidentifiedAccessTest.java | 6 +- .../api/util/OptionalUtilTest.java | 53 ++++++ libsignal/zkgroups-api/build.gradle | 3 + .../signal/zkgroup/InvalidInputException.java | 6 + .../signal/zkgroup/ServerPublicParams.java | 6 + .../zkgroup/VerificationFailedException.java | 4 + .../profiles/ClientZkProfileOperations.java | 20 ++ .../signal/zkgroup/profiles/ProfileKey.java | 50 +++++ .../profiles/ProfileKeyCommitment.java | 10 + .../profiles/ProfileKeyCredential.java | 13 ++ .../profiles/ProfileKeyCredentialRequest.java | 10 + .../ProfileKeyCredentialRequestContext.java | 10 + .../ProfileKeyCredentialResponse.java | 9 + .../zkgroup/profiles/ProfileKeyVersion.java | 10 + settings.gradle | 3 + 67 files changed, 1200 insertions(+), 321 deletions(-) rename app/src/test/java/org/thoughtcrime/securesms/{database => util}/SqlUtilTest.java (60%) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/FeatureFlags.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/ProfileAndCredential.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfileWrite.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/ClientZkOperations.java create mode 100644 libsignal/service/src/test/java/org/whispersystems/signalservice/api/util/OptionalUtilTest.java create mode 100644 libsignal/zkgroups-api/build.gradle create mode 100644 libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/InvalidInputException.java create mode 100644 libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/ServerPublicParams.java create mode 100644 libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/VerificationFailedException.java create mode 100644 libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ClientZkProfileOperations.java create mode 100644 libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKey.java create mode 100644 libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCommitment.java create mode 100644 libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredential.java create mode 100644 libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredentialRequest.java create mode 100644 libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredentialRequestContext.java create mode 100644 libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredentialResponse.java create mode 100644 libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyVersion.java diff --git a/app/build.gradle b/app/build.gradle index 24d8b290593..cc78bf6d03f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelperV1.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelperV1.java index e721ff6d174..dae0ad7d4fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelperV1.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelperV1.java @@ -23,7 +23,6 @@ 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.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 @@ private static boolean isValidContactNumber(@Nullable String number) { } private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException { - Optional 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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StorageSyncHelper.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StorageSyncHelper.java index efcc1ce6c83..34beb68a731 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StorageSyncHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/StorageSyncHelper.java @@ -18,6 +18,7 @@ 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 static final class ContactUpdate { 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 @@ SignalContactRecord getNewContact() { 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 static class LocalWriteResult { private final WriteOperationResult writeResult; private final Map storageKeyUpdates; - public LocalWriteResult(WriteOperationResult writeResult, Map storageKeyUpdates) { + private LocalWriteResult(WriteOperationResult writeResult, Map storageKeyUpdates) { this.writeResult = writeResult; this.storageKeyUpdates = storageKeyUpdates; } @@ -510,17 +515,17 @@ public LocalWriteResult(WriteOperationResult writeResult, Map localInserts; - final Set localUpdates; + final Set localUpdates; final Set remoteInserts; - final Set remoteUpdates; + final Set remoteUpdates; ContactRecordMergeResult(@NonNull Set localInserts, @NonNull Set localUpdates, @NonNull Set remoteInserts, @NonNull Set remoteUpdates) { - this.localInserts = localInserts; - this.localUpdates = localUpdates; + this.localInserts = localInserts; + this.localUpdates = localUpdates; this.remoteInserts = remoteInserts; this.remoteUpdates = remoteUpdates; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java index 9a4ede3c5a3..66d14153543 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java @@ -3,17 +3,27 @@ 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 @@ private 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 profileKeyOptional(@Nullable byte[] profileKey) { + return Optional.fromNullable(profileKeyOrNull(profileKey)); + } + + public static @NonNull Optional 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); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java index b9642325904..4ac03c9ae9a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java @@ -8,6 +8,7 @@ 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 static Optional getAccessFor(@NonNull Context con { 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 static Optional getAccessFor(@NonNull Context con public static Optional 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 static Optional getAccessForSync(@NonNull Context } } - 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: diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index 681b13cded5..b7403a38913 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -14,6 +14,8 @@ 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.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 @@ int getId() { 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 void applyStorageSyncUpdates(@NonNull Collection ins RecipientId recipientId = getByStorageKeyOrThrow(update.getNewContact().getKey()); + if (update.profileKeyChanged()) { + clearProfileKeyCredential(recipientId); + } + try { Optional oldIdentityRecord = identityDatabase.getIdentity(recipientId); IdentityKey identityKey = update.getNewContact().getIdentityKey().isPresent() ? new IdentityKey(update.getNewContact().getIdentityKey().get(), 0) : null; @@ -562,42 +570,44 @@ public Map getAllStorageSyncKeysMap() { } @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 Map getAllStorageSyncKeysMap() { 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 Map getAllStorageSyncKeysMap() { 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 void setUuidSupported(@NonNull RecipientId id, boolean supported) { Recipient.live(id).refresh(); } - public void setProfileKey(@NonNull RecipientId id, @Nullable byte[] profileKey) { + /** + * Updates the profile key. + *

+ * 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 @@ void markDirty(@NonNull RecipientId recipientId, @NonNull DirtyState dirtyState) * 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 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. + *

+ * 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 getByColumn(@NonNull String column, String value) { @@ -1374,6 +1450,7 @@ public static class RecipientSettings { 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 static class RecipientSettings { 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 static class RecipientSettings { 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 RegisteredState getRegistered() { return profileKey; } + public @Nullable byte[] getProfileKeyCredential() { + return profileKeyCredential; + } + public @Nullable String getSystemDisplayName() { return systemDisplayName; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 24a487136b0..969bed12829 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -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 void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } } + if (oldVersion < PROFILE_KEY_CREDENTIALS) { + db.execSQL("ALTER TABLE recipient ADD COLUMN profile_key_credential TEXT DEFAULT NULL"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java index 828402a71a5..acaadbd6fd5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java @@ -8,7 +8,9 @@ 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 @@ private void generateSingleContactUpdate(@NonNull RecipientId recipientId) 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 @@ private void generateFullContactUpdate() Optional verified = getVerifiedMessage(recipient, identity); Optional name = Optional.fromNullable(recipient.getName(context)); Optional color = Optional.of(recipient.getColor().serialize()); - Optional profileKey = Optional.fromNullable(recipient.getProfileKey()); + Optional profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()); boolean blocked = recipient.isBlocked(); Optional expireTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent(); Optional inboxPosition = Optional.fromNullable(inboxPositions.get(recipient.getId())); @@ -208,7 +210,7 @@ private void generateFullContactUpdate() 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())), diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java index f2007d83d6e..24103f8a130 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java @@ -3,14 +3,14 @@ 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 void onRun() throws IOException, UntrustedIdentityException { return; } - Optional profileKey = Optional.of(ProfileKeyUtil.getProfileKey(context)); + Optional profileKey = Optional.of(ProfileKeyUtil.getSelfProfileKey()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); DeviceContactsOutputStream out = new DeviceContactsOutputStream(baos); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java index d5de6419609..c6d86277135 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java @@ -4,6 +4,7 @@ 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.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 @@ private ProfileUploadJob(@NonNull Parameters parameters) { @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 @@ protected boolean onShouldRetry(@NonNull Exception e) { 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index a4e07a2878c..137fd724de0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -12,6 +12,7 @@ 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.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.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 @@ private void handleMessage(@NonNull byte[] plaintextDataBuffer, @NonNull Optiona 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 @@ private void handleDuplicateMessage(@NonNull String sender, int senderDeviceId, } 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)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index 86eda9fb62d..687c21ea873 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -3,7 +3,6 @@ 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.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 @@ private RefreshAttributesJob(@NonNull Job.Parameters parameters) { 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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index 3bd44b508a3..42e50b403ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -3,8 +3,11 @@ 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.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 @@ private RefreshOwnProfileJob(@NonNull Parameters parameters) { @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 = 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 void onFailure() { } 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 @@ private void setProfileName(@Nullable String encryptedName) { } } - private void setProfileAvatar(@Nullable String avatar) { + private static void setProfileAvatar(@Nullable String avatar) { ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), avatar)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java index 1c61fdbbfba..6790399dffe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -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 @@ private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Re @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!"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index 21d3bb3c2f8..6058e6092be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -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.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 @@ private void handleIndividualRecipient(Recipient recipient) throws IOException { } 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 @@ private void handlePhoneNumberRecipient(Recipient recipient) throws IOException setProfileCapabilities(recipient, profile.getCapabilities()); setIdentityKey(recipient, profile.getIdentityKey()); setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess()); + + if (recipientProfileKey != null) { + Optional 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 @@ private void setIdentityKey(Recipient recipient, String identityKeyValue) { 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 @@ private void setUnidentifiedAccessMode(Recipient recipient, String unidentifiedA 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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java index aed70f6b77d..1c3a93b20a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java @@ -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.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 @@ private RotateProfileKeyJob(@NonNull Job.Parameters parameters) { 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 @@ protected boolean onShouldRetry(@NonNull Exception exception) { 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 { @Override public @NonNull RotateProfileKeyJob create(@NonNull Parameters parameters, @NonNull Data data) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java index e21dcb9a343..93db338add0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java @@ -2,15 +2,21 @@ 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 static void setAvatar(@NonNull Context context, @NonNull RecipientId reci 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; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java index 0b42ea9da98..1d12d211085 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java @@ -20,17 +20,14 @@ 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 @@ void getCurrentUsername(@NonNull Consumer> callback) { @WorkerThread private @NonNull Optional 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 @@ void getCurrentUsername(@NonNull Consumer> callback) { 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java index 42ebda94880..7216c7b17cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java @@ -7,6 +7,7 @@ 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.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 SignalServiceNetworkAccess(Context context) { final SignalStorageUrl qatarGoogleStorage = new SignalStorageUrl("https://www.google.com.qa/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); final List 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() {{ put(COUNTRY_CODE_EGYPT, new SignalServiceConfiguration(new SignalServiceUrl[] {egyptGoogleService, baseGoogleService, baseAndroidService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, @@ -152,21 +160,24 @@ public SignalServiceNetworkAccess(Context context) { 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 SignalServiceNetworkAccess(Context context) { 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 SignalServiceNetworkAccess(Context context) { 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]); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 900cf31f21b..7acc15ee558 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -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 boolean isUuidSupported() { return profileKey; } + public @Nullable byte[] getProfileKeyCredential() { + return profileKeyCredential; + } + + public boolean hasProfileKeyCredential() { + return profileKeyCredential != null; + } + public @Nullable byte[] getStorageServiceKey() { return storageKey; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index a48eded9c08..33abadf253b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -47,6 +47,7 @@ public class RecipientDetails { final Optional 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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java index b19005e3c83..7bd5e857802 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java @@ -6,10 +6,11 @@ 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.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.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 @@ private static void verifyAccount(@NonNull Context context, @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 @@ private static void verifyAccount(@NonNull Context context, } } - 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 recipient = recipientDatabase.getByE164(e164number); if (recipient.isPresent()) { - return Recipient.resolved(recipient.get()).getProfileKey(); + return ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).getProfileKey()); } return null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 4f6c3b2153e..a6b7f92cc78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -312,4 +312,7 @@ static final class UpdateResult { return disk; } } + + /** Read and write versioned profile information. */ + public static final boolean VERSIONED_PROFILES = org.whispersystems.signalservice.FeatureFlags.VERSIONED_PROFILES; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java index e90221e0d88..800a3f0c2a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java @@ -6,6 +6,9 @@ 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.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 @@ /** * 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 = getUnidentifiedAccess(context, recipient); + Optional 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 static SignalServiceProfile retrieveProfile(@NonNull Context context, @No 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 static SignalServiceProfile retrieveProfile(@NonNull Context context, @No } @WorkerThread - private static SignalServiceProfile retrieveProfileInternal(@NonNull SignalServiceAddress address, Optional unidentifiedAccess) - throws IOException + private static @NonNull ProfileAndCredential retrieveProfileInternal(@NonNull SignalServiceAddress address, + @NonNull Optional profileKey, + @NonNull Optional unidentifiedAccess, + @NonNull SignalServiceProfile.RequestType requestType) + throws IOException { SignalServiceMessagePipe authPipe = IncomingMessageObserver.getPipe(); SignalServiceMessagePipe unidentifiedPipe = IncomingMessageObserver.getUnidentifiedPipe(); @@ -74,14 +89,18 @@ private static SignalServiceProfile retrieveProfileInternal(@NonNull SignalServi 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 getUnidentifiedAccess(@NonNull Context context, @NonNull Recipient recipient) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SqlUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SqlUtil.java index 5dad5cc49d0..9a2d20e1b60 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SqlUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SqlUtil.java @@ -7,8 +7,6 @@ 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 static boolean columnExists(@NonNull SQLiteDatabase db, @NonNull String t * change. In other words, if {@link SQLiteDatabase#update(String, ContentValues, String, String[])} * returns > 0, then you know something *actually* changed. */ - public static @NonNull Pair 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> valueSet = contentValues.valueSet(); @@ -73,6 +71,24 @@ public static boolean columnExists(@NonNull SQLiteDatabase db, @NonNull String t 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; + } } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/contacts/sync/StorageSyncHelperTest.java b/app/src/test/java/org/thoughtcrime/securesms/contacts/sync/StorageSyncHelperTest.java index 45bc2e3a2c3..4c5bce3105b 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/contacts/sync/StorageSyncHelperTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/contacts/sync/StorageSyncHelperTest.java @@ -3,6 +3,7 @@ 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.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 void createWriteOperation_generic() { assertByteListEquals(byteListOf(1), result.getDeletes()); } - private static Set setOf(E... vals) { - return new LinkedHashSet(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 Set setOf(E... values) { + return Sets.newHashSet(values); } private static Set recordSetOf(SignalContactRecord... contactRecords) { @@ -267,14 +323,21 @@ private static Set recordSetOf(SignalContactRecord... conta 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) { diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/SqlUtilTest.java b/app/src/test/java/org/thoughtcrime/securesms/util/SqlUtilTest.java similarity index 60% rename from app/src/test/java/org/thoughtcrime/securesms/database/SqlUtilTest.java rename to app/src/test/java/org/thoughtcrime/securesms/util/SqlUtilTest.java index 80abdea7a3a..c4b266e20c5 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/SqlUtilTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/util/SqlUtilTest.java @@ -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.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 void buildTrueUpdateQuery_simple() { ContentValues values = new ContentValues(); values.put("a", 2); - Pair 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 void buildTrueUpdateQuery_complexSelection() { ContentValues values = new ContentValues(); values.put("a", 4); - Pair 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 void buildTrueUpdateQuery_multipleContentValues() { values.put("b", 3); values.put("c", 4); - Pair 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 void buildTrueUpdateQuery_nullContentValue() { ContentValues values = new ContentValues(); values.put("a", (String) null); - Pair 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 void buildTrueUpdateQuery_complexContentValue() { values.put("d", (String) null); values.put("e", (String) null); - Pair 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()); } } diff --git a/libsignal/service/build.gradle b/libsignal/service/build.gradle index dc4985244cd..a534ef689cb 100644 --- a/libsignal/service/build.gradle +++ b/libsignal/service/build.gradle @@ -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' diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/FeatureFlags.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/FeatureFlags.java new file mode 100644 index 00000000000..0d2e4fa1d94 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/FeatureFlags.java @@ -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; +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 55d6fd245b4..e38cabe4ba9 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -9,6 +9,7 @@ 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.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.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 TurnServerInfo getTurnServerInfo() throws IOException { 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 void setProfileAvatar(byte[] key, StreamDetails avatar) 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); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java index a2a67226c55..67798d4c83f 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java @@ -8,11 +8,21 @@ 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.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; + private final ClientZkProfileOperations clientZkProfile; - SignalServiceMessagePipe(WebSocketConnection websocket, Optional credentialsProvider) { + SignalServiceMessagePipe(WebSocketConnection websocket, + Optional credentialsProvider, + ClientZkProfileOperations clientZkProfile) + { this.websocket = websocket; this.credentialsProvider = credentialsProvider; + this.clientZkProfile = clientZkProfile; this.websocket.connect(); } @@ -149,7 +164,12 @@ public SendMessageResponse send(OutgoingPushMessageList list, Optional unidentifiedAccess) throws IOException { + public ProfileAndCredential getProfile(SignalServiceAddress address, + Optional profileKey, + Optional unidentifiedAccess, + SignalServiceProfile.RequestType requestType) + throws IOException + { try { List headers = new LinkedList<>(); @@ -157,12 +177,30 @@ public SignalServiceProfile getProfile(SignalServiceAddress address, Optional 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 response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS); @@ -170,8 +208,13 @@ public SignalServiceProfile getProfile(SignalServiceAddress address, Optional unidentifiedAccess) - throws IOException + public ProfileAndCredential retrieveProfile(SignalServiceAddress address, + Optional profileKey, + Optional unidentifiedAccess, + SignalServiceProfile.RequestType requestType) + throws IOException, VerificationFailedException { - return socket.retrieveProfile(address, unidentifiedAccess); + Optional 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.absent()); + } } public SignalServiceProfile retrieveProfileByUsername(String username, Optional unidentifiedAccess) @@ -122,7 +142,7 @@ public SignalServiceProfile retrieveProfileByUsername(String username, Optional< 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 SignalServiceMessagePipe createMessagePipe() { 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 SignalServiceMessagePipe createUnidentifiedMessagePipe() { sleepTimer, urls.getNetworkInterceptors()); - return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider)); + return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile); } public List retrieveMessages() throws IOException { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java index db3c9c6b5fc..2e16aecbf91 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipher.java @@ -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 byte[] encryptName(byte[] input, int paddedLength) { 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 byte[] decryptName(byte[] input) throws InvalidCiphertextException { 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; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipherInputStream.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipherInputStream.java index 1c8f0d097d2..8798297f29a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipherInputStream.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipherInputStream.java @@ -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 ProfileCipherInputStream(InputStream in, byte[] key) throws IOException { 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) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipherOutputStream.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipherOutputStream.java index 39bb8bc088b..bd3d1c0de39 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipherOutputStream.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ProfileCipherOutputStream.java @@ -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) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccess.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccess.java index 7ae5c23edb6..41381c0fd8a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccess.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccess.java @@ -3,6 +3,7 @@ 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 SenderCertificate getUnidentifiedCertificate() { 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); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContact.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContact.java index 5412b318ba9..f391dcb2b2c 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContact.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContact.java @@ -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 avatar; private final Optional color; private final Optional verified; - private final Optional profileKey; + private final Optional profileKey; private final boolean blocked; private final Optional expirationTimer; - private final Optional inboxPosition; + private final Optional inboxPosition; private final boolean archived; public DeviceContact(SignalServiceAddress address, @@ -28,7 +29,7 @@ public DeviceContact(SignalServiceAddress address, Optional avatar, Optional color, Optional verified, - Optional profileKey, + Optional profileKey, boolean blocked, Optional expirationTimer, Optional inboxPosition, @@ -66,7 +67,7 @@ public Optional getVerified() { return verified; } - public Optional getProfileKey() { + public Optional getProfileKey() { return profileKey; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContactsInputStream.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContactsInputStream.java index b4470fcfdae..bfd92351275 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContactsInputStream.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContactsInputStream.java @@ -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 DeviceContact read() throws IOException { Optional avatar = Optional.absent(); Optional color = details.hasColor() ? Optional.of(details.getColor()) : Optional.absent(); Optional verified = Optional.absent(); - Optional profileKey = Optional.absent(); + Optional profileKey = Optional.absent(); boolean blocked = false; Optional expireTimer = Optional.absent(); Optional inboxPosition = Optional.absent(); @@ -84,7 +86,11 @@ public DeviceContact read() throws IOException { } 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) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContactsOutputStream.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContactsOutputStream.java index 9b4e35729db..3166e9af4a9 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContactsOutputStream.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceContactsOutputStream.java @@ -85,7 +85,7 @@ private void writeContactDetails(DeviceContact contact) throws IOException { } if (contact.getProfileKey().isPresent()) { - contactDetails.setProfileKey(ByteString.copyFrom(contact.getProfileKey().get())); + contactDetails.setProfileKey(ByteString.copyFrom(contact.getProfileKey().get().serialize())); } if (contact.getExpirationTimer().isPresent()) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/ProfileAndCredential.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/ProfileAndCredential.java new file mode 100644 index 00000000000..7af2b64bac5 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/ProfileAndCredential.java @@ -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; + + public ProfileAndCredential(SignalServiceProfile profile, + SignalServiceProfile.RequestType requestType, + Optional profileKeyCredential) + { + this.profile = profile; + this.requestType = requestType; + this.profileKeyCredential = profileKeyCredential; + } + + public SignalServiceProfile getProfile() { + return profile; + } + + public SignalServiceProfile.RequestType getRequestType() { + return requestType; + } + + public Optional getProfileKeyCredential() { + return profileKeyCredential; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java index 2ec4a131221..4acfaee3bd1 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java @@ -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 UUID getUuid() { 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 boolean isUuid() { 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; + } + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfileWrite.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfileWrite.java new file mode 100644 index 00000000000..57ded06a5ad --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfileWrite.java @@ -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; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java index c6d6cb10382..bce42051ae6 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java @@ -2,11 +2,12 @@ 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 nickname; - private final int protoVersion; + private final int protoVersion; private SignalContactRecord(byte[] key, SignalServiceAddress address, @@ -42,7 +43,7 @@ private SignalContactRecord(byte[] key, 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 boolean equals(Object o) { 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 Builder setProfileName(String profileName) { } public Builder setProfileKey(byte[] profileKey) { - this.profileKey= profileKey; + this.profileKey = profileKey; return this; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.java index 15807dbbeae..bdb6517db3f 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.java @@ -46,8 +46,8 @@ public static StorageItem localToRemoteStorageRecord(SignalStorageRecord record, } 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()); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.java new file mode 100644 index 00000000000..3c959d222c8 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.java @@ -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 a, Optional 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 bytes) { + return bytes.isPresent() ? Arrays.hashCode(bytes.get()) : 0; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/StreamDetails.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/StreamDetails.java index 89f16d83b7e..10595ae605b 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/StreamDetails.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/StreamDetails.java @@ -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 String getContentType() { public long getLength() { return length; } + + @Override + public void close() { + try { + stream.close(); + } catch (IOException e) { + Log.w(TAG, e); + } + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java index de6443ff136..97a1cbba445 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java @@ -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 networkInterceptors; + private final byte[] zkGroupServerPublicParams; public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls, SignalCdnUrl[] signalCdnUrls, SignalContactDiscoveryUrl[] signalContactDiscoveryUrls, SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls, SignalStorageUrl[] signalStorageUrls, - List networkInterceptors) + List networkInterceptors, + byte[] zkGroupServerPublicParams) { this.signalServiceUrls = signalServiceUrls; this.signalCdnUrls = signalCdnUrls; @@ -27,6 +28,7 @@ public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls, this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls; this.signalStorageUrls = signalStorageUrls; this.networkInterceptors = networkInterceptors; + this.zkGroupServerPublicParams = zkGroupServerPublicParams; } public SignalServiceUrl[] getSignalServiceUrls() { @@ -52,4 +54,8 @@ public SignalStorageUrl[] getSignalStorageUrls() { public List getNetworkInterceptors() { return networkInterceptors; } + + public byte[] getZkGroupServerPublicParams() { + return zkGroupServerPublicParams; + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/ClientZkOperations.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/ClientZkOperations.java new file mode 100644 index 00000000000..abe9cddd823 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/ClientZkOperations.java @@ -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; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ProfileAvatarUploadAttributes.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ProfileAvatarUploadAttributes.java index b5f52f831c9..9a215beb48f 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ProfileAvatarUploadAttributes.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/ProfileAvatarUploadAttributes.java @@ -4,8 +4,6 @@ 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; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index baeaac3c454..1abc55d12ad 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -9,6 +9,13 @@ 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.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.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 PushServiceSocket(SignalServiceConfiguration signalServiceConfiguration, 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 captchaToken, Optional challenge) throws IOException { @@ -543,6 +556,37 @@ public SignalServiceProfile retrieveProfileByUsername(String username, Optional< } } + public ProfileAndCredential retrieveProfile(UUID target, ProfileKey profileKey, Optional 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 void retrieveProfileAvatar(String path, File destination, int maxSizeByte } 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 void setProfileAvatar(ProfileAvatarData profileAvatar) } } + 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 diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/ProfileCipherOutputStreamFactory.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/ProfileCipherOutputStreamFactory.java index bfc79e12d32..ae0234339fa 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/ProfileCipherOutputStreamFactory.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/ProfileCipherOutputStreamFactory.java @@ -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 @@ public class ProfileCipherOutputStreamFactory implements OutputStreamFactory { - private final byte[] key; + private final ProfileKey key; - public ProfileCipherOutputStreamFactory(byte[] key) { + public ProfileCipherOutputStreamFactory(ProfileKey key) { this.key = key; } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/ProfileCipherTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/ProfileCipherTest.java index b77b64d1670..62c7b0f3ef1 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/ProfileCipherTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/ProfileCipherTest.java @@ -4,6 +4,8 @@ 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 void testEncryptDecrypt() throws InvalidCiphertextException { } 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 void testEmpty() throws Exception { } 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); diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccessTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccessTest.java index d611640feca..23e2f27d42d 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccessTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/crypto/UnidentifiedAccessTest.java @@ -3,6 +3,8 @@ 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)); } diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/util/OptionalUtilTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/util/OptionalUtilTest.java new file mode 100644 index 00000000000..f5830680997 --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/util/OptionalUtilTest.java @@ -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.absent(), Optional.absent())); + } + + @Test + public void first_non_absent_not_equal() { + assertFalse(OptionalUtil.byteArrayEquals(Optional.of(new byte[1]), Optional.absent())); + } + + @Test + public void second_non_absent_not_equal() { + assertFalse(OptionalUtil.byteArrayEquals(Optional.absent(), Optional.of(new byte[1]))); + } + + @Test + public void equal_contents() { + byte[] contentsA = new byte[]{1, 2, 3}; + byte[] contentsB = contentsA.clone(); + Optional a = Optional.of(contentsA); + Optional 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 a = Optional.of(contentsA); + Optional 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.absent())); + } + +} \ No newline at end of file diff --git a/libsignal/zkgroups-api/build.gradle b/libsignal/zkgroups-api/build.gradle new file mode 100644 index 00000000000..2ad904aa5a3 --- /dev/null +++ b/libsignal/zkgroups-api/build.gradle @@ -0,0 +1,3 @@ +apply plugin: 'java-library' + +sourceCompatibility = 1.7 diff --git a/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/InvalidInputException.java b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/InvalidInputException.java new file mode 100644 index 00000000000..7e048c65def --- /dev/null +++ b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/InvalidInputException.java @@ -0,0 +1,6 @@ +package org.signal.zkgroup; + +public final class InvalidInputException extends Exception { + public InvalidInputException() { + } +} diff --git a/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/ServerPublicParams.java b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/ServerPublicParams.java new file mode 100644 index 00000000000..da91238f7bf --- /dev/null +++ b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/ServerPublicParams.java @@ -0,0 +1,6 @@ +package org.signal.zkgroup; + +public final class ServerPublicParams { + public ServerPublicParams(byte[] zkGroupServerPublicParams) { + } +} diff --git a/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/VerificationFailedException.java b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/VerificationFailedException.java new file mode 100644 index 00000000000..b1e2d8df95c --- /dev/null +++ b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/VerificationFailedException.java @@ -0,0 +1,4 @@ +package org.signal.zkgroup; + +public final class VerificationFailedException extends Exception { +} diff --git a/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ClientZkProfileOperations.java b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ClientZkProfileOperations.java new file mode 100644 index 00000000000..68f27bcb661 --- /dev/null +++ b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ClientZkProfileOperations.java @@ -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(); + } +} diff --git a/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKey.java b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKey.java new file mode 100644 index 00000000000..27ee493a736 --- /dev/null +++ b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKey.java @@ -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); + } +} diff --git a/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCommitment.java b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCommitment.java new file mode 100644 index 00000000000..cd66a275132 --- /dev/null +++ b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCommitment.java @@ -0,0 +1,10 @@ +package org.signal.zkgroup.profiles; + +public final class ProfileKeyCommitment { + private ProfileKeyCommitment() { + } + + public byte[] serialize() { + throw new AssertionError(); + } +} diff --git a/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredential.java b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredential.java new file mode 100644 index 00000000000..ce40019cbe0 --- /dev/null +++ b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredential.java @@ -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(); + } +} diff --git a/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredentialRequest.java b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredentialRequest.java new file mode 100644 index 00000000000..3729e42b807 --- /dev/null +++ b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredentialRequest.java @@ -0,0 +1,10 @@ +package org.signal.zkgroup.profiles; + +public final class ProfileKeyCredentialRequest { + private ProfileKeyCredentialRequest() { + } + + public byte[] serialize() { + throw new AssertionError(); + } +} diff --git a/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredentialRequestContext.java b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredentialRequestContext.java new file mode 100644 index 00000000000..d3fe0260efc --- /dev/null +++ b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredentialRequestContext.java @@ -0,0 +1,10 @@ +package org.signal.zkgroup.profiles; + +public final class ProfileKeyCredentialRequestContext { + private ProfileKeyCredentialRequestContext() { + } + + public ProfileKeyCredentialRequest getRequest() { + throw new AssertionError(); + } +} diff --git a/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredentialResponse.java b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredentialResponse.java new file mode 100644 index 00000000000..a4c5af8f16c --- /dev/null +++ b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyCredentialResponse.java @@ -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(); + } +} diff --git a/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyVersion.java b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyVersion.java new file mode 100644 index 00000000000..19ffe25637a --- /dev/null +++ b/libsignal/zkgroups-api/src/main/java/org/signal/zkgroup/profiles/ProfileKeyVersion.java @@ -0,0 +1,10 @@ +package org.signal.zkgroup.profiles; + +public final class ProfileKeyVersion { + private ProfileKeyVersion() { + } + + public String serialize() { + throw new AssertionError(); + } +} diff --git a/settings.gradle b/settings.gradle index 20d98cbcf61..fa39af07073 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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')