-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(storage): add checksum validation on json read paths #13269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nidhiii-27
wants to merge
3
commits into
main
Choose a base branch
from
default-checksum-json-read
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
176 changes: 176 additions & 0 deletions
176
...ogle-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageRpcHasherHelper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| /* | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.google.cloud.storage; | ||
|
|
||
| import com.google.api.client.http.HttpResponse; | ||
| import com.google.api.core.InternalApi; | ||
| import com.google.common.io.BaseEncoding; | ||
| import com.google.common.primitives.Ints; | ||
| import java.io.IOException; | ||
| import java.io.OutputStream; | ||
| import java.nio.ByteBuffer; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.function.Supplier; | ||
|
|
||
| /** | ||
| * Internal utility class to perform client-side CRC32C checksum validation on downloaded data | ||
| * specifically for the {@code HttpStorageRpc} transport layer. | ||
| * | ||
| * <p>Since this class resides in the {@code com.google.cloud.storage} package, it has full, | ||
| * package-private compile-time access to internal components (like {@link Hasher} and {@link | ||
| * Crc32cValue}) without leaking GCS internal types into public client API surfaces. | ||
| */ | ||
| @InternalApi | ||
| public final class HttpStorageRpcHasherHelper { | ||
|
|
||
| public static final HttpStorageRpcHasherHelper INSTANCE = new HttpStorageRpcHasherHelper(); | ||
|
|
||
| private final Hasher hasher; | ||
|
|
||
| private HttpStorageRpcHasherHelper() { | ||
| hasher = Hasher.defaultHasher(); | ||
| } | ||
|
|
||
| /** | ||
| * Returns a wrapping output stream that hashes the written content if validation is enabled, or | ||
| * the original output stream otherwise. | ||
| */ | ||
| public OutputStream wrap(OutputStream out, boolean isChecksumValidationEnabled) { | ||
| boolean isHasherEnabled = !(hasher instanceof Hasher.NoOpHasher); | ||
| return (isChecksumValidationEnabled && isHasherEnabled) | ||
| ? new Crc32cHashingOutputStream(out) | ||
| : out; | ||
| } | ||
|
|
||
| /** | ||
| * Validates a raw byte array against GCS's expected base64-encoded value in response headers. | ||
| * | ||
| * @throws IOException if the checksums do not match. | ||
| */ | ||
| public void validate(HttpResponse response, byte[] content) throws IOException { | ||
| Map<String, String> hashes = extractHashesFromHeader(response); | ||
| String expectedCrc32cBase64 = hashes.get("crc32c"); | ||
| if (expectedCrc32cBase64 != null) { | ||
| validateCrc32c(expectedCrc32cBase64, content); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Validates the downloaded output stream against GCS's expected base64-encoded value in response | ||
| * headers. | ||
| * | ||
| * @throws IOException if the checksums do not match. | ||
| */ | ||
| public void validate(HttpResponse response, OutputStream activeStream) throws IOException { | ||
| if (activeStream instanceof Crc32cHashingOutputStream) { | ||
| Crc32cHashingOutputStream targetStream = (Crc32cHashingOutputStream) activeStream; | ||
| Map<String, String> hashes = extractHashesFromHeader(response); | ||
| String expectedCrc32cBase64 = hashes.get("crc32c"); | ||
| if (expectedCrc32cBase64 != null) { | ||
| validateCrc32c(expectedCrc32cBase64, targetStream.hash()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Validates a calculated CRC32C value against GCS's expected base64-encoded value. | ||
| * | ||
| * @throws IOException if the checksums do not match. | ||
| */ | ||
| public void validateCrc32c(String expectedCrc32cBase64, int calculatedCrc32c) throws IOException { | ||
| if (expectedCrc32cBase64 == null) { | ||
| return; | ||
| } | ||
| byte[] decoded = BaseEncoding.base64().decode(expectedCrc32cBase64); | ||
| int expectedVal = Ints.fromByteArray(decoded); | ||
|
|
||
| Crc32cValue<?> expected = Crc32cValue.of(expectedVal, 0); | ||
| Crc32cValue.Crc32cLengthKnown actual = Crc32cValue.of(calculatedCrc32c, 0); | ||
|
|
||
| // Invoke standard package-private validate path natively | ||
| System.out.println("validating checksum"); | ||
| hasher.validate(expected, actual); | ||
| } | ||
|
|
||
| /** | ||
| * Validates a downloaded raw byte array against GCS's expected base64-encoded value. | ||
| * | ||
| * @throws IOException if the checksums do not match. | ||
| */ | ||
| public void validateCrc32c(String expectedCrc32cBase64, byte[] content) throws IOException { | ||
| if (expectedCrc32cBase64 == null) { | ||
| return; | ||
| } | ||
| byte[] decoded = BaseEncoding.base64().decode(expectedCrc32cBase64); | ||
| int expectedVal = Ints.fromByteArray(decoded); | ||
|
|
||
| Crc32cValue<?> expected = Crc32cValue.of(expectedVal, 0); | ||
| System.out.println("validating checksum"); | ||
| hasher.validate( | ||
| expected, | ||
| new Supplier<ByteBuffer>() { | ||
| @Override | ||
| public ByteBuffer get() { | ||
| return ByteBuffer.wrap(content); | ||
| } | ||
| }); | ||
|
nidhiii-27 marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| @SuppressWarnings("UnstableApiUsage") | ||
| private static class Crc32cHashingOutputStream extends java.io.FilterOutputStream { | ||
| private final com.google.common.hash.Hasher hasher; | ||
|
|
||
| Crc32cHashingOutputStream(OutputStream out) { | ||
| super(out); | ||
| this.hasher = com.google.common.hash.Hashing.crc32c().newHasher(); | ||
| } | ||
|
|
||
| @Override | ||
| public void write(int b) throws IOException { | ||
| out.write(b); | ||
| hasher.putByte((byte) b); | ||
| } | ||
|
|
||
| @Override | ||
| public void write(byte[] b, int off, int len) throws IOException { | ||
| out.write(b, off, len); | ||
| hasher.putBytes(b, off, len); | ||
| } | ||
|
|
||
| int hash() { | ||
| return hasher.hash().asInt(); | ||
| } | ||
| } | ||
|
|
||
| private static Map<String, String> extractHashesFromHeader(HttpResponse response) { | ||
| List<String> hashHeaders = response.getHeaders().getHeaderStringValues("x-goog-hash"); | ||
| if (hashHeaders == null || hashHeaders.isEmpty()) { | ||
| return java.util.Collections.emptyMap(); | ||
| } | ||
|
|
||
| return hashHeaders.stream() | ||
| .flatMap(h -> java.util.Arrays.stream(h.split(","))) | ||
| .map(String::trim) | ||
| .filter(s -> !s.isEmpty()) | ||
| .map(s -> s.split("=", 2)) | ||
| .filter(a -> a.length == 2) | ||
| .filter(a -> "crc32c".equalsIgnoreCase(a[0]) || "md5".equalsIgnoreCase(a[0])) | ||
| .collect( | ||
| java.util.stream.Collectors.toMap(a -> a[0].toLowerCase(), a -> a[1], (v1, v2) -> v1)); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
...-cloud-storage/src/test/java/com/google/cloud/storage/HttpStorageRpcHasherHelperTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| /* | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.google.cloud.storage; | ||
|
|
||
| import static org.junit.Assert.assertArrayEquals; | ||
| import static org.junit.Assert.assertNotEquals; | ||
| import static org.junit.Assert.assertSame; | ||
| import static org.junit.Assert.assertThrows; | ||
| import static org.junit.Assert.assertTrue; | ||
|
|
||
| import com.google.common.hash.Hashing; | ||
| import java.io.ByteArrayOutputStream; | ||
| import java.io.IOException; | ||
| import java.io.OutputStream; | ||
| import org.junit.Test; | ||
|
|
||
| public class HttpStorageRpcHasherHelperTest { | ||
|
|
||
| private static final byte[] CONTENT_BYTES = "Hello, World!".getBytes(); | ||
| private static final String CONTENT_CRC32C_BASE64 = | ||
| "TVUQaA=="; // expected CRC32C of "Hello, World!" | ||
|
|
||
| @Test | ||
| public void testWrap_disabled_returnsOriginalStream() { | ||
| ByteArrayOutputStream original = new ByteArrayOutputStream(); | ||
| OutputStream wrapped = HttpStorageRpcHasherHelper.INSTANCE.wrap(original, false); | ||
| assertSame(original, wrapped); | ||
| } | ||
|
|
||
| @Test | ||
| public void testWrap_enabled_returnsHashingStream() throws IOException { | ||
| ByteArrayOutputStream original = new ByteArrayOutputStream(); | ||
| OutputStream wrapped = HttpStorageRpcHasherHelper.INSTANCE.wrap(original, true); | ||
| assertNotEquals(original, wrapped); | ||
|
|
||
| wrapped.write(CONTENT_BYTES); | ||
| wrapped.flush(); | ||
| wrapped.close(); | ||
|
|
||
| byte[] writtenBytes = original.toByteArray(); | ||
| assertArrayEquals(CONTENT_BYTES, writtenBytes); | ||
| } | ||
|
|
||
| @Test | ||
| public void testValidateCrc32c_int_expectSuccess() throws IOException { | ||
| int calculatedCrc32c = Hashing.crc32c().hashBytes(CONTENT_BYTES).asInt(); | ||
| // Should complete cleanly without throwing | ||
| HttpStorageRpcHasherHelper.INSTANCE.validateCrc32c(CONTENT_CRC32C_BASE64, calculatedCrc32c); | ||
| } | ||
|
|
||
| @Test | ||
| public void testValidateCrc32c_int_expectMismatchFailure() { | ||
| int calculatedCrc32c = 12345; // Incorrect hash | ||
| Hasher.ChecksumMismatchException ex = | ||
| assertThrows( | ||
| Hasher.ChecksumMismatchException.class, | ||
| () -> | ||
| HttpStorageRpcHasherHelper.INSTANCE.validateCrc32c( | ||
| CONTENT_CRC32C_BASE64, calculatedCrc32c)); | ||
| assertTrue(ex.getMessage().contains("Mismatch checksum value")); | ||
| } | ||
|
|
||
| @Test | ||
| public void testValidateCrc32c_byteArray_expectSuccess() throws IOException { | ||
| // Should complete cleanly without throwing | ||
| HttpStorageRpcHasherHelper.INSTANCE.validateCrc32c(CONTENT_CRC32C_BASE64, CONTENT_BYTES); | ||
| } | ||
|
|
||
| @Test | ||
| public void testValidateCrc32c_byteArray_expectMismatchFailure() { | ||
| byte[] wrongBytes = "Wrong bytes!".getBytes(); | ||
| Hasher.ChecksumMismatchException ex = | ||
| assertThrows( | ||
| Hasher.ChecksumMismatchException.class, | ||
| () -> | ||
| HttpStorageRpcHasherHelper.INSTANCE.validateCrc32c( | ||
| CONTENT_CRC32C_BASE64, wrongBytes)); | ||
| assertTrue(ex.getMessage().contains("Mismatch checksum value")); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.