Skip to content

Commit 0efa66f

Browse files
committed
StringUtils class & better API key validation (#928)
1 parent 090ebb4 commit 0efa66f

File tree

6 files changed

+230
-30
lines changed

6 files changed

+230
-30
lines changed

src/main/java/com/stripe/net/ApiResource.java

+2-6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.stripe.model.StripeObject;
2121
import com.stripe.model.StripeRawJsonObject;
2222
import com.stripe.model.StripeRawJsonObjectDeserializer;
23+
import com.stripe.util.StringUtils;
2324
import java.io.UnsupportedEncodingException;
2425
import java.net.URLEncoder;
2526
import java.nio.charset.StandardCharsets;
@@ -55,12 +56,7 @@ private static Gson createGson() {
5556

5657
private static String className(Class<?> clazz) {
5758
// Convert CamelCase to snake_case
58-
String className =
59-
clazz
60-
.getSimpleName()
61-
.replaceAll("(.)([A-Z][a-z]+)", "$1_$2")
62-
.replaceAll("([a-z0-9])([A-Z])", "$1_$2")
63-
.toLowerCase();
59+
String className = StringUtils.toSnakeCase(clazz.getSimpleName());
6460

6561
// Handle namespaced resources by checking if the class is in a sub-package, and if so prepend
6662
// it to the class name

src/main/java/com/stripe/net/StripeRequest.java

+25-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.stripe.exception.AuthenticationException;
66
import com.stripe.exception.StripeException;
77
import com.stripe.util.ReflectionUtils;
8+
import com.stripe.util.StringUtils;
89
import java.io.IOException;
910
import java.net.URL;
1011
import java.net.URLStreamHandler;
@@ -129,12 +130,30 @@ private static Map<String, String> buildHeaders(
129130

130131
// Authorization
131132
String apiKey = options.getApiKey();
132-
if (apiKey == null || apiKey.trim().isEmpty()) {
133+
if (apiKey == null) {
133134
throw new AuthenticationException(
134-
"No API key provided. (HINT: set your API key using 'Stripe.apiKey = <API-KEY>'. "
135-
+ "You can generate API keys from the Stripe web interface. "
136-
+ "See https://stripe.com/api for details or email [email protected] if you have "
137-
+ "questions.",
135+
"No API key provided. Set your API key using `Stripe.apiKey = \"<API-KEY>\"`. You can "
136+
+ "generate API keys from the Stripe Dashboard. See "
137+
+ "https://stripe.com/docs/api/authentication for details or contact support at "
138+
+ "https://support.stripe.com/email if you have any questions.",
139+
null,
140+
null,
141+
0);
142+
} else if (apiKey.isEmpty()) {
143+
throw new AuthenticationException(
144+
"Your API key is invalid, as it is an empty string. You can double-check your API key "
145+
+ "from the Stripe Dashboard. See "
146+
+ "https://stripe.com/docs/api/authentication for details or contact support at "
147+
+ "https://support.stripe.com/email if you have any questions.",
148+
null,
149+
null,
150+
0);
151+
} else if (StringUtils.containsWhitespace(apiKey)) {
152+
throw new AuthenticationException(
153+
"Your API key is invalid, as it contains whitespace. You can double-check your API key "
154+
+ "from the Stripe Dashboard. See "
155+
+ "https://stripe.com/docs/api/authentication for details or contact support at "
156+
+ "https://support.stripe.com/email if you have any questions.",
138157
null,
139158
null,
140159
0);
@@ -148,7 +167,7 @@ private static Map<String, String> buildHeaders(
148167
headers.put("Stripe-Version", options.getStripeVersion());
149168
} else {
150169
throw new IllegalStateException(
151-
"Either `stripeVersion`, or `stripeVersionOverride` " + "value must be set.");
170+
"Either `stripeVersion` or `stripeVersionOverride` value must be set.");
152171
}
153172

154173
// Stripe-Account

src/main/java/com/stripe/net/Webhook.java

+2-17
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import com.stripe.exception.SignatureVerificationException;
44
import com.stripe.model.Event;
5+
import com.stripe.util.StringUtils;
56
import java.nio.charset.StandardCharsets;
67
import java.security.InvalidKeyException;
7-
import java.security.MessageDigest;
88
import java.security.NoSuchAlgorithmException;
99
import java.util.ArrayList;
1010
import java.util.List;
@@ -93,7 +93,7 @@ public static boolean verifyHeader(
9393
// Check if expected signature is found in list of header's signatures
9494
boolean signatureFound = false;
9595
for (String signature : signatures) {
96-
if (Util.secureCompare(expectedSignature, signature)) {
96+
if (StringUtils.secureCompare(expectedSignature, signature)) {
9797
signatureFound = true;
9898
break;
9999
}
@@ -186,21 +186,6 @@ public static String computeHmacSha256(String key, String message)
186186
return result;
187187
}
188188

189-
/**
190-
* Compares two strings for equality. The time taken is independent of the number of characters
191-
* that match.
192-
*
193-
* @param a one of the strings to compare.
194-
* @param b the other string to compare.
195-
* @return true if the strings are equal, false otherwise.
196-
*/
197-
public static boolean secureCompare(String a, String b) {
198-
byte[] digesta = a.getBytes(StandardCharsets.UTF_8);
199-
byte[] digestb = b.getBytes(StandardCharsets.UTF_8);
200-
201-
return MessageDigest.isEqual(digesta, digestb);
202-
}
203-
204189
/**
205190
* Returns the current UTC timestamp in seconds.
206191
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.stripe.util;
2+
3+
import static java.util.Objects.requireNonNull;
4+
5+
import java.nio.charset.StandardCharsets;
6+
import java.security.MessageDigest;
7+
import java.util.regex.Pattern;
8+
9+
public final class StringUtils {
10+
private static Pattern whitespacePattern = Pattern.compile("\\s");
11+
12+
/**
13+
* Checks whether a string contains any whitespace characters or not.
14+
*
15+
* @param str the string to check.
16+
* @return {@code true} if the string contains any whitespace characters; otherwise, {@code
17+
* false}.
18+
*/
19+
public static boolean containsWhitespace(String str) {
20+
requireNonNull(str);
21+
return whitespacePattern.matcher(str).find();
22+
}
23+
24+
/**
25+
* Compares two strings for equality. The time taken is independent of the number of characters
26+
* that match.
27+
*
28+
* @param a one of the strings to compare.
29+
* @param b the other string to compare.
30+
* @return true if the strings are equal, false otherwise.
31+
*/
32+
public static boolean secureCompare(String a, String b) {
33+
byte[] digesta = a.getBytes(StandardCharsets.UTF_8);
34+
byte[] digestb = b.getBytes(StandardCharsets.UTF_8);
35+
36+
return MessageDigest.isEqual(digesta, digestb);
37+
}
38+
39+
/**
40+
* Converts the string to snake case.
41+
*
42+
* @param str the string to convert.
43+
* @return A string with the contents of the input string converted to snake case.
44+
*/
45+
public static String toSnakeCase(String str) {
46+
return str.replaceAll("(.)([A-Z][a-z]+)", "$1_$2")
47+
.replaceAll("([a-z0-9])([A-Z])", "$1_$2")
48+
.toLowerCase();
49+
}
50+
}

src/test/java/com/stripe/net/StripeRequestTest.java

+41-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public void testCtorRequestOptions() throws StripeException {
101101
}
102102

103103
@Test
104-
public void testCtorThrowsOnEmptyApiKey() throws StripeException {
104+
public void testCtorThrowsOnNullApiKey() throws StripeException {
105105
String origApiKey = Stripe.apiKey;
106106

107107
try {
@@ -119,4 +119,44 @@ public void testCtorThrowsOnEmptyApiKey() throws StripeException {
119119
Stripe.apiKey = origApiKey;
120120
}
121121
}
122+
123+
@Test
124+
public void testCtorThrowsOnEmptyApiKey() throws StripeException {
125+
String origApiKey = Stripe.apiKey;
126+
127+
try {
128+
Stripe.apiKey = "";
129+
130+
AuthenticationException e =
131+
assertThrows(
132+
AuthenticationException.class,
133+
() -> {
134+
new StripeRequest(
135+
ApiResource.RequestMethod.GET, "http://example.com/get", null, null);
136+
});
137+
assertTrue(e.getMessage().contains("Your API key is invalid, as it is an empty string."));
138+
} finally {
139+
Stripe.apiKey = origApiKey;
140+
}
141+
}
142+
143+
@Test
144+
public void testCtorThrowsOnApiKeyContainingWhitespace() throws StripeException {
145+
String origApiKey = Stripe.apiKey;
146+
147+
try {
148+
Stripe.apiKey = "sk_test_123\n";
149+
150+
AuthenticationException e =
151+
assertThrows(
152+
AuthenticationException.class,
153+
() -> {
154+
new StripeRequest(
155+
ApiResource.RequestMethod.GET, "http://example.com/get", null, null);
156+
});
157+
assertTrue(e.getMessage().contains("Your API key is invalid, as it contains whitespace."));
158+
} finally {
159+
Stripe.apiKey = origApiKey;
160+
}
161+
}
122162
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.stripe.util;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import lombok.Data;
9+
import org.junit.jupiter.api.Test;
10+
11+
public class StringUtilsTest {
12+
@Test
13+
public void testContainsWhitespace() {
14+
@Data
15+
class TestCase {
16+
private final String data;
17+
private final Boolean want;
18+
}
19+
20+
List<TestCase> testCases =
21+
new ArrayList<TestCase>() {
22+
private static final long serialVersionUID = 1L;
23+
24+
{
25+
add(new TestCase("sk_test_123", false));
26+
add(new TestCase("sk_test_4eC39HqLyjWDarjtT1zdp7dc", false));
27+
add(new TestCase("abc", false));
28+
add(new TestCase("sk-test-123", false));
29+
add(new TestCase("", false));
30+
add(new TestCase("sk_test_123\n", true));
31+
add(new TestCase("\nsk_test_123", true));
32+
add(new TestCase("sk_test_\n123", true));
33+
add(new TestCase("sk_test_123 ", true));
34+
add(new TestCase(" sk_test_123", true));
35+
add(new TestCase("sk_test_ 123", true));
36+
}
37+
};
38+
39+
for (TestCase testCase : testCases) {
40+
assertTrue(
41+
testCase.getWant() == StringUtils.containsWhitespace(testCase.getData()),
42+
String.format(
43+
"Expected containsWhitespace(\"%s\") to be %s",
44+
testCase.getData(), testCase.getWant() ? "true" : "false"));
45+
}
46+
}
47+
48+
@Test
49+
public void testSecureCompare() {
50+
@Data
51+
class TestCase {
52+
private final String[] data;
53+
private final Boolean want;
54+
}
55+
56+
List<TestCase> testCases =
57+
new ArrayList<TestCase>() {
58+
private static final long serialVersionUID = 1L;
59+
60+
{
61+
add(new TestCase(new String[] {"Hello", "Hello"}, true));
62+
add(new TestCase(new String[] {"Hello", "hello"}, false));
63+
add(new TestCase(new String[] {"Hello", "Helloo"}, false));
64+
add(new TestCase(new String[] {"Hello", "Hell"}, false));
65+
add(new TestCase(new String[] {"Hello", ""}, false));
66+
add(new TestCase(new String[] {"", "Hello"}, false));
67+
add(new TestCase(new String[] {"", ""}, true));
68+
add(new TestCase(new String[] {"\0AAAAAAAAA", "\0BBBBBBBBBBBB"}, false));
69+
}
70+
};
71+
72+
for (TestCase testCase : testCases) {
73+
assertTrue(
74+
testCase.getWant()
75+
== StringUtils.secureCompare(testCase.getData()[0], testCase.getData()[1]),
76+
String.format(
77+
"Expected secureCompare(\"%s\", \"%s\") to be %s",
78+
testCase.getData()[0], testCase.getData()[1], testCase.getWant() ? "true" : "false"));
79+
}
80+
}
81+
82+
@Test
83+
public void testToSnakeCase() {
84+
@Data
85+
class TestCase {
86+
private final String data;
87+
private final String want;
88+
}
89+
90+
List<TestCase> testCases =
91+
new ArrayList<TestCase>() {
92+
private static final long serialVersionUID = 1L;
93+
94+
{
95+
add(new TestCase("Foo", "foo"));
96+
add(new TestCase("FooBar", "foo_bar"));
97+
add(new TestCase("FooBar123", "foo_bar123"));
98+
add(new TestCase("Foo123Bar", "foo123_bar"));
99+
add(new TestCase("FOOBar", "foo_bar"));
100+
add(new TestCase("FooBAR", "foo_bar"));
101+
add(new TestCase("FOOBAR", "foobar"));
102+
add(new TestCase("FOO_BAR", "foo_bar"));
103+
}
104+
};
105+
106+
for (TestCase testCase : testCases) {
107+
assertEquals(testCase.getWant(), StringUtils.toSnakeCase(testCase.getData()));
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)