Versioned Profiles support (disabled).
This commit is contained in:
parent
f10d1eac61
commit
7ecb50a3fe
67 changed files with 1200 additions and 321 deletions
|
@ -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
|
||||
|
|
|
@ -23,7 +23,6 @@ import com.annimon.stream.Stream;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.thoughtcrime.securesms.crypto.SessionUtil;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
|
@ -37,19 +36,16 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
|||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Calendar;
|
||||
|
@ -343,25 +339,8 @@ class DirectoryHelperV1 {
|
|||
}
|
||||
|
||||
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
|
||||
SignalServiceMessagePipe authPipe = IncomingMessageObserver.getPipe();
|
||||
SignalServiceMessagePipe unidentifiedPipe = IncomingMessageObserver.getUnidentifiedPipe();
|
||||
SignalServiceMessagePipe pipe = unidentifiedPipe != null && unidentifiedAccess.isPresent() ? unidentifiedPipe : authPipe;
|
||||
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
|
||||
|
||||
if (pipe != null) {
|
||||
try {
|
||||
pipe.getProfile(address, unidentifiedAccess.get().getTargetUnidentifiedAccess());
|
||||
return true;
|
||||
} catch (NotFoundException e) {
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Websocket request failed. Falling back to REST.");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
ApplicationDependencies.getSignalServiceMessageReceiver().retrieveProfile(address, unidentifiedAccess.get().getTargetUnidentifiedAccess());
|
||||
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE);
|
||||
return true;
|
||||
} catch (NotFoundException e) {
|
||||
return false;
|
||||
|
|
|
@ -18,6 +18,7 @@ import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
|||
import org.whispersystems.signalservice.api.storage.SignalContactRecord.IdentityState;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
|
@ -362,7 +363,7 @@ public final class StorageSyncHelper {
|
|||
private final SignalContactRecord oldContact;
|
||||
private final SignalContactRecord newContact;
|
||||
|
||||
public ContactUpdate(@NonNull SignalContactRecord oldContact, @NonNull SignalContactRecord newContact) {
|
||||
ContactUpdate(@NonNull SignalContactRecord oldContact, @NonNull SignalContactRecord newContact) {
|
||||
this.oldContact = oldContact;
|
||||
this.newContact = newContact;
|
||||
}
|
||||
|
@ -377,6 +378,10 @@ public final class StorageSyncHelper {
|
|||
return newContact;
|
||||
}
|
||||
|
||||
public boolean profileKeyChanged() {
|
||||
return !OptionalUtil.byteArrayEquals(oldContact.getProfileKey(), newContact.getProfileKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -494,7 +499,7 @@ public final class StorageSyncHelper {
|
|||
private final WriteOperationResult writeResult;
|
||||
private final Map<RecipientId, byte[]> storageKeyUpdates;
|
||||
|
||||
public LocalWriteResult(WriteOperationResult writeResult, Map<RecipientId, byte[]> storageKeyUpdates) {
|
||||
private LocalWriteResult(WriteOperationResult writeResult, Map<RecipientId, byte[]> storageKeyUpdates) {
|
||||
this.writeResult = writeResult;
|
||||
this.storageKeyUpdates = storageKeyUpdates;
|
||||
}
|
||||
|
@ -510,17 +515,17 @@ public final class StorageSyncHelper {
|
|||
|
||||
private static final class ContactRecordMergeResult {
|
||||
final Set<SignalContactRecord> localInserts;
|
||||
final Set<ContactUpdate> localUpdates;
|
||||
final Set<ContactUpdate> localUpdates;
|
||||
final Set<SignalContactRecord> remoteInserts;
|
||||
final Set<ContactUpdate> remoteUpdates;
|
||||
final Set<ContactUpdate> remoteUpdates;
|
||||
|
||||
ContactRecordMergeResult(@NonNull Set<SignalContactRecord> localInserts,
|
||||
@NonNull Set<ContactUpdate> localUpdates,
|
||||
@NonNull Set<SignalContactRecord> remoteInserts,
|
||||
@NonNull Set<ContactUpdate> remoteUpdates)
|
||||
{
|
||||
this.localInserts = localInserts;
|
||||
this.localUpdates = localUpdates;
|
||||
this.localInserts = localInserts;
|
||||
this.localUpdates = localUpdates;
|
||||
this.remoteInserts = remoteInserts;
|
||||
this.remoteUpdates = remoteUpdates;
|
||||
}
|
||||
|
|
|
@ -3,17 +3,27 @@ package org.thoughtcrime.securesms.crypto;
|
|||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public final class ProfileKeyUtil {
|
||||
|
||||
private static final String TAG = Log.tag(ProfileKeyUtil.class);
|
||||
|
||||
private ProfileKeyUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Will inline later as part of Versioned profiles.
|
||||
*/
|
||||
/** @deprecated Use strongly typed {@link org.signal.zkgroup.profiles.ProfileKey}
|
||||
* from {@link #getSelfProfileKey()}
|
||||
* or {@code getSelfProfileKey().serialize()} if you need the bytes. */
|
||||
@Deprecated
|
||||
public static @NonNull byte[] getProfileKey(@NonNull Context context) {
|
||||
byte[] profileKey = Recipient.self().getProfileKey();
|
||||
|
@ -22,4 +32,48 @@ public final class ProfileKeyUtil {
|
|||
}
|
||||
return profileKey;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull ProfileKey getSelfProfileKey() {
|
||||
try {
|
||||
return new ProfileKey(Recipient.self().getProfileKey());
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable ProfileKey profileKeyOrNull(@Nullable byte[] profileKey) {
|
||||
if (profileKey != null) {
|
||||
try {
|
||||
return new ProfileKey(profileKey);
|
||||
} catch (InvalidInputException e) {
|
||||
Log.w(TAG, String.format(Locale.US, "Seen non-null profile key of wrong length %d", profileKey.length), e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static @NonNull ProfileKey profileKeyOrThrow(@NonNull byte[] profileKey) {
|
||||
try {
|
||||
return new ProfileKey(profileKey);
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull Optional<ProfileKey> profileKeyOptional(@Nullable byte[] profileKey) {
|
||||
return Optional.fromNullable(profileKeyOrNull(profileKey));
|
||||
}
|
||||
|
||||
public static @NonNull Optional<ProfileKey> profileKeyOptionalOrThrow(@NonNull byte[] profileKey) {
|
||||
return Optional.of(profileKeyOrThrow(profileKey));
|
||||
}
|
||||
|
||||
public static @NonNull ProfileKey createNew() {
|
||||
try {
|
||||
return new ProfileKey(Util.getSecretBytes(32));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import androidx.annotation.WorkerThread;
|
|||
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -42,7 +43,7 @@ public class UnidentifiedAccessUtil {
|
|||
{
|
||||
try {
|
||||
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
|
||||
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(ProfileKeyUtil.getProfileKey(context));
|
||||
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
|
||||
byte[] ourUnidentifiedAccessCertificate = recipient.resolve().isUuidSupported() && Recipient.self().isUuidSupported()
|
||||
? TextSecurePreferences.getUnidentifiedAccessCertificate(context)
|
||||
: TextSecurePreferences.getUnidentifiedAccessCertificateLegacy(context);
|
||||
|
@ -75,7 +76,7 @@ public class UnidentifiedAccessUtil {
|
|||
|
||||
public static Optional<UnidentifiedAccessPair> getAccessForSync(@NonNull Context context) {
|
||||
try {
|
||||
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(ProfileKeyUtil.getProfileKey(context));
|
||||
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
|
||||
byte[] ourUnidentifiedAccessCertificate = Recipient.self().isUuidSupported() ? TextSecurePreferences.getUnidentifiedAccessCertificate(context)
|
||||
: TextSecurePreferences.getUnidentifiedAccessCertificateLegacy(context);
|
||||
|
||||
|
@ -97,12 +98,8 @@ public class UnidentifiedAccessUtil {
|
|||
}
|
||||
}
|
||||
|
||||
public static @NonNull byte[] getSelfUnidentifiedAccessKey(@NonNull byte[] selfProfileKey) {
|
||||
return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKey);
|
||||
}
|
||||
|
||||
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
|
||||
byte[] theirProfileKey = recipient.resolve().getProfileKey();
|
||||
ProfileKey theirProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.resolve().getProfileKey());
|
||||
|
||||
switch (recipient.resolve().getUnidentifiedAccessMode()) {
|
||||
case UNKNOWN:
|
||||
|
|
|
@ -14,6 +14,8 @@ import com.google.android.gms.common.util.ArrayUtils;
|
|||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.sync.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
|
@ -31,7 +33,6 @@ import org.thoughtcrime.securesms.util.SqlUtil;
|
|||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
|
@ -80,6 +81,7 @@ public class RecipientDatabase extends Database {
|
|||
private static final String SYSTEM_CONTACT_URI = "system_contact_uri";
|
||||
private static final String SYSTEM_INFO_PENDING = "system_info_pending";
|
||||
private static final String PROFILE_KEY = "profile_key";
|
||||
private static final String PROFILE_KEY_CREDENTIAL = "profile_key_credential";
|
||||
private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar";
|
||||
private static final String PROFILE_SHARING = "profile_sharing";
|
||||
private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
|
||||
|
@ -100,7 +102,8 @@ public class RecipientDatabase extends Database {
|
|||
private static final String[] RECIPIENT_PROJECTION = new String[] {
|
||||
UUID, USERNAME, PHONE, EMAIL, GROUP_ID,
|
||||
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
|
||||
PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
|
||||
PROFILE_KEY, PROFILE_KEY_CREDENTIAL,
|
||||
SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
|
||||
PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
|
||||
UNIDENTIFIED_ACCESS_MODE,
|
||||
FORCE_SMS_SELECTION, UUID_SUPPORTED, STORAGE_SERVICE_KEY, DIRTY
|
||||
|
@ -242,6 +245,7 @@ public class RecipientDatabase extends Database {
|
|||
SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " +
|
||||
SYSTEM_INFO_PENDING + " INTEGER DEFAULT 0, " +
|
||||
PROFILE_KEY + " TEXT DEFAULT NULL, " +
|
||||
PROFILE_KEY_CREDENTIAL + " TEXT DEFAULT NULL, " +
|
||||
PROFILE_GIVEN_NAME + " TEXT DEFAULT NULL, " +
|
||||
PROFILE_FAMILY_NAME + " TEXT DEFAULT NULL, " +
|
||||
PROFILE_JOINED_NAME + " TEXT DEFAULT NULL, " +
|
||||
|
@ -428,6 +432,10 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
RecipientId recipientId = getByStorageKeyOrThrow(update.getNewContact().getKey());
|
||||
|
||||
if (update.profileKeyChanged()) {
|
||||
clearProfileKeyCredential(recipientId);
|
||||
}
|
||||
|
||||
try {
|
||||
Optional<IdentityRecord> oldIdentityRecord = identityDatabase.getIdentity(recipientId);
|
||||
IdentityKey identityKey = update.getNewContact().getIdentityKey().isPresent() ? new IdentityKey(update.getNewContact().getIdentityKey().get(), 0) : null;
|
||||
|
@ -562,42 +570,44 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
|
||||
@NonNull RecipientSettings getRecipientSettings(@NonNull Cursor cursor) {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
|
||||
UUID uuid = UuidUtil.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(UUID)));
|
||||
String username = cursor.getString(cursor.getColumnIndexOrThrow(USERNAME));
|
||||
String e164 = cursor.getString(cursor.getColumnIndexOrThrow(PHONE));
|
||||
String email = cursor.getString(cursor.getColumnIndexOrThrow(EMAIL));
|
||||
String groupId = cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID));
|
||||
boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKED)) == 1;
|
||||
String messageRingtone = cursor.getString(cursor.getColumnIndexOrThrow(MESSAGE_RINGTONE));
|
||||
String callRingtone = cursor.getString(cursor.getColumnIndexOrThrow(CALL_RINGTONE));
|
||||
int messageVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_VIBRATE));
|
||||
int callVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(CALL_VIBRATE));
|
||||
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
|
||||
String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR));
|
||||
int insightsBannerTier = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER));
|
||||
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
|
||||
int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_EXPIRATION_TIME));
|
||||
int registeredState = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED));
|
||||
String profileKeyString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY));
|
||||
String systemDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME));
|
||||
String systemContactPhoto = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHOTO_URI));
|
||||
String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
|
||||
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
|
||||
String profileGivenName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_GIVEN_NAME));
|
||||
String profileFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_FAMILY_NAME));
|
||||
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
|
||||
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
|
||||
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
|
||||
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
|
||||
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
|
||||
boolean uuidSupported = cursor.getInt(cursor.getColumnIndexOrThrow(UUID_SUPPORTED)) == 1;
|
||||
String storageKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_KEY));
|
||||
String identityKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
|
||||
int identityStatusRaw = cursor.getInt(cursor.getColumnIndexOrThrow(IDENTITY_STATUS));
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
|
||||
UUID uuid = UuidUtil.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(UUID)));
|
||||
String username = cursor.getString(cursor.getColumnIndexOrThrow(USERNAME));
|
||||
String e164 = cursor.getString(cursor.getColumnIndexOrThrow(PHONE));
|
||||
String email = cursor.getString(cursor.getColumnIndexOrThrow(EMAIL));
|
||||
String groupId = cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID));
|
||||
boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCKED)) == 1;
|
||||
String messageRingtone = cursor.getString(cursor.getColumnIndexOrThrow(MESSAGE_RINGTONE));
|
||||
String callRingtone = cursor.getString(cursor.getColumnIndexOrThrow(CALL_RINGTONE));
|
||||
int messageVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_VIBRATE));
|
||||
int callVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(CALL_VIBRATE));
|
||||
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
|
||||
String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR));
|
||||
int insightsBannerTier = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER));
|
||||
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
|
||||
int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_EXPIRATION_TIME));
|
||||
int registeredState = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED));
|
||||
String profileKeyString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY));
|
||||
String profileKeyCredentialString = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_KEY_CREDENTIAL));
|
||||
String systemDisplayName = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_DISPLAY_NAME));
|
||||
String systemContactPhoto = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHOTO_URI));
|
||||
String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL));
|
||||
String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI));
|
||||
String profileGivenName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_GIVEN_NAME));
|
||||
String profileFamilyName = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_FAMILY_NAME));
|
||||
String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR));
|
||||
boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1;
|
||||
String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL));
|
||||
int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE));
|
||||
boolean forceSmsSelection = cursor.getInt(cursor.getColumnIndexOrThrow(FORCE_SMS_SELECTION)) == 1;
|
||||
boolean uuidSupported = cursor.getInt(cursor.getColumnIndexOrThrow(UUID_SUPPORTED)) == 1;
|
||||
String storageKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_SERVICE_KEY));
|
||||
String identityKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
|
||||
int identityStatusRaw = cursor.getInt(cursor.getColumnIndexOrThrow(IDENTITY_STATUS));
|
||||
|
||||
MaterialColor color;
|
||||
byte[] profileKey = null;
|
||||
byte[] profileKey = null;
|
||||
byte[] profileKeyCredential = null;
|
||||
|
||||
try {
|
||||
color = serializedColor == null ? null : MaterialColor.fromSerialized(serializedColor);
|
||||
|
@ -613,6 +623,15 @@ public class RecipientDatabase extends Database {
|
|||
Log.w(TAG, e);
|
||||
profileKey = null;
|
||||
}
|
||||
|
||||
if (profileKeyCredentialString != null) {
|
||||
try {
|
||||
profileKeyCredential = Base64.decode(profileKeyCredentialString);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
profileKeyCredential = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
byte[] storageKey = null;
|
||||
|
@ -637,7 +656,8 @@ public class RecipientDatabase extends Database {
|
|||
Util.uri(messageRingtone), Util.uri(callRingtone),
|
||||
color, defaultSubscriptionId, expireMessages,
|
||||
RegisteredState.fromId(registeredState),
|
||||
profileKey, systemDisplayName, systemContactPhoto,
|
||||
profileKey, profileKeyCredential,
|
||||
systemDisplayName, systemContactPhoto,
|
||||
systemPhoneLabel, systemContactUri,
|
||||
ProfileName.fromParts(profileGivenName, profileFamilyName), signalProfileAvatar, profileSharing,
|
||||
notificationChannel, UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
|
||||
|
@ -776,9 +796,56 @@ public class RecipientDatabase extends Database {
|
|||
Recipient.live(id).refresh();
|
||||
}
|
||||
|
||||
public void setProfileKey(@NonNull RecipientId id, @Nullable byte[] profileKey) {
|
||||
/**
|
||||
* Updates the profile key.
|
||||
* <p>
|
||||
* If it changes, it clears out the profile key credential.
|
||||
*/
|
||||
public void setProfileKey(@NonNull RecipientId id, @Nullable ProfileKey profileKey) {
|
||||
String selection = ID + " = ?";
|
||||
String[] args = new String[]{id.serialize()};
|
||||
ContentValues valuesToCompare = new ContentValues(1);
|
||||
ContentValues valuesToSet = new ContentValues(2);
|
||||
String encodedProfileKey = profileKey == null ? null : Base64.encodeBytes(profileKey.serialize());
|
||||
|
||||
valuesToCompare.put(PROFILE_KEY, encodedProfileKey);
|
||||
|
||||
valuesToSet.put(PROFILE_KEY, encodedProfileKey);
|
||||
valuesToSet.putNull(PROFILE_KEY_CREDENTIAL);
|
||||
|
||||
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare);
|
||||
|
||||
if (update(updateQuery, valuesToSet)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the profile key credential as long as the profile key matches.
|
||||
*/
|
||||
public void setProfileKeyCredential(@NonNull RecipientId id,
|
||||
@NonNull ProfileKey profileKey,
|
||||
@NonNull ProfileKeyCredential profileKeyCredential)
|
||||
{
|
||||
String selection = ID + " = ? AND " + PROFILE_KEY + " = ?";
|
||||
String[] args = new String[]{id.serialize(), Base64.encodeBytes(profileKey.serialize())};
|
||||
ContentValues values = new ContentValues(1);
|
||||
|
||||
values.put(PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(profileKeyCredential.serialize()));
|
||||
|
||||
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
|
||||
if (update(updateQuery, values)) {
|
||||
// TODO [greyson] If we sync this in future, mark dirty
|
||||
//markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearProfileKeyCredential(@NonNull RecipientId id) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(PROFILE_KEY, profileKey == null ? null : Base64.encodeBytes(profileKey));
|
||||
values.putNull(PROFILE_KEY_CREDENTIAL);
|
||||
if (update(id, values)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
|
@ -1224,14 +1291,23 @@ public class RecipientDatabase extends Database {
|
|||
* Will update the database with the content values you specified. It will make an intelligent
|
||||
* query such that this will only return true if a row was *actually* updated.
|
||||
*/
|
||||
private boolean update(@NonNull RecipientId id, ContentValues contentValues) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
String selection = ID + " = ?";
|
||||
String[] args = new String[]{id.serialize()};
|
||||
private boolean update(@NonNull RecipientId id, @NonNull ContentValues contentValues) {
|
||||
String selection = ID + " = ?";
|
||||
String[] args = new String[]{id.serialize()};
|
||||
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, contentValues);
|
||||
|
||||
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, contentValues);
|
||||
return update(updateQuery, contentValues);
|
||||
}
|
||||
|
||||
return database.update(TABLE_NAME, contentValues, result.first(), result.second()) > 0;
|
||||
/**
|
||||
* Will update the database with the {@param contentValues} you specified.
|
||||
* <p>
|
||||
* This will only return true if a row was *actually* updated with respect to the where clause of the {@param updateQuery}.
|
||||
*/
|
||||
private boolean update(@NonNull SqlUtil.UpdateQuery updateQuery, @NonNull ContentValues contentValues) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
return database.update(TABLE_NAME, contentValues, updateQuery.getWhere(), updateQuery.getWhereArgs()) > 0;
|
||||
}
|
||||
|
||||
private @NonNull Optional<RecipientId> getByColumn(@NonNull String column, String value) {
|
||||
|
@ -1374,6 +1450,7 @@ public class RecipientDatabase extends Database {
|
|||
private final int expireMessages;
|
||||
private final RegisteredState registered;
|
||||
private final byte[] profileKey;
|
||||
private final byte[] profileKeyCredential;
|
||||
private final String systemDisplayName;
|
||||
private final String systemContactPhoto;
|
||||
private final String systemPhoneLabel;
|
||||
|
@ -1406,6 +1483,7 @@ public class RecipientDatabase extends Database {
|
|||
int expireMessages,
|
||||
@NonNull RegisteredState registered,
|
||||
@Nullable byte[] profileKey,
|
||||
@Nullable byte[] profileKeyCredential,
|
||||
@Nullable String systemDisplayName,
|
||||
@Nullable String systemContactPhoto,
|
||||
@Nullable String systemPhoneLabel,
|
||||
|
@ -1439,6 +1517,7 @@ public class RecipientDatabase extends Database {
|
|||
this.expireMessages = expireMessages;
|
||||
this.registered = registered;
|
||||
this.profileKey = profileKey;
|
||||
this.profileKeyCredential = profileKeyCredential;
|
||||
this.systemDisplayName = systemDisplayName;
|
||||
this.systemContactPhoto = systemContactPhoto;
|
||||
this.systemPhoneLabel = systemPhoneLabel;
|
||||
|
@ -1528,6 +1607,10 @@ public class RecipientDatabase extends Database {
|
|||
return profileKey;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getProfileKeyCredential() {
|
||||
return profileKeyCredential;
|
||||
}
|
||||
|
||||
public @Nullable String getSystemDisplayName() {
|
||||
return systemDisplayName;
|
||||
}
|
||||
|
|
|
@ -109,8 +109,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
private static final int MEGAPHONES = 45;
|
||||
private static final int MEGAPHONE_FIRST_APPEARANCE = 46;
|
||||
private static final int PROFILE_KEY_TO_DB = 47;
|
||||
private static final int PROFILE_KEY_CREDENTIALS = 48;
|
||||
|
||||
private static final int DATABASE_VERSION = 47;
|
||||
private static final int DATABASE_VERSION = 48;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
|
@ -743,6 +744,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
}
|
||||
}
|
||||
|
||||
if (oldVersion < PROFILE_KEY_CREDENTIALS) {
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN profile_key_credential TEXT DEFAULT NULL");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
|
|
@ -8,7 +8,9 @@ import android.provider.ContactsContract;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
|
@ -134,7 +136,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
getSystemAvatar(recipient.getContactUri()),
|
||||
Optional.fromNullable(recipient.getColor().serialize()),
|
||||
verifiedMessage,
|
||||
Optional.fromNullable(recipient.getProfileKey()),
|
||||
ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()),
|
||||
recipient.isBlocked(),
|
||||
recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages())
|
||||
: Optional.absent(),
|
||||
|
@ -181,7 +183,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
Optional<VerifiedMessage> verified = getVerifiedMessage(recipient, identity);
|
||||
Optional<String> name = Optional.fromNullable(recipient.getName(context));
|
||||
Optional<String> color = Optional.of(recipient.getColor().serialize());
|
||||
Optional<byte[]> profileKey = Optional.fromNullable(recipient.getProfileKey());
|
||||
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
|
||||
boolean blocked = recipient.isBlocked();
|
||||
Optional<Integer> expireTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent();
|
||||
Optional<Integer> inboxPosition = Optional.fromNullable(inboxPositions.get(recipient.getId()));
|
||||
|
@ -208,7 +210,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
Optional.absent(),
|
||||
Optional.of(self.getColor().serialize()),
|
||||
Optional.absent(),
|
||||
Optional.of(profileKey),
|
||||
ProfileKeyUtil.profileKeyOptionalOrThrow(self.getProfileKey()),
|
||||
false,
|
||||
self.getExpireMessages() > 0 ? Optional.of(self.getExpireMessages()) : Optional.absent(),
|
||||
Optional.fromNullable(inboxPositions.get(self.getId())),
|
||||
|
|
|
@ -3,14 +3,14 @@ package org.thoughtcrime.securesms.jobs;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
@ -66,7 +66,7 @@ public class MultiDeviceProfileKeyUpdateJob extends BaseJob {
|
|||
return;
|
||||
}
|
||||
|
||||
Optional<byte[]> profileKey = Optional.of(ProfileKeyUtil.getProfileKey(context));
|
||||
Optional<ProfileKey> profileKey = Optional.of(ProfileKeyUtil.getSelfProfileKey());
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
DeviceContactsOutputStream out = new DeviceContactsOutputStream(baos);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
|
@ -11,16 +12,11 @@ import org.thoughtcrime.securesms.jobmanager.Job;
|
|||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
public final class ProfileUploadJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "ProfileUploadJob";
|
||||
|
@ -47,8 +43,17 @@ public final class ProfileUploadJob extends BaseJob {
|
|||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
uploadProfileName();
|
||||
uploadAvatar();
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
ProfileName profileName = TextSecurePreferences.getProfileName(context);
|
||||
|
||||
try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
|
||||
if (FeatureFlags.VERSIONED_PROFILES) {
|
||||
accountManager.setVersionedProfile(profileKey, profileName.serialize(), avatar);
|
||||
} else {
|
||||
accountManager.setProfileName(profileKey, profileName.serialize());
|
||||
accountManager.setProfileAvatar(profileKey, avatar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -70,33 +75,6 @@ public final class ProfileUploadJob extends BaseJob {
|
|||
public void onFailure() {
|
||||
}
|
||||
|
||||
private void uploadProfileName() throws Exception {
|
||||
ProfileName profileName = TextSecurePreferences.getProfileName(context);
|
||||
accountManager.setProfileName(ProfileKeyUtil.getProfileKey(context), profileName.serialize());
|
||||
}
|
||||
|
||||
private void uploadAvatar() throws Exception {
|
||||
final RecipientId selfId = Recipient.self().getId();
|
||||
final byte[] avatar;
|
||||
|
||||
if (AvatarHelper.getAvatarFile(context, selfId).exists() && AvatarHelper.getAvatarFile(context, selfId).length() > 0) {
|
||||
avatar = Util.readFully(AvatarHelper.getInputStreamFor(context, Recipient.self().getId()));
|
||||
} else {
|
||||
avatar = null;
|
||||
}
|
||||
|
||||
final StreamDetails avatarDetails;
|
||||
if (avatar == null || avatar.length == 0) {
|
||||
avatarDetails = null;
|
||||
} else {
|
||||
avatarDetails = new StreamDetails(new ByteArrayInputStream(avatar),
|
||||
MediaUtil.IMAGE_JPEG,
|
||||
avatar.length);
|
||||
}
|
||||
|
||||
accountManager.setProfileAvatar(ProfileKeyUtil.getProfileKey(context), avatarDetails);
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory {
|
||||
|
||||
@NonNull
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
|
|||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
|
@ -20,6 +21,7 @@ import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
|
|||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
|
@ -102,7 +104,6 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
|||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
@ -271,8 +272,8 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
handleUnknownGroupMessage(content, message.getGroupInfo().get());
|
||||
}
|
||||
|
||||
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
|
||||
handleProfileKey(content, message);
|
||||
if (message.getProfileKey().isPresent()) {
|
||||
handleProfileKey(content, message.getProfileKey().get());
|
||||
}
|
||||
|
||||
if (content.isNeedsReceipt()) {
|
||||
|
@ -1175,13 +1176,15 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
}
|
||||
|
||||
private void handleProfileKey(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceDataMessage message)
|
||||
@NonNull byte[] messageProfileKeyBytes)
|
||||
{
|
||||
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
|
||||
Recipient recipient = Recipient.externalPush(context, content.getSender());
|
||||
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
|
||||
Recipient recipient = Recipient.externalPush(context, content.getSender());
|
||||
ProfileKey currentProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
|
||||
ProfileKey messageProfileKey = ProfileKeyUtil.profileKeyOrNull(messageProfileKeyBytes);
|
||||
|
||||
if (recipient.getProfileKey() == null || !MessageDigest.isEqual(recipient.getProfileKey(), message.getProfileKey().get())) {
|
||||
database.setProfileKey(recipient.getId(), message.getProfileKey().get());
|
||||
if (messageProfileKey != null && !messageProfileKey.equals(currentProfileKey)) {
|
||||
database.setProfileKey(recipient.getId(), messageProfileKey);
|
||||
database.setUnidentifiedAccessMode(recipient.getId(), RecipientDatabase.UnidentifiedAccessMode.UNKNOWN);
|
||||
ApplicationDependencies.getJobManager().add(new RetrieveProfileJob(recipient));
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.jobs;
|
|||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
|
@ -13,6 +12,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -48,7 +48,7 @@ public class RefreshAttributesJob extends BaseJob {
|
|||
public void onRun() throws IOException {
|
||||
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
|
||||
boolean fetchesMessages = TextSecurePreferences.isFcmDisabled(context);
|
||||
byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(ProfileKeyUtil.getProfileKey(context));
|
||||
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
|
||||
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
|
||||
String pin = null;
|
||||
String registrationLockToken = null;
|
||||
|
|
|
@ -3,8 +3,11 @@ package org.thoughtcrime.securesms.jobs;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
|
@ -12,9 +15,12 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
|
@ -58,11 +64,31 @@ public class RefreshOwnProfileJob extends BaseJob {
|
|||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, Recipient.self());
|
||||
Recipient self = Recipient.self();
|
||||
ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfile(context, self, getRequestType(self));
|
||||
SignalServiceProfile profile = profileAndCredential.getProfile();
|
||||
|
||||
setProfileName(profile.getName());
|
||||
setProfileAvatar(profile.getAvatar());
|
||||
setProfileCapabilities(profile.getCapabilities());
|
||||
Optional<ProfileKeyCredential> profileKeyCredential = profileAndCredential.getProfileKeyCredential();
|
||||
if (profileKeyCredential.isPresent()) {
|
||||
setProfileKeyCredential(self, ProfileKeyUtil.getSelfProfileKey(), profileKeyCredential.get());
|
||||
}
|
||||
}
|
||||
|
||||
private void setProfileKeyCredential(@NonNull Recipient recipient,
|
||||
@NonNull ProfileKey recipientProfileKey,
|
||||
@NonNull ProfileKeyCredential credential)
|
||||
{
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
recipientDatabase.setProfileKeyCredential(recipient.getId(), recipientProfileKey, credential);
|
||||
}
|
||||
|
||||
private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) {
|
||||
return FeatureFlags.VERSIONED_PROFILES && !recipient.hasProfileKeyCredential()
|
||||
? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL
|
||||
: SignalServiceProfile.RequestType.PROFILE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -75,7 +101,7 @@ public class RefreshOwnProfileJob extends BaseJob {
|
|||
|
||||
private void setProfileName(@Nullable String encryptedName) {
|
||||
try {
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(context);
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
String plaintextName = ProfileUtil.decryptName(profileKey, encryptedName);
|
||||
ProfileName profileName = ProfileName.fromSerialized(plaintextName);
|
||||
|
||||
|
@ -86,7 +112,7 @@ public class RefreshOwnProfileJob extends BaseJob {
|
|||
}
|
||||
}
|
||||
|
||||
private void setProfileAvatar(@Nullable String avatar) {
|
||||
private static void setProfileAvatar(@Nullable String avatar) {
|
||||
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), avatar));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
@ -37,8 +40,8 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
|||
private static final String KEY_PROFILE_AVATAR = "profile_avatar";
|
||||
private static final String KEY_RECIPIENT = "recipient";
|
||||
|
||||
private String profileAvatar;
|
||||
private Recipient recipient;
|
||||
private final String profileAvatar;
|
||||
private final Recipient recipient;
|
||||
|
||||
public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) {
|
||||
this(new Job.Parameters.Builder()
|
||||
|
@ -73,7 +76,7 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
|||
@Override
|
||||
public void onRun() throws IOException {
|
||||
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
|
||||
byte[] profileKey = recipient.resolve().getProfileKey();
|
||||
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.resolve().getProfileKey());
|
||||
|
||||
if (profileKey == null) {
|
||||
Log.w(TAG, "Recipient profile key is gone!");
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
|
@ -24,8 +27,10 @@ import org.thoughtcrime.securesms.util.ProfileUtil;
|
|||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -92,9 +97,11 @@ public class RetrieveProfileJob extends BaseJob {
|
|||
}
|
||||
|
||||
private void handlePhoneNumberRecipient(Recipient recipient) throws IOException {
|
||||
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, recipient);
|
||||
ProfileAndCredential profileAndCredential = ProfileUtil.retrieveProfile(context, recipient, getRequestType(recipient));
|
||||
SignalServiceProfile profile = profileAndCredential.getProfile();
|
||||
ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
|
||||
|
||||
if (recipient.getProfileKey() == null) {
|
||||
if (recipientProfileKey == null) {
|
||||
Log.i(TAG, "No profile key available for " + recipient.getId());
|
||||
} else {
|
||||
Log.i(TAG, "Profile key available for " + recipient.getId());
|
||||
|
@ -106,6 +113,27 @@ public class RetrieveProfileJob extends BaseJob {
|
|||
setProfileCapabilities(recipient, profile.getCapabilities());
|
||||
setIdentityKey(recipient, profile.getIdentityKey());
|
||||
setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());
|
||||
|
||||
if (recipientProfileKey != null) {
|
||||
Optional<ProfileKeyCredential> profileKeyCredential = profileAndCredential.getProfileKeyCredential();
|
||||
if (profileKeyCredential.isPresent()) {
|
||||
setProfileKeyCredential(recipient, recipientProfileKey, profileKeyCredential.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setProfileKeyCredential(@NonNull Recipient recipient,
|
||||
@NonNull ProfileKey recipientProfileKey,
|
||||
@NonNull ProfileKeyCredential credential)
|
||||
{
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
recipientDatabase.setProfileKeyCredential(recipient.getId(), recipientProfileKey, credential);
|
||||
}
|
||||
|
||||
private static SignalServiceProfile.RequestType getRequestType(@NonNull Recipient recipient) {
|
||||
return FeatureFlags.VERSIONED_PROFILES && !recipient.hasProfileKeyCredential()
|
||||
? SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL
|
||||
: SignalServiceProfile.RequestType.PROFILE;
|
||||
}
|
||||
|
||||
private void handleGroupRecipient(Recipient group) throws IOException {
|
||||
|
@ -141,7 +169,7 @@ public class RetrieveProfileJob extends BaseJob {
|
|||
|
||||
private void setUnidentifiedAccessMode(Recipient recipient, String unidentifiedAccessVerifier, boolean unrestrictedUnidentifiedAccess) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
byte[] profileKey = recipient.getProfileKey();
|
||||
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
|
||||
|
||||
if (unrestrictedUnidentifiedAccess && unidentifiedAccessVerifier != null) {
|
||||
if (recipient.getUnidentifiedAccessMode() != UnidentifiedAccessMode.UNRESTRICTED) {
|
||||
|
@ -175,7 +203,7 @@ public class RetrieveProfileJob extends BaseJob {
|
|||
|
||||
private void setProfileName(Recipient recipient, String profileName) {
|
||||
try {
|
||||
byte[] profileKey = recipient.getProfileKey();
|
||||
ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey());
|
||||
if (profileKey == null) return;
|
||||
|
||||
String plaintextProfileName = ProfileUtil.decryptName(profileKey, profileName);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
@ -11,15 +12,11 @@ import org.thoughtcrime.securesms.jobmanager.Job;
|
|||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class RotateProfileKeyJob extends BaseJob {
|
||||
|
||||
|
@ -52,12 +49,20 @@ public class RotateProfileKeyJob extends BaseJob {
|
|||
public void onRun() throws Exception {
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
byte[] profileKey = Util.getSecretBytes(32);
|
||||
ProfileKey profileKey = ProfileKeyUtil.createNew();
|
||||
Recipient self = Recipient.self();
|
||||
|
||||
recipientDatabase.setProfileKey(self.getId(), profileKey);
|
||||
accountManager.setProfileName(profileKey, TextSecurePreferences.getProfileName(context).serialize());
|
||||
accountManager.setProfileAvatar(profileKey, getProfileAvatar());
|
||||
try (StreamDetails avatarStream = AvatarHelper.getSelfProfileAvatarStream(context)) {
|
||||
if (FeatureFlags.VERSIONED_PROFILES) {
|
||||
accountManager.setVersionedProfile(profileKey,
|
||||
TextSecurePreferences.getProfileName(context).serialize(),
|
||||
avatarStream);
|
||||
} else {
|
||||
accountManager.setProfileName(profileKey, TextSecurePreferences.getProfileName(context).serialize());
|
||||
accountManager.setProfileAvatar(profileKey, avatarStream);
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
|
||||
}
|
||||
|
@ -72,19 +77,6 @@ public class RotateProfileKeyJob extends BaseJob {
|
|||
return exception instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
private @Nullable StreamDetails getProfileAvatar() {
|
||||
try {
|
||||
File avatarFile = AvatarHelper.getAvatarFile(context, Recipient.self().getId());
|
||||
|
||||
if (avatarFile.exists()) {
|
||||
return new StreamDetails(new FileInputStream(avatarFile), "image/jpeg", avatarFile.length());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<RotateProfileKeyJob> {
|
||||
@Override
|
||||
public @NonNull RotateProfileKeyJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
|
|
|
@ -2,15 +2,21 @@ package org.thoughtcrime.securesms.profiles;
|
|||
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -57,4 +63,24 @@ public class AvatarHelper {
|
|||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull StreamDetails avatarStream(@NonNull byte[] data) {
|
||||
return new StreamDetails(new ByteArrayInputStream(data), MediaUtil.IMAGE_JPEG, data.length);
|
||||
}
|
||||
|
||||
public static @Nullable StreamDetails getSelfProfileAvatarStream(@NonNull Context context) {
|
||||
File avatarFile = getAvatarFile(context, Recipient.self().getId());
|
||||
|
||||
if (avatarFile.exists() && avatarFile.length() > 0) {
|
||||
try {
|
||||
FileInputStream stream = new FileInputStream(avatarFile);
|
||||
|
||||
return new StreamDetails(stream, MediaUtil.IMAGE_JPEG, avatarFile.length());
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,17 +20,14 @@ import org.thoughtcrime.securesms.profiles.ProfileName;
|
|||
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
|
@ -132,7 +129,7 @@ class EditProfileRepository {
|
|||
@WorkerThread
|
||||
private @NonNull Optional<String> getUsernameInternal() {
|
||||
try {
|
||||
SignalServiceProfile profile = retrieveOwnProfile();
|
||||
SignalServiceProfile profile = ProfileUtil.retrieveProfile(context, Recipient.self(), SignalServiceProfile.RequestType.PROFILE).getProfile();
|
||||
TextSecurePreferences.setLocalUsername(context, profile.getUsername());
|
||||
DatabaseFactory.getRecipientDatabase(context).setUsername(Recipient.self().getId(), profile.getUsername());
|
||||
} catch (IOException e) {
|
||||
|
@ -141,22 +138,6 @@ class EditProfileRepository {
|
|||
return Optional.fromNullable(TextSecurePreferences.getLocalUsername(context));
|
||||
}
|
||||
|
||||
private SignalServiceProfile retrieveOwnProfile() throws IOException {
|
||||
SignalServiceAddress address = new SignalServiceAddress(TextSecurePreferences.getLocalUuid(context), TextSecurePreferences.getLocalNumber(context));
|
||||
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
|
||||
SignalServiceMessagePipe pipe = IncomingMessageObserver.getPipe();
|
||||
|
||||
if (pipe != null) {
|
||||
try {
|
||||
return pipe.getProfile(address, Optional.absent());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
return receiver.retrieveProfile(address, Optional.absent());
|
||||
}
|
||||
|
||||
public enum UploadResult {
|
||||
SUCCESS,
|
||||
ERROR_FILE_IO
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.net.UserAgentInterceptor;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||
|
@ -16,6 +17,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
|
|||
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
@ -144,7 +146,13 @@ public class SignalServiceNetworkAccess {
|
|||
final SignalStorageUrl qatarGoogleStorage = new SignalStorageUrl("https://www.google.com.qa/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC);
|
||||
|
||||
final List<Interceptor> interceptors = Collections.singletonList(new UserAgentInterceptor());
|
||||
final byte[] zkGroupServerPublicParams;
|
||||
|
||||
try {
|
||||
zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
this.censorshipConfiguration = new HashMap<String, SignalServiceConfiguration>() {{
|
||||
put(COUNTRY_CODE_EGYPT, new SignalServiceConfiguration(new SignalServiceUrl[] {egyptGoogleService, baseGoogleService, baseAndroidService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
|
||||
|
@ -152,21 +160,24 @@ public class SignalServiceNetworkAccess {
|
|||
new SignalContactDiscoveryUrl[] {egyptGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
|
||||
new SignalKeyBackupServiceUrl[] {egyptGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
|
||||
new SignalStorageUrl[] {egyptGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
|
||||
interceptors));
|
||||
interceptors,
|
||||
zkGroupServerPublicParams));
|
||||
|
||||
put(COUNTRY_CODE_UAE, new SignalServiceConfiguration(new SignalServiceUrl[] {uaeGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
|
||||
new SignalCdnUrl[] {uaeGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn},
|
||||
new SignalContactDiscoveryUrl[] {uaeGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
|
||||
new SignalKeyBackupServiceUrl[] {uaeGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
|
||||
new SignalStorageUrl[] {uaeGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
|
||||
interceptors));
|
||||
interceptors,
|
||||
zkGroupServerPublicParams));
|
||||
|
||||
put(COUNTRY_CODE_OMAN, new SignalServiceConfiguration(new SignalServiceUrl[] {omanGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
|
||||
new SignalCdnUrl[] {omanGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn},
|
||||
new SignalContactDiscoveryUrl[] {omanGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
|
||||
new SignalKeyBackupServiceUrl[] {omanGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
|
||||
new SignalStorageUrl[] {omanGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
|
||||
interceptors));
|
||||
interceptors,
|
||||
zkGroupServerPublicParams));
|
||||
|
||||
|
||||
put(COUNTRY_CODE_QATAR, new SignalServiceConfiguration(new SignalServiceUrl[] {qatarGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService},
|
||||
|
@ -174,7 +185,8 @@ public class SignalServiceNetworkAccess {
|
|||
new SignalContactDiscoveryUrl[] {qatarGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery},
|
||||
new SignalKeyBackupServiceUrl[] {qatarGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs},
|
||||
new SignalStorageUrl[] {qatarGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage},
|
||||
interceptors));
|
||||
interceptors,
|
||||
zkGroupServerPublicParams));
|
||||
}};
|
||||
|
||||
this.uncensoredConfiguration = new SignalServiceConfiguration(new SignalServiceUrl[] {new SignalServiceUrl(BuildConfig.SIGNAL_URL, new SignalServiceTrustStore(context))},
|
||||
|
@ -182,7 +194,8 @@ public class SignalServiceNetworkAccess {
|
|||
new SignalContactDiscoveryUrl[] {new SignalContactDiscoveryUrl(BuildConfig.SIGNAL_CONTACT_DISCOVERY_URL, new SignalServiceTrustStore(context))},
|
||||
new SignalKeyBackupServiceUrl[] { new SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, new SignalServiceTrustStore(context)) },
|
||||
new SignalStorageUrl[] {new SignalStorageUrl(BuildConfig.STORAGE_URL, new SignalServiceTrustStore(context))},
|
||||
interceptors);
|
||||
interceptors,
|
||||
zkGroupServerPublicParams);
|
||||
|
||||
this.censoredCountries = this.censorshipConfiguration.keySet().toArray(new String[0]);
|
||||
}
|
||||
|
|
|
@ -78,6 +78,7 @@ public class Recipient {
|
|||
private final int expireMessages;
|
||||
private final RegisteredState registered;
|
||||
private final byte[] profileKey;
|
||||
private final byte[] profileKeyCredential;
|
||||
private final String name;
|
||||
private final Uri systemContactPhoto;
|
||||
private final String customLabel;
|
||||
|
@ -297,6 +298,7 @@ public class Recipient {
|
|||
this.expireMessages = 0;
|
||||
this.registered = RegisteredState.UNKNOWN;
|
||||
this.profileKey = null;
|
||||
this.profileKeyCredential = null;
|
||||
this.name = null;
|
||||
this.systemContactPhoto = null;
|
||||
this.customLabel = null;
|
||||
|
@ -336,6 +338,7 @@ public class Recipient {
|
|||
this.expireMessages = details.expireMessages;
|
||||
this.registered = details.registered;
|
||||
this.profileKey = details.profileKey;
|
||||
this.profileKeyCredential = details.profileKeyCredential;
|
||||
this.name = details.name;
|
||||
this.systemContactPhoto = details.systemContactPhoto;
|
||||
this.customLabel = details.customLabel;
|
||||
|
@ -666,6 +669,14 @@ public class Recipient {
|
|||
return profileKey;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getProfileKeyCredential() {
|
||||
return profileKeyCredential;
|
||||
}
|
||||
|
||||
public boolean hasProfileKeyCredential() {
|
||||
return profileKeyCredential != null;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getStorageServiceKey() {
|
||||
return storageKey;
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ public class RecipientDetails {
|
|||
final Optional<Integer> defaultSubscriptionId;
|
||||
final RegisteredState registered;
|
||||
final byte[] profileKey;
|
||||
final byte[] profileKeyCredential;
|
||||
final String profileAvatar;
|
||||
final boolean profileSharing;
|
||||
final boolean systemContact;
|
||||
|
@ -90,6 +91,7 @@ public class RecipientDetails {
|
|||
this.defaultSubscriptionId = settings.getDefaultSubscriptionId();
|
||||
this.registered = settings.getRegistered();
|
||||
this.profileKey = settings.getProfileKey();
|
||||
this.profileKeyCredential = settings.getProfileKeyCredential();
|
||||
this.profileAvatar = settings.getProfileAvatar();
|
||||
this.profileSharing = settings.isProfileSharing();
|
||||
this.systemContact = systemContact;
|
||||
|
@ -134,6 +136,7 @@ public class RecipientDetails {
|
|||
this.defaultSubscriptionId = Optional.absent();
|
||||
this.registered = RegisteredState.UNKNOWN;
|
||||
this.profileKey = null;
|
||||
this.profileKeyCredential = null;
|
||||
this.profileAvatar = null;
|
||||
this.profileSharing = false;
|
||||
this.systemContact = true;
|
||||
|
|
|
@ -6,10 +6,11 @@ import android.os.AsyncTask;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SessionUtil;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
|
@ -28,7 +29,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
|
@ -39,6 +39,7 @@ import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
|||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
|
||||
|
@ -191,17 +192,17 @@ public final class CodeVerificationRequest {
|
|||
@Nullable String fcmToken)
|
||||
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
|
||||
{
|
||||
boolean isV2KbsPin = kbsTokenResponse != null;
|
||||
int registrationId = KeyHelper.generateRegistrationId(false);
|
||||
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
|
||||
byte[] profileKey = findExistingProfileKey(context, credentials.getE164number());
|
||||
boolean isV2KbsPin = kbsTokenResponse != null;
|
||||
int registrationId = KeyHelper.generateRegistrationId(false);
|
||||
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
|
||||
ProfileKey profileKey = findExistingProfileKey(context, credentials.getE164number());
|
||||
|
||||
if (profileKey == null) {
|
||||
profileKey = Util.getSecretBytes(32);
|
||||
profileKey = ProfileKeyUtil.createNew();
|
||||
Log.i(TAG, "No profile key found, created a new one");
|
||||
}
|
||||
|
||||
byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(profileKey);
|
||||
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(profileKey);
|
||||
|
||||
TextSecurePreferences.setLocalRegistrationId(context, registrationId);
|
||||
SessionUtil.archiveAllSessions(context);
|
||||
|
@ -269,12 +270,12 @@ public final class CodeVerificationRequest {
|
|||
}
|
||||
}
|
||||
|
||||
private static @Nullable byte[] findExistingProfileKey(@NonNull Context context, @NonNull String e164number) {
|
||||
private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
Optional<RecipientId> recipient = recipientDatabase.getByE164(e164number);
|
||||
|
||||
if (recipient.isPresent()) {
|
||||
return Recipient.resolved(recipient.get()).getProfileKey();
|
||||
return ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).getProfileKey());
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -312,4 +312,7 @@ public final class FeatureFlags {
|
|||
return disk;
|
||||
}
|
||||
}
|
||||
|
||||
/** Read and write versioned profile information. */
|
||||
public static final boolean VERSIONED_PROFILES = org.whispersystems.signalservice.FeatureFlags.VERSIONED_PROFILES;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
@ -19,6 +22,7 @@ import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
|||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
@ -28,22 +32,30 @@ import java.io.IOException;
|
|||
/**
|
||||
* Aids in the retrieval and decryption of profiles.
|
||||
*/
|
||||
public class ProfileUtil {
|
||||
public final class ProfileUtil {
|
||||
|
||||
private ProfileUtil() {
|
||||
}
|
||||
|
||||
private static final String TAG = Log.tag(ProfileUtil.class);
|
||||
|
||||
@WorkerThread
|
||||
public static SignalServiceProfile retrieveProfile(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
|
||||
public static @NonNull ProfileAndCredential retrieveProfile(@NonNull Context context,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull SignalServiceProfile.RequestType requestType)
|
||||
throws IOException
|
||||
{
|
||||
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess = getUnidentifiedAccess(context, recipient);
|
||||
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
|
||||
|
||||
SignalServiceProfile profile;
|
||||
ProfileAndCredential profile;
|
||||
|
||||
try {
|
||||
profile = retrieveProfileInternal(address, unidentifiedAccess);
|
||||
profile = retrieveProfileInternal(address, profileKey, unidentifiedAccess, requestType);
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (unidentifiedAccess.isPresent()) {
|
||||
profile = retrieveProfileInternal(address, Optional.absent());
|
||||
profile = retrieveProfileInternal(address, profileKey, Optional.absent(), requestType);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
@ -52,7 +64,7 @@ public class ProfileUtil {
|
|||
return profile;
|
||||
}
|
||||
|
||||
public static @Nullable String decryptName(@NonNull byte[] profileKey, @Nullable String encryptedName)
|
||||
public static @Nullable String decryptName(@NonNull ProfileKey profileKey, @Nullable String encryptedName)
|
||||
throws InvalidCiphertextException, IOException
|
||||
{
|
||||
if (encryptedName == null) {
|
||||
|
@ -64,8 +76,11 @@ public class ProfileUtil {
|
|||
}
|
||||
|
||||
@WorkerThread
|
||||
private static SignalServiceProfile retrieveProfileInternal(@NonNull SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess)
|
||||
throws IOException
|
||||
private static @NonNull ProfileAndCredential retrieveProfileInternal(@NonNull SignalServiceAddress address,
|
||||
@NonNull Optional<ProfileKey> profileKey,
|
||||
@NonNull Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
@NonNull SignalServiceProfile.RequestType requestType)
|
||||
throws IOException
|
||||
{
|
||||
SignalServiceMessagePipe authPipe = IncomingMessageObserver.getPipe();
|
||||
SignalServiceMessagePipe unidentifiedPipe = IncomingMessageObserver.getUnidentifiedPipe();
|
||||
|
@ -74,14 +89,18 @@ public class ProfileUtil {
|
|||
|
||||
if (pipe != null) {
|
||||
try {
|
||||
return pipe.getProfile(address, unidentifiedAccess);
|
||||
return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
Log.w(TAG, "Websocket request failed. Falling back to REST.", e);
|
||||
}
|
||||
}
|
||||
|
||||
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
|
||||
return receiver.retrieveProfile(address, unidentifiedAccess);
|
||||
try {
|
||||
return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new IOException("Verification Problem", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<UnidentifiedAccess> getUnidentifiedAccess(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
|
|
|
@ -7,8 +7,6 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
@ -46,9 +44,9 @@ public final class SqlUtil {
|
|||
* change. In other words, if {@link SQLiteDatabase#update(String, ContentValues, String, String[])}
|
||||
* returns > 0, then you know something *actually* changed.
|
||||
*/
|
||||
public static @NonNull Pair<String, String[]> buildTrueUpdateQuery(@NonNull String selection,
|
||||
@NonNull String[] args,
|
||||
@NonNull ContentValues contentValues)
|
||||
public static @NonNull UpdateQuery buildTrueUpdateQuery(@NonNull String selection,
|
||||
@NonNull String[] args,
|
||||
@NonNull ContentValues contentValues)
|
||||
{
|
||||
StringBuilder qualifier = new StringBuilder();
|
||||
Set<Map.Entry<String, Object>> valueSet = contentValues.valueSet();
|
||||
|
@ -73,6 +71,24 @@ public final class SqlUtil {
|
|||
i++;
|
||||
}
|
||||
|
||||
return new Pair<>("(" + selection + ") AND (" + qualifier + ")", fullArgs.toArray(new String[0]));
|
||||
return new UpdateQuery("(" + selection + ") AND (" + qualifier + ")", fullArgs.toArray(new String[0]));
|
||||
}
|
||||
|
||||
public static class UpdateQuery {
|
||||
private final String where;
|
||||
private final String[] whereArgs;
|
||||
|
||||
private UpdateQuery(@NonNull String where, @NonNull String[] whereArgs) {
|
||||
this.where = where;
|
||||
this.whereArgs = whereArgs;
|
||||
}
|
||||
|
||||
public String getWhere() {
|
||||
return where;
|
||||
}
|
||||
|
||||
public String[] getWhereArgs() {
|
||||
return whereArgs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.contacts.sync;
|
|||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
@ -21,13 +22,13 @@ import java.util.List;
|
|||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import edu.emory.mathcs.backport.java.util.Arrays;
|
||||
|
||||
import static junit.framework.TestCase.assertTrue;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
public class StorageSyncHelperTest {
|
||||
public final class StorageSyncHelperTest {
|
||||
|
||||
private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7");
|
||||
private static final UUID UUID_B = UuidUtil.parseOrThrow("32119989-77fb-4e18-af70-81d55185c6b1");
|
||||
|
@ -253,8 +254,63 @@ public class StorageSyncHelperTest {
|
|||
assertByteListEquals(byteListOf(1), result.getDeletes());
|
||||
}
|
||||
|
||||
private static <E> Set<E> setOf(E... vals) {
|
||||
return new LinkedHashSet<E>(Arrays.asList(vals));
|
||||
@Test
|
||||
public void contacts_with_same_profile_key_contents_are_equal() {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
|
||||
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setProfileKey(profileKey).build();
|
||||
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setProfileKey(profileKeyCopy).build();
|
||||
|
||||
assertEquals(a, b);
|
||||
assertEquals(a.hashCode(), b.hashCode());
|
||||
|
||||
assertFalse(contactUpdate(a, b).profileKeyChanged());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contacts_with_different_profile_key_contents_are_not_equal() {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
profileKeyCopy[0] = 1;
|
||||
|
||||
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setProfileKey(profileKey).build();
|
||||
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setProfileKey(profileKeyCopy).build();
|
||||
|
||||
assertNotEquals(a, b);
|
||||
assertNotEquals(a.hashCode(), b.hashCode());
|
||||
|
||||
assertTrue(contactUpdate(a, b).profileKeyChanged());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contacts_with_same_identity_key_contents_are_equal() {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
|
||||
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKey).build();
|
||||
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
|
||||
|
||||
assertEquals(a, b);
|
||||
assertEquals(a.hashCode(), b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contacts_with_different_identity_key_contents_are_not_equal() {
|
||||
byte[] profileKey = new byte[32];
|
||||
byte[] profileKeyCopy = profileKey.clone();
|
||||
profileKeyCopy[0] = 1;
|
||||
|
||||
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKey).build();
|
||||
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
|
||||
|
||||
assertNotEquals(a, b);
|
||||
assertNotEquals(a.hashCode(), b.hashCode());
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static <E> Set<E> setOf(E... values) {
|
||||
return Sets.newHashSet(values);
|
||||
}
|
||||
|
||||
private static Set<SignalStorageRecord> recordSetOf(SignalContactRecord... contactRecords) {
|
||||
|
@ -267,14 +323,21 @@ public class StorageSyncHelperTest {
|
|||
return storageRecords;
|
||||
}
|
||||
|
||||
private static SignalContactRecord.Builder contactBuilder(int key,
|
||||
UUID uuid,
|
||||
String e164,
|
||||
String profileName)
|
||||
{
|
||||
return new SignalContactRecord.Builder(byteArray(key), new SignalServiceAddress(uuid, e164))
|
||||
.setProfileName(profileName);
|
||||
}
|
||||
|
||||
private static SignalContactRecord contact(int key,
|
||||
UUID uuid,
|
||||
String e164,
|
||||
String profileName)
|
||||
{
|
||||
return new SignalContactRecord.Builder(byteArray(key), new SignalServiceAddress(uuid, e164))
|
||||
.setProfileName(profileName)
|
||||
.build();
|
||||
return contactBuilder(key, uuid, e164, profileName).build();
|
||||
}
|
||||
|
||||
private static StorageSyncHelper.ContactUpdate contactUpdate(SignalContactRecord oldContact, SignalContactRecord newContact) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package org.thoughtcrime.securesms.database;
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.ContentValues;
|
||||
|
@ -7,15 +7,13 @@ import org.junit.Test;
|
|||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, application = Application.class)
|
||||
public class SqlUtilTest {
|
||||
public final class SqlUtilTest {
|
||||
|
||||
@Test
|
||||
public void buildTrueUpdateQuery_simple() {
|
||||
|
@ -25,10 +23,10 @@ public class SqlUtilTest {
|
|||
ContentValues values = new ContentValues();
|
||||
values.put("a", 2);
|
||||
|
||||
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
|
||||
assertEquals("(_id = ?) AND (a != ? OR a IS NULL)", result.first());
|
||||
assertArrayEquals(new String[] { "1", "2" }, result.second());
|
||||
assertEquals("(_id = ?) AND (a != ? OR a IS NULL)", updateQuery.getWhere());
|
||||
assertArrayEquals(new String[] { "1", "2" }, updateQuery.getWhereArgs());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -39,10 +37,10 @@ public class SqlUtilTest {
|
|||
ContentValues values = new ContentValues();
|
||||
values.put("a", 4);
|
||||
|
||||
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
|
||||
assertEquals("(_id = ? AND (foo = ? OR bar != ?)) AND (a != ? OR a IS NULL)", result.first());
|
||||
assertArrayEquals(new String[] { "1", "2", "3", "4" }, result.second());
|
||||
assertEquals("(_id = ? AND (foo = ? OR bar != ?)) AND (a != ? OR a IS NULL)", updateQuery.getWhere());
|
||||
assertArrayEquals(new String[] { "1", "2", "3", "4" }, updateQuery.getWhereArgs());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -55,10 +53,10 @@ public class SqlUtilTest {
|
|||
values.put("b", 3);
|
||||
values.put("c", 4);
|
||||
|
||||
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
|
||||
assertEquals("(_id = ?) AND (a != ? OR a IS NULL OR b != ? OR b IS NULL OR c != ? OR c IS NULL)", result.first());
|
||||
assertArrayEquals(new String[] { "1", "2", "3", "4"}, result.second());
|
||||
assertEquals("(_id = ?) AND (a != ? OR a IS NULL OR b != ? OR b IS NULL OR c != ? OR c IS NULL)", updateQuery.getWhere());
|
||||
assertArrayEquals(new String[] { "1", "2", "3", "4"}, updateQuery.getWhereArgs());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -69,10 +67,10 @@ public class SqlUtilTest {
|
|||
ContentValues values = new ContentValues();
|
||||
values.put("a", (String) null);
|
||||
|
||||
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
|
||||
assertEquals("(_id = ?) AND (a NOT NULL)", result.first());
|
||||
assertArrayEquals(new String[] { "1" }, result.second());
|
||||
assertEquals("(_id = ?) AND (a NOT NULL)", updateQuery.getWhere());
|
||||
assertArrayEquals(new String[] { "1" }, updateQuery.getWhereArgs());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -87,9 +85,9 @@ public class SqlUtilTest {
|
|||
values.put("d", (String) null);
|
||||
values.put("e", (String) null);
|
||||
|
||||
Pair<String, String[]> result = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values);
|
||||
|
||||
assertEquals("(_id = ?) AND (a NOT NULL OR b != ? OR b IS NULL OR c != ? OR c IS NULL OR d NOT NULL OR e NOT NULL)", result.first());
|
||||
assertArrayEquals(new String[] { "1", "2", "3" }, result.second());
|
||||
assertEquals("(_id = ?) AND (a NOT NULL OR b != ? OR b IS NULL OR c != ? OR c IS NULL OR d NOT NULL OR e NOT NULL)", updateQuery.getWhere());
|
||||
assertArrayEquals(new String[] { "1", "2", "3" }, updateQuery.getWhereArgs());
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -9,6 +9,7 @@ package org.whispersystems.signalservice.api;
|
|||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
|
@ -17,18 +18,20 @@ import org.whispersystems.libsignal.logging.Log;
|
|||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
|
||||
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
|
||||
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
|
@ -56,6 +59,7 @@ import org.whispersystems.signalservice.internal.util.Util;
|
|||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyStore;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
@ -559,19 +563,27 @@ public class SignalServiceAccountManager {
|
|||
return this.pushServiceSocket.getTurnServerInfo();
|
||||
}
|
||||
|
||||
public void setProfileName(byte[] key, String name)
|
||||
public void setProfileName(ProfileKey key, String name)
|
||||
throws IOException
|
||||
{
|
||||
if (FeatureFlags.VERSIONED_PROFILES) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (name == null) name = "";
|
||||
|
||||
String ciphertextName = Base64.encodeBytesWithoutPadding(new ProfileCipher(key).encryptName(name.getBytes("UTF-8"), ProfileCipher.NAME_PADDED_LENGTH));
|
||||
String ciphertextName = Base64.encodeBytesWithoutPadding(new ProfileCipher(key).encryptName(name.getBytes(StandardCharsets.UTF_8), ProfileCipher.NAME_PADDED_LENGTH));
|
||||
|
||||
this.pushServiceSocket.setProfileName(ciphertextName);
|
||||
}
|
||||
|
||||
public void setProfileAvatar(byte[] key, StreamDetails avatar)
|
||||
public void setProfileAvatar(ProfileKey key, StreamDetails avatar)
|
||||
throws IOException
|
||||
{
|
||||
if (FeatureFlags.VERSIONED_PROFILES) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
ProfileAvatarData profileAvatarData = null;
|
||||
|
||||
if (avatar != null) {
|
||||
|
@ -584,6 +596,33 @@ public class SignalServiceAccountManager {
|
|||
this.pushServiceSocket.setProfileAvatar(profileAvatarData);
|
||||
}
|
||||
|
||||
public void setVersionedProfile(ProfileKey profileKey, String name, StreamDetails avatar)
|
||||
throws IOException
|
||||
{
|
||||
if (!FeatureFlags.VERSIONED_PROFILES) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (name == null) name = "";
|
||||
|
||||
byte[] ciphertextName = new ProfileCipher(profileKey).encryptName(name.getBytes(StandardCharsets.UTF_8), ProfileCipher.NAME_PADDED_LENGTH);
|
||||
boolean hasAvatar = avatar != null;
|
||||
ProfileAvatarData profileAvatarData = null;
|
||||
|
||||
if (hasAvatar) {
|
||||
profileAvatarData = new ProfileAvatarData(avatar.getStream(),
|
||||
ProfileCipherOutputStream.getCiphertextLength(avatar.getLength()),
|
||||
avatar.getContentType(),
|
||||
new ProfileCipherOutputStreamFactory(profileKey));
|
||||
}
|
||||
|
||||
this.pushServiceSocket.writeProfile(new SignalServiceProfileWrite(profileKey.getProfileKeyVersion().serialize(),
|
||||
ciphertextName,
|
||||
hasAvatar,
|
||||
profileKey.getCommitment().serialize()),
|
||||
profileAvatarData);
|
||||
}
|
||||
|
||||
public void setUsername(String username) throws IOException {
|
||||
this.pushServiceSocket.setUsername(username);
|
||||
}
|
||||
|
|
|
@ -8,11 +8,21 @@ package org.whispersystems.signalservice.api;
|
|||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyVersion;
|
||||
import org.whispersystems.libsignal.InvalidVersionException;
|
||||
import org.whispersystems.libsignal.util.Hex;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
|
@ -25,10 +35,10 @@ import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
|||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
@ -47,10 +57,15 @@ public class SignalServiceMessagePipe {
|
|||
|
||||
private final WebSocketConnection websocket;
|
||||
private final Optional<CredentialsProvider> credentialsProvider;
|
||||
private final ClientZkProfileOperations clientZkProfile;
|
||||
|
||||
SignalServiceMessagePipe(WebSocketConnection websocket, Optional<CredentialsProvider> credentialsProvider) {
|
||||
SignalServiceMessagePipe(WebSocketConnection websocket,
|
||||
Optional<CredentialsProvider> credentialsProvider,
|
||||
ClientZkProfileOperations clientZkProfile)
|
||||
{
|
||||
this.websocket = websocket;
|
||||
this.credentialsProvider = credentialsProvider;
|
||||
this.clientZkProfile = clientZkProfile;
|
||||
|
||||
this.websocket.connect();
|
||||
}
|
||||
|
@ -149,7 +164,12 @@ public class SignalServiceMessagePipe {
|
|||
}
|
||||
}
|
||||
|
||||
public SignalServiceProfile getProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
|
||||
public ProfileAndCredential getProfile(SignalServiceAddress address,
|
||||
Optional<ProfileKey> profileKey,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
SignalServiceProfile.RequestType requestType)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
List<String> headers = new LinkedList<>();
|
||||
|
||||
|
@ -157,12 +177,30 @@ public class SignalServiceMessagePipe {
|
|||
headers.add("Unidentified-Access-Key:" + Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
|
||||
}
|
||||
|
||||
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
|
||||
.setId(new SecureRandom().nextLong())
|
||||
.setVerb("GET")
|
||||
.setPath(String.format("/v1/profile/%s", address.getIdentifier()))
|
||||
.addAllHeaders(headers)
|
||||
.build();
|
||||
Optional<UUID> uuid = address.getUuid();
|
||||
SecureRandom random = new SecureRandom();
|
||||
ProfileKeyCredentialRequestContext requestContext = null;
|
||||
|
||||
WebSocketRequestMessage.Builder builder = WebSocketRequestMessage.newBuilder()
|
||||
.setId(random.nextLong())
|
||||
.setVerb("GET")
|
||||
.addAllHeaders(headers);
|
||||
|
||||
if (FeatureFlags.VERSIONED_PROFILES && requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL && uuid.isPresent() && profileKey.isPresent()) {
|
||||
ProfileKeyVersion profileKeyIdentifier = profileKey.get().getProfileKeyVersion();
|
||||
UUID target = uuid.get();
|
||||
requestContext = clientZkProfile.createProfileKeyCredentialRequestContext(random, target, profileKey.get());
|
||||
ProfileKeyCredentialRequest request = requestContext.getRequest();
|
||||
|
||||
String version = profileKeyIdentifier.serialize();
|
||||
String credentialRequest = Hex.toStringCondensed(request.serialize());
|
||||
|
||||
builder.setPath(String.format("/v1/profile/%s/%s/%s", target, version, credentialRequest));
|
||||
} else {
|
||||
builder.setPath(String.format("/v1/profile/%s", address.getIdentifier()));
|
||||
}
|
||||
|
||||
WebSocketRequestMessage requestMessage = builder.build();
|
||||
|
||||
Pair<Integer, String> response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS);
|
||||
|
||||
|
@ -170,8 +208,13 @@ public class SignalServiceMessagePipe {
|
|||
throw new IOException("Non-successful response: " + response.first());
|
||||
}
|
||||
|
||||
return JsonUtil.fromJson(response.second(), SignalServiceProfile.class);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
SignalServiceProfile signalServiceProfile = JsonUtil.fromJson(response.second(), SignalServiceProfile.class);
|
||||
ProfileKeyCredential profileKeyCredential = requestContext != null && signalServiceProfile.getProfileKeyCredentialResponse() != null
|
||||
? clientZkProfile.receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse())
|
||||
: null;
|
||||
|
||||
return new ProfileAndCredential(signalServiceProfile, requestType, Optional.fromNullable(profileKeyCredential));
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException | VerificationFailedException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,14 @@
|
|||
|
||||
package org.whispersystems.signalservice.api;
|
||||
|
||||
import org.signal.zkgroup.ServerPublicParams;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
|
@ -16,6 +22,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
|
|||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
|
@ -54,6 +61,7 @@ public class SignalServiceMessageReceiver {
|
|||
private final String signalAgent;
|
||||
private final ConnectivityListener connectivityListener;
|
||||
private final SleepTimer sleepTimer;
|
||||
private final ClientZkProfileOperations clientZkProfile;
|
||||
|
||||
/**
|
||||
* Construct a SignalServiceMessageReceiver.
|
||||
|
@ -91,6 +99,7 @@ public class SignalServiceMessageReceiver {
|
|||
this.signalAgent = signalAgent;
|
||||
this.connectivityListener = listener;
|
||||
this.sleepTimer = timer;
|
||||
this.clientZkProfile = new ClientZkProfileOperations(new ServerPublicParams(urls.getZkGroupServerPublicParams()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -110,10 +119,21 @@ public class SignalServiceMessageReceiver {
|
|||
return retrieveAttachment(pointer, destination, maxSizeBytes, null);
|
||||
}
|
||||
|
||||
public SignalServiceProfile retrieveProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess)
|
||||
throws IOException
|
||||
public ProfileAndCredential retrieveProfile(SignalServiceAddress address,
|
||||
Optional<ProfileKey> profileKey,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
SignalServiceProfile.RequestType requestType)
|
||||
throws IOException, VerificationFailedException
|
||||
{
|
||||
return socket.retrieveProfile(address, unidentifiedAccess);
|
||||
Optional<UUID> uuid = address.getUuid();
|
||||
|
||||
if (FeatureFlags.VERSIONED_PROFILES && requestType == SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL && uuid.isPresent() && profileKey.isPresent()) {
|
||||
return socket.retrieveProfile(uuid.get(), profileKey.get(), unidentifiedAccess);
|
||||
} else {
|
||||
return new ProfileAndCredential(socket.retrieveProfile(address, unidentifiedAccess),
|
||||
SignalServiceProfile.RequestType.PROFILE,
|
||||
Optional.<ProfileKeyCredential>absent());
|
||||
}
|
||||
}
|
||||
|
||||
public SignalServiceProfile retrieveProfileByUsername(String username, Optional<UnidentifiedAccess> unidentifiedAccess)
|
||||
|
@ -122,7 +142,7 @@ public class SignalServiceMessageReceiver {
|
|||
return socket.retrieveProfileByUsername(username, unidentifiedAccess);
|
||||
}
|
||||
|
||||
public InputStream retrieveProfileAvatar(String path, File destination, byte[] profileKey, int maxSizeBytes)
|
||||
public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, int maxSizeBytes)
|
||||
throws IOException
|
||||
{
|
||||
socket.retrieveProfileAvatar(path, destination, maxSizeBytes);
|
||||
|
@ -203,7 +223,7 @@ public class SignalServiceMessageReceiver {
|
|||
sleepTimer,
|
||||
urls.getNetworkInterceptors());
|
||||
|
||||
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider));
|
||||
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile);
|
||||
}
|
||||
|
||||
public SignalServiceMessagePipe createUnidentifiedMessagePipe() {
|
||||
|
@ -213,7 +233,7 @@ public class SignalServiceMessageReceiver {
|
|||
sleepTimer,
|
||||
urls.getNetworkInterceptors());
|
||||
|
||||
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider));
|
||||
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile);
|
||||
}
|
||||
|
||||
public List<SignalServiceEnvelope> retrieveMessages() throws IOException {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.whispersystems.signalservice.api.crypto;
|
||||
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
|
@ -21,9 +22,9 @@ public class ProfileCipher {
|
|||
|
||||
public static final int NAME_PADDED_LENGTH = 53;
|
||||
|
||||
private final byte[] key;
|
||||
private final ProfileKey key;
|
||||
|
||||
public ProfileCipher(byte[] key) {
|
||||
public ProfileCipher(ProfileKey key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
@ -40,7 +41,7 @@ public class ProfileCipher {
|
|||
byte[] nonce = Util.getSecretBytes(12);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
|
||||
|
||||
return ByteUtil.combine(nonce, cipher.doFinal(inputPadded));
|
||||
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | BadPaddingException | NoSuchPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
|
||||
|
@ -58,7 +59,7 @@ public class ProfileCipher {
|
|||
System.arraycopy(input, 0, nonce, 0, nonce.length);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
|
||||
|
||||
byte[] paddedPlaintext = cipher.doFinal(input, nonce.length, input.length - nonce.length);
|
||||
int plaintextLength = 0;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.whispersystems.signalservice.api.crypto;
|
||||
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
|
@ -24,7 +25,7 @@ public class ProfileCipherInputStream extends FilterInputStream {
|
|||
|
||||
private boolean finished = false;
|
||||
|
||||
public ProfileCipherInputStream(InputStream in, byte[] key) throws IOException {
|
||||
public ProfileCipherInputStream(InputStream in, ProfileKey key) throws IOException {
|
||||
super(in);
|
||||
|
||||
try {
|
||||
|
@ -33,7 +34,7 @@ public class ProfileCipherInputStream extends FilterInputStream {
|
|||
byte[] nonce = new byte[12];
|
||||
Util.readFully(in, nonce);
|
||||
|
||||
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce));
|
||||
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.whispersystems.signalservice.api.crypto;
|
|||
|
||||
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
|
||||
import org.signal.libsignal.metadata.certificate.SenderCertificate;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
|
@ -36,13 +37,13 @@ public class UnidentifiedAccess {
|
|||
return unidentifiedCertificate;
|
||||
}
|
||||
|
||||
public static byte[] deriveAccessKeyFrom(byte[] profileKey) {
|
||||
public static byte[] deriveAccessKeyFrom(ProfileKey profileKey) {
|
||||
try {
|
||||
byte[] nonce = new byte[12];
|
||||
byte[] input = new byte[16];
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(profileKey, "AES"), new GCMParameterSpec(128, nonce));
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(profileKey.serialize(), "AES"), new GCMParameterSpec(128, nonce));
|
||||
|
||||
byte[] ciphertext = cipher.doFinal(input);
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
@ -17,10 +18,10 @@ public class DeviceContact {
|
|||
private final Optional<SignalServiceAttachmentStream> avatar;
|
||||
private final Optional<String> color;
|
||||
private final Optional<VerifiedMessage> verified;
|
||||
private final Optional<byte[]> profileKey;
|
||||
private final Optional<ProfileKey> profileKey;
|
||||
private final boolean blocked;
|
||||
private final Optional<Integer> expirationTimer;
|
||||
private final Optional<Integer> inboxPosition;
|
||||
private final Optional<Integer> inboxPosition;
|
||||
private final boolean archived;
|
||||
|
||||
public DeviceContact(SignalServiceAddress address,
|
||||
|
@ -28,7 +29,7 @@ public class DeviceContact {
|
|||
Optional<SignalServiceAttachmentStream> avatar,
|
||||
Optional<String> color,
|
||||
Optional<VerifiedMessage> verified,
|
||||
Optional<byte[]> profileKey,
|
||||
Optional<ProfileKey> profileKey,
|
||||
boolean blocked,
|
||||
Optional<Integer> expirationTimer,
|
||||
Optional<Integer> inboxPosition,
|
||||
|
@ -66,7 +67,7 @@ public class DeviceContact {
|
|||
return verified;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getProfileKey() {
|
||||
public Optional<ProfileKey> getProfileKey() {
|
||||
return profileKey;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
|
@ -44,7 +46,7 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
|
|||
Optional<SignalServiceAttachmentStream> avatar = Optional.absent();
|
||||
Optional<String> color = details.hasColor() ? Optional.of(details.getColor()) : Optional.<String>absent();
|
||||
Optional<VerifiedMessage> verified = Optional.absent();
|
||||
Optional<byte[]> profileKey = Optional.absent();
|
||||
Optional<ProfileKey> profileKey = Optional.absent();
|
||||
boolean blocked = false;
|
||||
Optional<Integer> expireTimer = Optional.absent();
|
||||
Optional<Integer> inboxPosition = Optional.absent();
|
||||
|
@ -84,7 +86,11 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
|
|||
}
|
||||
|
||||
if (details.hasProfileKey()) {
|
||||
profileKey = Optional.fromNullable(details.getProfileKey().toByteArray());
|
||||
try {
|
||||
profileKey = Optional.fromNullable(new ProfileKey(details.getProfileKey().toByteArray()));
|
||||
} catch (InvalidInputException e) {
|
||||
Log.w(TAG, "Invalid profile key ignored", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (details.hasExpireTimer() && details.getExpireTimer() > 0) {
|
||||
|
|
|
@ -85,7 +85,7 @@ public class DeviceContactsOutputStream extends ChunkedOutputStream {
|
|||
}
|
||||
|
||||
if (contact.getProfileKey().isPresent()) {
|
||||
contactDetails.setProfileKey(ByteString.copyFrom(contact.getProfileKey().get()));
|
||||
contactDetails.setProfileKey(ByteString.copyFrom(contact.getProfileKey().get().serialize()));
|
||||
}
|
||||
|
||||
if (contact.getExpirationTimer().isPresent()) {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package org.whispersystems.signalservice.api.profiles;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
public final class ProfileAndCredential {
|
||||
|
||||
private final SignalServiceProfile profile;
|
||||
private final SignalServiceProfile.RequestType requestType;
|
||||
private final Optional<ProfileKeyCredential> profileKeyCredential;
|
||||
|
||||
public ProfileAndCredential(SignalServiceProfile profile,
|
||||
SignalServiceProfile.RequestType requestType,
|
||||
Optional<ProfileKeyCredential> profileKeyCredential)
|
||||
{
|
||||
this.profile = profile;
|
||||
this.requestType = requestType;
|
||||
this.profileKeyCredential = profileKeyCredential;
|
||||
}
|
||||
|
||||
public SignalServiceProfile getProfile() {
|
||||
return profile;
|
||||
}
|
||||
|
||||
public SignalServiceProfile.RequestType getRequestType() {
|
||||
return requestType;
|
||||
}
|
||||
|
||||
public Optional<ProfileKeyCredential> getProfileKeyCredential() {
|
||||
return profileKeyCredential;
|
||||
}
|
||||
}
|
|
@ -1,16 +1,28 @@
|
|||
package org.whispersystems.signalservice.api.profiles;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||
import org.whispersystems.libsignal.logging.Log;
|
||||
import org.whispersystems.signalservice.FeatureFlags;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class SignalServiceProfile {
|
||||
|
||||
public enum RequestType {
|
||||
PROFILE,
|
||||
PROFILE_AND_CREDENTIAL
|
||||
}
|
||||
|
||||
private static final String TAG = SignalServiceProfile.class.getSimpleName();
|
||||
|
||||
@JsonProperty
|
||||
private String identityKey;
|
||||
|
||||
|
@ -37,6 +49,12 @@ public class SignalServiceProfile {
|
|||
@JsonDeserialize(using = JsonUtil.UuidDeserializer.class)
|
||||
private UUID uuid;
|
||||
|
||||
@JsonProperty
|
||||
private byte[] credential;
|
||||
|
||||
@JsonIgnore
|
||||
private RequestType requestType;
|
||||
|
||||
public SignalServiceProfile() {}
|
||||
|
||||
public String getIdentityKey() {
|
||||
|
@ -71,6 +89,14 @@ public class SignalServiceProfile {
|
|||
return uuid;
|
||||
}
|
||||
|
||||
public RequestType getRequestType() {
|
||||
return requestType;
|
||||
}
|
||||
|
||||
public void setRequestType(RequestType requestType) {
|
||||
this.requestType = requestType;
|
||||
}
|
||||
|
||||
public static class Capabilities {
|
||||
@JsonProperty
|
||||
private boolean uuid;
|
||||
|
@ -81,4 +107,17 @@ public class SignalServiceProfile {
|
|||
return uuid;
|
||||
}
|
||||
}
|
||||
|
||||
public ProfileKeyCredentialResponse getProfileKeyCredentialResponse() {
|
||||
if (!FeatureFlags.VERSIONED_PROFILES) return null;
|
||||
|
||||
if (credential == null) return null;
|
||||
|
||||
try {
|
||||
return new ProfileKeyCredentialResponse(credential);
|
||||
} catch (InvalidInputException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -2,11 +2,12 @@ package org.whispersystems.signalservice.api.storage;
|
|||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public class SignalContactRecord {
|
||||
public final class SignalContactRecord {
|
||||
|
||||
private final byte[] key;
|
||||
private final SignalServiceAddress address;
|
||||
|
@ -18,7 +19,7 @@ public class SignalContactRecord {
|
|||
private final boolean blocked;
|
||||
private final boolean profileSharingEnabled;
|
||||
private final Optional<String> nickname;
|
||||
private final int protoVersion;
|
||||
private final int protoVersion;
|
||||
|
||||
private SignalContactRecord(byte[] key,
|
||||
SignalServiceAddress address,
|
||||
|
@ -42,7 +43,7 @@ public class SignalContactRecord {
|
|||
this.blocked = blocked;
|
||||
this.profileSharingEnabled = profileSharingEnabled;
|
||||
this.nickname = Optional.fromNullable(nickname);
|
||||
this.protoVersion = protoVersion;
|
||||
this.protoVersion = protoVersion;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
|
@ -98,18 +99,20 @@ public class SignalContactRecord {
|
|||
profileSharingEnabled == contact.profileSharingEnabled &&
|
||||
Arrays.equals(key, contact.key) &&
|
||||
Objects.equals(address, contact.address) &&
|
||||
Objects.equals(profileName, contact.profileName) &&
|
||||
Objects.equals(profileKey, contact.profileKey) &&
|
||||
Objects.equals(username, contact.username) &&
|
||||
Objects.equals(identityKey, contact.identityKey) &&
|
||||
profileName.equals(contact.profileName) &&
|
||||
OptionalUtil.byteArrayEquals(profileKey, contact.profileKey) &&
|
||||
username.equals(contact.username) &&
|
||||
OptionalUtil.byteArrayEquals(identityKey, contact.identityKey) &&
|
||||
identityState == contact.identityState &&
|
||||
Objects.equals(nickname, contact.nickname);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(address, profileName, profileKey, username, identityKey, identityState, blocked, profileSharingEnabled, nickname);
|
||||
int result = Objects.hash(address, profileName, username, identityState, blocked, profileSharingEnabled, nickname);
|
||||
result = 31 * result + Arrays.hashCode(key);
|
||||
result = 31 * result + OptionalUtil.byteArrayHashCode(profileKey);
|
||||
result = 31 * result + OptionalUtil.byteArrayHashCode(identityKey);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -138,7 +141,7 @@ public class SignalContactRecord {
|
|||
}
|
||||
|
||||
public Builder setProfileKey(byte[] profileKey) {
|
||||
this.profileKey= profileKey;
|
||||
this.profileKey = profileKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,8 +46,8 @@ public final class SignalStorageModels {
|
|||
}
|
||||
|
||||
public static SignalContactRecord remoteToLocalContactRecord(byte[] storageKey, ContactRecord contact) throws IOException {
|
||||
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164());
|
||||
SignalContactRecord.Builder builder = new SignalContactRecord.Builder(storageKey, address);
|
||||
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164());
|
||||
SignalContactRecord.Builder builder = new SignalContactRecord.Builder(storageKey, address);
|
||||
|
||||
if (contact.hasBlocked()) {
|
||||
builder.setBlocked(contact.getBlocked());
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package org.whispersystems.signalservice.api.util;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class OptionalUtil {
|
||||
|
||||
private OptionalUtil() {
|
||||
}
|
||||
|
||||
public static boolean byteArrayEquals(Optional<byte[]> a, Optional<byte[]> b) {
|
||||
if (a.isPresent() != b.isPresent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (a.isPresent()) {
|
||||
return Arrays.equals(a.get(), b.get());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static int byteArrayHashCode(Optional<byte[]> bytes) {
|
||||
return bytes.isPresent() ? Arrays.hashCode(bytes.get()) : 0;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,14 @@
|
|||
package org.whispersystems.signalservice.api.util;
|
||||
|
||||
import org.whispersystems.libsignal.logging.Log;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class StreamDetails {
|
||||
public final class StreamDetails implements Closeable {
|
||||
|
||||
private static final String TAG = StreamDetails.class.getSimpleName();
|
||||
|
||||
private final InputStream stream;
|
||||
private final String contentType;
|
||||
|
@ -26,4 +31,13 @@ public class StreamDetails {
|
|||
public long getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
package org.whispersystems.signalservice.internal.configuration;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.Interceptor;
|
||||
|
||||
public class SignalServiceConfiguration {
|
||||
public final class SignalServiceConfiguration {
|
||||
|
||||
private final SignalServiceUrl[] signalServiceUrls;
|
||||
private final SignalCdnUrl[] signalCdnUrls;
|
||||
|
@ -13,13 +12,15 @@ public class SignalServiceConfiguration {
|
|||
private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls;
|
||||
private final SignalStorageUrl[] signalStorageUrls;
|
||||
private final List<Interceptor> networkInterceptors;
|
||||
private final byte[] zkGroupServerPublicParams;
|
||||
|
||||
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
|
||||
SignalCdnUrl[] signalCdnUrls,
|
||||
SignalContactDiscoveryUrl[] signalContactDiscoveryUrls,
|
||||
SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls,
|
||||
SignalStorageUrl[] signalStorageUrls,
|
||||
List<Interceptor> networkInterceptors)
|
||||
List<Interceptor> networkInterceptors,
|
||||
byte[] zkGroupServerPublicParams)
|
||||
{
|
||||
this.signalServiceUrls = signalServiceUrls;
|
||||
this.signalCdnUrls = signalCdnUrls;
|
||||
|
@ -27,6 +28,7 @@ public class SignalServiceConfiguration {
|
|||
this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls;
|
||||
this.signalStorageUrls = signalStorageUrls;
|
||||
this.networkInterceptors = networkInterceptors;
|
||||
this.zkGroupServerPublicParams = zkGroupServerPublicParams;
|
||||
}
|
||||
|
||||
public SignalServiceUrl[] getSignalServiceUrls() {
|
||||
|
@ -52,4 +54,8 @@ public class SignalServiceConfiguration {
|
|||
public List<Interceptor> getNetworkInterceptors() {
|
||||
return networkInterceptors;
|
||||
}
|
||||
|
||||
public byte[] getZkGroupServerPublicParams() {
|
||||
return zkGroupServerPublicParams;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -4,8 +4,6 @@ package org.whispersystems.signalservice.internal.push;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class ProfileAvatarUploadAttributes {
|
||||
@JsonProperty
|
||||
private String url;
|
||||
|
||||
@JsonProperty
|
||||
private String key;
|
||||
|
@ -30,10 +28,6 @@ public class ProfileAvatarUploadAttributes {
|
|||
|
||||
public ProfileAvatarUploadAttributes() {}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,13 @@ package org.whispersystems.signalservice.internal.push;
|
|||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
|
||||
import org.signal.zkgroup.ServerPublicParams;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyVersion;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.whispersystems.libsignal.logging.Log;
|
||||
|
@ -17,11 +24,14 @@ import org.whispersystems.libsignal.state.PreKeyRecord;
|
|||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
||||
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
|
||||
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||
|
@ -48,6 +58,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResp
|
|||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
||||
|
@ -170,6 +181,7 @@ public class PushServiceSocket {
|
|||
private final CredentialsProvider credentialsProvider;
|
||||
private final String signalAgent;
|
||||
private final SecureRandom random;
|
||||
private final ClientZkOperations clientZkOperations;
|
||||
|
||||
public PushServiceSocket(SignalServiceConfiguration signalServiceConfiguration, CredentialsProvider credentialsProvider, String signalAgent) {
|
||||
this.credentialsProvider = credentialsProvider;
|
||||
|
@ -180,6 +192,7 @@ public class PushServiceSocket {
|
|||
this.keyBackupServiceClients = createConnectionHolders(signalServiceConfiguration.getSignalKeyBackupServiceUrls(), signalServiceConfiguration.getNetworkInterceptors());
|
||||
this.storageClients = createConnectionHolders(signalServiceConfiguration.getSignalStorageUrls(), signalServiceConfiguration.getNetworkInterceptors());
|
||||
this.random = new SecureRandom();
|
||||
this.clientZkOperations = FeatureFlags.ZK_GROUPS ? new ClientZkOperations(new ServerPublicParams(signalServiceConfiguration.getZkGroupServerPublicParams())) : null;
|
||||
}
|
||||
|
||||
public void requestSmsVerificationCode(boolean androidSmsRetriever, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
|
||||
|
@ -543,6 +556,37 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
public ProfileAndCredential retrieveProfile(UUID target, ProfileKey profileKey, Optional<UnidentifiedAccess> unidentifiedAccess)
|
||||
throws NonSuccessfulResponseCodeException, VerificationFailedException
|
||||
{
|
||||
if (!FeatureFlags.VERSIONED_PROFILES) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
try {
|
||||
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion();
|
||||
ProfileKeyCredentialRequestContext requestContext = clientZkOperations.getProfileOperations().createProfileKeyCredentialRequestContext(random, target, profileKey);
|
||||
ProfileKeyCredentialRequest request = requestContext.getRequest();
|
||||
|
||||
String version = profileKeyIdentifier.serialize();
|
||||
String credentialRequest = Hex.toStringCondensed(request.serialize());
|
||||
String subPath = String.format("%s/%s/%s", target, version, credentialRequest);
|
||||
|
||||
String response = makeServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, NO_HEADERS, unidentifiedAccess);
|
||||
|
||||
SignalServiceProfile signalServiceProfile = JsonUtil.fromJson(response, SignalServiceProfile.class);
|
||||
|
||||
ProfileKeyCredential profileKeyCredential = signalServiceProfile.getProfileKeyCredentialResponse() != null
|
||||
? clientZkOperations.getProfileOperations().receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse())
|
||||
: null;
|
||||
|
||||
return new ProfileAndCredential(signalServiceProfile, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, Optional.fromNullable(profileKeyCredential));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
|
||||
}
|
||||
}
|
||||
|
||||
public void retrieveProfileAvatar(String path, File destination, int maxSizeBytes)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
|
@ -550,12 +594,20 @@ public class PushServiceSocket {
|
|||
}
|
||||
|
||||
public void setProfileName(String name) throws NonSuccessfulResponseCodeException, PushNetworkException {
|
||||
if (FeatureFlags.VERSIONED_PROFILES) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
makeServiceRequest(String.format(PROFILE_PATH, "name/" + (name == null ? "" : URLEncoder.encode(name))), "PUT", "");
|
||||
}
|
||||
|
||||
public void setProfileAvatar(ProfileAvatarData profileAvatar)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
if (FeatureFlags.VERSIONED_PROFILES) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
String response = makeServiceRequest(String.format(PROFILE_PATH, "form/avatar"), "GET", null);
|
||||
ProfileAvatarUploadAttributes formAttributes;
|
||||
|
||||
|
@ -576,6 +628,35 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
public void writeProfile(SignalServiceProfileWrite signalServiceProfileWrite, ProfileAvatarData profileAvatar)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
if (!FeatureFlags.VERSIONED_PROFILES) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
String requestBody = JsonUtil.toJson(signalServiceProfileWrite);
|
||||
ProfileAvatarUploadAttributes formAttributes;
|
||||
|
||||
String response = makeServiceRequest(String.format(PROFILE_PATH, ""), "PUT", requestBody);
|
||||
|
||||
if (signalServiceProfileWrite.hasAvatar() && profileAvatar != null) {
|
||||
try {
|
||||
formAttributes = JsonUtil.fromJson(response, ProfileAvatarUploadAttributes.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
|
||||
}
|
||||
|
||||
uploadToCdn("", formAttributes.getAcl(), formAttributes.getKey(),
|
||||
formAttributes.getPolicy(), formAttributes.getAlgorithm(),
|
||||
formAttributes.getCredential(), formAttributes.getDate(),
|
||||
formAttributes.getSignature(), profileAvatar.getData(),
|
||||
profileAvatar.getContentType(), profileAvatar.getDataLength(),
|
||||
profileAvatar.getOutputStreamFactory(), null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void setUsername(String username) throws IOException {
|
||||
makeServiceRequest(String.format(SET_USERNAME_PATH, username), "PUT", "", NO_HEADERS, new ResponseCodeHandler() {
|
||||
@Override
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.whispersystems.signalservice.internal.push.http;
|
||||
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.api.crypto.DigestingOutputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
|
||||
|
||||
|
@ -9,9 +10,9 @@ import java.io.OutputStream;
|
|||
|
||||
public class ProfileCipherOutputStreamFactory implements OutputStreamFactory {
|
||||
|
||||
private final byte[] key;
|
||||
private final ProfileKey key;
|
||||
|
||||
public ProfileCipherOutputStreamFactory(byte[] key) {
|
||||
public ProfileCipherOutputStreamFactory(ProfileKey key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ package org.whispersystems.signalservice.api.crypto;
|
|||
import junit.framework.TestCase;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
@ -16,8 +18,8 @@ public class ProfileCipherTest extends TestCase {
|
|||
Security.insertProviderAt(Conscrypt.newProvider(), 1);
|
||||
}
|
||||
|
||||
public void testEncryptDecrypt() throws InvalidCiphertextException {
|
||||
byte[] key = Util.getSecretBytes(32);
|
||||
public void testEncryptDecrypt() throws InvalidCiphertextException, InvalidInputException {
|
||||
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
|
||||
ProfileCipher cipher = new ProfileCipher(key);
|
||||
byte[] name = cipher.encryptName("Clement\0Duval".getBytes(), ProfileCipher.NAME_PADDED_LENGTH);
|
||||
byte[] plaintext = cipher.decryptName(name);
|
||||
|
@ -25,7 +27,7 @@ public class ProfileCipherTest extends TestCase {
|
|||
}
|
||||
|
||||
public void testEmpty() throws Exception {
|
||||
byte[] key = Util.getSecretBytes(32);
|
||||
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
|
||||
ProfileCipher cipher = new ProfileCipher(key);
|
||||
byte[] name = cipher.encryptName("".getBytes(), 26);
|
||||
byte[] plaintext = cipher.decryptName(name);
|
||||
|
@ -34,7 +36,7 @@ public class ProfileCipherTest extends TestCase {
|
|||
}
|
||||
|
||||
public void testStreams() throws Exception {
|
||||
byte[] key = Util.getSecretBytes(32);
|
||||
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ProfileCipherOutputStream out = new ProfileCipherOutputStream(baos, key);
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ package org.whispersystems.signalservice.api.crypto;
|
|||
import junit.framework.TestCase;
|
||||
|
||||
import org.conscrypt.OpenSSLProvider;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
|
||||
import java.security.Security;
|
||||
import java.util.Arrays;
|
||||
|
@ -15,11 +17,11 @@ public class UnidentifiedAccessTest extends TestCase {
|
|||
|
||||
private final byte[] EXPECTED_RESULT = {(byte)0x5a, (byte)0x72, (byte)0x3a, (byte)0xce, (byte)0xe5, (byte)0x2c, (byte)0x5e, (byte)0xa0, (byte)0x2b, (byte)0x92, (byte)0xa3, (byte)0xa3, (byte)0x60, (byte)0xc0, (byte)0x95, (byte)0x95};
|
||||
|
||||
public void testKeyDerivation() {
|
||||
public void testKeyDerivation() throws InvalidInputException {
|
||||
byte[] key = new byte[32];
|
||||
Arrays.fill(key, (byte)0x02);
|
||||
|
||||
byte[] result = UnidentifiedAccess.deriveAccessKeyFrom(key);
|
||||
byte[] result = UnidentifiedAccess.deriveAccessKeyFrom(new ProfileKey(key));
|
||||
assertTrue(Arrays.equals(result, EXPECTED_RESULT));
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package org.whispersystems.signalservice.api.util;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class OptionalUtilTest {
|
||||
|
||||
@Test
|
||||
public void absent_are_equal() {
|
||||
assertTrue(OptionalUtil.byteArrayEquals(Optional.<byte[]>absent(), Optional.<byte[]>absent()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void first_non_absent_not_equal() {
|
||||
assertFalse(OptionalUtil.byteArrayEquals(Optional.of(new byte[1]), Optional.<byte[]>absent()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void second_non_absent_not_equal() {
|
||||
assertFalse(OptionalUtil.byteArrayEquals(Optional.<byte[]>absent(), Optional.of(new byte[1])));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equal_contents() {
|
||||
byte[] contentsA = new byte[]{1, 2, 3};
|
||||
byte[] contentsB = contentsA.clone();
|
||||
Optional<byte[]> a = Optional.of(contentsA);
|
||||
Optional<byte[]> b = Optional.of(contentsB);
|
||||
assertTrue(OptionalUtil.byteArrayEquals(a, b));
|
||||
assertEquals(OptionalUtil.byteArrayHashCode(a), OptionalUtil.byteArrayHashCode(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void in_equal_contents() {
|
||||
byte[] contentsA = new byte[]{1, 2, 3};
|
||||
byte[] contentsB = new byte[]{4, 5, 6};
|
||||
Optional<byte[]> a = Optional.of(contentsA);
|
||||
Optional<byte[]> b = Optional.of(contentsB);
|
||||
assertFalse(OptionalUtil.byteArrayEquals(a, b));
|
||||
assertNotEquals(OptionalUtil.byteArrayHashCode(a), OptionalUtil.byteArrayHashCode(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hash_code_absent() {
|
||||
assertEquals(0, OptionalUtil.byteArrayHashCode(Optional.<byte[]>absent()));
|
||||
}
|
||||
|
||||
}
|
3
libsignal/zkgroups-api/build.gradle
Normal file
3
libsignal/zkgroups-api/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
|||
apply plugin: 'java-library'
|
||||
|
||||
sourceCompatibility = 1.7
|
|
@ -0,0 +1,6 @@
|
|||
package org.signal.zkgroup;
|
||||
|
||||
public final class InvalidInputException extends Exception {
|
||||
public InvalidInputException() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package org.signal.zkgroup;
|
||||
|
||||
public final class ServerPublicParams {
|
||||
public ServerPublicParams(byte[] zkGroupServerPublicParams) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package org.signal.zkgroup;
|
||||
|
||||
public final class VerificationFailedException extends Exception {
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.signal.zkgroup.profiles;
|
||||
|
||||
public final class ProfileKeyCommitment {
|
||||
private ProfileKeyCommitment() {
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.signal.zkgroup.profiles;
|
||||
|
||||
public final class ProfileKeyCredentialRequest {
|
||||
private ProfileKeyCredentialRequest() {
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.signal.zkgroup.profiles;
|
||||
|
||||
public final class ProfileKeyCredentialRequestContext {
|
||||
private ProfileKeyCredentialRequestContext() {
|
||||
}
|
||||
|
||||
public ProfileKeyCredentialRequest getRequest() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.signal.zkgroup.profiles;
|
||||
|
||||
public final class ProfileKeyVersion {
|
||||
private ProfileKeyVersion() {
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
|
|
Loading…
Add table
Reference in a new issue