Skip to content

Commit 6ce5377

Browse files
authored
feat: redact secret key material in credential toString() (#90)
PR: #90
1 parent a594c7e commit 6ce5377

4 files changed

Lines changed: 72 additions & 0 deletions

File tree

sdk-core/api/sdk-core.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,14 @@ public final class org/dexpace/sdk/core/http/auth/KeyCredential : org/dexpace/sd
201201
public final fun getApiKey ()Ljava/lang/String;
202202
public final fun getHeaderName ()Lorg/dexpace/sdk/core/http/common/HttpHeaderName;
203203
public final fun getPrefix ()Ljava/lang/String;
204+
public fun toString ()Ljava/lang/String;
204205
}
205206

206207
public final class org/dexpace/sdk/core/http/auth/NamedKeyCredential : org/dexpace/sdk/core/http/auth/Credential {
207208
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
208209
public final fun getKey ()Ljava/lang/String;
209210
public final fun getName ()Ljava/lang/String;
211+
public fun toString ()Ljava/lang/String;
210212
}
211213

212214
public final class org/dexpace/sdk/core/http/common/CommonMediaTypes {

sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/auth/KeyCredential.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,12 @@ public class KeyCredential
3434
init {
3535
require(apiKey.isNotBlank()) { "apiKey must not be blank" }
3636
}
37+
38+
/**
39+
* Redacts the secret [apiKey]. Without this override any log line, exception message,
40+
* or debugger that stringifies the credential would expose the key; this emits
41+
* `apiKey=***` instead while keeping the non-secret [headerName] and [prefix] for
42+
* diagnostics. Identity equality is unaffected.
43+
*/
44+
override fun toString(): String = "KeyCredential(apiKey=***, headerName=$headerName, prefix=$prefix)"
3745
}

sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/auth/NamedKeyCredential.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,12 @@ public class NamedKeyCredential(public val name: String, public val key: String)
2424
require(name.isNotBlank()) { "name must not be blank" }
2525
require(key.isNotBlank()) { "key must not be blank" }
2626
}
27+
28+
/**
29+
* Redacts the secret [key]. Without this override any log line, exception message, or
30+
* debugger that stringifies the credential would expose the key; this emits `key=***`
31+
* instead while keeping the non-secret [name] for diagnostics. Identity equality is
32+
* unaffected.
33+
*/
34+
override fun toString(): String = "NamedKeyCredential(name=$name, key=***)"
2735
}

sdk-core/src/test/kotlin/org/dexpace/sdk/core/http/auth/CredentialTest.kt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import org.dexpace.sdk.core.http.common.HttpHeaderName
1111
import kotlin.test.Test
1212
import kotlin.test.assertEquals
1313
import kotlin.test.assertFailsWith
14+
import kotlin.test.assertFalse
15+
import kotlin.test.assertNotEquals
1416
import kotlin.test.assertNull
17+
import kotlin.test.assertTrue
1518

1619
class CredentialTest {
1720
// ----------------- KeyCredential -----------------
@@ -54,6 +57,31 @@ class CredentialTest {
5457
assertEquals("k", (cred as KeyCredential).apiKey)
5558
}
5659

60+
@Test
61+
fun `KeyCredential toString redacts the apiKey but keeps non-secret fields`() {
62+
val cred = KeyCredential("super-secret-key", prefix = "SharedAccessKey")
63+
val rendered = cred.toString()
64+
assertFalse(rendered.contains("super-secret-key"), "toString must not contain the raw apiKey")
65+
assertTrue(rendered.contains("apiKey=***"), "toString must redact the apiKey")
66+
assertTrue(rendered.contains("SharedAccessKey"), "toString should keep the non-secret prefix for diagnostics")
67+
assertTrue(
68+
rendered.contains(HttpHeaderName.AUTHORIZATION.toString()),
69+
"toString should keep the non-secret headerName for diagnostics",
70+
)
71+
}
72+
73+
@Test
74+
fun `KeyCredential toString redacts the apiKey with default fields`() {
75+
val cred = KeyCredential("another-secret")
76+
val rendered = cred.toString()
77+
assertFalse(rendered.contains("another-secret"), "toString must not contain the raw apiKey")
78+
assertTrue(rendered.contains("apiKey=***"), "toString must redact the apiKey")
79+
assertTrue(
80+
rendered.contains(HttpHeaderName.AUTHORIZATION.toString()),
81+
"toString should keep the default headerName for diagnostics",
82+
)
83+
}
84+
5785
// ----------------- NamedKeyCredential -----------------
5886

5987
@Test
@@ -92,4 +120,30 @@ class CredentialTest {
92120
val cred: Credential = NamedKeyCredential("acct", "secret")
93121
assertEquals("acct", (cred as NamedKeyCredential).name)
94122
}
123+
124+
@Test
125+
fun `NamedKeyCredential toString redacts the key but keeps the name`() {
126+
val cred = NamedKeyCredential("acct-name", "super-secret-key")
127+
val rendered = cred.toString()
128+
assertFalse(rendered.contains("super-secret-key"), "toString must not contain the raw key")
129+
assertTrue(rendered.contains("key=***"), "toString must redact the key")
130+
assertTrue(rendered.contains("acct-name"), "toString should keep the non-secret name for diagnostics")
131+
}
132+
133+
@Test
134+
fun `NamedKeyCredential keeps identity equality - redaction is toString-only`() {
135+
// These are reference types (not data classes): equality is identity-based and the
136+
// redacting toString override must not change that. Same instance equals itself;
137+
// distinct instances with the same fields are not equal.
138+
val cred = NamedKeyCredential("acct", "secret")
139+
assertEquals(cred, cred)
140+
assertNotEquals<NamedKeyCredential>(cred, NamedKeyCredential("acct", "secret"))
141+
}
142+
143+
@Test
144+
fun `KeyCredential keeps identity equality - redaction is toString-only`() {
145+
val cred = KeyCredential("secret-key")
146+
assertEquals(cred, cred)
147+
assertNotEquals<KeyCredential>(cred, KeyCredential("secret-key"))
148+
}
95149
}

0 commit comments

Comments
 (0)