Phone number privacy settings and certificate support behind feature flag.

This commit is contained in:
Alan Evans 2020-09-03 18:35:17 -03:00 committed by Cody Henthorne
parent abd3d4b546
commit 7b24e66ed3
23 changed files with 520 additions and 144 deletions

View file

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.crypto;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
@ -10,6 +11,9 @@ import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.keyvalue.CertificateType;
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
@ -44,21 +48,17 @@ public class UnidentifiedAccessUtil {
try { try {
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
byte[] ourUnidentifiedAccessCertificate = TextSecurePreferences.getUnidentifiedAccessCertificate(context); byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(recipient);
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
ourUnidentifiedAccessKey = Util.getSecretBytes(16); ourUnidentifiedAccessKey = Util.getSecretBytes(16);
} }
Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) + Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) +
" | Our access key present? " + (ourUnidentifiedAccessKey != null) +
" | Our certificate present? " + (ourUnidentifiedAccessCertificate != null) + " | Our certificate present? " + (ourUnidentifiedAccessCertificate != null) +
" | UUID certificate supported? " + recipient.isUuidSupported()); " | UUID certificate supported? " + recipient.isUuidSupported());
if (theirUnidentifiedAccessKey != null && if (theirUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
ourUnidentifiedAccessKey != null &&
ourUnidentifiedAccessCertificate != null)
{
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey, return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey,
ourUnidentifiedAccessCertificate), ourUnidentifiedAccessCertificate),
new UnidentifiedAccess(ourUnidentifiedAccessKey, new UnidentifiedAccess(ourUnidentifiedAccessKey,
@ -75,13 +75,13 @@ public class UnidentifiedAccessUtil {
public static Optional<UnidentifiedAccessPair> getAccessForSync(@NonNull Context context) { public static Optional<UnidentifiedAccessPair> getAccessForSync(@NonNull Context context) {
try { try {
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
byte[] ourUnidentifiedAccessCertificate = TextSecurePreferences.getUnidentifiedAccessCertificate(context); byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(Recipient.self());
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
ourUnidentifiedAccessKey = Util.getSecretBytes(16); ourUnidentifiedAccessKey = Util.getSecretBytes(16);
} }
if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) { if (ourUnidentifiedAccessCertificate != null) {
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(ourUnidentifiedAccessKey, return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(ourUnidentifiedAccessKey,
ourUnidentifiedAccessCertificate), ourUnidentifiedAccessCertificate),
new UnidentifiedAccess(ourUnidentifiedAccessKey, new UnidentifiedAccess(ourUnidentifiedAccessKey,
@ -95,6 +95,23 @@ public class UnidentifiedAccessUtil {
} }
} }
private static byte[] getUnidentifiedAccessCertificate(@NonNull Recipient recipient) {
CertificateType certificateType;
PhoneNumberPrivacyValues.PhoneNumberSharingMode sendPhoneNumberTo = SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode();
switch (sendPhoneNumberTo) {
case EVERYONE: certificateType = CertificateType.UUID_AND_E164; break;
case CONTACTS: certificateType = recipient.isSystemContact() ? CertificateType.UUID_AND_E164 : CertificateType.UUID_ONLY; break;
case NOBODY : certificateType = CertificateType.UUID_ONLY; break;
default : throw new AssertionError();
}
Log.i(TAG, String.format("Certificate type for %s with setting %s -> %s", recipient.getId(), sendPhoneNumberTo, certificateType));
return SignalStore.certificateValues()
.getUnidentifiedAccessCertificate(certificateType);
}
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) { private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
ProfileKey theirProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.resolve().getProfileKey()); ProfileKey theirProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.resolve().getProfileKey());

View file

@ -27,6 +27,8 @@ import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.CertificateType;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
@ -46,8 +48,8 @@ import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
@ -58,10 +60,12 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -330,23 +334,34 @@ public abstract class PushSendJob extends SendJob {
protected void rotateSenderCertificateIfNecessary() throws IOException { protected void rotateSenderCertificateIfNecessary() throws IOException {
try { try {
byte[] certificateBytes = TextSecurePreferences.getUnidentifiedAccessCertificate(context); Collection<CertificateType> requiredCertificateTypes = SignalStore.phoneNumberPrivacy()
.getRequiredCertificateTypes();
if (certificateBytes == null) { Log.i(TAG, "Ensuring we have these certificates " + requiredCertificateTypes);
throw new InvalidCertificateException("No certificate was present.");
for (CertificateType certificateType : requiredCertificateTypes) {
byte[] certificateBytes = SignalStore.certificateValues()
.getUnidentifiedAccessCertificate(certificateType);
if (certificateBytes == null) {
throw new InvalidCertificateException(String.format("No certificate %s was present.", certificateType));
}
SenderCertificate certificate = new SenderCertificate(certificateBytes);
if (System.currentTimeMillis() > (certificate.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER)) {
throw new InvalidCertificateException(String.format(Locale.US, "Certificate %s is expired, or close to it. Expires on: %d, currently: %d", certificateType, certificate.getExpiration(), System.currentTimeMillis()));
}
Log.d(TAG, String.format("Certificate %s is valid", certificateType));
} }
SenderCertificate certificate = new SenderCertificate(certificateBytes); Log.d(TAG, "All certificates are valid.");
if (System.currentTimeMillis() > (certificate.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER)) {
throw new InvalidCertificateException("Certificate is expired, or close to it. Expires on: " + certificate.getExpiration() + ", currently: " + System.currentTimeMillis());
}
Log.d(TAG, "Certificate is valid.");
} catch (InvalidCertificateException e) { } catch (InvalidCertificateException e) {
Log.w(TAG, "Certificate was invalid at send time. Fetching a new one.", e); Log.w(TAG, "A certificate was invalid at send time. Fetching new ones.", e);
RotateCertificateJob certificateJob = new RotateCertificateJob(context); if (!ApplicationDependencies.getJobManager().runSynchronously(new RotateCertificateJob(), 5000).isPresent()) {
certificateJob.onRun(); throw new IOException("Timeout rotating certificate");
}
} }
} }

View file

@ -1,35 +1,37 @@
package org.thoughtcrime.securesms.jobs; package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.CertificateType;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException; import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class RotateCertificateJob extends BaseJob { public final class RotateCertificateJob extends BaseJob {
public static final String KEY = "RotateCertificateJob"; public static final String KEY = "RotateCertificateJob";
private static final String TAG = RotateCertificateJob.class.getSimpleName(); private static final String TAG = Log.tag(RotateCertificateJob.class);
public RotateCertificateJob(Context context) { public RotateCertificateJob() {
this(new Job.Parameters.Builder() this(new Job.Parameters.Builder()
.setQueue("__ROTATE_SENDER_CERTIFICATE__") .setQueue("__ROTATE_SENDER_CERTIFICATE__")
.addConstraint(NetworkConstraint.KEY) .addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1)) .setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED) .setMaxAttempts(Parameters.UNLIMITED)
.build()); .build());
setContext(context);
} }
private RotateCertificateJob(@NonNull Job.Parameters parameters) { private RotateCertificateJob(@NonNull Job.Parameters parameters) {
@ -57,10 +59,25 @@ public class RotateCertificateJob extends BaseJob {
} }
synchronized (RotateCertificateJob.class) { synchronized (RotateCertificateJob.class) {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
byte[] certificate = accountManager.getSenderCertificate(); Collection<CertificateType> certificateTypes = SignalStore.phoneNumberPrivacy()
.getAllCertificateTypes();
TextSecurePreferences.setUnidentifiedAccessCertificate(context, certificate); Log.i(TAG, "Rotating these certificates " + certificateTypes);
for (CertificateType certificateType: certificateTypes) {
byte[] certificate;
switch (certificateType) {
case UUID_AND_E164: certificate = accountManager.getSenderCertificate(); break;
case UUID_ONLY : certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy(); break;
default : throw new AssertionError();
}
Log.i(TAG, String.format("Successfully got %s certificate", certificateType));
SignalStore.certificateValues()
.setUnidentifiedAccessCertificate(certificateType, certificate);
}
} }
} }

View file

@ -0,0 +1,6 @@
package org.thoughtcrime.securesms.keyvalue;
public enum CertificateType {
UUID_AND_E164,
UUID_ONLY
}

View file

@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import java.util.Map;
public final class CertificateValues extends SignalStoreValues {
private static final String UD_CERTIFICATE_UUID_AND_E164 = "certificate.uuidAndE164";
private static final String UD_CERTIFICATE_UUID_ONLY = "certificate.uuidOnly";
CertificateValues(@NonNull KeyValueStore store) {
super(store);
}
@Override
void onFirstEverAppLaunch() {
}
@WorkerThread
public void setUnidentifiedAccessCertificate(@NonNull CertificateType certificateType,
@Nullable byte[] certificate)
{
KeyValueStore.Writer writer = getStore().beginWrite();
switch (certificateType) {
case UUID_AND_E164: writer.putBlob(UD_CERTIFICATE_UUID_AND_E164, certificate); break;
case UUID_ONLY : writer.putBlob(UD_CERTIFICATE_UUID_ONLY, certificate); break;
default : throw new AssertionError();
}
writer.commit();
}
public @Nullable byte[] getUnidentifiedAccessCertificate(@NonNull CertificateType certificateType) {
switch (certificateType) {
case UUID_AND_E164: return getBlob(UD_CERTIFICATE_UUID_AND_E164, null);
case UUID_ONLY : return getBlob(UD_CERTIFICATE_UUID_ONLY, null);
default : throw new AssertionError();
}
}
}

View file

@ -0,0 +1,86 @@
package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.FeatureFlags;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
public final class PhoneNumberPrivacyValues extends SignalStoreValues {
public static final String SHARING_MODE = "phoneNumberPrivacy.sharingMode";
public static final String LISTING_MODE = "phoneNumberPrivacy.listingMode";
private static final Collection<CertificateType> REGULAR_CERTIFICATE = Collections.singletonList(CertificateType.UUID_AND_E164);
private static final Collection<CertificateType> PRIVACY_CERTIFICATE = Collections.singletonList(CertificateType.UUID_ONLY);
private static final Collection<CertificateType> BOTH_CERTIFICATES = Collections.unmodifiableCollection(Arrays.asList(CertificateType.UUID_AND_E164, CertificateType.UUID_ONLY));
PhoneNumberPrivacyValues(@NonNull KeyValueStore store) {
super(store);
}
@Override
void onFirstEverAppLaunch() {
// TODO [ALAN] PhoneNumberPrivacy: During registration, set the attribute to so that new registrations start out as not listed
//getStore().beginWrite()
// .putInteger(LISTING_MODE, PhoneNumberListingMode.UNLISTED.ordinal())
// .apply();
}
public @NonNull PhoneNumberSharingMode getPhoneNumberSharingMode() {
if (!FeatureFlags.phoneNumberPrivacy()) return PhoneNumberSharingMode.EVERYONE;
return PhoneNumberSharingMode.values()[getInteger(SHARING_MODE, PhoneNumberSharingMode.EVERYONE.ordinal())];
}
public void setPhoneNumberSharingMode(@NonNull PhoneNumberSharingMode phoneNumberSharingMode) {
putInteger(SHARING_MODE, phoneNumberSharingMode.ordinal());
}
public @NonNull PhoneNumberListingMode getPhoneNumberListingMode() {
if (!FeatureFlags.phoneNumberPrivacy()) return PhoneNumberListingMode.LISTED;
return PhoneNumberListingMode.values()[getInteger(LISTING_MODE, PhoneNumberListingMode.LISTED.ordinal())];
}
public void setPhoneNumberListingMode(@NonNull PhoneNumberListingMode phoneNumberListingMode) {
putInteger(LISTING_MODE, phoneNumberListingMode.ordinal());
}
/**
* If you respect {@link #getPhoneNumberSharingMode}, then you will only ever need to fetch and store
* these certificates types.
*/
public Collection<CertificateType> getRequiredCertificateTypes() {
switch (getPhoneNumberSharingMode()) {
case EVERYONE: return REGULAR_CERTIFICATE;
case CONTACTS: return BOTH_CERTIFICATES;
case NOBODY : return PRIVACY_CERTIFICATE;
default : throw new AssertionError();
}
}
/**
* All certificate types required according to the feature flags.
*/
public Collection<CertificateType> getAllCertificateTypes() {
return FeatureFlags.phoneNumberPrivacy() ? BOTH_CERTIFICATES : REGULAR_CERTIFICATE;
}
/**
* Serialized, do not change ordinal/order
*/
public enum PhoneNumberSharingMode {
EVERYONE,
CONTACTS,
NOBODY
}
/**
* Serialized, do not change ordinal/order
*/
public enum PhoneNumberListingMode {
LISTED,
UNLISTED
}
}

View file

@ -13,32 +13,36 @@ public final class SignalStore {
private static final SignalStore INSTANCE = new SignalStore(); private static final SignalStore INSTANCE = new SignalStore();
private final KeyValueStore store; private final KeyValueStore store;
private final KbsValues kbsValues; private final KbsValues kbsValues;
private final RegistrationValues registrationValues; private final RegistrationValues registrationValues;
private final PinValues pinValues; private final PinValues pinValues;
private final RemoteConfigValues remoteConfigValues; private final RemoteConfigValues remoteConfigValues;
private final StorageServiceValues storageServiceValues; private final StorageServiceValues storageServiceValues;
private final UiHints uiHints; private final UiHints uiHints;
private final TooltipValues tooltipValues; private final TooltipValues tooltipValues;
private final MiscellaneousValues misc; private final MiscellaneousValues misc;
private final InternalValues internalValues; private final InternalValues internalValues;
private final EmojiValues emojiValues; private final EmojiValues emojiValues;
private final SettingsValues settingsValues; private final SettingsValues settingsValues;
private final CertificateValues certificateValues;
private final PhoneNumberPrivacyValues phoneNumberPrivacyValues;
private SignalStore() { private SignalStore() {
this.store = ApplicationDependencies.getKeyValueStore(); this.store = ApplicationDependencies.getKeyValueStore();
this.kbsValues = new KbsValues(store); this.kbsValues = new KbsValues(store);
this.registrationValues = new RegistrationValues(store); this.registrationValues = new RegistrationValues(store);
this.pinValues = new PinValues(store); this.pinValues = new PinValues(store);
this.remoteConfigValues = new RemoteConfigValues(store); this.remoteConfigValues = new RemoteConfigValues(store);
this.storageServiceValues = new StorageServiceValues(store); this.storageServiceValues = new StorageServiceValues(store);
this.uiHints = new UiHints(store); this.uiHints = new UiHints(store);
this.tooltipValues = new TooltipValues(store); this.tooltipValues = new TooltipValues(store);
this.misc = new MiscellaneousValues(store); this.misc = new MiscellaneousValues(store);
this.internalValues = new InternalValues(store); this.internalValues = new InternalValues(store);
this.emojiValues = new EmojiValues(store); this.emojiValues = new EmojiValues(store);
this.settingsValues = new SettingsValues(store); this.settingsValues = new SettingsValues(store);
this.certificateValues = new CertificateValues(store);
this.phoneNumberPrivacyValues = new PhoneNumberPrivacyValues(store);
} }
public static void onFirstEverAppLaunch() { public static void onFirstEverAppLaunch() {
@ -52,6 +56,8 @@ public final class SignalStore {
misc().onFirstEverAppLaunch(); misc().onFirstEverAppLaunch();
internalValues().onFirstEverAppLaunch(); internalValues().onFirstEverAppLaunch();
settings().onFirstEverAppLaunch(); settings().onFirstEverAppLaunch();
certificateValues().onFirstEverAppLaunch();
phoneNumberPrivacy().onFirstEverAppLaunch();
} }
public static @NonNull KbsValues kbsValues() { public static @NonNull KbsValues kbsValues() {
@ -98,6 +104,14 @@ public final class SignalStore {
return INSTANCE.settingsValues; return INSTANCE.settingsValues;
} }
public static @NonNull CertificateValues certificateValues() {
return INSTANCE.certificateValues;
}
public static @NonNull PhoneNumberPrivacyValues phoneNumberPrivacy() {
return INSTANCE.phoneNumberPrivacyValues;
}
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() { public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
return new GroupsV2AuthorizationSignalStoreCache(getStore()); return new GroupsV2AuthorizationSignalStoreCache(getStore());
} }

View file

@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import java.io.IOException; import java.io.IOException;
import java.util.UUID; import java.util.UUID;
@ -58,7 +57,6 @@ public class UuidMigrationJob extends MigrationJob {
ensureSelfRecipientExists(context); ensureSelfRecipientExists(context);
fetchOwnUuid(context); fetchOwnUuid(context);
rotateSealedSenderCerts(context);
} }
@Override @Override
@ -78,14 +76,6 @@ public class UuidMigrationJob extends MigrationJob {
TextSecurePreferences.setLocalUuid(context, localUuid); TextSecurePreferences.setLocalUuid(context, localUuid);
} }
private static void rotateSealedSenderCerts(@NonNull Context context) throws IOException {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
byte[] certificate = accountManager.getSenderCertificate();
TextSecurePreferences.setUnidentifiedAccessCertificate(context, certificate);
}
public static class Factory implements Job.Factory<UuidMigrationJob> { public static class Factory implements Job.Factory<UuidMigrationJob> {
@Override @Override
public @NonNull UuidMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { public @NonNull UuidMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {

View file

@ -7,6 +7,9 @@ import android.graphics.Color;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.os.Bundle; import android.os.Bundle;
import android.text.InputType; import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.TextAppearanceSpan;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -14,6 +17,7 @@ import android.widget.EditText;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.autofill.HintConstants; import androidx.autofill.HintConstants;
@ -37,6 +41,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.keyvalue.KbsValues; import org.thoughtcrime.securesms.keyvalue.KbsValues;
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
import org.thoughtcrime.securesms.keyvalue.PinValues; import org.thoughtcrime.securesms.keyvalue.PinValues;
import org.thoughtcrime.securesms.keyvalue.SettingsValues; import org.thoughtcrime.securesms.keyvalue.SettingsValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
@ -45,13 +50,13 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.KbsConstants; import org.thoughtcrime.securesms.lock.v2.KbsConstants;
import org.thoughtcrime.securesms.lock.v2.RegistrationLockUtil; import org.thoughtcrime.securesms.lock.v2.RegistrationLockUtil;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.megaphone.Megaphones; import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog; import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
@ -67,8 +72,10 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
private static final String TAG = Log.tag(AppProtectionPreferenceFragment.class); private static final String TAG = Log.tag(AppProtectionPreferenceFragment.class);
private static final String PREFERENCE_CATEGORY_BLOCKED = "preference_category_blocked"; private static final String PREFERENCE_CATEGORY_BLOCKED = "preference_category_blocked";
private static final String PREFERENCE_UNIDENTIFIED_LEARN_MORE = "pref_unidentified_learn_more"; private static final String PREFERENCE_UNIDENTIFIED_LEARN_MORE = "pref_unidentified_learn_more";
private static final String PREFERENCE_WHO_CAN_SEE_PHONE_NUMBER = "pref_who_can_see_phone_number";
private static final String PREFERENCE_WHO_CAN_FIND_BY_PHONE_NUMBER = "pref_who_can_find_by_phone_number";
private CheckBoxPreference disablePassphrase; private CheckBoxPreference disablePassphrase;
@ -99,6 +106,18 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
this.findPreference(PREFERENCE_UNIDENTIFIED_LEARN_MORE).setOnPreferenceClickListener(new UnidentifiedLearnMoreClickListener()); this.findPreference(PREFERENCE_UNIDENTIFIED_LEARN_MORE).setOnPreferenceClickListener(new UnidentifiedLearnMoreClickListener());
disablePassphrase.setOnPreferenceChangeListener(new DisablePassphraseClickListener()); disablePassphrase.setOnPreferenceChangeListener(new DisablePassphraseClickListener());
if (FeatureFlags.phoneNumberPrivacy()) {
Preference whoCanSeePhoneNumber = this.findPreference(PREFERENCE_WHO_CAN_SEE_PHONE_NUMBER);
Preference whoCanFindByPhoneNumber = this.findPreference(PREFERENCE_WHO_CAN_FIND_BY_PHONE_NUMBER);
whoCanSeePhoneNumber.setPreferenceDataStore(null);
whoCanSeePhoneNumber.setOnPreferenceClickListener(new PhoneNumberPrivacyWhoCanSeeClickListener());
whoCanFindByPhoneNumber.setPreferenceDataStore(null);
whoCanFindByPhoneNumber.setOnPreferenceClickListener(new PhoneNumberPrivacyWhoCanFindClickListener());
} else {
this.findPreference("category_phone_number_privacy").setVisible(false);
}
SwitchPreferenceCompat linkPreviewPref = (SwitchPreferenceCompat) this.findPreference(SettingsValues.LINK_PREVIEWS); SwitchPreferenceCompat linkPreviewPref = (SwitchPreferenceCompat) this.findPreference(SettingsValues.LINK_PREVIEWS);
linkPreviewPref.setChecked(SignalStore.settings().isLinkPreviewsEnabled()); linkPreviewPref.setChecked(SignalStore.settings().isLinkPreviewsEnabled());
@ -138,6 +157,9 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
signalPinReminders.setEnabled(false); signalPinReminders.setEnabled(false);
registrationLockV2.setEnabled(false); registrationLockV2.setEnabled(false);
} }
initializePhoneNumberPrivacyWhoCanSeeSummary();
initializePhoneNumberPrivacyWhoCanFindSummary();
} }
@Override @Override
@ -164,6 +186,27 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds)); String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds));
} }
private void initializePhoneNumberPrivacyWhoCanSeeSummary() {
Preference preference = findPreference(PREFERENCE_WHO_CAN_SEE_PHONE_NUMBER);
switch (SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode()) {
case EVERYONE: preference.setSummary(R.string.PhoneNumberPrivacy_everyone); break;
case CONTACTS: preference.setSummary(R.string.PhoneNumberPrivacy_my_contacts); break;
case NOBODY : preference.setSummary(R.string.PhoneNumberPrivacy_nobody); break;
default : throw new AssertionError();
}
}
private void initializePhoneNumberPrivacyWhoCanFindSummary() {
Preference preference = findPreference(PREFERENCE_WHO_CAN_FIND_BY_PHONE_NUMBER);
switch (SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode()) {
case LISTED : preference.setSummary(R.string.PhoneNumberPrivacy_everyone); break;
case UNLISTED: preference.setSummary(R.string.PhoneNumberPrivacy_nobody); break;
default : throw new AssertionError();
}
}
private void initializeVisibility() { private void initializeVisibility() {
if (TextSecurePreferences.isPasswordDisabled(getContext())) { if (TextSecurePreferences.isPasswordDisabled(getContext())) {
findPreference("pref_enable_passphrase_temporary").setVisible(false); findPreference("pref_enable_passphrase_temporary").setVisible(false);
@ -504,4 +547,86 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
} }
} }
} }
private final class PhoneNumberPrivacyWhoCanSeeClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
PhoneNumberPrivacyValues phoneNumberPrivacyValues = SignalStore.phoneNumberPrivacy();
final PhoneNumberPrivacyValues.PhoneNumberSharingMode[] value = { phoneNumberPrivacyValues.getPhoneNumberSharingMode() };
new AlertDialog.Builder(requireActivity())
.setTitle(R.string.preferences_app_protection__see_my_phone_number)
.setCancelable(true)
.setSingleChoiceItems(items(requireContext()), value[0].ordinal(), (dialog, which) -> value[0] = PhoneNumberPrivacyValues.PhoneNumberSharingMode.values()[which])
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
PhoneNumberPrivacyValues.PhoneNumberSharingMode phoneNumberSharingMode = value[0];
phoneNumberPrivacyValues.setPhoneNumberSharingMode(phoneNumberSharingMode);
Log.i(TAG, String.format("PhoneNumberSharingMode changed to %s. Scheduling storage value sync", phoneNumberSharingMode));
StorageSyncHelper.scheduleSyncForDataChange();
initializePhoneNumberPrivacyWhoCanSeeSummary();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
return true;
}
private CharSequence[] items(Context context) {
return new CharSequence[]{
titleAndDescription(context, context.getString(R.string.PhoneNumberPrivacy_everyone), context.getString(R.string.PhoneNumberPrivacy_everyone_see_description)),
titleAndDescription(context, context.getString(R.string.PhoneNumberPrivacy_my_contacts), context.getString(R.string.PhoneNumberPrivacy_my_contacts_see_description)),
context.getString(R.string.PhoneNumberPrivacy_nobody) };
}
}
private final class PhoneNumberPrivacyWhoCanFindClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
PhoneNumberPrivacyValues phoneNumberPrivacyValues = SignalStore.phoneNumberPrivacy();
final PhoneNumberPrivacyValues.PhoneNumberListingMode[] value = { phoneNumberPrivacyValues.getPhoneNumberListingMode() };
new AlertDialog.Builder(requireActivity())
.setTitle(R.string.preferences_app_protection__find_me_by_phone_number)
.setCancelable(true)
.setSingleChoiceItems(items(requireContext()),
value[0].ordinal(),
(dialog, which) -> value[0] = PhoneNumberPrivacyValues.PhoneNumberListingMode.values()[which])
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
PhoneNumberPrivacyValues.PhoneNumberListingMode phoneNumberListingMode = value[0];
phoneNumberPrivacyValues.setPhoneNumberListingMode(phoneNumberListingMode);
Log.i(TAG, String.format("PhoneNumberListingMode changed to %s. Scheduling storage value sync", phoneNumberListingMode));
StorageSyncHelper.scheduleSyncForDataChange();
initializePhoneNumberPrivacyWhoCanFindSummary();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
return true;
}
private CharSequence[] items(Context context) {
return new CharSequence[]{
titleAndDescription(context, context.getString(R.string.PhoneNumberPrivacy_everyone), context.getString(R.string.PhoneNumberPrivacy_everyone_find_description)),
context.getString(R.string.PhoneNumberPrivacy_nobody) };
}
}
/** Adds a detail row for radio group descriptions. */
private static CharSequence titleAndDescription(@NonNull Context context, @NonNull String header, @NonNull String description) {
SpannableStringBuilder builder = new SpannableStringBuilder();
builder.append("\n");
builder.append(header);
builder.append("\n");
builder.setSpan(new TextAppearanceSpan(context, android.R.style.TextAppearance_Small), builder.length(), builder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
builder.append(description);
builder.append("\n");
return builder;
}
} }

