Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Changelog
## [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.
## [6.4.0] - 2025-05-14
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
* B2HeadersImpl implements the B2Headers interface.
*/
public class B2HeadersImpl implements B2Headers {
private static final String REDACTED_VALUE = "[redacted]";

private Map<String,String> pairs;

private B2HeadersImpl(Map<String, String> pairs) {
Expand Down Expand Up @@ -72,8 +74,20 @@ public B2HeadersImpl build() {

@Override
public String toString() {
final Map<String, String> pairsForDisplay = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
pairs.forEach((name, value) -> pairsForDisplay.put(name, getDisplayValue(name, 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);
}

private static String getDisplayValue(String name, String value) {
return value != null && isSensitiveHeader(name) ? REDACTED_VALUE : value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -98,7 +100,7 @@ public boolean equals(Object o) {
public String toString() {
return "B2AccountAuthorization{" +
"accountId='" + accountId + '\'' +
", authorizationToken='" + authorizationToken + '\'' +
", authorizationToken='" + (authorizationToken == null ? null : REDACTED_VALUE) + '\'' +
", apiUrl='" + apiUrl + '\'' +
", downloadUrl='" + downloadUrl + '\'' +
", recommendedPartSize=" + recommendedPartSize +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -17,7 +18,7 @@ public class B2EventNotificationRule implements Comparable<B2EventNotificationRu
private static final Comparator<B2EventNotificationRule> 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);
Expand Down Expand Up @@ -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;
}
return first.toString().compareTo(second.toString());
}

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<B2WebhookCustomHeader> first,
TreeSet<B2WebhookCustomHeader> second) {
if (first == second) {
return 0;
}
if (first == null) {
return -1;
}
if (second == null) {
return 1;
}

final Iterator<B2WebhookCustomHeader> firstIterator = first.iterator();
final Iterator<B2WebhookCustomHeader> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -124,7 +126,7 @@ public String toString() {
return "B2FileSseForRequest{" +
"mode='" + mode + "', " +
"algorithm=" + algorithm + ", " +
"customerKey=" + customerKey + ", " +
"customerKey=" + (customerKey == null ? null : REDACTED_VALUE) + ", " +
"customerKeyMd5=" + customerKeyMd5 +
'}';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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://".
Expand Down Expand Up @@ -105,7 +107,7 @@ public String toString() {
return "B2WebhookConfiguration{" +
"url='" + url + '\'' +
", customHeaders=" + customHeaders +
", hmacSha256SigningSecret='" + hmacSha256SigningSecret + '\'' +
", hmacSha256SigningSecret='" + (hmacSha256SigningSecret == null ? null : REDACTED_VALUE) + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*/
public class B2WebhookCustomHeader implements Comparable<B2WebhookCustomHeader> {

private static final String REDACTED_VALUE = "[redacted]";

private static final Comparator<B2WebhookCustomHeader> COMPARATOR =
Comparator.comparing(B2WebhookCustomHeader::getName, String.CASE_INSENSITIVE_ORDER)
.thenComparing(B2WebhookCustomHeader::getValue);
Expand Down Expand Up @@ -71,7 +73,7 @@ public int hashCode() {
public String toString() {
return "B2CustomWebhookHeader{" +
"name='" + name + '\'' +
", value='" + value + '\'' +
", value='" + (value == null ? null : REDACTED_VALUE) + '\'' +
'}';
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -243,4 +244,39 @@ 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));
}

@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]"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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));
}

@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]'"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -31,4 +33,23 @@ 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]'"));
}

@Test
public void testToStringShowsNullValue() {
final String value = new B2WebhookCustomHeader("X-Webhook-Authorization", null).toString();

assertTrue(value.contains("value='null'"));
assertFalse(value.contains("value='[redacted]'"));
}
}
Loading
Loading