From 9bc24d28cb6a7981538b29af99851f2c102cb66e Mon Sep 17 00:00:00 2001 From: Felipe Fumero Date: Fri, 30 Oct 2026 16:04:37 -0600 Subject: [PATCH 1/4] fix: redact credentials from toString --- CHANGELOG.md | 4 ++ .../structures/B2AccountAuthorization.java | 4 +- .../structures/B2FileSseForRequest.java | 4 +- .../B2AccountAuthorizationCacheTest.java | 2 +- .../B2AccountAuthorizationTest.java | 59 +++++++++++++++++++ .../structures/B2FileSseForRequestTest.java | 31 +++++++++- 6 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 core/src/test/java/com/backblaze/b2/client/structures/B2AccountAuthorizationTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4922260..5b7768762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Changelog +## [Unreleased] +### Changed +* `toString()` of `B2AccountAuthorization` and `B2FileSseForRequest` no longer includes the authorization token or SSE-C customer key. + ## [6.5.0] - 2025-12-15 * Add `readBucketLifecycleRules` and `writeBucketLifecycleRules` capabilities. ## [6.4.0] - 2025-05-14 diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2AccountAuthorization.java b/core/src/main/java/com/backblaze/b2/client/structures/B2AccountAuthorization.java index 43b6e5530..fa839da99 100644 --- a/core/src/main/java/com/backblaze/b2/client/structures/B2AccountAuthorization.java +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2AccountAuthorization.java @@ -9,6 +9,8 @@ import java.util.Objects; public class B2AccountAuthorization { + private static final String REDACTED_VALUE = "[redacted]"; + @B2Json.required private final String accountId; @B2Json.required @@ -98,7 +100,7 @@ public boolean equals(Object o) { public String toString() { return "B2AccountAuthorization{" + "accountId='" + accountId + '\'' + - ", authorizationToken='" + authorizationToken + '\'' + + ", authorizationToken='" + REDACTED_VALUE + '\'' + ", apiUrl='" + apiUrl + '\'' + ", downloadUrl='" + downloadUrl + '\'' + ", recommendedPartSize=" + recommendedPartSize + diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2FileSseForRequest.java b/core/src/main/java/com/backblaze/b2/client/structures/B2FileSseForRequest.java index b0ca5454d..924187df4 100644 --- a/core/src/main/java/com/backblaze/b2/client/structures/B2FileSseForRequest.java +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2FileSseForRequest.java @@ -15,6 +15,8 @@ import static com.backblaze.b2.client.structures.B2ServerSideEncryptionMode.SSE_C; public class B2FileSseForRequest { + private static final String REDACTED_VALUE = "[redacted]"; + /** * The SSE mode, e.g. SSE-B2 or SSE-C */ @@ -124,7 +126,7 @@ public String toString() { return "B2FileSseForRequest{" + "mode='" + mode + "', " + "algorithm=" + algorithm + ", " + - "customerKey=" + customerKey + ", " + + "customerKey=" + REDACTED_VALUE + ", " + "customerKeyMd5=" + customerKeyMd5 + '}'; } diff --git a/core/src/test/java/com/backblaze/b2/client/B2AccountAuthorizationCacheTest.java b/core/src/test/java/com/backblaze/b2/client/B2AccountAuthorizationCacheTest.java index 9696abd9e..bd41185cc 100644 --- a/core/src/test/java/com/backblaze/b2/client/B2AccountAuthorizationCacheTest.java +++ b/core/src/test/java/com/backblaze/b2/client/B2AccountAuthorizationCacheTest.java @@ -136,7 +136,7 @@ public void test_forCoverage() { //noinspection ResultOfMethodCallIgnored a.hashCode(); - assertEquals("B2AccountAuthorization{accountId='1', authorizationToken='accountToken1', apiUrl='apiUrl1', downloadUrl='downloadUrl1', recommendedPartSize=1000, absoluteMinimumPartSize=100, allowed=B2Allowed(capabilities=[listFiles], bucketId=null, bucketName=null, namePrefix=), s3ApiUrl=s3ApiUrl1}", a.toString()); + assertEquals("B2AccountAuthorization{accountId='1', authorizationToken='[redacted]', apiUrl='apiUrl1', downloadUrl='downloadUrl1', recommendedPartSize=1000, absoluteMinimumPartSize=100, allowed=B2Allowed(capabilities=[listFiles], bucketId=null, bucketName=null, namePrefix=), s3ApiUrl=s3ApiUrl1}", a.toString()); } } diff --git a/core/src/test/java/com/backblaze/b2/client/structures/B2AccountAuthorizationTest.java b/core/src/test/java/com/backblaze/b2/client/structures/B2AccountAuthorizationTest.java new file mode 100644 index 000000000..1532a462a --- /dev/null +++ b/core/src/test/java/com/backblaze/b2/client/structures/B2AccountAuthorizationTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2026, Backblaze Inc. All Rights Reserved. + * License https://www.backblaze.com/using_b2_code.html + */ +package com.backblaze.b2.client.structures; + +import com.backblaze.b2.json.B2Json; +import com.backblaze.b2.util.B2Collections; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class B2AccountAuthorizationTest { + + @Test + public void testToStringRedactsAuthorizationToken() { + final B2AccountAuthorization authorization = new B2AccountAuthorization( + "account-id", + "SECRET-ACCOUNT-TOKEN", + "https://api.example.com", + "https://download.example.com", + 100, + 50, + new B2Allowed(B2Collections.listOf(B2Capabilities.LIST_FILES), "bucket-id", "bucket-name", "prefix"), + "https://s3.example.com"); + + final String value = authorization.toString(); + + assertFalse(value.contains("SECRET-ACCOUNT-TOKEN")); + assertTrue(value.contains("authorizationToken='[redacted]'")); + assertTrue(value.contains("accountId='account-id'")); + assertTrue(value.contains("apiUrl='https://api.example.com'")); + assertTrue(value.contains("downloadUrl='https://download.example.com'")); + assertTrue(value.contains("recommendedPartSize=100")); + assertTrue(value.contains("absoluteMinimumPartSize=50")); + assertTrue(value.contains("allowed=")); + assertTrue(value.contains("s3ApiUrl=https://s3.example.com")); + } + + @Test + public void testJsonRoundTripKeepsAuthorizationToken() { + final B2AccountAuthorization authorization = new B2AccountAuthorization( + "account-id", + "SECRET-ACCOUNT-TOKEN", + "https://api.example.com", + "https://download.example.com", + 100, + 50, + new B2Allowed(B2Collections.listOf(B2Capabilities.LIST_FILES), "bucket-id", "bucket-name", "prefix"), + "https://s3.example.com"); + + final String json = B2Json.toJsonOrThrowRuntime(authorization); + + assertTrue(json.contains("\"authorizationToken\": \"SECRET-ACCOUNT-TOKEN\"")); + assertEquals(authorization, B2Json.fromJsonOrThrowRuntime(json, B2AccountAuthorization.class)); + } +} diff --git a/core/src/test/java/com/backblaze/b2/client/structures/B2FileSseForRequestTest.java b/core/src/test/java/com/backblaze/b2/client/structures/B2FileSseForRequestTest.java index 2fb44102a..2617ebba4 100644 --- a/core/src/test/java/com/backblaze/b2/client/structures/B2FileSseForRequestTest.java +++ b/core/src/test/java/com/backblaze/b2/client/structures/B2FileSseForRequestTest.java @@ -9,6 +9,8 @@ import java.util.Base64; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /* * Copyright 2020, Backblaze, Inc. All rights reserved. @@ -48,6 +50,33 @@ public void testFullSseCustomerManagedKeyConfig() { final String convertedJson = B2Json.toJsonOrThrowRuntime(defaultConfig); assertEquals(defaultConfig, converted); assertEquals(jsonString, convertedJson); + assertTrue(convertedJson.contains(key)); + } + + @Test + public void testToStringRedactsCustomerKey() { + final B2FileSseForRequest encryption = B2FileSseForRequest.createSseCAes256( + "SECRET-SSE-C-CUSTOMER-KEY", + "customer-key-md5"); + + final String value = encryption.toString(); + + assertFalse(value.contains("SECRET-SSE-C-CUSTOMER-KEY")); + assertTrue(value.contains("customerKey=[redacted]")); + assertTrue(value.contains("mode='SSE-C'")); + assertTrue(value.contains("algorithm=AES256")); + assertTrue(value.contains("customerKeyMd5=customer-key-md5")); + } + + @Test + public void testContainingUploadRequestToStringDoesNotIncludeCustomerKey() { + final String customerKey = "SECRET-SSE-C-CUSTOMER-KEY"; + final B2UploadFileRequest request = B2UploadFileRequest.builder( + "bucket-id", "file-name", "text/plain", null) + .setServerSideEncryption(B2FileSseForRequest.createSseCAes256(customerKey, "customer-key-md5")) + .build(); + + assertFalse(request.toString().contains(customerKey)); } @Test @@ -138,4 +167,4 @@ public void testNull() { "}"; B2Json.fromJsonOrThrowRuntime(jsonString, B2FileSseForRequest.class); } -} \ No newline at end of file +} From 92a24b11fd201e1664b5d56c8f0ec504ba749b7b Mon Sep 17 00:00:00 2001 From: Felipe Fumero Date: Fri, 30 Oct 2026 16:19:49 -0600 Subject: [PATCH 2/4] fix: redact additional credentials from toString --- CHANGELOG.md | 2 +- .../client/contentSources/B2HeadersImpl.java | 14 ++++++++++- .../structures/B2WebhookConfiguration.java | 4 +++- .../structures/B2WebhookCustomHeader.java | 4 +++- .../contentSources/B2HeadersImplTest.java | 23 +++++++++++++++++++ .../structures/B2CustomWebhookHeaderTest.java | 13 +++++++++++ .../B2WebhookConfigurationTest.java | 23 +++++++++++++++++++ 7 files changed, 79 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7768762..90d3d12cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog ## [Unreleased] ### Changed -* `toString()` of `B2AccountAuthorization` and `B2FileSseForRequest` no longer includes the authorization token or SSE-C customer key. +* `toString()` of `B2AccountAuthorization`, `B2FileSseForRequest`, `B2WebhookConfiguration`, and `B2HeadersImpl` no longer includes the authorization token, SSE-C customer key, or webhook credentials. ## [6.5.0] - 2025-12-15 * Add `readBucketLifecycleRules` and `writeBucketLifecycleRules` capabilities. diff --git a/core/src/main/java/com/backblaze/b2/client/contentSources/B2HeadersImpl.java b/core/src/main/java/com/backblaze/b2/client/contentSources/B2HeadersImpl.java index 4267bc5fd..23decf35d 100644 --- a/core/src/main/java/com/backblaze/b2/client/contentSources/B2HeadersImpl.java +++ b/core/src/main/java/com/backblaze/b2/client/contentSources/B2HeadersImpl.java @@ -15,6 +15,8 @@ * B2HeadersImpl implements the B2Headers interface. */ public class B2HeadersImpl implements B2Headers { + private static final String REDACTED_VALUE = "[redacted]"; + private Map pairs; private B2HeadersImpl(Map pairs) { @@ -72,8 +74,18 @@ public B2HeadersImpl build() { @Override public String toString() { + final Map pairsForDisplay = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + pairs.forEach((name, value) -> pairsForDisplay.put( + name, + isSensitiveHeader(name) ? REDACTED_VALUE : value)); + return "B2HeadersImpl{" + - "pairs=" + pairs + + "pairs=" + pairsForDisplay + '}'; } + + private static boolean isSensitiveHeader(String name) { + return B2Headers.AUTHORIZATION.equalsIgnoreCase(name) || + B2Headers.SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY.equalsIgnoreCase(name); + } } diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookConfiguration.java b/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookConfiguration.java index 55d862849..3ddbd3982 100644 --- a/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookConfiguration.java +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookConfiguration.java @@ -14,6 +14,8 @@ * Webhook destination for B2EventNotificationRule */ public class B2WebhookConfiguration extends B2EventNotificationTargetConfiguration { + private static final String REDACTED_VALUE = "[redacted]"; + /** * The URL endpoint for the webhook, including the protocol, which * must be "https://". @@ -105,7 +107,7 @@ public String toString() { return "B2WebhookConfiguration{" + "url='" + url + '\'' + ", customHeaders=" + customHeaders + - ", hmacSha256SigningSecret='" + hmacSha256SigningSecret + '\'' + + ", hmacSha256SigningSecret='" + REDACTED_VALUE + '\'' + '}'; } } diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookCustomHeader.java b/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookCustomHeader.java index 2fec63259..cb699be7f 100644 --- a/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookCustomHeader.java +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookCustomHeader.java @@ -16,6 +16,8 @@ */ public class B2WebhookCustomHeader implements Comparable { + private static final String REDACTED_VALUE = "[redacted]"; + private static final Comparator COMPARATOR = Comparator.comparing(B2WebhookCustomHeader::getName, String.CASE_INSENSITIVE_ORDER) .thenComparing(B2WebhookCustomHeader::getValue); @@ -71,7 +73,7 @@ public int hashCode() { public String toString() { return "B2CustomWebhookHeader{" + "name='" + name + '\'' + - ", value='" + value + '\'' + + ", value='" + REDACTED_VALUE + '\'' + '}'; } diff --git a/core/src/test/java/com/backblaze/b2/client/contentSources/B2HeadersImplTest.java b/core/src/test/java/com/backblaze/b2/client/contentSources/B2HeadersImplTest.java index e165f23bd..a1037b4ab 100644 --- a/core/src/test/java/com/backblaze/b2/client/contentSources/B2HeadersImplTest.java +++ b/core/src/test/java/com/backblaze/b2/client/contentSources/B2HeadersImplTest.java @@ -15,6 +15,7 @@ import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -243,4 +244,26 @@ public void testBuilderThatCopies() { .build(); } } + + @Test + public void testToStringRedactsSensitiveHeaderValuesCaseInsensitively() { + final B2Headers headers = B2HeadersImpl + .builder() + .set("aUtHoRiZaTiOn", "SECRET-AUTHORIZATION") + .set("x-Bz-SeRvEr-SiDe-EnCrYpTiOn-CuStOmEr-KeY", "SECRET-SSE-C-KEY") + .set(B2Headers.SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5, "customer-key-md5") + .set("X-Normal-Header", "normal-value") + .build(); + + final String value = headers.toString(); + + assertFalse(value.contains("SECRET-AUTHORIZATION")); + assertFalse(value.contains("SECRET-SSE-C-KEY")); + assertTrue(value.contains("aUtHoRiZaTiOn=[redacted]")); + assertTrue(value.contains("x-Bz-SeRvEr-SiDe-EnCrYpTiOn-CuStOmEr-KeY=[redacted]")); + assertTrue(value.contains(B2Headers.SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY_MD5 + "=customer-key-md5")); + assertTrue(value.contains("X-Normal-Header=normal-value")); + assertEquals("SECRET-AUTHORIZATION", headers.getValueOrNull(B2Headers.AUTHORIZATION)); + assertEquals("SECRET-SSE-C-KEY", headers.getValueOrNull(B2Headers.SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY)); + } } diff --git a/core/src/test/java/com/backblaze/b2/client/structures/B2CustomWebhookHeaderTest.java b/core/src/test/java/com/backblaze/b2/client/structures/B2CustomWebhookHeaderTest.java index 4b31ff97b..9d40d5481 100644 --- a/core/src/test/java/com/backblaze/b2/client/structures/B2CustomWebhookHeaderTest.java +++ b/core/src/test/java/com/backblaze/b2/client/structures/B2CustomWebhookHeaderTest.java @@ -10,6 +10,8 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public class B2CustomWebhookHeaderTest extends B2BaseTest { @@ -31,4 +33,15 @@ public void testToJsonAndBack() { assertEquals(defaultConfig, converted); assertEquals(jsonString, convertedJson); } + + @Test + public void testToStringRedactsValue() { + final B2WebhookCustomHeader header = new B2WebhookCustomHeader("X-Webhook-Authorization", "SECRET-WEBHOOK-HEADER"); + + final String value = header.toString(); + + assertFalse(value.contains("SECRET-WEBHOOK-HEADER")); + assertTrue(value.contains("name='X-Webhook-Authorization'")); + assertTrue(value.contains("value='[redacted]'")); + } } diff --git a/core/src/test/java/com/backblaze/b2/client/structures/B2WebhookConfigurationTest.java b/core/src/test/java/com/backblaze/b2/client/structures/B2WebhookConfigurationTest.java index f59ed31b5..736e3c8e6 100644 --- a/core/src/test/java/com/backblaze/b2/client/structures/B2WebhookConfigurationTest.java +++ b/core/src/test/java/com/backblaze/b2/client/structures/B2WebhookConfigurationTest.java @@ -13,6 +13,8 @@ import static com.backblaze.b2.util.B2Collections.listOf; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class B2WebhookConfigurationTest extends B2BaseTest { @@ -137,4 +139,25 @@ public void testToJsonAndBack() { assertEquals(defaultConfig, converted); assertEquals(jsonString, convertedJson); } + + @Test + public void testToStringRedactsSecrets() { + final B2WebhookConfiguration configuration = new B2WebhookConfiguration( + "https://www.example.com", + new TreeSet<>(listOf(new B2WebhookCustomHeader("X-Webhook-Authorization", "SECRET-WEBHOOK-HEADER"))), + "SECRET-WEBHOOK-SIGNING-SECRET", + null); + + final String value = configuration.toString(); + final String json = B2Json.toJsonOrThrowRuntime(configuration); + + assertFalse(value.contains("SECRET-WEBHOOK-SIGNING-SECRET")); + assertFalse(value.contains("SECRET-WEBHOOK-HEADER")); + assertTrue(value.contains("url='https://www.example.com'")); + assertTrue(value.contains("X-Webhook-Authorization")); + assertTrue(value.contains("hmacSha256SigningSecret='[redacted]'")); + assertTrue(value.contains("value='[redacted]'")); + assertTrue(json.contains("\"hmacSha256SigningSecret\": \"SECRET-WEBHOOK-SIGNING-SECRET\"")); + assertTrue(json.contains("\"value\": \"SECRET-WEBHOOK-HEADER\"")); + } } From 40c52f632ed2996aefc7b2370fe5f54c588ced06 Mon Sep 17 00:00:00 2001 From: Felipe Fumero Date: Fri, 30 Oct 2026 16:45:30 -0600 Subject: [PATCH 3/4] fix: preserve event rule ordering Remove the vacuous upload-request toString test. --- CHANGELOG.md | 2 +- .../client/contentSources/B2HeadersImpl.java | 8 +- .../structures/B2AccountAuthorization.java | 2 +- .../structures/B2EventNotificationRule.java | 97 ++++++++++++++++++- .../structures/B2FileSseForRequest.java | 2 +- .../structures/B2WebhookConfiguration.java | 2 +- .../structures/B2WebhookCustomHeader.java | 2 +- .../contentSources/B2HeadersImplTest.java | 13 +++ .../B2AccountAuthorizationTest.java | 18 ++++ .../structures/B2CustomWebhookHeaderTest.java | 8 ++ .../B2EventNotificationRuleTest.java | 61 ++++++++++++ .../structures/B2FileSseForRequestTest.java | 13 +-- .../B2WebhookConfigurationTest.java | 8 ++ 13 files changed, 219 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d3d12cc..e8da9e058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog ## [Unreleased] ### Changed -* `toString()` of `B2AccountAuthorization`, `B2FileSseForRequest`, `B2WebhookConfiguration`, and `B2HeadersImpl` no longer includes the authorization token, SSE-C customer key, or webhook credentials. +* `toString()` of credential-bearing structures (`B2AccountAuthorization`, `B2FileSseForRequest`, `B2WebhookConfiguration`, `B2WebhookCustomHeader`, and `B2HeadersImpl`) omits sensitive values; `B2WebhookCustomHeader` omits all header values. ## [6.5.0] - 2025-12-15 * Add `readBucketLifecycleRules` and `writeBucketLifecycleRules` capabilities. diff --git a/core/src/main/java/com/backblaze/b2/client/contentSources/B2HeadersImpl.java b/core/src/main/java/com/backblaze/b2/client/contentSources/B2HeadersImpl.java index 23decf35d..5dcf9ba41 100644 --- a/core/src/main/java/com/backblaze/b2/client/contentSources/B2HeadersImpl.java +++ b/core/src/main/java/com/backblaze/b2/client/contentSources/B2HeadersImpl.java @@ -75,9 +75,7 @@ public B2HeadersImpl build() { @Override public String toString() { final Map pairsForDisplay = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - pairs.forEach((name, value) -> pairsForDisplay.put( - name, - isSensitiveHeader(name) ? REDACTED_VALUE : value)); + pairs.forEach((name, value) -> pairsForDisplay.put(name, getDisplayValue(name, value))); return "B2HeadersImpl{" + "pairs=" + pairsForDisplay + @@ -88,4 +86,8 @@ private static boolean isSensitiveHeader(String name) { return B2Headers.AUTHORIZATION.equalsIgnoreCase(name) || B2Headers.SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY.equalsIgnoreCase(name); } + + private static String getDisplayValue(String name, String value) { + return value != null && isSensitiveHeader(name) ? REDACTED_VALUE : value; + } } diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2AccountAuthorization.java b/core/src/main/java/com/backblaze/b2/client/structures/B2AccountAuthorization.java index fa839da99..0cb52715d 100644 --- a/core/src/main/java/com/backblaze/b2/client/structures/B2AccountAuthorization.java +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2AccountAuthorization.java @@ -100,7 +100,7 @@ public boolean equals(Object o) { public String toString() { return "B2AccountAuthorization{" + "accountId='" + accountId + '\'' + - ", authorizationToken='" + REDACTED_VALUE + '\'' + + ", authorizationToken='" + (authorizationToken == null ? null : REDACTED_VALUE) + '\'' + ", apiUrl='" + apiUrl + '\'' + ", downloadUrl='" + downloadUrl + '\'' + ", recommendedPartSize=" + recommendedPartSize + diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2EventNotificationRule.java b/core/src/main/java/com/backblaze/b2/client/structures/B2EventNotificationRule.java index a46734c3a..ba9db7837 100644 --- a/core/src/main/java/com/backblaze/b2/client/structures/B2EventNotificationRule.java +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2EventNotificationRule.java @@ -7,6 +7,7 @@ import com.backblaze.b2.json.B2Json; import java.util.Comparator; +import java.util.Iterator; import java.util.Objects; import java.util.TreeSet; @@ -17,7 +18,7 @@ public class B2EventNotificationRule implements Comparable COMPARATOR = Comparator.comparing(B2EventNotificationRule::getName) .thenComparing(rule -> String.join(",", rule.getEventTypes())) .thenComparing(B2EventNotificationRule::getObjectNamePrefix) - .thenComparing(rule -> rule.getTargetConfiguration().toString()) + .thenComparing(B2EventNotificationRule::getTargetConfiguration, B2EventNotificationRule::compareTargetConfigurations) .thenComparing(B2EventNotificationRule::isEnabled) .thenComparing(B2EventNotificationRule::isSuspended) .thenComparing(B2EventNotificationRule::getSuspensionReason); @@ -172,4 +173,98 @@ public String toString() { public int compareTo(B2EventNotificationRule r) { return COMPARATOR.compare(this, r); } + + private static int compareTargetConfigurations(B2EventNotificationTargetConfiguration first, + B2EventNotificationTargetConfiguration second) { + if (first == second) { + return 0; + } + if (first == null) { + return -1; + } + if (second == null) { + return 1; + } + if (first instanceof B2WebhookConfiguration && second instanceof B2WebhookConfiguration) { + return compareWebhookConfigurations((B2WebhookConfiguration) first, (B2WebhookConfiguration) second); + } + + final int classComparison = first.getClass().getName().compareTo(second.getClass().getName()); + if (classComparison != 0) { + return classComparison; + } + if (first.equals(second)) { + return 0; + } + throw new IllegalArgumentException("Unsupported event notification target configuration type: " + first.getClass().getName()); + } + + private static int compareWebhookConfigurations(B2WebhookConfiguration first, + B2WebhookConfiguration second) { + int comparison = first.getUrl().compareTo(second.getUrl()); + if (comparison != 0) { + return comparison; + } + + comparison = compareCustomHeaders(first.getCustomHeaders(), second.getCustomHeaders()); + if (comparison != 0) { + return comparison; + } + + comparison = compareNullableStrings(first.getHmacSha256SigningSecret(), second.getHmacSha256SigningSecret()); + if (comparison != 0) { + return comparison; + } + + return compareNullableIntegers(first.getMaxEventsPerBatch(), second.getMaxEventsPerBatch()); + } + + private static int compareCustomHeaders(TreeSet first, + TreeSet second) { + if (first == second) { + return 0; + } + if (first == null) { + return -1; + } + if (second == null) { + return 1; + } + + final Iterator firstIterator = first.iterator(); + final Iterator secondIterator = second.iterator(); + while (firstIterator.hasNext() && secondIterator.hasNext()) { + final int comparison = firstIterator.next().compareTo(secondIterator.next()); + if (comparison != 0) { + return comparison; + } + } + return Boolean.compare(firstIterator.hasNext(), secondIterator.hasNext()); + } + + private static int compareNullableStrings(String first, String second) { + if (first == second) { + return 0; + } + if (first == null) { + return -1; + } + if (second == null) { + return 1; + } + return first.compareTo(second); + } + + private static int compareNullableIntegers(Integer first, Integer second) { + if (first == second) { + return 0; + } + if (first == null) { + return -1; + } + if (second == null) { + return 1; + } + return first.compareTo(second); + } } diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2FileSseForRequest.java b/core/src/main/java/com/backblaze/b2/client/structures/B2FileSseForRequest.java index 924187df4..97bb39b1a 100644 --- a/core/src/main/java/com/backblaze/b2/client/structures/B2FileSseForRequest.java +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2FileSseForRequest.java @@ -126,7 +126,7 @@ public String toString() { return "B2FileSseForRequest{" + "mode='" + mode + "', " + "algorithm=" + algorithm + ", " + - "customerKey=" + REDACTED_VALUE + ", " + + "customerKey=" + (customerKey == null ? null : REDACTED_VALUE) + ", " + "customerKeyMd5=" + customerKeyMd5 + '}'; } diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookConfiguration.java b/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookConfiguration.java index 3ddbd3982..66bdc094d 100644 --- a/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookConfiguration.java +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookConfiguration.java @@ -107,7 +107,7 @@ public String toString() { return "B2WebhookConfiguration{" + "url='" + url + '\'' + ", customHeaders=" + customHeaders + - ", hmacSha256SigningSecret='" + REDACTED_VALUE + '\'' + + ", hmacSha256SigningSecret='" + (hmacSha256SigningSecret == null ? null : REDACTED_VALUE) + '\'' + '}'; } } diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookCustomHeader.java b/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookCustomHeader.java index cb699be7f..90fb405f1 100644 --- a/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookCustomHeader.java +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2WebhookCustomHeader.java @@ -73,7 +73,7 @@ public int hashCode() { public String toString() { return "B2CustomWebhookHeader{" + "name='" + name + '\'' + - ", value='" + REDACTED_VALUE + '\'' + + ", value='" + (value == null ? null : REDACTED_VALUE) + '\'' + '}'; } diff --git a/core/src/test/java/com/backblaze/b2/client/contentSources/B2HeadersImplTest.java b/core/src/test/java/com/backblaze/b2/client/contentSources/B2HeadersImplTest.java index a1037b4ab..5189946ac 100644 --- a/core/src/test/java/com/backblaze/b2/client/contentSources/B2HeadersImplTest.java +++ b/core/src/test/java/com/backblaze/b2/client/contentSources/B2HeadersImplTest.java @@ -266,4 +266,17 @@ public void testToStringRedactsSensitiveHeaderValuesCaseInsensitively() { assertEquals("SECRET-AUTHORIZATION", headers.getValueOrNull(B2Headers.AUTHORIZATION)); assertEquals("SECRET-SSE-C-KEY", headers.getValueOrNull(B2Headers.SERVER_SIDE_ENCRYPTION_CUSTOMER_KEY)); } + + @Test + public void testToStringShowsNullSensitiveHeaderValue() { + final B2Headers headers = B2HeadersImpl + .builder() + .set("aUtHoRiZaTiOn", null) + .build(); + + final String value = headers.toString(); + + assertTrue(value.contains("aUtHoRiZaTiOn=null")); + assertFalse(value.contains("aUtHoRiZaTiOn=[redacted]")); + } } diff --git a/core/src/test/java/com/backblaze/b2/client/structures/B2AccountAuthorizationTest.java b/core/src/test/java/com/backblaze/b2/client/structures/B2AccountAuthorizationTest.java index 1532a462a..3a864040c 100644 --- a/core/src/test/java/com/backblaze/b2/client/structures/B2AccountAuthorizationTest.java +++ b/core/src/test/java/com/backblaze/b2/client/structures/B2AccountAuthorizationTest.java @@ -56,4 +56,22 @@ public void testJsonRoundTripKeepsAuthorizationToken() { assertTrue(json.contains("\"authorizationToken\": \"SECRET-ACCOUNT-TOKEN\"")); assertEquals(authorization, B2Json.fromJsonOrThrowRuntime(json, B2AccountAuthorization.class)); } + + @Test + public void testToStringShowsNullAuthorizationToken() { + final B2AccountAuthorization authorization = new B2AccountAuthorization( + "account-id", + null, + "https://api.example.com", + "https://download.example.com", + 100, + 50, + new B2Allowed(B2Collections.listOf(B2Capabilities.LIST_FILES), "bucket-id", "bucket-name", "prefix"), + "https://s3.example.com"); + + final String value = authorization.toString(); + + assertTrue(value.contains("authorizationToken='null'")); + assertFalse(value.contains("authorizationToken='[redacted]'")); + } } diff --git a/core/src/test/java/com/backblaze/b2/client/structures/B2CustomWebhookHeaderTest.java b/core/src/test/java/com/backblaze/b2/client/structures/B2CustomWebhookHeaderTest.java index 9d40d5481..87a8ff31f 100644 --- a/core/src/test/java/com/backblaze/b2/client/structures/B2CustomWebhookHeaderTest.java +++ b/core/src/test/java/com/backblaze/b2/client/structures/B2CustomWebhookHeaderTest.java @@ -44,4 +44,12 @@ public void testToStringRedactsValue() { assertTrue(value.contains("name='X-Webhook-Authorization'")); assertTrue(value.contains("value='[redacted]'")); } + + @Test + public void testToStringShowsNullValue() { + final String value = new B2WebhookCustomHeader("X-Webhook-Authorization", null).toString(); + + assertTrue(value.contains("value='null'")); + assertFalse(value.contains("value='[redacted]'")); + } } diff --git a/core/src/test/java/com/backblaze/b2/client/structures/B2EventNotificationRuleTest.java b/core/src/test/java/com/backblaze/b2/client/structures/B2EventNotificationRuleTest.java index d0601bc13..b2b3656e6 100644 --- a/core/src/test/java/com/backblaze/b2/client/structures/B2EventNotificationRuleTest.java +++ b/core/src/test/java/com/backblaze/b2/client/structures/B2EventNotificationRuleTest.java @@ -12,6 +12,8 @@ import static com.backblaze.b2.util.B2Collections.listOf; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class B2EventNotificationRuleTest extends B2BaseTest { @@ -77,4 +79,63 @@ public void testToJsonAndBack() { assertEquals(defaultConfig, converted); assertEquals(jsonString, convertedJson); } + + @Test + public void testCompareToDistinguishesSigningSecrets() { + final B2EventNotificationRule first = makeWebhookRule("signing-secret-one", "header-value", 20); + final B2EventNotificationRule second = makeWebhookRule("signing-secret-two", "header-value", 20); + + assertFalse(first.equals(second)); + assertTrue(first.compareTo(second) != 0); + + final TreeSet rules = new TreeSet<>(listOf(first, second)); + assertEquals(2, rules.size()); + } + + @Test + public void testCompareToDistinguishesCustomHeaderValues() { + final B2EventNotificationRule first = makeWebhookRule("signing-secret", "header-value-one", 20); + final B2EventNotificationRule second = makeWebhookRule("signing-secret", "header-value-two", 20); + + assertFalse(first.equals(second)); + assertTrue(first.compareTo(second) != 0); + + final TreeSet rules = new TreeSet<>(listOf(first, second)); + assertEquals(2, rules.size()); + } + + @Test + public void testCompareToDistinguishesMaxEventsPerBatch() { + final B2EventNotificationRule first = makeWebhookRule("signing-secret", "header-value", 1); + final B2EventNotificationRule second = makeWebhookRule("signing-secret", "header-value", 2); + + assertFalse(first.equals(second)); + assertTrue(first.compareTo(second) != 0); + } + + @Test + public void testCompareToIsConsistentWithEquals() { + final B2EventNotificationRule first = makeWebhookRule("signing-secret", "header-value", 20); + final B2EventNotificationRule second = makeWebhookRule("signing-secret", "header-value", 20); + + assertEquals(first, second); + assertEquals(0, first.compareTo(second)); + } + + private B2EventNotificationRule makeWebhookRule(String signingSecret, + String customHeaderValue, + int maxEventsPerBatch) { + return new B2EventNotificationRule( + "myRule", + new TreeSet<>(listOf("b2:ObjectCreated:Upload")), + "", + new B2WebhookConfiguration( + "https://www.example.com", + new TreeSet<>(listOf(new B2WebhookCustomHeader("X-Webhook-Authorization", customHeaderValue))), + signingSecret, + maxEventsPerBatch), + true, + false, + ""); + } } diff --git a/core/src/test/java/com/backblaze/b2/client/structures/B2FileSseForRequestTest.java b/core/src/test/java/com/backblaze/b2/client/structures/B2FileSseForRequestTest.java index 2617ebba4..b206691fd 100644 --- a/core/src/test/java/com/backblaze/b2/client/structures/B2FileSseForRequestTest.java +++ b/core/src/test/java/com/backblaze/b2/client/structures/B2FileSseForRequestTest.java @@ -69,14 +69,11 @@ public void testToStringRedactsCustomerKey() { } @Test - public void testContainingUploadRequestToStringDoesNotIncludeCustomerKey() { - final String customerKey = "SECRET-SSE-C-CUSTOMER-KEY"; - final B2UploadFileRequest request = B2UploadFileRequest.builder( - "bucket-id", "file-name", "text/plain", null) - .setServerSideEncryption(B2FileSseForRequest.createSseCAes256(customerKey, "customer-key-md5")) - .build(); - - assertFalse(request.toString().contains(customerKey)); + public void testToStringShowsNullCustomerKey() { + final String value = B2FileSseForRequest.createSseB2Aes256().toString(); + + assertTrue(value.contains("customerKey=null")); + assertFalse(value.contains("customerKey=[redacted]")); } @Test diff --git a/core/src/test/java/com/backblaze/b2/client/structures/B2WebhookConfigurationTest.java b/core/src/test/java/com/backblaze/b2/client/structures/B2WebhookConfigurationTest.java index 736e3c8e6..22ed4478d 100644 --- a/core/src/test/java/com/backblaze/b2/client/structures/B2WebhookConfigurationTest.java +++ b/core/src/test/java/com/backblaze/b2/client/structures/B2WebhookConfigurationTest.java @@ -160,4 +160,12 @@ public void testToStringRedactsSecrets() { assertTrue(json.contains("\"hmacSha256SigningSecret\": \"SECRET-WEBHOOK-SIGNING-SECRET\"")); assertTrue(json.contains("\"value\": \"SECRET-WEBHOOK-HEADER\"")); } + + @Test + public void testToStringShowsNullSigningSecret() { + final String value = new B2WebhookConfiguration("https://www.example.com").toString(); + + assertTrue(value.contains("hmacSha256SigningSecret='null'")); + assertFalse(value.contains("hmacSha256SigningSecret='[redacted]'")); + } } From ccdb191ee3d1aa495ca984816740bcb090b05c68 Mon Sep 17 00:00:00 2001 From: Felipe Fumero Date: Fri, 30 Oct 2026 16:59:03 -0600 Subject: [PATCH 4/4] fix: order unknown event notification targets without throwing --- CHANGELOG.md | 1 + .../structures/B2EventNotificationRule.java | 2 +- .../B2EventNotificationRuleTest.java | 35 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8da9e058..4ab06730a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] ### Changed * `toString()` of credential-bearing structures (`B2AccountAuthorization`, `B2FileSseForRequest`, `B2WebhookConfiguration`, `B2WebhookCustomHeader`, and `B2HeadersImpl`) omits sensitive values; `B2WebhookCustomHeader` omits all header values. +* Event notification rule ordering compares webhook target settings (url, custom headers, signing secret, maxEventsPerBatch) directly instead of through `toString()`. ## [6.5.0] - 2025-12-15 * Add `readBucketLifecycleRules` and `writeBucketLifecycleRules` capabilities. diff --git a/core/src/main/java/com/backblaze/b2/client/structures/B2EventNotificationRule.java b/core/src/main/java/com/backblaze/b2/client/structures/B2EventNotificationRule.java index ba9db7837..7f6a0290e 100644 --- a/core/src/main/java/com/backblaze/b2/client/structures/B2EventNotificationRule.java +++ b/core/src/main/java/com/backblaze/b2/client/structures/B2EventNotificationRule.java @@ -196,7 +196,7 @@ private static int compareTargetConfigurations(B2EventNotificationTargetConfigur if (first.equals(second)) { return 0; } - throw new IllegalArgumentException("Unsupported event notification target configuration type: " + first.getClass().getName()); + return first.toString().compareTo(second.toString()); } private static int compareWebhookConfigurations(B2WebhookConfiguration first, diff --git a/core/src/test/java/com/backblaze/b2/client/structures/B2EventNotificationRuleTest.java b/core/src/test/java/com/backblaze/b2/client/structures/B2EventNotificationRuleTest.java index b2b3656e6..667524736 100644 --- a/core/src/test/java/com/backblaze/b2/client/structures/B2EventNotificationRuleTest.java +++ b/core/src/test/java/com/backblaze/b2/client/structures/B2EventNotificationRuleTest.java @@ -122,6 +122,41 @@ public void testCompareToIsConsistentWithEquals() { assertEquals(0, first.compareTo(second)); } + @Test + public void testCompareToOrdersUnknownTargetConfigurationsWithoutThrowing() { + final B2EventNotificationRule first = new B2EventNotificationRule( + "myRule", + new TreeSet<>(listOf("b2:ObjectCreated:Upload")), + "", + new B2EventNotificationTargetConfiguration(), + true, + false, + ""); + final B2EventNotificationRule second = new B2EventNotificationRule( + "myRule", + new TreeSet<>(listOf("b2:ObjectCreated:Upload")), + "", + new B2EventNotificationTargetConfiguration(), + true, + false, + ""); + + try { + first.compareTo(second); + } catch (IllegalArgumentException e) { + fail("compareTo must support unknown target configurations: " + e.getMessage()); + } + + final TreeSet rules = new TreeSet<>(); + try { + rules.add(first); + rules.add(second); + } catch (IllegalArgumentException e) { + fail("TreeSet must support unknown target configurations: " + e.getMessage()); + } + assertEquals(2, rules.size()); + } + private B2EventNotificationRule makeWebhookRule(String signingSecret, String customHeaderValue, int maxEventsPerBatch) {