diff --git a/security/src/main/java/module-info.java b/security/src/main/java/module-info.java index 5c4eff4ba0..1cc84f4912 100644 --- a/security/src/main/java/module-info.java +++ b/security/src/main/java/module-info.java @@ -1,7 +1,7 @@ module tessera.security { // requires java.base; requires java.xml.bind; - // requires cryptacular; + requires cryptacular; requires org.slf4j; requires tessera.config; requires tessera.shared; diff --git a/tessera-core/src/main/java/com/quorum/tessera/transaction/TransactionManager.java b/tessera-core/src/main/java/com/quorum/tessera/transaction/TransactionManager.java index dba060fa75..1dc9fec73f 100644 --- a/tessera-core/src/main/java/com/quorum/tessera/transaction/TransactionManager.java +++ b/tessera-core/src/main/java/com/quorum/tessera/transaction/TransactionManager.java @@ -6,6 +6,7 @@ import com.quorum.tessera.encryption.PublicKey; import java.util.List; import java.util.ServiceLoader; +import java.util.Set; public interface TransactionManager { @@ -27,6 +28,8 @@ public interface TransactionManager { List getParticipants(MessageHash transactionHash); + Set getMandatoryRecipients(MessageHash transactionHash); + /** * @see Enclave#defaultPublicKey() * @return diff --git a/tessera-core/src/main/java/com/quorum/tessera/transaction/internal/TransactionManagerImpl.java b/tessera-core/src/main/java/com/quorum/tessera/transaction/internal/TransactionManagerImpl.java index 1fca8d1f74..4a09fa3ed0 100644 --- a/tessera-core/src/main/java/com/quorum/tessera/transaction/internal/TransactionManagerImpl.java +++ b/tessera-core/src/main/java/com/quorum/tessera/transaction/internal/TransactionManagerImpl.java @@ -513,6 +513,12 @@ public List getParticipants(final MessageHash transactionHash) { return payload.getRecipientKeys(); } + @Override + public Set getMandatoryRecipients(MessageHash transactionHash) { + final EncodedPayload payload = this.fetchPayload(transactionHash); + return payload.getMandatoryRecipients(); + } + @Override public PublicKey defaultPublicKey() { return enclave.defaultPublicKey(); diff --git a/tessera-core/src/test/java/com/quorum/tessera/transaction/internal/TransactionManagerTest.java b/tessera-core/src/test/java/com/quorum/tessera/transaction/internal/TransactionManagerTest.java index 8e6c6c12f9..6200a074ec 100644 --- a/tessera-core/src/test/java/com/quorum/tessera/transaction/internal/TransactionManagerTest.java +++ b/tessera-core/src/test/java/com/quorum/tessera/transaction/internal/TransactionManagerTest.java @@ -1717,6 +1717,35 @@ public void getParticipantsReturnsAllRecipients() { verify(encryptedTransactionDAO).retrieveByHash(any(MessageHash.class)); } + @Test + public void getMandatoryRecipients() { + + MessageHash transactionHash = mock(MessageHash.class); + when(transactionHash.getHashBytes()).thenReturn("DUMMY_TRANSACTION".getBytes()); + + final PublicKey senderKey = mock(PublicKey.class); + final PublicKey recipientKey = mock(PublicKey.class); + + final byte[] input = "SOMEDATA".getBytes(); + final EncryptedTransaction encryptedTransaction = mock(EncryptedTransaction.class); + final EncodedPayload encodedPayload = mock(EncodedPayload.class); + when(encodedPayload.getRecipientKeys()).thenReturn(List.of(senderKey, recipientKey)); + when(encodedPayload.getMandatoryRecipients()).thenReturn(Set.of(recipientKey)); + when(encryptedTransaction.getEncodedPayload()).thenReturn(input); + + when(encryptedTransactionDAO.retrieveByHash(transactionHash)) + .thenReturn(Optional.of(encryptedTransaction)); + + when(payloadEncoder.decode(input)).thenReturn(encodedPayload); + + final Set participants = transactionManager.getMandatoryRecipients(transactionHash); + + assertThat(participants).containsExactly(recipientKey); + + verify(payloadEncoder).decode(input); + verify(encryptedTransactionDAO).retrieveByHash(any(MessageHash.class)); + } + @Test public void defaultPublicKey() { transactionManager.defaultPublicKey(); diff --git a/tessera-jaxrs/transaction-jaxrs/src/main/java/com/quorum/tessera/q2t/TransactionResource4.java b/tessera-jaxrs/transaction-jaxrs/src/main/java/com/quorum/tessera/q2t/TransactionResource4.java index 09a2db2b2d..a9f7f59b61 100644 --- a/tessera-jaxrs/transaction-jaxrs/src/main/java/com/quorum/tessera/q2t/TransactionResource4.java +++ b/tessera-jaxrs/transaction-jaxrs/src/main/java/com/quorum/tessera/q2t/TransactionResource4.java @@ -1,6 +1,7 @@ package com.quorum.tessera.q2t; import static com.quorum.tessera.version.MandatoryRecipientsVersion.MIME_TYPE_JSON_4; +import static javax.ws.rs.core.MediaType.TEXT_PLAIN; import com.quorum.tessera.api.SendRequest; import com.quorum.tessera.api.SendResponse; @@ -12,6 +13,12 @@ import com.quorum.tessera.encryption.PublicKey; import com.quorum.tessera.privacygroup.PrivacyGroupManager; import com.quorum.tessera.transaction.TransactionManager; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.net.URI; import java.net.URLEncoder; @@ -232,4 +239,42 @@ public Response sendSignedTransaction( return Response.created(location).entity(responseEntity).build(); } + + @Operation( + summary = "/transaction/{hash}/mandatory", + operationId = "getMandatoryRecipients", + description = "get list of mandatory recipient public keys for a transaction") + @ApiResponse( + responseCode = "200", + description = "comma-separated list of mandatory recipients", + content = + @Content( + schema = + @Schema( + type = "string", + description = "comma-separated list of mandatory recipients"), + examples = + @ExampleObject( + "ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc=,BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo="))) + @GET + @Path("/transaction/{hash}/mandatory") + @Produces(TEXT_PLAIN) + public Response getMandatoryRecipients( + @Parameter( + description = "hash indicating encrypted payload to get mandatory recipients for", + schema = @Schema(format = "base64")) + @PathParam("hash") + final String ptmHash) { + LOGGER.debug("Received mandatory recipients list API request for key {}", ptmHash); + + MessageHash transactionHash = + Optional.of(ptmHash).map(Base64.getDecoder()::decode).map(MessageHash::new).get(); + + final String mandatoryRecipients = + transactionManager.getMandatoryRecipients(transactionHash).stream() + .map(PublicKey::encodeToBase64) + .collect(Collectors.joining(",")); + + return Response.ok(mandatoryRecipients).build(); + } } diff --git a/tessera-jaxrs/transaction-jaxrs/src/test/java/com/quorum/tessera/q2t/TransactionResource4Test.java b/tessera-jaxrs/transaction-jaxrs/src/test/java/com/quorum/tessera/q2t/TransactionResource4Test.java index f4b1af42d2..17cc92c301 100644 --- a/tessera-jaxrs/transaction-jaxrs/src/test/java/com/quorum/tessera/q2t/TransactionResource4Test.java +++ b/tessera-jaxrs/transaction-jaxrs/src/test/java/com/quorum/tessera/q2t/TransactionResource4Test.java @@ -12,6 +12,7 @@ import com.quorum.tessera.privacygroup.PrivacyGroupManager; import com.quorum.tessera.transaction.TransactionManager; import java.util.Base64; +import java.util.Set; import javax.ws.rs.core.Response; import org.junit.After; import org.junit.Before; @@ -160,4 +161,23 @@ public void sendSignedTransactionWithMandatoryRecipients() { .containsExactlyInAnyOrder(base64AffectedHash1, base64AffectedHash2); assertThat(obj.getMandatoryRecipients()).hasSize(1); } + + @Test + public void getMandatoryRecipients() { + byte[] data = "DUMMY_HASH".getBytes(); + + final String dummyPtmHash = Base64.getEncoder().encodeToString(data); + + PublicKey recipient = mock(PublicKey.class); + when(recipient.encodeToBase64()).thenReturn("BASE64ENCODEDKEY"); + + when(transactionManager.getMandatoryRecipients(any(MessageHash.class))) + .thenReturn(Set.of(recipient)); + + Response response = transactionResource.getMandatoryRecipients(dummyPtmHash); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getEntity()).isEqualTo("BASE64ENCODEDKEY"); + verify(transactionManager).getMandatoryRecipients(any(MessageHash.class)); + } } diff --git a/tests/acceptance-test/src/test/java/com/quorum/tessera/test/rest/SendMandatoryRecipientsIT.java b/tests/acceptance-test/src/test/java/com/quorum/tessera/test/rest/SendMandatoryRecipientsIT.java index acbd8c30d5..fe4f374d25 100644 --- a/tests/acceptance-test/src/test/java/com/quorum/tessera/test/rest/SendMandatoryRecipientsIT.java +++ b/tests/acceptance-test/src/test/java/com/quorum/tessera/test/rest/SendMandatoryRecipientsIT.java @@ -138,6 +138,25 @@ public void sendTransactionWithMandatoryRecipients() throws UnsupportedEncodingE EncodedPayload payloadC = PayloadEncoder.create().decode(payloadOnC); assertThat(payloadC.getMandatoryRecipients()) .containsExactly(PublicKey.from(Base64.getDecoder().decode(c.getPublicKey()))); + + // Validate recipients data in Node C using /mandatory call + + final String encodedHash = URLEncoder.encode(hash, UTF_8.toString()); + + final Response getMandatoryResponse = + c.getRestClient() + .target(c.getQ2TUri()) + .path("/transaction") + .path(encodedHash) + .path("/mandatory") + .request() + .buildGet() + .invoke(); + + assertThat(getMandatoryResponse).isNotNull(); + assertThat(getMandatoryResponse.getStatus()).isEqualTo(200); + String mandatoryList = getMandatoryResponse.readEntity(String.class); + assertThat(mandatoryList).isEqualTo(c.getPublicKey()); } @Test