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]

Reply via email to