Skip to content

Commit

Permalink
Merge pull request #1851 from dedis/work-be2-ons-authentication-proto…
Browse files Browse the repository at this point in the history
…col-messages

Add federation authentication protocol messages + tests + examples in Scala
  • Loading branch information
onsriahi14 authored May 17, 2024
2 parents c70add9 + 7ff35ae commit abf0af1
Show file tree
Hide file tree
Showing 27 changed files with 833 additions and 11 deletions.
170 changes: 159 additions & 11 deletions be2-scala/src/main/scala/ch/epfl/pop/json/MessageDataProtocol.scala
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package ch.epfl.pop.json

import ch.epfl.pop.json.ObjectProtocol._
import ch.epfl.pop.model.network.method.message.data.coin._
import ch.epfl.pop.model.network.method.message.data.election.VersionType
import ch.epfl.pop.model.network.method.message.data.election._
import ch.epfl.pop.model.network.method.message.data.lao._
import ch.epfl.pop.model.network.method.message.data.meeting._
import ch.epfl.pop.json.HighLevelProtocol.messageFormat
import ch.epfl.pop.json.ObjectProtocol.*
import ch.epfl.pop.model.network.method.message.Message
import ch.epfl.pop.model.network.method.message.data.coin.*
import ch.epfl.pop.model.network.method.message.data.election.*
import ch.epfl.pop.model.network.method.message.data.federation.*
import ch.epfl.pop.model.network.method.message.data.lao.*
import ch.epfl.pop.model.network.method.message.data.meeting.*
import ch.epfl.pop.model.network.method.message.data.popcha.Authenticate
import ch.epfl.pop.model.network.method.message.data.rollCall._
import ch.epfl.pop.model.network.method.message.data.socialMedia._
import ch.epfl.pop.model.network.method.message.data.witness._
import ch.epfl.pop.model.network.method.message.data.rollCall.*
import ch.epfl.pop.model.network.method.message.data.socialMedia.*
import ch.epfl.pop.model.network.method.message.data.witness.*
import ch.epfl.pop.model.network.method.message.data.{ActionType, MessageData, ObjectType}
import ch.epfl.pop.model.objects._
import spray.json._
import ch.epfl.pop.model.objects.*
import spray.json.*

import scala.collection.immutable.ListMap
import scala.util.Try
Expand Down Expand Up @@ -459,4 +461,150 @@ object MessageDataProtocol extends DefaultJsonProtocol {
POPCHA_ADDRESS -> obj.popchaAddress.toJson
)
}

implicit object FederationChallengeFormat extends JsonFormat[FederationChallenge] {
final private val PARAM_CHALLENGE_VALUE: String = "value"
final private val PARAM_CHALLENGE_VALID_UNTIL: String = "valid_until"

override def read(json: JsValue): FederationChallenge = json.asJsObject().getFields(PARAM_CHALLENGE_VALUE, PARAM_CHALLENGE_VALID_UNTIL) match {
case Seq(value @ JsString(_), validUntil @ JsNumber(_)) => FederationChallenge(
value.convertTo[Base16Data],
validUntil.convertTo[Timestamp]
)
case _ => throw new IllegalArgumentException(s"Can't parse json value $json to a FederationChallenge object")
}

override def write(obj: FederationChallenge): JsValue = {
var jsObjectContent: ListMap[String, JsValue] = ListMap[String, JsValue](
PARAM_OBJECT -> JsString(obj._object.toString),
PARAM_ACTION -> JsString(obj.action.toString),
PARAM_CHALLENGE_VALUE -> obj.value.toJson,
PARAM_CHALLENGE_VALID_UNTIL -> obj.validUntil.toJson
)
JsObject(jsObjectContent)
}
}

