From 01c83249e8068acba9b43bc8349a37798a5c26ff Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 31 Aug 2026 14:16:20 -0400 Subject: [PATCH] Fix sealed sender certificates and group PNI handling for numberless accounts. --- .../securesms/database/GroupTable.kt | 2 +- .../securesms/groups/GroupManagerV2.java | 7 ++++--- .../securesms/jobs/RotateCertificateJob.java | 19 +++++++++++++++---- .../keyvalue/PhoneNumberPrivacyValues.java | 12 +++++++++--- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt index 5f6811d7cd..5cc9afcc00 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt @@ -608,7 +608,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : if (groupRecord.isPresent && groupRecord.get().hasV2GroupProperties) { val pendingMembers: List = groupRecord.get().requireV2GroupProperties().decryptedGroup.pendingMembers val invitedByAci: ByteString? = DecryptedGroupUtil.findPendingByServiceId(pendingMembers, Recipient.self().requireAci()) - .or { DecryptedGroupUtil.findPendingByServiceId(pendingMembers, Recipient.self().requirePni()) } + .or { Recipient.self().pni.flatMap { pni -> DecryptedGroupUtil.findPendingByServiceId(pendingMembers, pni) } } .map { it.addedByAci } .orElse(null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index 41bb63ef2c..b6471c696b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -106,6 +106,7 @@ final class GroupManagerV2 { private final GroupsV2Authorization authorization; private final ServiceIds serviceIds; private final ACI selfAci; + @Nullable private final PNI selfPni; private final GroupCandidateHelper groupCandidateHelper; private final SendGroupUpdateHelper sendGroupUpdateHelper; @@ -137,7 +138,7 @@ final class GroupManagerV2 { this.authorization = authorization; this.serviceIds = serviceIds; this.selfAci = serviceIds.getAci(); - this.selfPni = serviceIds.requirePni(); + this.selfPni = serviceIds.getPni(); this.groupCandidateHelper = groupCandidateHelper; this.sendGroupUpdateHelper = sendGroupUpdateHelper; } @@ -476,7 +477,7 @@ final class GroupManagerV2 { DecryptedGroup decryptedGroup = v2GroupProperties.getDecryptedGroup(); Optional selfMember = DecryptedGroupUtil.findMemberByAci(decryptedGroup.members, selfAci); Optional aciPendingMember = DecryptedGroupUtil.findPendingByServiceId(decryptedGroup.pendingMembers, selfAci); - Optional pniPendingMember = DecryptedGroupUtil.findPendingByServiceId(decryptedGroup.pendingMembers, selfPni); + Optional pniPendingMember = selfPni != null ? DecryptedGroupUtil.findPendingByServiceId(decryptedGroup.pendingMembers, selfPni) : Optional.empty(); Optional selfPendingMember = Optional.empty(); ServiceId serviceId = selfAci; @@ -573,7 +574,7 @@ final class GroupManagerV2 { } Optional aciInPending = DecryptedGroupUtil.findPendingByServiceId(group.pendingMembers, selfAci); - Optional pniInPending = DecryptedGroupUtil.findPendingByServiceId(group.pendingMembers, selfPni); + Optional pniInPending = selfPni != null ? DecryptedGroupUtil.findPendingByServiceId(group.pendingMembers, selfPni) : Optional.empty(); GroupCandidate selfGroupCandidate = groupCandidateHelper.recipientIdToCandidate(Recipient.self().getId()); 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 5c877121ea..9248ec6cc4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; +import org.signal.network.exceptions.NonSuccessfulResponseCodeException; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SealedSenderConstraint; @@ -65,16 +66,26 @@ public final class RotateCertificateJob extends BaseJob { synchronized (RotateCertificateJob.class) { Collection certificateTypes = SignalStore.phoneNumberPrivacy() .getAllCertificateTypes(); + Collection requiredTypes = SignalStore.phoneNumberPrivacy() + .getRequiredCertificateTypes(); Log.i(TAG, "Rotating these certificates " + certificateTypes); for (CertificateType certificateType: certificateTypes) { byte[] certificate; - switch (certificateType) { - case ACI_AND_E164: certificate = NetworkResultUtil.toBasicLegacy(SignalNetwork.certificate().getSenderCertificate()); break; - case ACI_ONLY : certificate = NetworkResultUtil.toBasicLegacy(SignalNetwork.certificate().getSenderCertificateForPhoneNumberPrivacy()); break; - default : throw new AssertionError(); + try { + switch (certificateType) { + case ACI_AND_E164: certificate = NetworkResultUtil.toBasicLegacy(SignalNetwork.certificate().getSenderCertificate()); break; + case ACI_ONLY : certificate = NetworkResultUtil.toBasicLegacy(SignalNetwork.certificate().getSenderCertificateForPhoneNumberPrivacy()); break; + default : throw new AssertionError(); + } + } catch (NonSuccessfulResponseCodeException e) { + if (requiredTypes.contains(certificateType)) { + throw e; + } + Log.w(TAG, String.format("The server rejected the request for the non-required %s certificate. Skipping it.", certificateType), e); + continue; } Log.i(TAG, String.format("Successfully got %s certificate", certificateType)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PhoneNumberPrivacyValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PhoneNumberPrivacyValues.java index b76e74486c..ab4a94ce6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PhoneNumberPrivacyValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PhoneNumberPrivacyValues.java @@ -80,7 +80,9 @@ public final class PhoneNumberPrivacyValues extends SignalStoreValues { * these certificates types. */ public Collection getRequiredCertificateTypes() { - if (isPhoneNumberSharingEnabled()) { + if (SignalStore.account().isPhoneNumberless()) { + return ACI_ONLY_CERTIFICATE; + } else if (isPhoneNumberSharingEnabled()) { return ACI_AND_E164_CERTIFICATE; } else { return ACI_ONLY_CERTIFICATE; @@ -88,10 +90,14 @@ public final class PhoneNumberPrivacyValues extends SignalStoreValues { } /** - * All certificate types required according to the feature flags. + * All certificate types the account can fetch. */ public Collection getAllCertificateTypes() { - return BOTH_CERTIFICATES; + if (SignalStore.account().isPhoneNumberless()) { + return ACI_ONLY_CERTIFICATE; + } else { + return BOTH_CERTIFICATES; + } } public enum PhoneNumberSharingMode {