From 1de01a9aa41eab8fea5cc69063b4311aa48a3269 Mon Sep 17 00:00:00 2001 From: Sina Madani Date: Wed, 29 Nov 2023 17:42:38 +0000 Subject: [PATCH] fix: Call modification & signature verification (#498) QoL update for consistency --- CHANGELOG.md | 4 +- .../vonage/client/auth/RequestSigning.java | 19 ++++++ .../com/vonage/client/voice/CallEvent.java | 24 ++++++++ .../client/voice/TransferDestination.java | 4 -- .../com/vonage/client/voice/VoiceClient.java | 19 +++--- .../client/auth/RequestSigningTest.java | 61 ++++++++----------- .../vonage/client/voice/VoiceClientTest.java | 20 +++--- 7 files changed, 92 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e303f41f..c34af47ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -# [8.0.0] - 2023-11-?? +# [8.0.0] - 2023-11-30 - Added `redirect_url` parameter to `SilentAuthWorkflow` - Bumped Jackson version to 2.16.0 +- Use String instead of UUID in `VoiceClient` call modification methods +- Added public `verifyRequestSignature` method to `RequestSigning` # [8.0.0-rc2] - 2023-11-07 - Removed packages: diff --git a/src/main/java/com/vonage/client/auth/RequestSigning.java b/src/main/java/com/vonage/client/auth/RequestSigning.java index 9ee1205cc..c7153154e 100644 --- a/src/main/java/com/vonage/client/auth/RequestSigning.java +++ b/src/main/java/com/vonage/client/auth/RequestSigning.java @@ -146,6 +146,25 @@ protected static void constructSignatureForRequestParameters(List params.add(new BasicNameValuePair(PARAM_SIGNATURE, hashed)); } + /** + * Verifies the signature in an HttpServletRequest. Hashing strategy is MD5. + * + * @param contentType The request Content-Type header. + * @param inputStream The request data stream. + * @param parameterMap The request parameters. + * @param secretKey The pre-shared secret key used by the sender of the request to create the signature. + * + * @return true if the signature is correct for this request and secret key. + * + * @since 8.0.0 + */ + public static boolean verifyRequestSignature(InputStream inputStream, + String contentType, + Map parameterMap, + String secretKey) { + return verifyRequestSignature(contentType, inputStream, parameterMap, secretKey, System.currentTimeMillis()); + } + /** * Verifies the signature in an HttpServletRequest. Hashing strategy is MD5. * diff --git a/src/main/java/com/vonage/client/voice/CallEvent.java b/src/main/java/com/vonage/client/voice/CallEvent.java index 53e149864..1d197268b 100644 --- a/src/main/java/com/vonage/client/voice/CallEvent.java +++ b/src/main/java/com/vonage/client/voice/CallEvent.java @@ -20,6 +20,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.vonage.client.Jsonable; +/** + * Represents metadata about a call. + */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public class CallEvent implements Jsonable { @@ -27,21 +30,42 @@ public class CallEvent implements Jsonable { private CallStatus status; private CallDirection direction; + /** + * The unique identifier for this call leg. The UUID is created when your call request is accepted by Vonage. + * You use the UUID in all requests for individual live calls. + * + * @return The call ID. + */ @JsonProperty("uuid") public String getUuid() { return uuid; } + /** + * The unique identifier for the conversation this call leg is part of. + * + * @return The conversation ID as a string. + */ @JsonProperty("conversation_uuid") public String getConversationUuid() { return conversationUuid; } + /** + * The status of the call. + * + * @return The call's status as an enum. + */ @JsonProperty("status") public CallStatus getStatus() { return status; } + /** + * Whether the call is inbound or outbound. + * + * @return The call direction as an enum. + */ @JsonProperty("direction") public CallDirection getDirection() { return direction; diff --git a/src/main/java/com/vonage/client/voice/TransferDestination.java b/src/main/java/com/vonage/client/voice/TransferDestination.java index 3392f17b7..1f123b3b7 100644 --- a/src/main/java/com/vonage/client/voice/TransferDestination.java +++ b/src/main/java/com/vonage/client/voice/TransferDestination.java @@ -34,10 +34,6 @@ class TransferDestination { this(Type.NCCO, null, ncco); } - public TransferDestination(Type type, String url) { - this(type, url, null); - } - public TransferDestination(Type type, String url, Ncco ncco) { this.type = type; this.urls = url != null ? new String[]{url} : null; diff --git a/src/main/java/com/vonage/client/voice/VoiceClient.java b/src/main/java/com/vonage/client/voice/VoiceClient.java index 95826cfda..0ba9e7049 100644 --- a/src/main/java/com/vonage/client/voice/VoiceClient.java +++ b/src/main/java/com/vonage/client/voice/VoiceClient.java @@ -25,7 +25,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; -import java.util.UUID; import java.util.function.Function; /** @@ -142,8 +141,8 @@ public CallInfoPage listCalls(CallsFilter filter) throws VonageResponseParseExce /** * Look up the status of a single call initiated by {@link #createCall(Call)}. * - * @param uuid (required) The UUID of the call, obtained from the object returned by {@link #createCall(Call)}. This - * value can be obtained with {@link CallEvent#getUuid()}. + * @param uuid (required) The UUID of the call, obtained from the object returned by {@link #createCall(Call)}. + * This value can be obtained with {@link CallEvent#getUuid()}. * * @return A CallInfo object, representing the response from the Vonage Voice API. * @@ -172,8 +171,8 @@ public DtmfResponse sendDtmf(String uuid, String digits) throws VonageResponsePa return sendDtmf.execute(new DtmfPayload(digits, validateUuid(uuid))); } - private void modifyCall(UUID callId, ModifyCallAction action) throws VoiceResponseException { - modifyCall.execute(new ModifyCallPayload(action, callId.toString())); + private void modifyCall(String callId, ModifyCallAction action) throws VoiceResponseException { + modifyCall.execute(new ModifyCallPayload(action, validateUuid(callId))); } /** @@ -186,7 +185,7 @@ private void modifyCall(UUID callId, ModifyCallAction action) throws VoiceRespon * * @since 7.11.0 */ - public void earmuffCall(UUID callId) throws VoiceResponseException { + public void earmuffCall(String callId) throws VoiceResponseException { modifyCall(callId, ModifyCallAction.EARMUFF); } @@ -200,7 +199,7 @@ public void earmuffCall(UUID callId) throws VoiceResponseException { * * @since 7.11.0 */ - public void unearmuffCall(UUID callId) throws VoiceResponseException { + public void unearmuffCall(String callId) throws VoiceResponseException { modifyCall(callId, ModifyCallAction.UNEARMUFF); } @@ -213,7 +212,7 @@ public void unearmuffCall(UUID callId) throws VoiceResponseException { * * @since 7.11.0 */ - public void muteCall(UUID callId) throws VoiceResponseException { + public void muteCall(String callId) throws VoiceResponseException { modifyCall(callId, ModifyCallAction.MUTE); } @@ -226,7 +225,7 @@ public void muteCall(UUID callId) throws VoiceResponseException { * * @since 7.11.0 */ - public void unmuteCall(UUID callId) throws VoiceResponseException { + public void unmuteCall(String callId) throws VoiceResponseException { modifyCall(callId, ModifyCallAction.UNMUTE); } @@ -239,7 +238,7 @@ public void unmuteCall(UUID callId) throws VoiceResponseException { * * @since 7.11.0 */ - public void terminateCall(UUID callId) throws VoiceResponseException { + public void terminateCall(String callId) throws VoiceResponseException { modifyCall(callId, ModifyCallAction.HANGUP); } diff --git a/src/test/java/com/vonage/client/auth/RequestSigningTest.java b/src/test/java/com/vonage/client/auth/RequestSigningTest.java index d0b491323..b9a19204c 100644 --- a/src/test/java/com/vonage/client/auth/RequestSigningTest.java +++ b/src/test/java/com/vonage/client/auth/RequestSigningTest.java @@ -15,6 +15,7 @@ */ package com.vonage.client.auth; +import static com.vonage.client.auth.RequestSigning.*; import com.vonage.client.auth.hashutils.HashUtil; import org.apache.http.NameValuePair; import org.apache.http.message.BasicNameValuePair; @@ -39,13 +40,13 @@ public void testConstructSignatureForRequestParameters() { params.add(new BasicNameValuePair("a", "alphabet")); params.add(new BasicNameValuePair("b", "bananas")); - RequestSigning.constructSignatureForRequestParameters(params, "abcde", 2100); + constructSignatureForRequestParameters(params, "abcde", 2100); Map paramMap = constructParamMap(params); // md5 -s "&a=alphabet&b=bananas×tamp=2100abcde" String expected = "7d43241108912b32cc315b48ce681acf"; assertEquals(expected, paramMap.get(RequestSigning.PARAM_SIGNATURE)); - RequestSigning.constructSignatureForRequestParameters(params, "abcde"); + constructSignatureForRequestParameters(params, "abcde"); paramMap = constructParamMap(params); assertNotEquals(expected, paramMap.get(RequestSigning.PARAM_SIGNATURE)); } @@ -56,7 +57,7 @@ public void testConstructSignatureForRequestParametersWithSha1Hash() { params.add(new BasicNameValuePair("a", "alphabet")); params.add(new BasicNameValuePair("b", "bananas")); - RequestSigning.constructSignatureForRequestParameters(params, "abcde", 2100, HashUtil.HashType.HMAC_SHA1); + constructSignatureForRequestParameters(params, "abcde", 2100, HashUtil.HashType.HMAC_SHA1); Map paramMap = constructParamMap(params); // md5 -s "&a=alphabet&b=bananas×tamp=2100" assertEquals("b7f749de27b4adcf736cc95c9a7e059a16c85127", paramMap.get(RequestSigning.PARAM_SIGNATURE)); @@ -68,7 +69,7 @@ public void testConstructSignatureForRequestParametersWithHmacMd5Hash() { params.add(new BasicNameValuePair("a", "alphabet")); params.add(new BasicNameValuePair("b", "bananas")); - RequestSigning.constructSignatureForRequestParameters(params, "abcde", 2100, HashUtil.HashType.HMAC_MD5); + constructSignatureForRequestParameters(params, "abcde", 2100, HashUtil.HashType.HMAC_MD5); Map paramMap = constructParamMap(params); // md5 -s "&a=alphabet&b=bananas×tamp=2100" assertEquals("e0afe267aefd6dd18a848c1681517a19", paramMap.get(RequestSigning.PARAM_SIGNATURE)); @@ -80,7 +81,7 @@ public void testConstructSignatureForRequestParametersWithHmacSha256Hash() { params.add(new BasicNameValuePair("a", "alphabet")); params.add(new BasicNameValuePair("b", "bananas")); - RequestSigning.constructSignatureForRequestParameters(params, "abcde", 2100, HashUtil.HashType.HMAC_SHA256); + constructSignatureForRequestParameters(params, "abcde", 2100, HashUtil.HashType.HMAC_SHA256); Map paramMap = constructParamMap(params); // md5 -s "&a=alphabet&b=bananas×tamp=2100" assertEquals("8d1b0428276b6a070578225914c3502cc0687a454dfbbbb370c76a14234cb546", paramMap.get(RequestSigning.PARAM_SIGNATURE)); @@ -92,7 +93,7 @@ public void testConstructSignatureForRequestParametersWithHmacSha512Hash() { params.add(new BasicNameValuePair("a", "alphabet")); params.add(new BasicNameValuePair("b", "bananas")); - RequestSigning.constructSignatureForRequestParameters(params, "abcde", 2100, HashUtil.HashType.HMAC_SHA512); + constructSignatureForRequestParameters(params, "abcde", 2100, HashUtil.HashType.HMAC_SHA512); Map paramMap = constructParamMap(params); // md5 -s "&a=alphabet&b=bananas×tamp=2100" assertEquals("1c834a1f6a377d4473971387b065cb38e2ad6c4869ba77b7b53e207a344e87ba04b456dfc697b371a2d1ce476d01dafd4394aa97525eff23badad39d2389a710", paramMap.get(RequestSigning.PARAM_SIGNATURE)); @@ -106,7 +107,7 @@ public void testConstructSignatureForRequestParametersSkipsSignature() { params.add(new BasicNameValuePair("sig", "7d43241108912b32cc315b48ce681acf")); - RequestSigning.constructSignatureForRequestParameters(params, "abcde", 2100); + constructSignatureForRequestParameters(params, "abcde", 2100); Map paramMap = constructParamMap(params); // md5 -s "&a=alphabet&b=bananas×tamp=2100abcde" assertEquals("7d43241108912b32cc315b48ce681acf", paramMap.get(RequestSigning.PARAM_SIGNATURE)); @@ -118,7 +119,7 @@ public void testConstructSignatureForRequestParametersSkipsNullValues() { params.add(new BasicNameValuePair("a", "alphabet")); params.add(new BasicNameValuePair("b", null)); - RequestSigning.constructSignatureForRequestParameters(params, "abcde", 2100); + constructSignatureForRequestParameters(params, "abcde", 2100); Map paramMap = constructParamMap(params); // md5 -s "&a=alphabet×tamp=2100abcde" assertEquals("a3368bf718ba104dcb392d8877e8eb2b", paramMap.get(RequestSigning.PARAM_SIGNATURE)); @@ -134,10 +135,7 @@ private static Map constructParamMap(List params) @Test public void testVerifyRequestSignature() { - assertTrue(RequestSigning.verifyRequestSignature( - RequestSigning.APPLICATION_JSON, null, constructDummyParams(), - "abcde", 2100000 - )); + assertTrue(verifySignature(constructDummyParams())); } @Test @@ -145,7 +143,7 @@ public void testVerifyRequestSignatureWithSha1Hash() { Map params = constructDummyParams(); params.put("sig", new String[]{"b7f749de27b4adcf736cc95c9a7e059a16c85127"}); - assertTrue(RequestSigning.verifyRequestSignature( + assertTrue(verifyRequestSignature( RequestSigning.APPLICATION_JSON, null, params, "abcde", 2100000, HashUtil.HashType.HMAC_SHA1 )); @@ -154,7 +152,7 @@ public void testVerifyRequestSignatureWithSha1Hash() { @Test public void testVerifySignatureRequestJson() throws Exception { HttpServletRequest request = constructDummyRequestJson(); - assertTrue(RequestSigning.verifyRequestSignature( + assertTrue(verifyRequestSignature( RequestSigning.APPLICATION_JSON, request.getInputStream(), constructDummyParams(), "abcde", 2100000, HashUtil.HashType.HMAC_SHA1 )); @@ -165,7 +163,7 @@ public void testVerifyRequestSignatureWithHmacSha256Hash() { Map params = constructDummyParams(); params.put("sig", new String[]{"8d1b0428276b6a070578225914c3502cc0687a454dfbbbb370c76a14234cb546"}); - assertTrue(RequestSigning.verifyRequestSignature( + assertTrue(verifyRequestSignature( RequestSigning.APPLICATION_JSON, null, params, "abcde", 2100000, HashUtil.HashType.HMAC_SHA256 )); @@ -175,7 +173,7 @@ public void testVerifyRequestSignatureWithHmacSha256Hash() { public void testVerifyRequestSignatureWithHmacMd5Hash() throws Exception { Map params = constructDummyParams(); params.put("sig", new String[]{"e0afe267aefd6dd18a848c1681517a19"}); - assertTrue(RequestSigning.verifyRequestSignature( + assertTrue(verifyRequestSignature( RequestSigning.APPLICATION_JSON, null, params, "abcde", 2100000, HashUtil.HashType.HMAC_MD5 )); @@ -186,7 +184,7 @@ public void testVerifyRequestSignatureWithHmacSha512Hash() { Map params = constructDummyParams(); params.put("sig", new String[]{"1c834a1f6a377d4473971387b065cb38e2ad6c4869ba77b7b53e207a344e87ba04b456dfc697b371a2d1ce476d01dafd4394aa97525eff23badad39d2389a710"}); - assertTrue(RequestSigning.verifyRequestSignature( + assertTrue(verifyRequestSignature( RequestSigning.APPLICATION_JSON, null, params, "abcde", 2100000, HashUtil.HashType.HMAC_SHA512 )); @@ -194,26 +192,17 @@ public void testVerifyRequestSignatureWithHmacSha512Hash() { @Test public void testVerifyRequestSignatureNoSig() { - assertFalse(RequestSigning.verifyRequestSignature( - RequestSigning.APPLICATION_JSON, null, constructDummyParamsNoSignature(), - "abcde", 2100000 - )); + assertFalse(verifySignature(constructDummyParamsNoSignature())); } @Test public void testVerifyRequestSignatureBadTimestamp() { - assertFalse(RequestSigning.verifyRequestSignature( - RequestSigning.APPLICATION_JSON, null, constructDummyParamsInvalidTimestamp(), - "abcde", 2100000 - )); + assertFalse(verifySignature(constructDummyParamsInvalidTimestamp())); } @Test public void testVerifyRequestSignatureMissingTimestamp() { - assertFalse(RequestSigning.verifyRequestSignature( - RequestSigning.APPLICATION_JSON, null, constructDummyParamsNoTimestamp(), - "abcde", 2100000 - )); + assertFalse(verifySignature(constructDummyParamsNoTimestamp())); } @Test @@ -221,18 +210,18 @@ public void testVerifyRequestSignatureHandlesNullParams() { Map params = constructDummyParams(); params.put("b", new String[]{ null }); params.put("sig", new String[]{"a3368bf718ba104dcb392d8877e8eb2b"}); + assertTrue(verifySignature(params)); + } - assertTrue(RequestSigning.verifyRequestSignature( - RequestSigning.APPLICATION_JSON, null, params, - "abcde", 2100000 - )); + @Test + public void testVerifyRequestSignatureCurrentTimeMillis() { + assertFalse(verifyRequestSignature(null, APPLICATION_JSON, constructDummyParams(), "abcde")); } private HttpServletRequest constructDummyRequest() { return constructDummyRequest(null); } - private HttpServletRequest constructDummyRequestJson() throws Exception { HttpServletRequest request = mock(HttpServletRequest.class); String dummyJson = "{\"a\":\"alphabet\",\"b\":\"bananas\",\"timestamp\":\"2100\",\"sig\":\"b7f749de27b4adcf736cc95c9a7e059a16c85127\"}"; @@ -325,4 +314,8 @@ private HttpServletRequest constructDummyRequest(final Map nul return request; } + + private static boolean verifySignature(Map params) { + return verifyRequestSignature(APPLICATION_JSON, null, params, "abcde", 2100000); + } } diff --git a/src/test/java/com/vonage/client/voice/VoiceClientTest.java b/src/test/java/com/vonage/client/voice/VoiceClientTest.java index 35bcf3162..82f026d7a 100644 --- a/src/test/java/com/vonage/client/voice/VoiceClientTest.java +++ b/src/test/java/com/vonage/client/voice/VoiceClientTest.java @@ -130,37 +130,37 @@ public void testSendDtmf() throws Exception { @Test public void testTerminateCall() throws Exception { - stubResponseAndRun(204, () -> client.terminateCall(SAMPLE_CALL_UUID)); + stubResponseAndRun(204, () -> client.terminateCall(SAMPLE_CALL_ID)); stubResponseAndAssertThrows(204, () -> client.terminateCall(null), NullPointerException.class); - stubResponseAndAssertThrows(404, () -> client.terminateCall(SAMPLE_CALL_UUID), VoiceResponseException.class); + stubResponseAndAssertThrows(404, () -> client.terminateCall(SAMPLE_CALL_ID), VoiceResponseException.class); } @Test public void testMuteCall() throws Exception { - stubResponseAndRun(204, () -> client.muteCall(SAMPLE_CALL_UUID)); + stubResponseAndRun(204, () -> client.muteCall(SAMPLE_CALL_ID)); stubResponseAndAssertThrows(204, () -> client.muteCall(null), NullPointerException.class); - stubResponseAndAssertThrows(404, () -> client.muteCall(SAMPLE_CALL_UUID), VoiceResponseException.class); + stubResponseAndAssertThrows(404, () -> client.muteCall(SAMPLE_CALL_ID), VoiceResponseException.class); } @Test public void testUnmuteCall() throws Exception { - stubResponseAndRun(204, () -> client.unmuteCall(SAMPLE_CALL_UUID)); + stubResponseAndRun(204, () -> client.unmuteCall(SAMPLE_CALL_ID)); stubResponseAndAssertThrows(204, () -> client.unmuteCall(null), NullPointerException.class); - stubResponseAndAssertThrows(404, () -> client.unmuteCall(SAMPLE_CALL_UUID), VoiceResponseException.class); + stubResponseAndAssertThrows(404, () -> client.unmuteCall(SAMPLE_CALL_ID), VoiceResponseException.class); } @Test public void testEarmuffCall() throws Exception { - stubResponseAndRun(204, () -> client.earmuffCall(SAMPLE_CALL_UUID)); + stubResponseAndRun(204, () -> client.earmuffCall(SAMPLE_CALL_ID)); stubResponseAndAssertThrows(204, () -> client.earmuffCall(null), NullPointerException.class); - stubResponseAndAssertThrows(404, () -> client.earmuffCall(SAMPLE_CALL_UUID), VoiceResponseException.class); + stubResponseAndAssertThrows(404, () -> client.earmuffCall(SAMPLE_CALL_ID), VoiceResponseException.class); } @Test public void testUnearmuffCall() throws Exception { - stubResponseAndRun(204, () -> client.unearmuffCall(SAMPLE_CALL_UUID)); + stubResponseAndRun(204, () -> client.unearmuffCall(SAMPLE_CALL_ID)); stubResponseAndAssertThrows(204, () -> client.unearmuffCall(null), NullPointerException.class); - stubResponseAndAssertThrows(404, () -> client.unearmuffCall(SAMPLE_CALL_UUID), VoiceResponseException.class); + stubResponseAndAssertThrows(404, () -> client.unearmuffCall(SAMPLE_CALL_ID), VoiceResponseException.class); } @Test