implicit object FederationExpectFormat extends JsonFormat[FederationExpect] {
final private val PARAM_ID: String = "lao_id"
final private val PARAM_SERVER_ADDRESS: String = "server_address"
final private val PARAM_PUBLIC_KEY: String = "public_key"
final private val PARAM_CHALLENGE: String = "challenge"

override def read(json: JsValue): FederationExpect = json.asJsObject().getFields(PARAM_ID, PARAM_SERVER_ADDRESS, PARAM_PUBLIC_KEY, PARAM_CHALLENGE) match {
case Seq(laoId @ JsString(_), JsString(serverAddress), publicKey @ JsString(_), challenge @ JsObject(_)) =>
FederationExpect(
laoId.convertTo[Hash],
serverAddress,
publicKey.convertTo[PublicKey],
challenge.convertTo[Message]
)
case _ => throw new IllegalArgumentException(s"Can't parse json value $json to a FederationExpect object")
}

override def write(obj: FederationExpect): JsValue = {
var jsObjectContent: ListMap[String, JsValue] = ListMap[String, JsValue](
PARAM_OBJECT -> JsString(obj._object.toString),
PARAM_ACTION -> JsString(obj.action.toString),
PARAM_ID -> obj.laoId.toJson,
PARAM_SERVER_ADDRESS -> obj.serverAddress.toJson,
PARAM_PUBLIC_KEY -> obj.publicKey.toJson,
PARAM_CHALLENGE -> obj.challenge.toJson
)
JsObject(jsObjectContent)
}
}

implicit object FederationInitFormat extends JsonFormat[FederationInit] {
final private val PARAM_ID: String = "lao_id"
final private val PARAM_SERVER_ADDRESS: String = "server_address"
final private val PARAM_PUBLIC_KEY: String = "public_key"
final private val PARAM_CHALLENGE: String = "challenge"

override def read(json: JsValue): FederationInit = json.asJsObject().getFields(PARAM_ID, PARAM_SERVER_ADDRESS, PARAM_PUBLIC_KEY, PARAM_CHALLENGE) match {
case Seq(laoId @ JsString(_), JsString(serverAddress), publicKey @ JsString(_), challenge @ JsObject(_)) =>
FederationInit(
laoId.convertTo[Hash],
serverAddress,
publicKey.convertTo[PublicKey],
challenge.convertTo[Message]
)
case _ => throw new IllegalArgumentException(s"Can't parse json value $json to a FederationInit object")
}

override def write(obj: FederationInit): JsValue = {
var jsObjectContent: ListMap[String, JsValue] = ListMap[String, JsValue](
PARAM_OBJECT -> JsString(obj._object.toString),
PARAM_ACTION -> JsString(obj.action.toString),
PARAM_ID -> obj.laoId.toJson,
PARAM_SERVER_ADDRESS -> obj.serverAddress.toJson,
PARAM_PUBLIC_KEY -> obj.publicKey.toJson,
PARAM_CHALLENGE -> obj.challenge.toJson
)
JsObject(jsObjectContent)
}
}

implicit object FederationChallengeRequestFormat extends JsonFormat[FederationChallengeRequest] {
final private val PARAM_TIMESTAMP: String = "timestamp"

override def read(json: JsValue): FederationChallengeRequest = json.asJsObject().getFields(PARAM_TIMESTAMP) match {
case Seq(timestamp @ JsNumber(_)) => FederationChallengeRequest(
timestamp.convertTo[Timestamp]
)
case _ => throw new IllegalArgumentException(s"Can't parse json value $json to a FederationChallengeRequest object")
}

override def write(obj: FederationChallengeRequest): JsValue = {
var jsObjectContent: ListMap[String, JsValue] = ListMap[String, JsValue](
PARAM_OBJECT -> JsString(obj._object.toString),
PARAM_ACTION -> JsString(obj.action.toString),
PARAM_TIMESTAMP -> obj.timestamp.toJson
)
JsObject(jsObjectContent)
}
}

