From 55e029e81da377a621de5a8ea5d9e6a0206c592a Mon Sep 17 00:00:00 2001 From: jeffrey-signal Date: Thu, 12 Mar 2026 17:33:54 -0400 Subject: [PATCH] Treat member labels as unset if they can't be decrypted. --- .../api/groupsv2/GroupsV2Operations.java | 28 +++++++-- ...roupsV2Operations_decrypt_change_Test.java | 58 +++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java index 79da821de5..d6112c893f 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java @@ -769,8 +769,8 @@ public final class GroupsV2Operations { modifyMemberLabels.add( new DecryptedModifyMemberLabel.Builder() .aciBytes(decryptAciToBinary(action.userId)) - .labelEmoji(Objects.requireNonNullElse(decryptString(action.labelEmoji), "")) - .labelString(Objects.requireNonNullElse(decryptString(action.labelString), "")) + .labelEmoji(decryptMemberLabelEmoji(action.labelEmoji)) + .labelString(decryptMemberLabelText(action.labelString)) .build() ); } @@ -817,8 +817,8 @@ public final class GroupsV2Operations { private DecryptedMember.Builder decryptMember(Member member) throws InvalidGroupStateException, VerificationFailedException, InvalidInputException { - String labelEmoji = Objects.requireNonNullElse(decryptString(member.labelEmoji), ""); - String labelString = Objects.requireNonNullElse(decryptString(member.labelString), ""); + String labelEmoji = decryptMemberLabelEmoji(member.labelEmoji); + String labelString = decryptMemberLabelText(member.labelString); if (member.presentation.size() == 0) { ACI aci = decryptAci(member.userId); @@ -1072,6 +1072,26 @@ public final class GroupsV2Operations { return new String(decryptedBytes, StandardCharsets.UTF_8); } + @Nonnull + private String decryptMemberLabelText(@Nullable ByteString cipherText) { + try { + return Objects.requireNonNullElse(decryptString(cipherText), ""); + } catch (VerificationFailedException e) { + Log.w(TAG, "Failed to decrypt member label string, treating as unset"); + return ""; + } + } + + @Nonnull + private String decryptMemberLabelEmoji(@Nullable ByteString cipherText) { + try { + return Objects.requireNonNullElse(decryptString(cipherText), ""); + } catch (VerificationFailedException e) { + Log.w(TAG, "Failed to decrypt member label emoji, treating as unset"); + return ""; + } + } + /** * Verifies signature and parses actions on a group change. */ diff --git a/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java b/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java index 82cd0ef634..f60c256262 100644 --- a/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java +++ b/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java @@ -476,6 +476,64 @@ public final class GroupsV2Operations_decrypt_change_Test { ); } + @Test + public void member_label_text_is_treated_as_unset_on_decryption_error_field26() { + ACI aci = ACI.from(UUID.fromString("d1d1d1d1-0000-4000-8000-000000000001")); + ByteString invalidBytes = ByteString.of(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 }); + + GroupChange.Actions.Builder change = new GroupChange.Actions.Builder() + .modifyMemberLabels( + List.of( + new GroupChange.Actions.ModifyMemberLabelAction.Builder() + .userId(groupOperations.encryptServiceId(aci)) + .labelString(invalidBytes) + .build() + ) + ); + + DecryptedGroupChange.Builder expected = new DecryptedGroupChange.Builder() + .modifyMemberLabels( + List.of( + new DecryptedModifyMemberLabel.Builder() + .aciBytes(aci.toByteString()) + .labelEmoji("") + .labelString("") + .build() + ) + ); + + assertDecryption(change, expected); + } + + @Test + public void member_label_emoji_is_treated_as_unset_on_decryption_error_field26() { + ACI aci = ACI.from(UUID.fromString("d1d1d1d1-0000-4000-8000-000000000001")); + ByteString invalidBytes = ByteString.of(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 }); + + GroupChange.Actions.Builder change = new GroupChange.Actions.Builder() + .modifyMemberLabels( + List.of( + new GroupChange.Actions.ModifyMemberLabelAction.Builder() + .userId(groupOperations.encryptServiceId(aci)) + .labelEmoji(invalidBytes) + .build() + ) + ); + + DecryptedGroupChange.Builder expected = new DecryptedGroupChange.Builder() + .modifyMemberLabels( + List.of( + new DecryptedModifyMemberLabel.Builder() + .aciBytes(aci.toByteString()) + .labelEmoji("") + .labelString("") + .build() + ) + ); + + assertDecryption(change, expected); + } + @Test public void can_pass_through_new_member_label_access_field_27() { GroupChange.Actions.Builder encryptedChange = groupOperations.createChangeMemberLabelRights(AccessControl.AccessRequired.ADMINISTRATOR);