diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java index 849ec6e25a..ed43952748 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.crypto; import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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.zkgroup.profiles.ProfileKey; 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.recipients.Recipient; import org.thoughtcrime.securesms.util.Base64; @@ -44,21 +48,17 @@ public class UnidentifiedAccessUtil { try { byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); - byte[] ourUnidentifiedAccessCertificate = TextSecurePreferences.getUnidentifiedAccessCertificate(context); + byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(recipient); if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { ourUnidentifiedAccessKey = Util.getSecretBytes(16); } Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) + - " | Our access key present? " + (ourUnidentifiedAccessKey != null) + " | Our certificate present? " + (ourUnidentifiedAccessCertificate != null) + " | UUID certificate supported? " + recipient.isUuidSupported()); - if (theirUnidentifiedAccessKey != null && - ourUnidentifiedAccessKey != null && - ourUnidentifiedAccessCertificate != null) - { + if (theirUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) { return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey, ourUnidentifiedAccessCertificate), new UnidentifiedAccess(ourUnidentifiedAccessKey, @@ -75,13 +75,13 @@ public class UnidentifiedAccessUtil { public static Optional getAccessForSync(@NonNull Context context) { try { byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); - byte[] ourUnidentifiedAccessCertificate = TextSecurePreferences.getUnidentifiedAccessCertificate(context); + byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(Recipient.self()); if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { ourUnidentifiedAccessKey = Util.getSecretBytes(16); } - if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) { + if (ourUnidentifiedAccessCertificate != null) { return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(ourUnidentifiedAccessKey, ourUnidentifiedAccessCertificate), 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) { ProfileKey theirProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.resolve().getProfileKey()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index b041aadad8..8dd0cc68ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -27,6 +27,8 @@ import org.thoughtcrime.securesms.events.PartProgressEvent; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; 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.logging.Log; 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.signalservice.api.crypto.UnidentifiedAccessPair; 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.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview; 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.IOException; import java.io.InputStream; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -330,23 +334,34 @@ public abstract class PushSendJob extends SendJob { protected void rotateSenderCertificateIfNecessary() throws IOException { try { - byte[] certificateBytes = TextSecurePreferences.getUnidentifiedAccessCertificate(context); + Collection requiredCertificateTypes = SignalStore.phoneNumberPrivacy() + .getRequiredCertificateTypes(); - if (certificateBytes == null) { - throw new InvalidCertificateException("No certificate was present."); + Log.i(TAG, "Ensuring we have these certificates " + requiredCertificateTypes); + + 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); - - 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."); + Log.d(TAG, "All certificates are valid."); } catch (InvalidCertificateException e) { - Log.w(TAG, "Certificate was invalid at send time. Fetching a new one.", e); - RotateCertificateJob certificateJob = new RotateCertificateJob(context); - certificateJob.onRun(); + Log.w(TAG, "A certificate was invalid at send time. Fetching new ones.", e); + if (!ApplicationDependencies.getJobManager().runSynchronously(new RotateCertificateJob(), 5000).isPresent()) { + throw new IOException("Timeout rotating certificate"); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java index dbd7f177c5..33ca757868 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java @@ -1,35 +1,37 @@ package org.thoughtcrime.securesms.jobs; - -import android.content.Context; import androidx.annotation.NonNull; 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.keyvalue.CertificateType; +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.push.exceptions.PushNetworkException; import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.TimeUnit; -public class RotateCertificateJob extends BaseJob { +public final class RotateCertificateJob extends BaseJob { 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() .setQueue("__ROTATE_SENDER_CERTIFICATE__") .addConstraint(NetworkConstraint.KEY) .setLifespan(TimeUnit.DAYS.toMillis(1)) .setMaxAttempts(Parameters.UNLIMITED) .build()); - setContext(context); } private RotateCertificateJob(@NonNull Job.Parameters parameters) { @@ -57,10 +59,25 @@ public class RotateCertificateJob extends BaseJob { } synchronized (RotateCertificateJob.class) { - SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - byte[] certificate = accountManager.getSenderCertificate(); + SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + Collection 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); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/CertificateType.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/CertificateType.java new file mode 100644 index 0000000000..c678698349 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/CertificateType.java @@ -0,0 +1,6 @@ +package org.thoughtcrime.securesms.keyvalue; + +public enum CertificateType { + UUID_AND_E164, + UUID_ONLY +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/CertificateValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/CertificateValues.java new file mode 100644 index 0000000000..61a7ba70f0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/CertificateValues.java @@ -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(); + } + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PhoneNumberPrivacyValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PhoneNumberPrivacyValues.java new file mode 100644 index 0000000000..1215c60c89 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PhoneNumberPrivacyValues.java @@ -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 REGULAR_CERTIFICATE = Collections.singletonList(CertificateType.UUID_AND_E164); + private static final Collection PRIVACY_CERTIFICATE = Collections.singletonList(CertificateType.UUID_ONLY); + private static final Collection 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 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 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 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java index a948b78a35..81887e2aa1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java @@ -13,32 +13,36 @@ public final class SignalStore { private static final SignalStore INSTANCE = new SignalStore(); - private final KeyValueStore store; - private final KbsValues kbsValues; - private final RegistrationValues registrationValues; - private final PinValues pinValues; - private final RemoteConfigValues remoteConfigValues; - private final StorageServiceValues storageServiceValues; - private final UiHints uiHints; - private final TooltipValues tooltipValues; - private final MiscellaneousValues misc; - private final InternalValues internalValues; - private final EmojiValues emojiValues; - private final SettingsValues settingsValues; + private final KeyValueStore store; + private final KbsValues kbsValues; + private final RegistrationValues registrationValues; + private final PinValues pinValues; + private final RemoteConfigValues remoteConfigValues; + private final StorageServiceValues storageServiceValues; + private final UiHints uiHints; + private final TooltipValues tooltipValues; + private final MiscellaneousValues misc; + private final InternalValues internalValues; + private final EmojiValues emojiValues; + private final SettingsValues settingsValues; + private final CertificateValues certificateValues; + private final PhoneNumberPrivacyValues phoneNumberPrivacyValues; private SignalStore() { - this.store = ApplicationDependencies.getKeyValueStore(); - this.kbsValues = new KbsValues(store); - this.registrationValues = new RegistrationValues(store); - this.pinValues = new PinValues(store); - this.remoteConfigValues = new RemoteConfigValues(store); - this.storageServiceValues = new StorageServiceValues(store); - this.uiHints = new UiHints(store); - this.tooltipValues = new TooltipValues(store); - this.misc = new MiscellaneousValues(store); - this.internalValues = new InternalValues(store); - this.emojiValues = new EmojiValues(store); - this.settingsValues = new SettingsValues(store); + this.store = ApplicationDependencies.getKeyValueStore(); + this.kbsValues = new KbsValues(store); + this.registrationValues = new RegistrationValues(store); + this.pinValues = new PinValues(store); + this.remoteConfigValues = new RemoteConfigValues(store); + this.storageServiceValues = new StorageServiceValues(store); + this.uiHints = new UiHints(store); + this.tooltipValues = new TooltipValues(store); + this.misc = new MiscellaneousValues(store); + this.internalValues = new InternalValues(store); + this.emojiValues = new EmojiValues(store); + this.settingsValues = new SettingsValues(store); + this.certificateValues = new CertificateValues(store); + this.phoneNumberPrivacyValues = new PhoneNumberPrivacyValues(store); } public static void onFirstEverAppLaunch() { @@ -52,6 +56,8 @@ public final class SignalStore { misc().onFirstEverAppLaunch(); internalValues().onFirstEverAppLaunch(); settings().onFirstEverAppLaunch(); + certificateValues().onFirstEverAppLaunch(); + phoneNumberPrivacy().onFirstEverAppLaunch(); } public static @NonNull KbsValues kbsValues() { @@ -98,6 +104,14 @@ public final class SignalStore { return INSTANCE.settingsValues; } + public static @NonNull CertificateValues certificateValues() { + return INSTANCE.certificateValues; + } + + public static @NonNull PhoneNumberPrivacyValues phoneNumberPrivacy() { + return INSTANCE.phoneNumberPrivacyValues; + } + public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() { return new GroupsV2AuthorizationSignalStoreCache(getStore()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/UuidMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/UuidMigrationJob.java index 5036967580..590ffeafaf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/UuidMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/UuidMigrationJob.java @@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; import java.io.IOException; import java.util.UUID; @@ -58,7 +57,6 @@ public class UuidMigrationJob extends MigrationJob { ensureSelfRecipientExists(context); fetchOwnUuid(context); - rotateSealedSenderCerts(context); } @Override @@ -78,14 +76,6 @@ public class UuidMigrationJob extends MigrationJob { 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 { @Override public @NonNull UuidMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java index 7d5954ada0..0f3bf4aa00 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java @@ -7,6 +7,9 @@ import android.graphics.Color; import android.graphics.Typeface; import android.os.Bundle; import android.text.InputType; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.TextAppearanceSpan; import android.util.DisplayMetrics; import android.view.View; import android.view.ViewGroup; @@ -14,6 +17,7 @@ import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; 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.RefreshAttributesJob; import org.thoughtcrime.securesms.keyvalue.KbsValues; +import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues; import org.thoughtcrime.securesms.keyvalue.PinValues; import org.thoughtcrime.securesms.keyvalue.SettingsValues; 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.RegistrationLockUtil; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.megaphone.MegaphoneRepository; import org.thoughtcrime.securesms.megaphone.Megaphones; import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.CommunicationActions; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; 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 PREFERENCE_CATEGORY_BLOCKED = "preference_category_blocked"; - private static final String PREFERENCE_UNIDENTIFIED_LEARN_MORE = "pref_unidentified_learn_more"; + 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_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; @@ -99,6 +106,18 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment this.findPreference(PREFERENCE_UNIDENTIFIED_LEARN_MORE).setOnPreferenceClickListener(new UnidentifiedLearnMoreClickListener()); 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); linkPreviewPref.setChecked(SignalStore.settings().isLinkPreviewsEnabled()); @@ -138,6 +157,9 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment signalPinReminders.setEnabled(false); registrationLockV2.setEnabled(false); } + + initializePhoneNumberPrivacyWhoCanSeeSummary(); + initializePhoneNumberPrivacyWhoCanFindSummary(); } @Override @@ -164,6 +186,27 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment 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() { if (TextSecurePreferences.isPasswordDisabled(getContext())) { 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; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java index 44f0c16fcd..c132ba1a77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java @@ -174,7 +174,7 @@ public final class CodeVerificationRequest { private static void handleSuccessfulRegistration(@NonNull Context context) { JobManager jobManager = ApplicationDependencies.getJobManager(); jobManager.add(new DirectoryRefreshJob(false)); - jobManager.add(new RotateCertificateJob(context)); + jobManager.add(new RotateCertificateJob()); DirectoryRefreshListener.schedule(context); RotateSignedPreKeyListener.schedule(context); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/RotateSenderCertificateListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/RotateSenderCertificateListener.java index b06c755719..9f8c0805d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/RotateSenderCertificateListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/RotateSenderCertificateListener.java @@ -4,7 +4,6 @@ package org.thoughtcrime.securesms.service; import android.content.Context; import android.content.Intent; -import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.RotateCertificateJob; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -22,7 +21,7 @@ public class RotateSenderCertificateListener extends PersistentAlarmManagerListe @Override protected long onAlarm(Context context, long scheduledTime) { - ApplicationDependencies.getJobManager().add(new RotateCertificateJob(context)); + ApplicationDependencies.getJobManager().add(new RotateCertificateJob()); long nextTime = System.currentTimeMillis() + INTERVAL; TextSecurePreferences.setUnidentifiedAccessCertificateRotationTime(context, nextTime); diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java index eba2ca3916..277a5210cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java @@ -6,6 +6,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.logging.Log; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.storage.SignalAccountRecord; +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; import java.util.Arrays; import java.util.Collection; @@ -55,16 +56,18 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger> update) { if (!update.isPresent()) { return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 86376347ce..c69b4912e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -49,20 +49,21 @@ public final class FeatureFlags { private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2); - private static final String USERNAMES = "android.usernames"; - private static final String ATTACHMENTS_V3 = "android.attachmentsV3.2"; - 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_2 = "android.groupsv2.2"; - 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_JOIN_VERSION = "android.groupsv2.joinVersion"; - 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 CDS_VERSION = "android.cdsVersion"; - private static final String INTERNAL_USER = "android.internalUser"; - private static final String MENTIONS = "android.mentions"; - private static final String VERIFY_V2 = "android.verifyV2"; + private static final String USERNAMES = "android.usernames"; + private static final String ATTACHMENTS_V3 = "android.attachmentsV3.2"; + 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_2 = "android.groupsv2.2"; + 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_JOIN_VERSION = "android.groupsv2.joinVersion"; + 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 CDS_VERSION = "android.cdsVersion"; + private static final String INTERNAL_USER = "android.internalUser"; + private static final String MENTIONS = "android.mentions"; + 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 @@ -279,6 +280,14 @@ public final class FeatureFlags { 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. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java index e879ebf28c..8036cdfdfe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -182,7 +182,6 @@ public class TextSecurePreferences { 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 = "pref_unidentified_access_certificate_uuid"; public static final String UNIVERSAL_UNIDENTIFIED_ACCESS = "pref_universal_unidentified_access"; public static final String SHOW_UNIDENTIFIED_DELIVERY_INDICATORS = "pref_show_unidentifed_delivery_indicators"; 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); } - 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) { return getBooleanPreference(context, UNIVERSAL_UNIDENTIFIED_ACCESS, false); } diff --git a/app/src/main/res/layout/profile_create_fragment.xml b/app/src/main/res/layout/profile_create_fragment.xml index b2ade4f095..6619d483ae 100644 --- a/app/src/main/res/layout/profile_create_fragment.xml +++ b/app/src/main/res/layout/profile_create_fragment.xml @@ -165,7 +165,8 @@ android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/name_container" /> + app:layout_constraintTop_toBottomOf="@id/name_container" + tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@id/profile_overview_username_label" + tools:visibility="visible" /> + app:srcCompat="@drawable/ic_compose_solid_24" + tools:visibility="visible" /> Disable Signal\'s built-in emoji support Relay all calls through the Signal server to avoid revealing your IP address to your contact. Enabling will reduce call quality. Always relay calls + Who can… App access Communication Chats @@ -2505,6 +2506,14 @@ Signal Registration - Verification Code for Android Never Unknown + See my phone number + Find me by phone number + Everyone + My contacts + Nobody + Your phone number will be visible to all people and groups you message. + 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. + Only your contacts will see your phone number on Signal. Screen lock Lock Signal access with Android screen lock or fingerprint Screen lock inactivity timeout diff --git a/app/src/main/res/xml/preferences_app_protection.xml b/app/src/main/res/xml/preferences_app_protection.xml index 13bc8b0c8f..730c14d931 100644 --- a/app/src/main/res/xml/preferences_app_protection.xml +++ b/app/src/main/res/xml/preferences_app_protection.xml @@ -1,6 +1,21 @@ + + + + + + + + +