implicit object FederationResultFormat extends JsonFormat[FederationResult] {
final private val PARAM_STATUS: String = "status"
final private val PARAM_REASON: String = "reason"
final private val PARAM_PUBLIC_KEY: String = "public_key"
final private val PARAM_CHALLENGE = "challenge"

override def read(json: JsValue): FederationResult =
val jsObj = json.asJsObject
val status = jsObj.fields.get(PARAM_STATUS).collect {
case JsString(s) => s
}.getOrElse(throw new IllegalArgumentException(s"Can't parse json value $json to a FederationResult object"))

val reason = jsObj.fields.get(PARAM_REASON).collect {
case JsString(r) => r
}

val publicKey = jsObj.fields.get(PARAM_PUBLIC_KEY).collect {
case key @ JsString(_) => key.convertTo[PublicKey]
}

val challengeMessage = jsObj.fields.get(PARAM_CHALLENGE).collect {
case jsObj: JsObject => jsObj.convertTo[Message]
}.getOrElse(throw new IllegalArgumentException(s"Can't parse json value $json to a FederationResult object"))

(reason, publicKey) match {
case (None, Some(key)) => FederationResult(status, key, challengeMessage)
case (Some(r), None) => FederationResult(status, r, challengeMessage)
case _ => throw new IllegalArgumentException(s"Can't parse json value $json to a FederationResult object")
}

override def write(obj: FederationResult): JsValue = {
val fields = scala.collection.mutable.Map[String, JsValue]()
fields += PARAM_STATUS -> obj.status.toJson
fields += PARAM_CHALLENGE -> obj.challenge.toJson

obj.reason.foreach(r => fields += PARAM_REASON -> JsString(r))
obj.publicKey.foreach(key => fields += PARAM_PUBLIC_KEY -> key.toJson)
JsObject(fields.toMap)

}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ object ObjectProtocol extends DefaultJsonProtocol {
override def write(obj: Base64Data): JsValue = JsString(obj.data)
}

implicit object Base16DataFormat extends JsonFormat[Base16Data] {
override def read(json: JsValue): Base16Data = json match {
case JsString(data) => Base16Data(data)
case _ => throw new IllegalArgumentException(s"Can't parse json value $json to a Base16Data object")
}

override def write(obj: Base16Data): JsValue = JsString(obj.data)
}

implicit object ChannelFormat extends JsonFormat[Channel] {
override def read(json: JsValue): Channel = json match {
case JsString(value) => Channel(value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ enum ActionType(val action: String):
case post_transaction extends ActionType("post_transaction")
// popcha
case authenticate extends ActionType("authenticate")
// federation actions
case init extends ActionType("init")
case expect extends ActionType("expect")
case challenge_request extends ActionType("challenge_request")
case challenge extends ActionType("challenge")
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ enum ObjectType(val objectType: String):
case reaction extends ObjectType("reaction")
case coin extends ObjectType("coin")
case popcha extends ObjectType("popcha")
case federation extends ObjectType("federation")
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ch.epfl.pop.model.network.method.message.data.federation

import ch.epfl.pop.json.MessageDataProtocol.FederationChallengeFormat
import ch.epfl.pop.model.network.Parsable
import ch.epfl.pop.model.network.method.message.data.{ActionType, MessageData, ObjectType}
import ch.epfl.pop.model.objects.{Base16Data, Timestamp}
import spray.json.*

final case class FederationChallenge(
value: Base16Data,
validUntil: Timestamp
) extends MessageData {
override val _object: ObjectType = ObjectType.federation
override val action: ActionType = ActionType.challenge
}

object FederationChallenge extends Parsable {
def apply(value: Base16Data, validUntil: Timestamp): FederationChallenge = {
new FederationChallenge(value, validUntil)
}
override def buildFromJson(payload: String): FederationChallenge = payload.parseJson.asJsObject.convertTo[FederationChallenge]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ch.epfl.pop.model.network.method.message.data.federation

import ch.epfl.pop.json.MessageDataProtocol.FederationChallengeRequestFormat
import ch.epfl.pop.model.network.Parsable
import ch.epfl.pop.model.network.method.message.data.{ActionType, MessageData, ObjectType}
import ch.epfl.pop.model.objects.Timestamp
import spray.json.*

final case class FederationChallengeRequest(
timestamp: Timestamp
) extends MessageData {
override val _object: ObjectType = ObjectType.federation
override val action: ActionType = ActionType.challenge_request
}

object FederationChallengeRequest extends Parsable {
def apply(timestamp: Timestamp): FederationChallengeRequest = {
new FederationChallengeRequest(timestamp)
}

override def buildFromJson(payload: String): FederationChallengeRequest = payload.parseJson.asJsObject.convertTo[FederationChallengeRequest]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ch.epfl.pop.model.network.method.message.data.federation

import ch.epfl.pop.json.MessageDataProtocol.FederationExpectFormat
import ch.epfl.pop.model.network.Parsable
import ch.epfl.pop.model.network.method.message.Message
import ch.epfl.pop.model.network.method.message.data.{ActionType, MessageData, ObjectType}
import ch.epfl.pop.model.objects.{Hash, PublicKey}
import spray.json.*

final case class FederationExpect(
laoId: Hash,
serverAddress: String,
publicKey: PublicKey,
challenge: Message
) extends MessageData {
override val _object: ObjectType = ObjectType.federation
override val action: ActionType = ActionType.expect
}

object FederationExpect extends Parsable {
def apply(laoId: Hash, serverAddress: String, publicKey: PublicKey, challenge: Message): FederationExpect = {
new FederationExpect(laoId, serverAddress, publicKey, challenge)
}

override def buildFromJson(payload: String): FederationExpect = payload.parseJson.asJsObject.convertTo[FederationExpect]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ch.epfl.pop.model.network.method.message.data.federation

import ch.epfl.pop.json.MessageDataProtocol.FederationInitFormat
import ch.epfl.pop.model.network.Parsable
import ch.epfl.pop.model.network.method.message.Message
import ch.epfl.pop.model.network.method.message.data.{ActionType, MessageData, ObjectType}
import ch.epfl.pop.model.objects.{Hash, PublicKey}
import spray.json.*

final case class FederationInit(
laoId: Hash,
serverAddress: String,
publicKey: PublicKey,
challenge: Message
) extends MessageData {
override val _object: ObjectType = ObjectType.federation
override val action: ActionType = ActionType.init
}

object FederationInit extends Parsable {
def apply(laoId: Hash, serverAddress: String, publicKey: PublicKey, challenge: Message): FederationInit = {
new FederationInit(laoId, serverAddress, publicKey, challenge)
}

override def buildFromJson(payload: String): FederationInit = payload.parseJson.asJsObject.convertTo[FederationInit]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ch.epfl.pop.model.network.method.message.data.federation

import ch.epfl.pop.json.MessageDataProtocol.FederationResultFormat
import ch.epfl.pop.model.network.Parsable
import ch.epfl.pop.model.network.method.message.Message
import ch.epfl.pop.model.network.method.message.data.{ActionType, MessageData, ObjectType}
import ch.epfl.pop.model.objects.PublicKey
import spray.json.*

final case class FederationResult(
status: String,
reason: Option[String],
publicKey: Option[PublicKey],
challenge: Message
) extends MessageData {
def this(status: String, publicKey: PublicKey, challenge: Message) = this(status, None, Some(publicKey), challenge)
def this(status: String, reason: String, challenge: Message) = this(status, Some(reason), None, challenge)
override val _object: ObjectType = ObjectType.federation
override val action: ActionType = ActionType.result
}

object FederationResult extends Parsable {
def apply(status: String, reason: Option[String], publicKey: Option[PublicKey], challenge: Message): FederationResult = {
new FederationResult(status, reason, publicKey, challenge)
}
def apply(status: String, publicKey: PublicKey, challenge: Message): FederationResult = new FederationResult(status, None, Some(publicKey), challenge)
def apply(status: String, reason: String, challenge: Message): FederationResult = new FederationResult(status, Some(reason), None, challenge)

override def buildFromJson(payload: String): FederationResult = payload.parseJson.asJsObject.convertTo[FederationResult]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ch.epfl.pop.model.objects

import java.nio.charset.StandardCharsets

final case class Base16Data(data: String) {

/** Decode the hexadecimal data to a string using UTF-8 charset. */
def decodeToString(): String = new String(this.decode(), StandardCharsets.UTF_8)

/** Decode the hexadecimal string to a byte array. */
def decode(): Array[Byte] = Base16Data.hexStringToByteArray(data)

/** Get the byte array of the original string data. */
def getBytes: Array[Byte] = data.getBytes(StandardCharsets.UTF_8)

override def equals(that: Any): Boolean = that match {
case that: Base16Data => this.data.equalsIgnoreCase(that.data)
case _ => false
}

/** Returns the string representation of the hexadecimal data. */
override def toString: String = data
}

object Base16Data {

/** Convert a byte array to a hexadecimal string. */
def byteArrayToHexString(bytes: Array[Byte]): String = {
bytes.map("%02x".format(_)).mkString
}

/** Convert a string to hexadecimal. */
def stringToHexString(s: String): String = byteArrayToHexString(s.getBytes(StandardCharsets.UTF_8))

/** Create a Base16Data instance from a string, ensuring it represents exactly 32 bytes when decoded. */
def encode(data: String): Base16Data = {
val hexString = stringToHexString(data)
Base16Data(hexString)
}

/** Create a Base16Data instance from a byte array, ensuring it is exactly 32 bytes long. */
def encode(data: Array[Byte]): Base16Data = {
Base16Data(byteArrayToHexString(data))
}

/** Helper method to convert a hexadecimal string into a byte array. */
def hexStringToByteArray(hex: String): Array[Byte] = {
hex.sliding(2, 2).toArray.map(Integer.parseInt(_, 16).toByte)
}
}
Loading

0 comments on commit abf0af1

Please sign in to comment.