Add a base64url gRPC validator and apply it to backup copy

This commit is contained in:
Ravi Khadiwala 2026-04-10 13:13:35 -05:00 committed by ravi-signal
parent 5bb7edcade
commit c4a48dd1e6
6 changed files with 81 additions and 3 deletions

View File

@ -17,6 +17,7 @@ import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.grpc.validators.Base64UrlFieldValidator;
import org.whispersystems.textsecuregcm.grpc.validators.E164FieldValidator;
import org.whispersystems.textsecuregcm.grpc.validators.EnumSpecifiedFieldValidator;
import org.whispersystems.textsecuregcm.grpc.validators.ExactlySizeFieldValidator;
@ -37,7 +38,8 @@ public class ValidatingInterceptor implements ServerInterceptor {
"org.signal.chat.require.e164", new E164FieldValidator(),
"org.signal.chat.require.exactlySize", new ExactlySizeFieldValidator(),
"org.signal.chat.require.range", new RangeFieldValidator(),
"org.signal.chat.require.size", new SizeFieldValidator()
"org.signal.chat.require.size", new SizeFieldValidator(),
"org.signal.chat.require.base64url", new Base64UrlFieldValidator()
);
@Override

View File

@ -0,0 +1,39 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.grpc.validators;
import com.google.protobuf.Descriptors;
import org.whispersystems.textsecuregcm.util.ImpossiblePhoneNumberException;
import org.whispersystems.textsecuregcm.util.NonNormalizedPhoneNumberException;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Base64;
import java.util.Objects;
import java.util.Set;
/// Validate that a string field is a valid base64 url string (padded or unpadded)
public class Base64UrlFieldValidator extends BaseFieldValidator<Boolean> {
public Base64UrlFieldValidator() {
super("base64url", Set.of(Descriptors.FieldDescriptor.Type.STRING), MissingOptionalAction.SUCCEED, false);
}
@Override
protected Boolean resolveExtensionValue(final Object extensionValue) throws FieldValidationException {
return requireFlagExtension(extensionValue);
}
@Override
protected void validateStringValue(
final Boolean extensionValue,
final String fieldValue) throws FieldValidationException {
try {
Base64.getUrlDecoder().decode(fieldValue);
} catch (IllegalArgumentException e) {
throw new FieldValidationException("value is not valid base64 url");
}
}
}

View File

@ -390,7 +390,7 @@ message CopyMediaItem {
uint32 source_attachment_cdn = 1 [(require.range).min = 1, (require.range).max = 3];
// The attachment key of the object to copy into the backup
string source_key = 2 [(require.nonEmpty) = true];
string source_key = 2 [(require.nonEmpty) = true, (require.base64url) = true];
// The length of the source attachment before the encryption applied by the
// copy operation

View File

@ -147,6 +147,21 @@ extend google.protobuf.FieldOptions {
*````
*/
optional bool present = 70007;
/*
* Requires a value of a string field to be a valid base64 URL string. The
* string may be padded or unpadded. If the field is `optional`, this check
* allows a value to be not set.
*
* ```
* import "org/signal/chat/require.proto";
*
* message Data {
* string myString = 1 [(require.base64url)];
* }
* ```
*/
optional bool base64url = 70008;
}
message SizeConstraint {

View File

@ -103,6 +103,25 @@ public class ValidatingInterceptorTest {
));
}
@ParameterizedTest
@ValueSource(strings = {"1====", "zzz?", "123/", "123+"})
public void testBase64UrlValidationFailure(final String invalidBase64Url) {
assertStatusException(Status.INVALID_ARGUMENT, () -> stub.validationsEndpoint(
builderWithValidDefaults()
.setBase64Url(invalidBase64Url)
.build()
));
}
@ParameterizedTest
@ValueSource(strings = {"abc=", "ab==", "aa", "ab", "abc", "A_b-Cd"})
public void testBase64UrlPermittedValues(final String validBase64Url) {
assertDoesNotThrow(() -> stub.validationsEndpoint(
builderWithValidDefaults()
.setBase64Url(validBase64Url)
.build()));
}
@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 3, 4, 6, 1000})
public void testExactlySizeValidationFailure(final int size) throws Exception {
@ -450,7 +469,8 @@ public class ValidatingInterceptorTest {
.setI32Range(15)
.setNested(NestedMessage.getDefaultInstance())
.addRepeatedNested(NestedMessage.getDefaultInstance())
.putMapNested("test", NestedMessage.getDefaultInstance());
.putMapNested("test", NestedMessage.getDefaultInstance())
.setBase64Url("123aBc_-");
}
private static void assertStatusException(final Status expected, final Executable serviceCall) {

View File

@ -77,6 +77,8 @@ message ValidationsRequest {
RequirePresentMessage oneOfMessage = 31 [(require.present) = true];
bytes oneOfNonEmptyBytes = 32 [(require.nonEmpty) = true];
}
optional string base64url = 33 [(require.base64url) = true];
}
message NestedMessage {