This is an automated email from the ASF dual-hosted git repository. rcordier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 789dc08e5bab35c029e901208411ca65814a6e67 Author: Benoit Tellier <[email protected]> AuthorDate: Thu Oct 28 14:28:40 2021 +0700 JAMES-3539 PushListener should encrypt payloads if required --- .../james/jmap/pushsubscription/PushListener.scala | 16 ++++--- .../jmap/pushsubscription/PushListenerTest.scala | 52 +++++++++++++++++++++- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushListener.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushListener.scala index ff0dc01..857cc64 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushListener.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushListener.scala @@ -58,11 +58,17 @@ class PushListener @Inject()(pushRepository: PushSubscriptionRepository, stateChangeEvent .asStateChange .filter(pushSubscription.types.toSet) - .fold(SMono.empty[Unit])(stateChange => SMono(webPushClient.push(pushSubscription.url, asPushRequest(stateChange)))) + .fold(SMono.empty[Unit])(stateChange => SMono(webPushClient.push(pushSubscription.url, asPushRequest(stateChange, pushSubscription)))) - private def asPushRequest(stateChange: StateChange): PushRequest = - PushRequest(ttl = PushTTL.MAX, payload = asBytes(stateChange)) + private def asPushRequest(stateChange: StateChange, pushSubscription: PushSubscription): PushRequest = + PushRequest(ttl = PushTTL.MAX, + contentCoding = pushSubscription.keys.map(_ => Aes128gcm), + payload = asBytes(stateChange, pushSubscription)) - private def asBytes(stateChange: StateChange) = - Json.stringify(pushSerializer.serializeSSE(stateChange)).getBytes(StandardCharsets.UTF_8) + private def asBytes(stateChange: StateChange, pushSubscription: PushSubscription) = { + val clearTextPayload = Json.stringify(pushSerializer.serializeSSE(stateChange)).getBytes(StandardCharsets.UTF_8) + pushSubscription.keys + .map(keys => keys.encrypt(clearTextPayload)) + .getOrElse(clearTextPayload) + } } diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala index b281a6c..b962dc3 100644 --- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala +++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala @@ -20,13 +20,18 @@ package org.apache.james.jmap.pushsubscription import java.nio.charset.StandardCharsets +import java.security.interfaces.{ECPrivateKey, ECPublicKey} import java.time.Clock -import java.util.UUID +import java.util.{Base64, UUID} import com.google.common.collect.ImmutableSet +import com.google.common.hash.Hashing +import com.google.crypto.tink.apps.webpush.WebPushHybridDecrypt +import com.google.crypto.tink.subtle.EllipticCurves.CurveType +import com.google.crypto.tink.subtle.{EllipticCurves, Random} import org.apache.james.core.Username import org.apache.james.events.Event.EventId -import org.apache.james.jmap.api.model.{DeviceClientId, PushSubscriptionCreationRequest, PushSubscriptionServerURL, TypeName} +import org.apache.james.jmap.api.model.{DeviceClientId, PushSubscriptionCreationRequest, PushSubscriptionKeys, PushSubscriptionServerURL, TypeName} import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository import org.apache.james.jmap.change.{EmailDeliveryTypeName, EmailTypeName, MailboxTypeName, StateChangeEvent, TypeStateFactory} import org.apache.james.jmap.core.UuidState @@ -145,4 +150,47 @@ class PushListenerTest { .isEqualTo(s"""{"@type":"StateChange","changed":{"$bobAccountId":{"Email":"${state1.value.toString}","Mailbox":"${state2.value.toString}"}}}""") }) } + + @Test + def pushShouldEncryptMessages(): Unit = { + val uaKeyPair = EllipticCurves.generateKeyPair(CurveType.NIST_P256) + val uaPrivateKey: ECPrivateKey = uaKeyPair.getPrivate.asInstanceOf[ECPrivateKey] + val uaPublicKey: ECPublicKey = uaKeyPair.getPublic.asInstanceOf[ECPublicKey] + val authSecret = Random.randBytes(16) + + val hybridDecrypt = new WebPushHybridDecrypt.Builder() + .withAuthSecret(authSecret) + .withRecipientPublicKey(uaPublicKey) + .withRecipientPrivateKey(uaPrivateKey) + .build + + val id = SMono(pushSubscriptionRepository.save(bob, PushSubscriptionCreationRequest( + deviceClientId = DeviceClientId("junit"), + keys = Some(PushSubscriptionKeys(p256dh = Base64.getEncoder.encodeToString(uaPublicKey.getEncoded), + auth = Base64.getEncoder.encodeToString(authSecret))), + url = url, + types = Seq(EmailTypeName, MailboxTypeName)))).block().id + SMono(pushSubscriptionRepository.validateVerificationCode(bob, id)).block() + + val state1 = UuidState(UUID.randomUUID()) + val state2 = UuidState(UUID.randomUUID()) + val state3 = UuidState(UUID.randomUUID()) + SMono(testee.reactiveEvent(StateChangeEvent(EventId.random(), bob, + Map(EmailTypeName -> state1, MailboxTypeName -> state2, EmailDeliveryTypeName -> state3)))).block() + + val argumentCaptor: ArgumentCaptor[PushRequest] = ArgumentCaptor.forClass(classOf[PushRequest]) + verify(webPushClient).push(ArgumentMatchers.eq(url), argumentCaptor.capture()) + val decryptedPayload = s"""{"@type":"StateChange","changed":{"$bobAccountId":{"Email":"${state1.value.toString}","Mailbox":"${state2.value.toString}"}}}""" + val encryptedPayload = argumentCaptor.getValue.payload + SoftAssertions.assertSoftly(softly => { + // We positionned well the Content-Encoding header + softly.assertThat(argumentCaptor.getValue.contentCoding.toJava).contains(Aes128gcm) + // We are able to decrypt the payload + softly.assertThat(new String(hybridDecrypt.decrypt(encryptedPayload, null), StandardCharsets.UTF_8)) + .isEqualTo(decryptedPayload) + // The content had been well modified by the encryption + softly.assertThat(Hashing.sha256().hashBytes(encryptedPayload)) + .isNotEqualTo(Hashing.sha256().hashBytes(decryptedPayload.getBytes(StandardCharsets.UTF_8))) + }) + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