View file

@ -174,7 +174,7 @@ public final class CodeVerificationRequest {
private static void handleSuccessfulRegistration(@NonNull Context context) { private static void handleSuccessfulRegistration(@NonNull Context context) {
JobManager jobManager = ApplicationDependencies.getJobManager(); JobManager jobManager = ApplicationDependencies.getJobManager();
jobManager.add(new DirectoryRefreshJob(false)); jobManager.add(new DirectoryRefreshJob(false));
jobManager.add(new RotateCertificateJob(context)); jobManager.add(new RotateCertificateJob());
DirectoryRefreshListener.schedule(context); DirectoryRefreshListener.schedule(context);
RotateSignedPreKeyListener.schedule(context); RotateSignedPreKeyListener.schedule(context);

View file

@ -4,7 +4,6 @@ package org.thoughtcrime.securesms.service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.RotateCertificateJob; import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -22,7 +21,7 @@ public class RotateSenderCertificateListener extends PersistentAlarmManagerListe
@Override @Override
protected long onAlarm(Context context, long scheduledTime) { protected long onAlarm(Context context, long scheduledTime) {
ApplicationDependencies.getJobManager().add(new RotateCertificateJob(context)); ApplicationDependencies.getJobManager().add(new RotateCertificateJob());
long nextTime = System.currentTimeMillis() + INTERVAL; long nextTime = System.currentTimeMillis() + INTERVAL;
TextSecurePreferences.setUnidentifiedAccessCertificateRotationTime(context, nextTime); TextSecurePreferences.setUnidentifiedAccessCertificateRotationTime(context, nextTime);

View file

@ -6,6 +6,7 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.storage.SignalAccountRecord; import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -55,16 +56,18 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
familyName = local.getFamilyName().or(""); familyName = local.getFamilyName().or("");
} }
byte[] unknownFields = remote.serializeUnknownFields(); byte[] unknownFields = remote.serializeUnknownFields();
String avatarUrlPath = remote.getAvatarUrlPath().or(local.getAvatarUrlPath()).or(""); String avatarUrlPath = remote.getAvatarUrlPath().or(local.getAvatarUrlPath()).or("");
byte[] profileKey = remote.getProfileKey().or(local.getProfileKey()).orNull(); byte[] profileKey = remote.getProfileKey().or(local.getProfileKey()).orNull();
boolean noteToSelfArchived = remote.isNoteToSelfArchived(); boolean noteToSelfArchived = remote.isNoteToSelfArchived();
boolean readReceipts = remote.isReadReceiptsEnabled(); boolean readReceipts = remote.isReadReceiptsEnabled();
boolean typingIndicators = remote.isTypingIndicatorsEnabled(); boolean typingIndicators = remote.isTypingIndicatorsEnabled();
boolean sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled(); boolean sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled();
boolean linkPreviews = remote.isLinkPreviewsEnabled(); boolean linkPreviews = remote.isLinkPreviewsEnabled();
boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews); boolean unlisted = remote.isPhoneNumberUnlisted();
boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews); AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode = remote.getPhoneNumberSharingMode();
boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted);
boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted );
if (matchesRemote) { if (matchesRemote) {
return remote; return remote;
@ -82,6 +85,9 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
.setTypingIndicatorsEnabled(typingIndicators) .setTypingIndicatorsEnabled(typingIndicators)
.setSealedSenderIndicatorsEnabled(sealedSenderIndicators) .setSealedSenderIndicatorsEnabled(sealedSenderIndicators)
.setLinkPreviewsEnabled(linkPreviews) .setLinkPreviewsEnabled(linkPreviews)
.setUnlistedPhoneNumber(unlisted)
.setPhoneNumberSharingMode(phoneNumberSharingMode)
.setUnlistedPhoneNumber(unlisted)
.build(); .build();
} }
} }
@ -96,7 +102,9 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
boolean readReceipts, boolean readReceipts,
boolean typingIndicators, boolean typingIndicators,
boolean sealedSenderIndicators, boolean sealedSenderIndicators,
boolean linkPreviewsEnabled) boolean linkPreviewsEnabled,
AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode,
boolean unlistedPhoneNumber)
{ {
return Arrays.equals(contact.serializeUnknownFields(), unknownFields) && return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
Objects.equals(contact.getGivenName().or(""), givenName) && Objects.equals(contact.getGivenName().or(""), givenName) &&
@ -107,6 +115,8 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
contact.isReadReceiptsEnabled() == readReceipts && contact.isReadReceiptsEnabled() == readReceipts &&
contact.isTypingIndicatorsEnabled() == typingIndicators && contact.isTypingIndicatorsEnabled() == typingIndicators &&
contact.isSealedSenderIndicatorsEnabled() == sealedSenderIndicators && contact.isSealedSenderIndicatorsEnabled() == sealedSenderIndicators &&
contact.isLinkPreviewsEnabled() == linkPreviewsEnabled; contact.isLinkPreviewsEnabled() == linkPreviewsEnabled &&
contact.getPhoneNumberSharingMode() == phoneNumberSharingMode &&
contact.isPhoneNumberUnlisted() == unlistedPhoneNumber;
} }
} }

