diff --git a/src/main/java/com/didww/sdk/callback/RequestValidator.java b/src/main/java/com/didww/sdk/callback/RequestValidator.java index 648137b..77d8c1c 100644 --- a/src/main/java/com/didww/sdk/callback/RequestValidator.java +++ b/src/main/java/com/didww/sdk/callback/RequestValidator.java @@ -4,6 +4,8 @@ import javax.crypto.spec.SecretKeySpec; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Locale; import java.util.Map; import java.util.TreeMap; @@ -21,22 +23,27 @@ public boolean validate(String url, Map payload, String signatur if (signature == null || signature.isEmpty()) { return false; } - return validSignature(url, payload).equals(signature); + byte[] expectedBytes = computeHmac(url, payload); + byte[] signatureBytes = hexToBytes(signature); + if (signatureBytes.length == 0) { + return false; + } + return MessageDigest.isEqual(expectedBytes, signatureBytes); } - private String validSignature(String url, Map payload) { + private byte[] computeHmac(String url, Map payload) { TreeMap sorted = new TreeMap<>(payload); StringBuilder data = new StringBuilder(normalizeUrl(url)); for (Map.Entry entry : sorted.entrySet()) { data.append(entry.getKey()).append(entry.getValue()); } - return hmacSha1(data.toString(), apiKey); + return hmacSha1Bytes(data.toString(), apiKey); } private String normalizeUrl(String url) { try { URI uri = URI.create(url); - String scheme = uri.getScheme(); + String scheme = uri.getScheme().toLowerCase(Locale.ROOT); String userInfo = uri.getUserInfo() != null ? uri.getUserInfo() + "@" : ""; String host = uri.getHost(); @@ -49,7 +56,7 @@ private String normalizeUrl(String url) { port = 80; } - String path = uri.getRawPath() != null ? uri.getRawPath() : ""; + String path = uri.getRawPath(); String query = uri.getRawQuery() != null ? "?" + uri.getRawQuery() : ""; String fragment = uri.getRawFragment() != null ? "#" + uri.getRawFragment() : ""; @@ -59,18 +66,29 @@ private String normalizeUrl(String url) { } } - private static String hmacSha1(String data, String key) { + private static byte[] hmacSha1Bytes(String data, String key) { try { Mac mac = Mac.getInstance("HmacSHA1"); mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA1")); - byte[] rawHmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); - StringBuilder sb = new StringBuilder(); - for (byte b : rawHmac) { - sb.append(String.format("%02x", b)); - } - return sb.toString(); + return mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { - throw new RuntimeException("Failed to compute HMAC-SHA1", e); + throw new RuntimeException("Failed to compute HMAC-SHA1", e); // NOSONAR + } + } + + private static byte[] hexToBytes(String hex) { + if (hex.length() % 2 != 0) { + return new byte[0]; + } + byte[] bytes = new byte[hex.length() / 2]; + for (int i = 0; i < bytes.length; i++) { + int hi = Character.digit(hex.charAt(i * 2), 16); + int lo = Character.digit(hex.charAt(i * 2 + 1), 16); + if (hi == -1 || lo == -1) { + return new byte[0]; + } + bytes[i] = (byte) ((hi << 4) | lo); } + return bytes; } } diff --git a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java index e983d17..1f67cb4 100644 --- a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java +++ b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java @@ -1,14 +1,26 @@ package com.didww.sdk.callback; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.util.LinkedHashMap; import java.util.Map; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; class RequestValidatorTest { + private static Map ordersPayload() { + Map payload = new LinkedHashMap<>(); + payload.put("status", "completed"); + payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); + payload.put("type", "orders"); + return payload; + } + @Test void testSandbox() { RequestValidator validator = new RequestValidator("SOMEAPIKEY"); @@ -25,44 +37,74 @@ void testSandbox() { @Test void testValidRequest() { RequestValidator validator = new RequestValidator("SOMEAPIKEY"); - Map payload = new LinkedHashMap<>(); - payload.put("status", "completed"); - payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); - payload.put("type", "orders"); - assertThat(validator.validate("http://example.com/callbacks", payload, "fe99e416c3547f2f59002403ec856ea386d05b2f")).isTrue(); + assertThat(validator.validate("http://example.com/callbacks", ordersPayload(), "fe99e416c3547f2f59002403ec856ea386d05b2f")).isTrue(); } @Test void testValidRequestWithQueryAndFragment() { RequestValidator validator = new RequestValidator("OTHERAPIKEY"); - Map payload = new LinkedHashMap<>(); - payload.put("status", "completed"); - payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); - payload.put("type", "orders"); - assertThat(validator.validate("http://example.com/callbacks?foo=bar#baz", payload, "32754ba93ac1207e540c0cf90371e7786b3b1cde")).isTrue(); + assertThat(validator.validate("http://example.com/callbacks?foo=bar#baz", ordersPayload(), "32754ba93ac1207e540c0cf90371e7786b3b1cde")).isTrue(); } @Test void testEmptySignatureRequest() { RequestValidator validator = new RequestValidator("SOMEAPIKEY"); - Map payload = new LinkedHashMap<>(); - payload.put("status", "completed"); - payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); - payload.put("type", "orders"); - assertThat(validator.validate("http://example.com/callbacks", payload, "")).isFalse(); + assertThat(validator.validate("http://example.com/callbacks", ordersPayload(), "")).isFalse(); } @Test void testInvalidSignatureRequest() { RequestValidator validator = new RequestValidator("SOMEAPIKEY"); + + assertThat(validator.validate("http://example.com/callbacks", ordersPayload(), "fbdb1d1b18aa6c08324b7d64b71fb76370690e1d")).isFalse(); + } + + // https://doc.didww.com/api3/2022-05-10/callbacks-details.html#algorithm-implementation-details + @Test + void testDocumentationExample() { + RequestValidator validator = new RequestValidator("szrdgh6547umt7tht7xbqhj6g9gdbyp7"); + String url = "https://mycompany.com/didww_callbacks?opaque=123"; Map payload = new LinkedHashMap<>(); + payload.put("id", "bf2cee72-6caa-4ae2-917e-bea01945691e"); payload.put("status", "completed"); - payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); payload.put("type", "orders"); - assertThat(validator.validate("http://example.com/callbacks", payload, "fbdb1d1b18aa6c08324b7d64b71fb76370690e1d")).isFalse(); + assertThat(validator.validate(url, payload, "30f66e9d72eb5e193051fd02952f70d8e934b4ff")).isTrue(); + } + + static Stream urlNormalizationVectors() { + return Stream.of( + Arguments.of("http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), + Arguments.of("http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), + Arguments.of("http://foo.com:443/bar", "904eaa65c0759afac0e4d8912de424e2dfb96ea1"), + Arguments.of("http://foo.com:8182/bar", "eb8fcfb3d7ed4b4c2265d73cf93c31ba614384d1"), + Arguments.of("http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"), + Arguments.of("http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"), + Arguments.of("http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"), + Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"), + Arguments.of("https://foo.com:443/bar", "f26a771c302319a7094accbe2989bad67fff2928"), + Arguments.of("https://foo.com:80/bar", "bd45af5253b72f6383c6af7dc75250f12b73a4e1"), + Arguments.of("https://foo.com:8384/bar", "9c9fec4b7ebd6e1c461cb8e4ffe4f2987a19a5d3"), + Arguments.of("https://foo.com/bar?qwe=asd", "4a0e98ddf286acadd1d5be1b0ed85a4e541c3137"), + Arguments.of("https://qwe:asd@foo.com/bar", "7a8cd4a6c349910dfecaf9807e56a63787250bbd"), + Arguments.of("https://foo.com/bar#baz", "5024919770ea5ca2e3ccc07cb940323d79819508"), + Arguments.of("http://[::1]/bar", "e0e9b83e4046d097f54b3ae64b08cbb4a539f601"), + Arguments.of("http://[::1]:80/bar", "e0e9b83e4046d097f54b3ae64b08cbb4a539f601"), + Arguments.of("http://[::1]:9090/bar", "ebec110ec5debd0e0fd086ff2f02e48ca665b543"), + Arguments.of("https://[::1]/bar", "f3cfe6f523fdf1d4eaadc310fcd3ed92e1e324b0"), + Arguments.of("http://foo.com/hello%20world", "eb64035b2e8f356ff1442898a39ec94d5c3e2fc8"), + Arguments.of("http://foo.com/foo%2Fbar", "db24428442b012fa0972a453ba1ba98e755bba10") + ); + } + + @ParameterizedTest + @MethodSource("urlNormalizationVectors") + void testUrlNormalization(String url, String expectedSignature) { + RequestValidator validator = new RequestValidator("SOMEAPIKEY"); + + assertThat(validator.validate(url, ordersPayload(), expectedSignature)).isTrue(); } }