From 149de6c4645affb57016e069babbef97fc74196d Mon Sep 17 00:00:00 2001 From: Chris Eager Date: Fri, 10 Apr 2026 18:06:37 -0500 Subject: [PATCH] Add UAK validator to AccountAttributes --- .../entities/AccountAttributes.java | 13 +++++++ .../controllers/AccountControllerTest.java | 37 +++++++++++++++++-- .../RegistrationControllerTest.java | 10 +++-- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java index 45fd8e2da..19079505d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java @@ -139,9 +139,22 @@ public class AccountAttributes { return this; } + @VisibleForTesting + public AccountAttributes withUnrestrictedUnidentifiedAccess(final boolean unrestrictedUnidentifiedAccess) { + this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; + return this; + } + @AssertTrue @Schema(hidden = true) public boolean isEachRegistrationIdValid() { return validRegistrationId(registrationId) && validRegistrationId(phoneNumberIdentityRegistrationId); } + + @AssertTrue + @Schema(hidden = true) + public boolean isUakValid() { + return (unrestrictedUnidentifiedAccess && (unidentifiedAccessKey == null || unidentifiedAccessKey.length == 0)) + || (!unrestrictedUnidentifiedAccess && (unidentifiedAccessKey != null && unidentifiedAccessKey.length == 16)); + } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java index b9aeb8b65..ea212d91a 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java @@ -36,6 +36,7 @@ import java.security.SecureRandom; import java.time.Duration; import java.time.Instant; import java.util.Base64; +import java.util.Collection; import java.util.Collections; import java.util.HexFormat; import java.util.List; @@ -776,13 +777,40 @@ class AccountControllerTest { } } + @ParameterizedTest + @MethodSource + void testSetAccountAttributesUnrestrictedUnidentifiedAccess(final boolean unrestrictedUnidentifiedAccess, final byte[] unidentifiedAccessKey, final int expectedStatus) { + + try (final Response response = resources.getJerseyTest() + .target("/v1/accounts/attributes/") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new AccountAttributes(false, 2222, 3333, null, null, true, null) + .withUnidentifiedAccessKey(unidentifiedAccessKey) + .withUnrestrictedUnidentifiedAccess(unrestrictedUnidentifiedAccess)))) { + + assertThat(response.getStatus()).isEqualTo(expectedStatus); + } + } + + static Collection testSetAccountAttributesUnrestrictedUnidentifiedAccess() { + return List.of( + Arguments.argumentSet("restricted, non-empty UAK", false, new byte[16], 204), + Arguments.argumentSet("unrestricted, empty UAK", true, new byte[0], 204), + Arguments.argumentSet("restricted, empty UAK", false, new byte[0], 422), + Arguments.argumentSet("unrestricted, non-empty UAK", true, new byte[16], 422) + ); + } + + @Test void testSetAccountAttributesNoDiscoverabilityChange() { try (final Response response = resources.getJerseyTest() .target("/v1/accounts/attributes/") .request() .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.json(new AccountAttributes(false, 2222, 3333, null, null, true, null)))) { + .put(Entity.json(new AccountAttributes(false, 2222, 3333, null, null, true, null) + .withUnidentifiedAccessKey(new byte[16])))) { assertThat(response.getStatus()).isEqualTo(204); } @@ -794,7 +822,8 @@ class AccountControllerTest { .target("/v1/accounts/attributes/") .request() .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.UNDISCOVERABLE_UUID, AuthHelper.UNDISCOVERABLE_PASSWORD)) - .put(Entity.json(new AccountAttributes(false, 2222, 3333, null, null, true, null)))) { + .put(Entity.json(new AccountAttributes(false, 2222, 3333, null, null, true, null) + .withUnidentifiedAccessKey(new byte[16])))) { assertThat(response.getStatus()).isEqualTo(204); } @@ -811,6 +840,7 @@ class AccountControllerTest { .request() .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.UNDISCOVERABLE_UUID, AuthHelper.UNDISCOVERABLE_PASSWORD)) .put(Entity.json(new AccountAttributes(false, 2222, 3333, null, null, true, null) + .withUnidentifiedAccessKey(new byte[16]) .withRecoveryPassword(recoveryPassword)))) { assertThat(response.getStatus()).isEqualTo(204); @@ -824,7 +854,8 @@ class AccountControllerTest { .target("/v1/accounts/attributes/") .request() .header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.json(new AccountAttributes(false, 2222, 3333, null, null, false, null)))) { + .put(Entity.json(new AccountAttributes(false, 2222, 3333, null, null, false, null) + .withUnidentifiedAccessKey(new byte[16])))) { assertThat(response.getStatus()).isEqualTo(204); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java index 0a1641126..f54d8891f 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java @@ -89,6 +89,7 @@ import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.KeysHelper; import org.whispersystems.textsecuregcm.util.MockUtils; import org.whispersystems.textsecuregcm.util.SystemMapper; +import org.whispersystems.textsecuregcm.util.TestRandomUtil; @ExtendWith(DropwizardExtensionsSupport.class) class RegistrationControllerTest { @@ -901,10 +902,12 @@ class RegistrationControllerTest { final Set deviceCapabilities = DeviceCapability.CAPABILITIES_REQUIRED_FOR_NEW_DEVICES; final AccountAttributes fetchesMessagesAccountAttributes = - new AccountAttributes(true, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, deviceCapabilities); + new AccountAttributes(true, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, deviceCapabilities) + .withUnidentifiedAccessKey(TestRandomUtil.nextBytes(16)); final AccountAttributes pushAccountAttributes = - new AccountAttributes(false, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, deviceCapabilities); + new AccountAttributes(false, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, deviceCapabilities) + .withUnidentifiedAccessKey(TestRandomUtil.nextBytes(16)); final String apnsToken = "apns-token"; final String gcmToken = "gcm-token"; @@ -1017,7 +1020,8 @@ class RegistrationControllerTest { final AccountAttributes accountAttributes = new AccountAttributes(true, registrationId, pniRegistrationId, "name".getBytes(StandardCharsets.UTF_8), REGLOCK, - true, deviceCapabilities); + true, deviceCapabilities) + .withUnidentifiedAccessKey(TestRandomUtil.nextBytes(16)); return new RegistrationRequest( Base64.getEncoder().encodeToString(sessionId.getBytes(StandardCharsets.UTF_8)),