View file

@ -14,11 +14,11 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob; import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -32,6 +32,7 @@ import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.util.OptionalUtil; import org.whispersystems.signalservice.api.util.OptionalUtil;
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -398,11 +399,22 @@ public final class StorageSyncHelper {
.setReadReceiptsEnabled(TextSecurePreferences.isReadReceiptsEnabled(context)) .setReadReceiptsEnabled(TextSecurePreferences.isReadReceiptsEnabled(context))
.setSealedSenderIndicatorsEnabled(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context)) .setSealedSenderIndicatorsEnabled(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context))
.setLinkPreviewsEnabled(SignalStore.settings().isLinkPreviewsEnabled()) .setLinkPreviewsEnabled(SignalStore.settings().isLinkPreviewsEnabled())
.setUnlistedPhoneNumber(SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode() == PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED)
.setPhoneNumberSharingMode(localToRemotePhoneNumberSharingMode(SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode()))
.build(); .build();
return SignalStorageRecord.forAccount(account); return SignalStorageRecord.forAccount(account);
} }
private static AccountRecord.PhoneNumberSharingMode localToRemotePhoneNumberSharingMode(PhoneNumberPrivacyValues.PhoneNumberSharingMode phoneNumberPhoneNumberSharingMode) {
switch (phoneNumberPhoneNumberSharingMode) {
case EVERYONE: return AccountRecord.PhoneNumberSharingMode.EVERYBODY;
case CONTACTS: return AccountRecord.PhoneNumberSharingMode.CONTACTS_ONLY;
case NOBODY : return AccountRecord.PhoneNumberSharingMode.NOBODY;
default : throw new AssertionError();
}
}
public static void applyAccountStorageSyncUpdates(@NonNull Context context, Optional<StorageSyncHelper.RecordUpdate<SignalAccountRecord>> update) { public static void applyAccountStorageSyncUpdates(@NonNull Context context, Optional<StorageSyncHelper.RecordUpdate<SignalAccountRecord>> update) {
if (!update.isPresent()) { if (!update.isPresent()) {
return; return;

View file

@ -49,20 +49,21 @@ public final class FeatureFlags {
private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2); private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2);
private static final String USERNAMES = "android.usernames"; private static final String USERNAMES = "android.usernames";
private static final String ATTACHMENTS_V3 = "android.attachmentsV3.2"; private static final String ATTACHMENTS_V3 = "android.attachmentsV3.2";
private static final String REMOTE_DELETE = "android.remoteDelete"; private static final String REMOTE_DELETE = "android.remoteDelete";
private static final String GROUPS_V2_OLD_1 = "android.groupsv2"; private static final String GROUPS_V2_OLD_1 = "android.groupsv2";
private static final String GROUPS_V2_OLD_2 = "android.groupsv2.2"; private static final String GROUPS_V2_OLD_2 = "android.groupsv2.2";
private static final String GROUPS_V2 = "android.groupsv2.3"; private static final String GROUPS_V2 = "android.groupsv2.3";
private static final String GROUPS_V2_CREATE = "android.groupsv2.create.3"; private static final String GROUPS_V2_CREATE = "android.groupsv2.create.3";
private static final String GROUPS_V2_JOIN_VERSION = "android.groupsv2.joinVersion"; private static final String GROUPS_V2_JOIN_VERSION = "android.groupsv2.joinVersion";
private static final String GROUPS_V2_LINKS_VERSION = "android.groupsv2.manageGroupLinksVersion"; private static final String GROUPS_V2_LINKS_VERSION = "android.groupsv2.manageGroupLinksVersion";
private static final String GROUPS_V2_CAPACITY = "global.groupsv2.maxGroupSize"; private static final String GROUPS_V2_CAPACITY = "global.groupsv2.maxGroupSize";
private static final String CDS_VERSION = "android.cdsVersion"; private static final String CDS_VERSION = "android.cdsVersion";
private static final String INTERNAL_USER = "android.internalUser"; private static final String INTERNAL_USER = "android.internalUser";
private static final String MENTIONS = "android.mentions"; private static final String MENTIONS = "android.mentions";
private static final String VERIFY_V2 = "android.verifyV2"; private static final String VERIFY_V2 = "android.verifyV2";
private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion";
/** /**
* We will only store remote values for flags in this set. If you want a flag to be controllable * We will only store remote values for flags in this set. If you want a flag to be controllable
@ -279,6 +280,14 @@ public final class FeatureFlags {
return getBoolean(VERIFY_V2, false); return getBoolean(VERIFY_V2, false);
} }
/**
* Whether the user can choose phone number privacy settings, and;
* Whether to fetch and store the secondary certificate
*/
public static boolean phoneNumberPrivacy() {
return getVersionFlag(PHONE_NUMBER_PRIVACY_VERSION) == VersionFlag.ON;
}
/** Only for rendering debug info. */ /** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() { public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES); return new TreeMap<>(REMOTE_VALUES);

View file

@ -182,7 +182,6 @@ public class TextSecurePreferences {
private static final String NEEDS_MESSAGE_PULL = "pref_needs_message_pull"; private static final String NEEDS_MESSAGE_PULL = "pref_needs_message_pull";
private static final String UNIDENTIFIED_ACCESS_CERTIFICATE_ROTATION_TIME_PREF = "pref_unidentified_access_certificate_rotation_time"; private static final String UNIDENTIFIED_ACCESS_CERTIFICATE_ROTATION_TIME_PREF = "pref_unidentified_access_certificate_rotation_time";
private static final String UNIDENTIFIED_ACCESS_CERTIFICATE = "pref_unidentified_access_certificate_uuid";
public static final String UNIVERSAL_UNIDENTIFIED_ACCESS = "pref_universal_unidentified_access"; public static final String UNIVERSAL_UNIDENTIFIED_ACCESS = "pref_universal_unidentified_access";
public static final String SHOW_UNIDENTIFIED_DELIVERY_INDICATORS = "pref_show_unidentifed_delivery_indicators"; public static final String SHOW_UNIDENTIFIED_DELIVERY_INDICATORS = "pref_show_unidentifed_delivery_indicators";
private static final String UNIDENTIFIED_DELIVERY_ENABLED = "pref_unidentified_delivery_enabled"; private static final String UNIDENTIFIED_DELIVERY_ENABLED = "pref_unidentified_delivery_enabled";
@ -598,26 +597,6 @@ public class TextSecurePreferences {
setLongPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE_ROTATION_TIME_PREF, value); setLongPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE_ROTATION_TIME_PREF, value);
} }
public static void setUnidentifiedAccessCertificate(Context context, byte[] value) {
setStringPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE, Base64.encodeBytes(value));
}
public static byte[] getUnidentifiedAccessCertificate(Context context) {
return parseCertificate(getStringPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE, null));
}
private static byte[] parseCertificate(String raw) {
try {
if (raw != null) {
return Base64.decode(raw);
}
} catch (IOException e) {
Log.w(TAG, e);
}
return null;
}
public static boolean isUniversalUnidentifiedAccess(Context context) { public static boolean isUniversalUnidentifiedAccess(Context context) {
return getBooleanPreference(context, UNIVERSAL_UNIDENTIFIED_ACCESS, false); return getBooleanPreference(context, UNIVERSAL_UNIDENTIFIED_ACCESS, false);
} }

View file

@ -165,7 +165,8 @@
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/name_container" /> app:layout_constraintTop_toBottomOf="@id/name_container"
tools:visibility="visible" />
<EditText <EditText
android:id="@+id/profile_overview_username" android:id="@+id/profile_overview_username"
@ -182,7 +183,8 @@
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/profile_overview_username_edit_button" app:layout_constraintEnd_toStartOf="@id/profile_overview_username_edit_button"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/profile_overview_username_label" /> app:layout_constraintTop_toBottomOf="@id/profile_overview_username_label"
tools:visibility="visible" />
<ImageView <ImageView
android:id="@+id/profile_overview_username_edit_button" android:id="@+id/profile_overview_username_edit_button"
@ -196,7 +198,8 @@
app:layout_constraintBottom_toBottomOf="@id/profile_overview_username" app:layout_constraintBottom_toBottomOf="@id/profile_overview_username"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/profile_overview_username" app:layout_constraintTop_toTopOf="@id/profile_overview_username"
app:srcCompat="@drawable/ic_compose_solid_24" /> app:srcCompat="@drawable/ic_compose_solid_24"
tools:visibility="visible" />
<org.thoughtcrime.securesms.util.views.LearnMoreTextView <org.thoughtcrime.securesms.util.views.LearnMoreTextView
android:id="@+id/description_text" android:id="@+id/description_text"

View file

@ -2106,6 +2106,7 @@
<string name="preferences_advanced__disable_signal_built_in_emoji_support">Disable Signal\'s built-in emoji support</string> <string name="preferences_advanced__disable_signal_built_in_emoji_support">Disable Signal\'s built-in emoji support</string>
<string name="preferences_advanced__relay_all_calls_through_the_signal_server_to_avoid_revealing_your_ip_address">Relay all calls through the Signal server to avoid revealing your IP address to your contact. Enabling will reduce call quality.</string> <string name="preferences_advanced__relay_all_calls_through_the_signal_server_to_avoid_revealing_your_ip_address">Relay all calls through the Signal server to avoid revealing your IP address to your contact. Enabling will reduce call quality.</string>
<string name="preferences_advanced__always_relay_calls">Always relay calls</string> <string name="preferences_advanced__always_relay_calls">Always relay calls</string>
<string name="preferences_app_protection__who_can">Who can…</string>
<string name="preferences_app_protection__app_access">App access</string> <string name="preferences_app_protection__app_access">App access</string>
<string name="preferences_app_protection__communication">Communication</string> <string name="preferences_app_protection__communication">Communication</string>
<string name="preferences_chats__chats">Chats</string> <string name="preferences_chats__chats">Chats</string>
@ -2505,6 +2506,14 @@
<string name="RegistrationActivity_code_support_subject">Signal Registration - Verification Code for Android</string> <string name="RegistrationActivity_code_support_subject">Signal Registration - Verification Code for Android</string>
<string name="BackupUtil_never">Never</string> <string name="BackupUtil_never">Never</string>
<string name="BackupUtil_unknown">Unknown</string> <string name="BackupUtil_unknown">Unknown</string>
<string name="preferences_app_protection__see_my_phone_number">See my phone number</string>
<string name="preferences_app_protection__find_me_by_phone_number">Find me by phone number</string>
<string name="PhoneNumberPrivacy_everyone">Everyone</string>
<string name="PhoneNumberPrivacy_my_contacts">My contacts</string>
<string name="PhoneNumberPrivacy_nobody">Nobody</string>
<string name="PhoneNumberPrivacy_everyone_see_description">Your phone number will be visible to all people and groups you message.</string>
<string name="PhoneNumberPrivacy_everyone_find_description">Anyone who has your phone number in their contacts will see you as a contact on Signal. Others will be able to find you in search.</string>
<string name="PhoneNumberPrivacy_my_contacts_see_description">Only your contacts will see your phone number on Signal.</string>
<string name="preferences_app_protection__screen_lock">Screen lock</string> <string name="preferences_app_protection__screen_lock">Screen lock</string>
<string name="preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint">Lock Signal access with Android screen lock or fingerprint</string> <string name="preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint">Lock Signal access with Android screen lock or fingerprint</string>
<string name="preferences_app_protection__screen_lock_inactivity_timeout">Screen lock inactivity timeout</string> <string name="preferences_app_protection__screen_lock_inactivity_timeout">Screen lock inactivity timeout</string>

View file

@ -1,6 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="category_phone_number_privacy"
android:title="@string/preferences_app_protection__who_can">
<Preference
android:key="pref_who_can_see_phone_number"
android:title="@string/preferences_app_protection__see_my_phone_number" />
<Preference
android:key="pref_who_can_find_by_phone_number"
android:title="@string/preferences_app_protection__find_me_by_phone_number" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/preferences_app_protection__app_access"> <PreferenceCategory android:title="@string/preferences_app_protection__app_access">
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat <org.thoughtcrime.securesms.components.SwitchPreferenceCompat

View file

@ -9,7 +9,6 @@ package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
@ -80,13 +79,10 @@ import java.security.KeyStore;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SignatureException; import java.security.SignatureException;
import java.sql.Time;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -148,8 +144,8 @@ public class SignalServiceAccountManager {
return this.pushServiceSocket.getSenderCertificate(); return this.pushServiceSocket.getSenderCertificate();
} }
public byte[] getSenderCertificateLegacy() throws IOException { public byte[] getSenderCertificateForPhoneNumberPrivacy() throws IOException {
return this.pushServiceSocket.getSenderCertificateLegacy(); return this.pushServiceSocket.getUuidOnlySenderCertificate();
} }
/** /**

View file

@ -586,6 +586,7 @@ public class SignalServiceMessageSender {
quoteBuilder.setAuthorUuid(message.getQuote().get().getAuthor().getUuid().get().toString()); quoteBuilder.setAuthorUuid(message.getQuote().get().getAuthor().getUuid().get().toString());
} }
// TODO [Alan] PhoneNumberPrivacy: Do not set this number
if (message.getQuote().get().getAuthor().getNumber().isPresent()) { if (message.getQuote().get().getAuthor().getNumber().isPresent()) {
quoteBuilder.setAuthorE164(message.getQuote().get().getAuthor().getNumber().get()); quoteBuilder.setAuthorE164(message.getQuote().get().getAuthor().getNumber().get());
} }
@ -682,6 +683,7 @@ public class SignalServiceMessageSender {
.setRemove(message.getReaction().get().isRemove()) .setRemove(message.getReaction().get().isRemove())
.setTargetSentTimestamp(message.getReaction().get().getTargetSentTimestamp()); .setTargetSentTimestamp(message.getReaction().get().getTargetSentTimestamp());
// TODO [Alan] PhoneNumberPrivacy: Do not set this number
if (message.getReaction().get().getTargetAuthor().getNumber().isPresent()) { if (message.getReaction().get().getTargetAuthor().getNumber().isPresent()) {
reactionBuilder.setTargetAuthorE164(message.getReaction().get().getTargetAuthor().getNumber().get()); reactionBuilder.setTargetAuthorE164(message.getReaction().get().getTargetAuthor().getNumber().get());
} }

View file

@ -80,6 +80,14 @@ public final class SignalAccountRecord implements SignalRecord {
return proto.getLinkPreviews(); return proto.getLinkPreviews();
} }
public AccountRecord.PhoneNumberSharingMode getPhoneNumberSharingMode() {
return proto.getPhoneNumberSharingMode();
}
public boolean isPhoneNumberUnlisted() {
return proto.getUnlistedPhoneNumber();
}
AccountRecord toProto() { AccountRecord toProto() {
return proto; return proto;
} }
@ -159,6 +167,16 @@ public final class SignalAccountRecord implements SignalRecord {
return this; return this;
} }
public Builder setPhoneNumberSharingMode(AccountRecord.PhoneNumberSharingMode mode) {
builder.setPhoneNumberSharingMode(mode);
return this;
}
public Builder setUnlistedPhoneNumber(boolean unlisted) {
builder.setUnlistedPhoneNumber(unlisted);
return this;
}
public SignalAccountRecord build() { public SignalAccountRecord build() {
AccountRecord proto = builder.build(); AccountRecord proto = builder.build();

View file

@ -181,8 +181,8 @@ public class PushServiceSocket {
private static final String PROFILE_PATH = "/v1/profile/%s"; private static final String PROFILE_PATH = "/v1/profile/%s";
private static final String PROFILE_USERNAME_PATH = "/v1/profile/username/%s"; private static final String PROFILE_USERNAME_PATH = "/v1/profile/username/%s";
private static final String SENDER_CERTIFICATE_LEGACY_PATH = "/v1/certificate/delivery"; private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery?includeUuid=true";
private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery?includeUuid=true"; private static final String SENDER_CERTIFICATE_NO_E164_PATH = "/v1/certificate/delivery?includeUuid=true&includeE164=false";
private static final String KBS_AUTH_PATH = "/v1/backup/auth"; private static final String KBS_AUTH_PATH = "/v1/backup/auth";
@ -363,13 +363,13 @@ public class PushServiceSocket {
makeServiceRequest(REGISTRATION_LOCK_PATH, "DELETE", null); makeServiceRequest(REGISTRATION_LOCK_PATH, "DELETE", null);
} }
public byte[] getSenderCertificateLegacy() throws IOException { public byte[] getSenderCertificate() throws IOException {
String responseText = makeServiceRequest(SENDER_CERTIFICATE_LEGACY_PATH, "GET", null); String responseText = makeServiceRequest(SENDER_CERTIFICATE_PATH, "GET", null);
return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate(); return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate();
} }
public byte[] getSenderCertificate() throws IOException { public byte[] getUuidOnlySenderCertificate() throws IOException {
String responseText = makeServiceRequest(SENDER_CERTIFICATE_PATH, "GET", null); String responseText = makeServiceRequest(SENDER_CERTIFICATE_NO_E164_PATH, "GET", null);
return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate(); return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate();
} }

View file

@ -97,15 +97,24 @@ message GroupV2Record {
} }
message AccountRecord { message AccountRecord {
bytes profileKey = 1;
string givenName = 2; enum PhoneNumberSharingMode {
string familyName = 3; EVERYBODY = 0;
string avatarUrlPath = 4; CONTACTS_ONLY = 1;
bool noteToSelfArchived = 5; NOBODY = 2;
bool readReceipts = 6; }
bool sealedSenderIndicators = 7;
bool typingIndicators = 8; bytes profileKey = 1;
bool proxiedLinkPreviews = 9; string givenName = 2;
string familyName = 3;
string avatarUrlPath = 4;
bool noteToSelfArchived = 5;
bool readReceipts = 6;
bool sealedSenderIndicators = 7;
bool typingIndicators = 8;
bool proxiedLinkPreviews = 9;
// 10 is reserved for unread // 10 is reserved for unread
bool linkPreviews = 11; bool linkPreviews = 11;
PhoneNumberSharingMode phoneNumberSharingMode = 12;
bool unlistedPhoneNumber = 13;
} }