From 844cca761a3046d51255082c5b7e58e1329ce8a6 Mon Sep 17 00:00:00 2001 From: Rohan Sircar Date: Sat, 22 Aug 2020 20:42:48 +0530 Subject: [PATCH] Added encryption/decryption login --- build.sbt | 17 +- project/plugin.sbt | 1 + src/main/resources/application.conf | 2 +- src/main/resources/styles/chat.css | 10 +- .../doge/chatto/ApplicationController.scala | 16 +- .../wow/doge/chatto/control/MessageBox.scala | 5 +- .../wow/doge/chatto/control/UserBox.scala | 1 - .../wow/doge/chatto/control/UserBox2.scala | 1 - .../chatto/controller/ChatController.scala | 196 ++++++++++-------- .../chatto/controller/LoginController.scala | 11 - .../controller/MainViewController.scala | 7 +- .../scala/wow/doge/chatto/model/Message.scala | 58 ++++++ .../wow/doge/chatto/model/MessageCipher.scala | 30 +++ .../chatto/service/EncryptionService.scala | 11 + .../service/EncryptionServiceImpl.scala | 135 ++++++++++++ .../wow/doge/chatto/service/UserService.scala | 90 +++----- 16 files changed, 411 insertions(+), 180 deletions(-) create mode 100644 src/main/scala/wow/doge/chatto/model/Message.scala create mode 100644 src/main/scala/wow/doge/chatto/model/MessageCipher.scala create mode 100644 src/main/scala/wow/doge/chatto/service/EncryptionService.scala create mode 100644 src/main/scala/wow/doge/chatto/service/EncryptionServiceImpl.scala diff --git a/build.sbt b/build.sbt index f08d3a6..5c830a0 100644 --- a/build.sbt +++ b/build.sbt @@ -23,6 +23,14 @@ val osName = System.getProperty("os.name") match { fork := true +inThisBuild( + List( + scalaVersion := scalaVersion.value, // 2.11.12, or 2.13.2 + semanticdbEnabled := true, // enable SemanticDB + semanticdbVersion := scalafixSemanticdb.revision // use Scalafix compatible version + ) +) + libraryDependencies ++= Seq( "base", "controls", @@ -40,6 +48,7 @@ libraryDependencies += "com.sfxcode.sapphire" %% "sapphire-extension" % "1.0.6" libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3" libraryDependencies ++= Seq( + "org.typelevel" %% "cats-core" % "2.1.1", "org.scalafx" %% "scalafx" % "12.0.2-R18", "org.scalafx" %% "scalafx-extras" % "0.3.4", "com.softwaremill.sttp.client" %% "json4s" % "2.1.1", @@ -57,7 +66,11 @@ libraryDependencies ++= Seq( ) libraryDependencies += "org.asynchttpclient" % "async-http-client" % "2.12.1" libraryDependencies += "com.softwaremill.macwire" %% "macros" % "2.3.3" -scalacOptions ++= Seq("-Ymacro-annotations", "-deprecation") +scalacOptions ++= Seq( + "-Ymacro-annotations", + "-deprecation", + "-Ywarn-unused:imports" +) libraryDependencies += "org.scalafx" %% "scalafxml-core-sfx8" % "0.5" // https://mvnrepository.com/artifact/com.jfoenix/jfoenix @@ -90,6 +103,6 @@ javaFxTitle := "chatto-sapphire" javaFxCategory := "Aplication" -javaFxNativeBundles := "image" +javaFxNativeBundles := "deb" javaFxVerbose := true diff --git a/project/plugin.sbt b/project/plugin.sbt index 56e0685..3771e1c 100644 --- a/project/plugin.sbt +++ b/project/plugin.sbt @@ -4,3 +4,4 @@ addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.9.0") addSbtPlugin("com.quadstingray" % "sbt-javafx" % "1.5.2") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.17") \ No newline at end of file diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 98fce76..001a54d 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -1,5 +1,5 @@ sapphire.core.fxml.basePath="/fxml/" -application.name = "Application" +application.name = "Chatto" project.name = "chatto-sapphire" project.version = "0.1.0-SNAPSHOT" stage.default { diff --git a/src/main/resources/styles/chat.css b/src/main/resources/styles/chat.css index 3cb6782..4f0d2de 100644 --- a/src/main/resources/styles/chat.css +++ b/src/main/resources/styles/chat.css @@ -87,7 +87,15 @@ -fx-background-color: transparent; } -.chat-message-box { +.chat-message-sender-box { + -fx-text-fill: white; + -fx-background-color: LIGHTGREEN; + /* -fx-background-color: #82ccdd; */ + -fx-background-radius: 30px; + -fx-padding: 20px; +} + +.chat-message-receiver-box { -fx-text-fill: white; /* -fx-background-color: LIGHTGREEN; */ -fx-background-color: #82ccdd; diff --git a/src/main/scala/wow/doge/chatto/ApplicationController.scala b/src/main/scala/wow/doge/chatto/ApplicationController.scala index 9a715d2..9f08679 100644 --- a/src/main/scala/wow/doge/chatto/ApplicationController.scala +++ b/src/main/scala/wow/doge/chatto/ApplicationController.scala @@ -3,30 +3,17 @@ package wow.doge.chatto import javax.enterprise.context.ApplicationScoped import javax.enterprise.inject.Produces import javax.inject.Named -import com.typesafe.config.ConfigFactory import com.sfxcode.sapphire.core.controller.DefaultWindowController // import org.asynchttpclient.Dsl._ import wow.doge.chatto.controller.MainViewController import sttp.client.asynchttpclient.future.AsyncHttpClientFutureBackend -import sttp.client._ import scala.concurrent.ExecutionContext.Implicits.global import scala.async.Async.{async, await} -import sttp.client.json4s._ -import org.json4s._ // import org.json4s.native.JsonMethods._ -import org.json4s.jackson.JsonMethods._ -import org.json4s.JsonDSL._ -import scala.util.Success -import scala.util.Failure -import com.softwaremill.quicklens._ import org.scalafx.extras._ -import wow.doge.chatto.service.UserService import javax.inject._ import javafx.application.Platform -import com.sfxcode.sapphire.core.controller.SceneControllerDidChangeEvent -import javax.enterprise.event.Observes -import com.sfxcode.sapphire.core.controller.SceneControllerWillChangeEvent @Named @ApplicationScoped class ApplicationController extends DefaultWindowController { @@ -55,6 +42,9 @@ class ApplicationController extends DefaultWindowController { @Produces def httpBackend = backend + // @Produces + // def encryptionService: EncryptionService = EncryptionServiceImpl() + def replacePrimarySceneContent(): Unit = { // Styling reloadStyles() diff --git a/src/main/scala/wow/doge/chatto/control/MessageBox.scala b/src/main/scala/wow/doge/chatto/control/MessageBox.scala index 3ceda6f..b441b3c 100644 --- a/src/main/scala/wow/doge/chatto/control/MessageBox.scala +++ b/src/main/scala/wow/doge/chatto/control/MessageBox.scala @@ -1,11 +1,8 @@ package wow.doge.chatto.control import javafx.scene.layout.HBox -import javafx.scene.control.Label import scalafx.Includes._ -import wow.doge.chatto.controller.ChatData -import com.sfxcode.sapphire.core.value.FXBean -import wow.doge.chatto.controller.Message +import wow.doge.chatto.model.Message import com.sandec.mdfx.MDFXNode import javafx.geometry.Pos import javafx.scene.layout.Priority diff --git a/src/main/scala/wow/doge/chatto/control/UserBox.scala b/src/main/scala/wow/doge/chatto/control/UserBox.scala index bbbb4d4..157f8fe 100644 --- a/src/main/scala/wow/doge/chatto/control/UserBox.scala +++ b/src/main/scala/wow/doge/chatto/control/UserBox.scala @@ -5,7 +5,6 @@ import javafx.fxml.FXML import javafx.scene.control.RadioButton import javafx.scene.control.Label import javafx.fxml.FXMLLoader -import scalafx.Includes._ class UserBox() extends VBox() { @FXML private var _userRadioButton: RadioButton = _ diff --git a/src/main/scala/wow/doge/chatto/control/UserBox2.scala b/src/main/scala/wow/doge/chatto/control/UserBox2.scala index 3fcd4c3..e67e118 100644 --- a/src/main/scala/wow/doge/chatto/control/UserBox2.scala +++ b/src/main/scala/wow/doge/chatto/control/UserBox2.scala @@ -4,7 +4,6 @@ import javafx.scene.layout.HBox import javafx.scene.control.Label import scalafx.Includes._ import wow.doge.chatto.controller.ChatData -import com.sfxcode.sapphire.core.value.FXBean class UserBox2(val username: String, val chatData: ChatData) extends HBox() { val usernameLabel = new Label(username) { diff --git a/src/main/scala/wow/doge/chatto/controller/ChatController.scala b/src/main/scala/wow/doge/chatto/controller/ChatController.scala index 2e211a1..b76704f 100644 --- a/src/main/scala/wow/doge/chatto/controller/ChatController.scala +++ b/src/main/scala/wow/doge/chatto/controller/ChatController.scala @@ -3,53 +3,30 @@ package wow.doge.chatto.controller import javafx.fxml.FXML import javafx.scene.control.Label import javafx.scene.control.Button -import javafx.scene.layout.FlowPane import javafx.scene.control.TextArea -import javafx.scene.control.ListView import javafx.scene.layout.HBox import javafx.scene.layout.VBox import scalafx.Includes._ -import wow.doge.chatto.control.UserBox -import javafx.application.Platform import javax.inject.Inject import org.scalafx.extras._ -import wow.doge.chatto.messagebuble.BubbledMDFXNode import wow.doge.chatto.service.UserService -import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext.Implicits.global import com.typesafe.scalalogging.LazyLogging import com.sfxcode.sapphire.core.value.FXBean import wow.doge.chatto.AppDataHandler import com.jfoenix.controls.JFXListView import scala.async.Async.{async, await} -import javafx.scene.paint.Color import scalafx.collections.ObservableBuffer -import javafx.beans.property.SimpleListProperty -import wow.doge.chatto.control.UserBox2 -import javafx.beans.value.ChangeListener import com.sfxcode.sapphire.core.value.KeyBindings import com.sfxcode.sapphire.core.value.FXBeanAdapter -import scalafx.collections.ObservableMap -import com.sfxcode.sapphire.core.value.BeanConversions -import javafx.util.converter.DateStringConverter -import javafx.beans.binding.Bindings import wow.doge.chatto.service.ActiveUser -import scala.collection.mutable import scala.collection.concurrent.TrieMap -import wow.doge.chatto.messagebuble.BubbleSpec -import javafx.scene.layout.Background -import javafx.scene.layout.BackgroundFill -import javafx.geometry.Pos -import scalafx.beans.property.ReadOnlyBufferProperty -import scalafx.beans.property.ReadOnlyBufferWrapper import javafx.beans.property.ReadOnlyListProperty import scalafx.beans.property.BufferProperty import javafx.collections.FXCollections -import com.sandec.mdfx.MDFXNode import javafx.scene.layout.BorderPane import javafx.scene.layout.Priority import net.synedra.validatorfx.Validator -import wow.doge.chatto.control.JFXSmoothScroll import javafx.scene.control.ContextMenu import javafx.scene.control.MenuItem import javafx.scene.input.Clipboard @@ -57,16 +34,21 @@ import javafx.scene.input.ClipboardContent import scalafx.scene.input.KeyCodeCombination import scalafx.scene.input.KeyCode import scalafx.scene.input.KeyCombination -import javafx.scene.input.DataFormat import wow.doge.chatto.control.MessageBox import javafx.scene.control.SelectionMode -import scalafx.beans.property.BooleanProperty import javafx.scene.control.ListCell import java.time.Instant import com.github.marlonlom.utilities.timeago.TimeAgo +import wow.doge.chatto.service.EncryptionService +import wow.doge.chatto.model.MessageCipher +import wow.doge.chatto.model.MessageType +import scala.util.Try +import java.time.ZonedDateTime +import wow.doge.chatto.model.Message class ChatController @Inject() ( userService: UserService, + encryptionService: EncryptionService, appDataHandler: AppDataHandler ) extends AbstractViewController with LazyLogging { @@ -86,19 +68,19 @@ class ChatController @Inject() ( @FXML private var isOnlineLabel: Label = _ @FXML private var selectedUserBox: HBox = _ - private val usersBuffer = ObservableBuffer.empty[ActiveUser] - private val usersListProperty = BufferProperty(usersBuffer) + private lazy val usersBuffer = ObservableBuffer.empty[ActiveUser] + private lazy val usersListProperty = BufferProperty(usersBuffer) /** * Readonly property wrapping an unmodifiable list. * Synchronized with the internal users list property. * Attemping to modify the internal list will throw an exception */ - val usersListROProp: ReadOnlyListProperty[ActiveUser] = BufferProperty( + lazy val usersListROProp: ReadOnlyListProperty[ActiveUser] = BufferProperty( FXCollections.unmodifiableObservableList(usersListProperty()) ) - private val chatDataStore = TrieMap.empty[String, ChatDataProperty] + private lazy val chatDataStore = TrieMap.empty[String, ChatDataProperty] private lazy val chatDataAdapter = FXBeanAdapter[ChatData](this) @@ -108,7 +90,9 @@ class ChatController @Inject() ( chatMainPane.hgrow = Priority.ALWAYS chatListView.selectionModel().selectionMode = SelectionMode.MULTIPLE - chatListView.setCellFactory(_ => new ChatListCell()) + chatListView.setCellFactory(_ => + new ChatListCell(appDataHandler.appData.credentials.username) + ) chatDataAdapter.set(FXBean(ChatData.empty)) @@ -149,6 +133,8 @@ class ChatController @Inject() ( } } + usersListView.items <== usersListProperty + usersListView .selectionModel() .selectedItemProperty() @@ -164,10 +150,45 @@ class ChatController @Inject() ( } chatDataAdapter.set(chatDataBean) maybeCDP.foreach(cdp => { + cdp.messages().clear() lastActiveLabel.text <== cdp.lastActive isOnlineLabel.text <== cdp.isActive.asString() - chatListView.items <== cdp.messages + // chatListView.items <== cdp.messages + // logger.debug(s"1 ${cdp.messages}") }) + async { + val maybeMessages = await { + offFXAndWait { + userService + .getMessages(appDataHandler.appData.credentials, nv.userName) + .map(_.body) + } + } + logger.debug(maybeMessages.toString) + onFX { + // maybeMessages.foreach( + // _.map(m => + // maybeCDP.foreach(cdp => { + // cdp.messages() ++= m + // // logger.debug( + // // s"2 ${chatDataStore.get(nv.userName).map(_.messages())}" + // // ) + // chatListView.items <== cdp.messages + // }) + // ) + // ) + + for { + tryMessages <- maybeMessages + messages <- tryMessages.toEither + cdp <- maybeCDP.toRight("CDP is null") + _ <- Right { + cdp.messages ++= messages + chatListView.items <== cdp.messages + } + } yield () + } + } }) }) @@ -185,12 +206,6 @@ class ChatController @Inject() ( chatListMenu.items += copyMessageMenuItem chatListView.contextMenu = chatListMenu - usersListView.items <== usersListProperty - - val validator = new Validator() - - submitButton.disable <== validator.containsErrorsProperty() - submitButton.onAction = (e) => { if (!chatInput.text().equals("") && !chatInput.text().equals(" ") && @@ -203,11 +218,19 @@ class ChatController @Inject() ( // cdp.username(), // chatInput.text() // ) - cdp.messages += Message.empty.copy(message = chatInput.text()) + cdp.messages += Message( + fromUser = appDataHandler.appData.credentials.username, + toUser = chatDataAdapter.get.bean.userName, + chatInput.text(), + Instant.now() + ) + // Message.empty.copy(message = chatInput.text()) +=: cdp.messages() }) } } + val validator = new Validator() + validator .createCheck() .withMethod(c => { @@ -245,17 +268,28 @@ class ChatController @Inject() ( maybeActiveUsers.foreach(users => usersBuffer ++= users) } - chatDataStore - .map { case (key, value) => value } - .foreach(cdp => { - cdp.messages ++= Seq( - Message.empty.copy(message = "hi"), - Message.empty.copy(message = "hello"), - Message.empty.copy(message = "bye") - ) - }) + // chatDataStore + // .map { case (key, value) => value } + // .foreach(cdp => { + // cdp.messages ++= Seq( + // Message.empty.copy(message = "hi"), + // Message.empty.copy(message = "hello"), + // Message.empty.copy(message = "bye") + // ) + // }) + + // simulate update + val maybeCDP = for { + usersMap <- maybeActiveUsers.map(_.groupBy(_.userName)) + user <- usersMap.get("user1").toRight("") + cdp <- chatDataStore.get("user1").toRight("") + } yield (cdp) + maybeCDP.foreach(cdp => { + cdp.isActive() = true + }) } } + def func() = { val x = offFXAndWait { 2 + 3 @@ -272,6 +306,9 @@ class ChatController @Inject() ( chatDataAdapter.set(FXBean(ChatData.empty)) usersListView.items().clear() chatListView.items().clear() + // chatDataStore.foreach { + // case (_, cdp) => cdp.messages.clear() + // } chatDataStore.clear() usersBuffer.clear() chatInput.clear() @@ -291,55 +328,42 @@ final case class ChatData( .getOrElse("User has not logged in yet") lazy val onlineString = activeUser.online.toString() } -object ChatData { +final object ChatData { def empty = { ChatData("empty", ActiveUser.empty, ObservableBuffer.empty[Message]) } } -class ChatDataProperty(chatData: ChatData) { +final class ChatDataProperty(chatData: ChatData) { val bean = FXBean(chatData) val username = bean.getStringProperty("userName") val isActive = bean.getBooleanProperty("activeUser.online") val lastActive = bean.getStringProperty("lastActiveString") val messages = BufferProperty(chatData.messages) -} -final case class Message( - fromUser: String, - toUser: String, - message: String, - messageTime: Instant -) -object Message { - lazy val markdownStyleSheet = - getClass().getResource("/styles/markdown.css").toExternalForm() - - def empty = Message("", "", "", Instant.MIN) - - def createMdMessageBox( - message: Message - ) = { - val mdfxNode = new MDFXNode(message.message); - mdfxNode - .getStylesheets() - .add(markdownStyleSheet) - mdfxNode.setMaxWidth(500) - mdfxNode.vgrow = Priority.ALWAYS - mdfxNode.setAlignment(Pos.CENTER) - mdfxNode.styleClass = Seq("chat-message-box") - - val box = new HBox() - box.setAlignment(Pos.CENTER_RIGHT) - // box.maxWidth(500) - box.hgrow = Priority.ALWAYS - box.vgrow = Priority.ALWAYS - box.children ++= Seq(mdfxNode) - box.fillHeight = true - box + def updateItem(chatData: ChatData) = { + username() = chatData.userName + isActive() = chatData.activeUser.online + lastActive() = chatData.lastActiveString + messages() ++= chatData.messages } } -final class ChatListCell extends ListCell[Message] { +final case class EncryptedMessage( + fromUser: String, + toUser: String, + messageCipher: MessageCipher, + messageTime: ZonedDateTime +) { + def toMessage( + passphrase: String, + decryptionFn: (String, MessageCipher) => Try[String] + ): Try[Message] = + decryptionFn(passphrase, this.messageCipher) + .map(ms => Message(this.fromUser, this.toUser, ms, messageTime.toInstant)) + +} + +final class ChatListCell(principal: String) extends ListCell[Message] { private val messageBox = new MessageBox() override def updateItem(item: Message, empty: Boolean): Unit = { super.updateItem(item, empty) @@ -349,7 +373,11 @@ final class ChatListCell extends ListCell[Message] { } else { // messageBox.setItem(item) // setGraphic(messageBox) - setGraphic(Message.createMdMessageBox(item)) + if (principal.equals(item.fromUser)) { + setGraphic(Message.createMdMessageBox(item, MessageType.Sender)) + } else { + setGraphic(Message.createMdMessageBox(item, MessageType.Receiver)) + } } } } diff --git a/src/main/scala/wow/doge/chatto/controller/LoginController.scala b/src/main/scala/wow/doge/chatto/controller/LoginController.scala index a74cd88..d2750e4 100644 --- a/src/main/scala/wow/doge/chatto/controller/LoginController.scala +++ b/src/main/scala/wow/doge/chatto/controller/LoginController.scala @@ -1,18 +1,12 @@ package wow.doge.chatto.controller import com.typesafe.scalalogging.LazyLogging -import com.sfxcode.sapphire.core.controller.ViewController import javafx.fxml.FXML import com.jfoenix.controls.JFXButton import com.jfoenix.controls.JFXTextField import com.jfoenix.controls.JFXPasswordField import scalafx.Includes._ -import scalafx.event.ActionEvent -import com.sfxcode.sapphire.core.value.KeyBindings -import scalafx.scene.layout.VBox -import com.sfxcode.sapphire.core.value.FXBean import javax.inject.Inject -import com.sfxcode.sapphire.core.value.FXBeanAdapter import wow.doge.chatto.service.UserService import scala.concurrent.ExecutionContext.Implicits.global import scala.util.Success @@ -20,18 +14,13 @@ import scala.util.Failure import javafx.scene.control.Label import javafx.scene.input.KeyCode import scala.async.Async.{async, await} -import wow.doge.chatto.AppData import wow.doge.chatto.UserCredentials import sttp.client._ -import scala.concurrent.Future -import sttp.client.asynchttpclient.WebSocketHandler import wow.doge.chatto.types.AppTypes.HttpBackend import wow.doge.chatto.types.AppTypes import org.scalafx.extras._ import wow.doge.chatto.AppDataHandler import com.sfxcode.sapphire.core.value.BeanConversions -import javafx.scene.layout.StackPane -import com.jfoenix.controls.JFXSpinner class LoginController @Inject() ( userService: UserService, diff --git a/src/main/scala/wow/doge/chatto/controller/MainViewController.scala b/src/main/scala/wow/doge/chatto/controller/MainViewController.scala index 33788ea..6bc8929 100644 --- a/src/main/scala/wow/doge/chatto/controller/MainViewController.scala +++ b/src/main/scala/wow/doge/chatto/controller/MainViewController.scala @@ -2,18 +2,13 @@ package wow.doge.chatto.controller import javafx.fxml.FXML import javafx.scene.control.MenuBar -import javafx.scene.layout.Pane -import javax.enterprise.event.Observes import com.sfxcode.sapphire.core.controller.ViewController -import com.sfxcode.sapphire.core.scene.{ContentDidChangeEvent, ContentManager} +import com.sfxcode.sapphire.core.scene.ContentManager import com.typesafe.scalalogging.LazyLogging -import wow.doge.chatto.messagebuble.BubbledMDFXNode -import scalafx.scene.layout.GridPane import scalafx.Includes._ import javafx.scene.layout.HBox import javafx.scene.layout.Priority -import wow.doge.chatto.control.UserBox class MainViewController extends ViewController with LazyLogging { diff --git a/src/main/scala/wow/doge/chatto/model/Message.scala b/src/main/scala/wow/doge/chatto/model/Message.scala new file mode 100644 index 0000000..46d4f1f --- /dev/null +++ b/src/main/scala/wow/doge/chatto/model/Message.scala @@ -0,0 +1,58 @@ +package wow.doge.chatto.model + +import java.time.Instant +import com.sandec.mdfx.MDFXNode +import javafx.scene.layout.HBox +import scalafx.Includes._ +import javafx.scene.layout.Priority +import javafx.geometry.Pos +import wow.doge.chatto.model.MessageType.Sender +import wow.doge.chatto.model.MessageType.Receiver + +final case class Message( + fromUser: String, + toUser: String, + message: String, + messageTime: Instant +) +object Message { + lazy val markdownStyleSheet = + getClass().getResource("/styles/markdown.css").toExternalForm() + + def empty = Message("", "", "", Instant.MIN) + + def createMdMessageBox( + message: Message, + messageType: MessageType + ) = { + val mdfxNode = new MDFXNode(message.message); + mdfxNode + .getStylesheets() + .add(markdownStyleSheet) + mdfxNode.setMaxWidth(500) + mdfxNode.vgrow = Priority.ALWAYS + mdfxNode.setAlignment(Pos.CENTER) + mdfxNode.styleClass = messageType match { + case Sender => Seq("chat-message-sender-box") + case Receiver => Seq("chat-message-receiver-box") + } + + val box = new HBox() + messageType match { + case Receiver => box.setAlignment(Pos.CENTER_LEFT) + case Sender => box.setAlignment(Pos.CENTER_RIGHT) + } + // box.maxWidth(500) + box.hgrow = Priority.ALWAYS + box.vgrow = Priority.ALWAYS + box.children ++= Seq(mdfxNode) + box.fillHeight = true + box + } +} + +sealed trait MessageType +object MessageType { + case object Sender extends MessageType + case object Receiver extends MessageType +} diff --git a/src/main/scala/wow/doge/chatto/model/MessageCipher.scala b/src/main/scala/wow/doge/chatto/model/MessageCipher.scala new file mode 100644 index 0000000..223de2c --- /dev/null +++ b/src/main/scala/wow/doge/chatto/model/MessageCipher.scala @@ -0,0 +1,30 @@ +package wow.doge.chatto.model + +import org.json4s.FieldSerializer +import org.json4s.FieldSerializer._ + +case class MessageCipher( + iv: String, + v: Int, + iterations: Int, + keySize: Int, + tagSize: Int, + mode: String, + adata: String, + cipher: String, + salt: String, + cipherText: String +) + +object MessageCipher { + val rename = FieldSerializer[MessageCipher]( + renameTo("iterations", "iter") orElse + renameTo("keySize", "ks") orElse + renameTo("tagSize", "ts") orElse + renameTo("cipherText", "ct"), + renameFrom("iter", "iterations") orElse + renameFrom("ks", "keySize") orElse + renameFrom("ts", "tagSize") orElse + renameFrom("ct", "cipherText") + ) +} diff --git a/src/main/scala/wow/doge/chatto/service/EncryptionService.scala b/src/main/scala/wow/doge/chatto/service/EncryptionService.scala new file mode 100644 index 0000000..a9ec6f7 --- /dev/null +++ b/src/main/scala/wow/doge/chatto/service/EncryptionService.scala @@ -0,0 +1,11 @@ +package wow.doge.chatto.service + +import wow.doge.chatto.model.MessageCipher +import scala.util.Try + +trait EncryptionService { + + def encrypt(password: String, plainText: String): String Either MessageCipher + def decrypt(password: String, cipher: MessageCipher): Try[String] + +} diff --git a/src/main/scala/wow/doge/chatto/service/EncryptionServiceImpl.scala b/src/main/scala/wow/doge/chatto/service/EncryptionServiceImpl.scala new file mode 100644 index 0000000..1821eb7 --- /dev/null +++ b/src/main/scala/wow/doge/chatto/service/EncryptionServiceImpl.scala @@ -0,0 +1,135 @@ +package wow.doge.chatto.service + +import wow.doge.chatto.model.MessageCipher; +import java.util.Base64; + +import javax.crypto.Cipher;; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.SecureRandom +import scala.util.Try + +class EncryptionServiceImpl extends EncryptionService { + + override def encrypt( + password: String, + plainText: String + ): String Either MessageCipher = { + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val secureRandom = new SecureRandom() + val saltLength = 12 + val keyLength = 128 + val iterationCount = 10000 + val tagSize = 128 + + val encode = (bytes: Array[Byte]) => + Base64.getEncoder().encodeToString(bytes) + + val salt = Array(saltLength.toByte) + secureRandom.nextBytes(salt) + val nonce = Array(12.toByte) + secureRandom.nextBytes(nonce) + + // val spec = + // new PBEKeySpec(password.toCharArray(), salt, iterationCount, keyLength) + + // val tmp = factory.generateSecret(spec) + // val secretKey = new SecretKeySpec(tmp.getEncoded(), "AES") + // val cipher = Cipher.getInstance("AES/GCM/NoPadding") + // cipher.init( + // Cipher.ENCRYPT_MODE, + // secretKey, + // new GCMParameterSpec(128, nonce) + // ) + + // val cipherTextByte = cipher.doFinal(plainText.getBytes) + + val messageCipher = for { + factory <- { + Try(SecretKeyFactory.getInstance("PBKDFWithHmacSHA56")).toOption + .toRight("Failed to get skf instance") + } + + spec <- { + Try( + new PBEKeySpec( + password.toCharArray(), + salt, + iterationCount, + keyLength + ) + ).toOption.toRight("Failed to get pbekeyspec") + } + + secret <- Try(factory.generateSecret(spec)).toOption + .toRight("Failed to get secret") + + secretKey <- Try(new SecretKeySpec(secret.getEncoded(), "AES")).toOption + .toRight("Failed to get secret key") + + cipher <- Try(Cipher.getInstance("AES/GCM/NoPadding")).toOption + .toRight("Failed to get cipher instance") + _ <- Right( + cipher.init( + Cipher.ENCRYPT_MODE, + secretKey, + new GCMParameterSpec(128, nonce) + ) + ) + + cipherTextByte <- Try(cipher.doFinal(plainText.getBytes())).toOption + .toRight("Failed to generate cipher") + + messageCipher = MessageCipher( + v = 1, + salt = encode(salt), + mode = "gcm", + iterations = iterationCount, + cipher = "aes", + adata = "", + cipherText = encode(cipherTextByte), + iv = encode(nonce), + keySize = keyLength, + tagSize = tagSize + ) + } yield (messageCipher) + + messageCipher + } + + override def decrypt( + password: String, + messageCipher: MessageCipher + ): Try[String] = { + val decode = (text: String) => { + Base64.getDecoder().decode(text) + } + + Try { + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val spec = new PBEKeySpec( + password.toCharArray(), + decode(messageCipher.salt), + messageCipher.iterations, + messageCipher.keySize + ); + val tmp = factory.generateSecret(spec); + val secretKey = new SecretKeySpec(tmp.getEncoded(), "AES"); + + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init( + Cipher.DECRYPT_MODE, + secretKey, + new GCMParameterSpec(128, decode(messageCipher.iv)) + ); + new String(cipher.doFinal(decode(messageCipher.cipherText))); + } + } + +} + +object EncryptionServiceImpl { + def apply() = new EncryptionServiceImpl() +} diff --git a/src/main/scala/wow/doge/chatto/service/UserService.scala b/src/main/scala/wow/doge/chatto/service/UserService.scala index d1a76d2..fcbc0f8 100644 --- a/src/main/scala/wow/doge/chatto/service/UserService.scala +++ b/src/main/scala/wow/doge/chatto/service/UserService.scala @@ -1,93 +1,71 @@ package wow.doge.chatto.service -import scala.concurrent.ExecutionContext -import scala.concurrent.ExecutionContext.Implicits.global -import scala.async.Async.{async, await} import sttp.client.json4s._ import org.json4s._ import sttp.client._ -import scala.concurrent.Future -import sttp.client.asynchttpclient.WebSocketHandler import javax.inject.Inject -import scala.util.Success -import scala.util.Failure -import wow.doge.chatto.AppData import wow.doge.chatto.types.AppTypes.HttpBackend import com.typesafe.scalalogging.LazyLogging -import org.scalafx.extras._ -import wow.doge.chatto.ApplicationController import wow.doge.chatto.UserCredentials import javax.inject._ import wow.doge.chatto.AppDataHandler -import org.json4s.jackson.JsonMethods._ import org.json4s.ext.JavaTimeSerializers import java.time.ZonedDateTime -import org.json4s.jackson.Serialization._ +import wow.doge.chatto.model.MessageCipher +import wow.doge.chatto.controller.EncryptedMessage +import cats.implicits._ -class UserService @Inject() (appDataHandler: AppDataHandler)( +class UserService @Inject() ( + appDataHandler: AppDataHandler, + encryptionService: EncryptionService +)( implicit backend: HttpBackend ) extends LazyLogging { private implicit lazy val serialization = org.json4s.jackson.Serialization private implicit lazy val formats = - DefaultFormats ++ JavaTimeSerializers.all + DefaultFormats ++ JavaTimeSerializers.all + MessageCipher.rename private val domain = "http://localhost:8080" private lazy val baseUrl = uri"$domain/api/chat" - // private lazy val authBasicRequest = (credentials: UserCredentials) => - // basicRequest.auth - // .basic(credentials.username, credentials.password) - // .header("X-AUTH-TOKEN", credentials.token) private lazy val tokenBasicRequest = (token: String) => { basicRequest.header("X-AUTH-TOKEN", token) } - def func1() = async { - val willBeResponse = func2() - val r = await { willBeResponse } - r.body.map(println) - } - - def func2() = - basicRequest - .get(uri"https://httpbin.org/get") - .response(asJson[HttpBinResponse]) - .send() - private def endpoint(uri: String) = uri"$baseUrl/$uri" - def getUsers(credentials: UserCredentials) = async { - // logger.debug(s"${appDataHandler.appData}") - // println( - // write[ActiveUser]( - // ActiveUser("hmm what is it", true, Some(ZonedDateTime.now())) - // ) - // ) - await { - // authBasicRequest(credentials) - tokenBasicRequest(credentials.token) - .get(uri"http://localhost:8080/api/chat/get/users") - .response(asJson[List[String]]) - .send() - } - } + def getUsers(credentials: UserCredentials) = + tokenBasicRequest(credentials.token) + .get(uri"http://localhost:8080/api/chat/get/users") + .response(asJson[List[String]]) + .send() - def getMessages(credentials: UserCredentials) = async { - // logger.debug(s"${appDataHandler.appData}") - await { - // authBasicRequest(credentials) - tokenBasicRequest(credentials.token) - .get(uri"http://localhost:8080/api/chat/get/users") - .response(asJson[List[String]]) - .send() - } - } + def getEncryptedMessages(credentials: UserCredentials, user: String) = + Request + .messagesPaginated(credentials, user) + .send() + + def getMessages(credentials: UserCredentials, user: String) = + Request + .messagesPaginated(credentials, user) + .mapResponseRight( + _.map(_.toMessage("password", encryptionService.decrypt)).sequence + ) + .send() def getActiveUsers(credentials: UserCredentials) = - // authBasicRequest(credentials) tokenBasicRequest(credentials.token) .get(uri"http://localhost:8080/api/chat/get/active-users") .response(asJson[List[ActiveUser]]) .send() + object Request { + lazy val messagesPaginated = (credentials: UserCredentials, user: String) => + tokenBasicRequest(credentials.token) + .get( + uri"http://localhost:8080/api/chat/get/messages/$user?page=0&size=9" + ) + .response(asJson[List[EncryptedMessage]]) + } + } final case class HttpBinResponse(