All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.europa.ec.eudi.sdjwt.Dsl.kt Maven / Gradle / Ivy

There is a newer version: 0.6.1
Show newest version
/*
 * Copyright (c) 2023 European Commission
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package eu.europa.ec.eudi.sdjwt

import eu.europa.ec.eudi.sdjwt.SdObjectElement.*
import kotlinx.serialization.json.*

/**
 * Selectively disclosable claims that will be encoded with the flat option.
 * Effectively, this is a specification that will feed [SdJwtFactory] in
 * order to produce the [SdJwt]
 *
 * @param content the content of the object. Each of its claims could be always or selectively disclosable
 * @param minimumDigests This is an optional hint, that expresses the minimum number of digests at the immediate level
 * of this [SdObject], that the [SdJwtFactory] will try to satisfy. [SdJwtFactory] will add decoy digests if
 * the number of actual [DisclosureDigest] is less than the [hint][minimumDigests]
 */
class SdObject(
    private val content: Map,
    val minimumDigests: MinimumDigests?,
) : Map by content

/**
 * Adds to then current [SdObject] another [SdObject] producing
 * a new [SdObject] containing the merged claims.
 *
 * If the two [SdObject] contain claims with common names, then the resulting [SdObject]
 * will preserve the claims of [that]
 *
 * ```
 *   val sdObj1 = buildSdObject(minimumDigests=2) {
 *      sd{
 *          put("a", "foo")
 *          put("b", "bar")
 *      }
 *   }
 *
 *   val sdObj2 = buildSdObject(minimumDigests=2) {
 *      plain {
 *          put("a", "ddd")
 *      }
 *   }
 *
 *   sdObj1 + sdObj2 // will contain "a" to Plain("ddd") and "b" to Sd("bar")
 *
 * ```
 * @param that the other [SdObject]
 * @receiver the current [SdObject]
 * @return a new [SdObject] as described above
 */
operator fun SdObject.plus(that: SdObject): SdObject {
    fun MinimumDigests?.valueOrZero() = this?.value ?: 0
    val newMinimumDigests =
        if (this.minimumDigests == null && that.minimumDigests == null) null
        else MinimumDigests(this.minimumDigests.valueOrZero() + that.minimumDigests.valueOrZero())
    return SdObject(
        (this as Map) + (that as Map),
        newMinimumDigests,
    )
}

/**
 * A [JsonElement] that is either always or selectively disclosable
 */
sealed interface DisclosableJsonElement {
    /**
     * A [JsonElement] that is always disclosable
     */
    @JvmInline
    value class Plain(val value: JsonElement) : DisclosableJsonElement

    /**
     * A [JsonElement] that is selectively disclosable (as a whole)
     */
    @JvmInline
    value class Sd(val value: JsonElement) : DisclosableJsonElement {
        init {
            require(value != JsonNull) { "Null cannot be selectively disclosable" }
        }
    }
}

/**
 * The elements of a selectively disclosable array
 */
sealed interface SdArrayElement {
    /**
     * An element which contains any [JsonElement] that is either always or selectively (as a whole) disclosable
     */
    @JvmInline
    value class Disclosable(val disclosable: DisclosableJsonElement) : SdArrayElement

    /**
     * An element that is a selectively disclosable object
     */
    @JvmInline
    value class DisclosableObj(val sdObject: SdObject) : SdArrayElement

    companion object {
        fun plain(content: JsonElement): SdArrayElement = Disclosable(DisclosableJsonElement.Plain(content))
        fun sd(content: JsonElement): SdArrayElement = Disclosable(DisclosableJsonElement.Sd(content))
        fun sd(obj: SdObject): SdArrayElement = DisclosableObj(obj)
    }
}

/**
 * The elements within a [disclosable object][SdObject]
 */
sealed interface SdObjectElement {

    data class Disclosable(val disclosable: DisclosableJsonElement) : SdObjectElement

    /**
     * A selectively disclosable array
     * Each of its elements could be always or selectively disclosable
     */
    class SdArray(private val content: List, val minimumDigests: MinimumDigests?) :
        SdObjectElement, List by content

    /**
     * Selectively disclosable array that will be encoded with the recursive option
     */
    data class RecursiveSdArray(val content: SdArray) : SdObjectElement

    /**
     * Selectively disclosable claims that will be encoded with the structured option
     */
    data class StructuredSdObject(val content: SdObject) : SdObjectElement

    /**
     * Selectively disclosable claims that will be encoded with the recursive option
     */
    data class RecursiveSdObject(val content: SdObject) : SdObjectElement

    companion object {
        fun plain(content: JsonElement): Disclosable = Disclosable(DisclosableJsonElement.Plain(content))
        fun sd(content: JsonElement): Disclosable = Disclosable(DisclosableJsonElement.Sd(content))
        fun sdRec(obj: SdObject): RecursiveSdObject = RecursiveSdObject(obj)
        fun sdStruct(obj: SdObject): StructuredSdObject = StructuredSdObject(obj)
        fun sd(es: List, minimumDigests: Int?): SdArray = SdArray(es, minimumDigests.atLeastDigests())
        fun sdRec(es: List, minimumDigests: Int?): RecursiveSdArray = RecursiveSdArray(sd(es, minimumDigests))
    }
}

@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
internal annotation class SdElementDsl

/**
 * [SdArray] is actually a [List] of [elements][SdArrayElement]
 *
 * So we can use as builder a [MutableList]
 *
 * @see buildSdArray
 */
typealias SdArrayBuilder = (@SdElementDsl MutableList)

/**
 * [SdObject] is actually a [Map] of [elements][SdObjectElement]
 *
 * So we can use as a builder [MutableMap]
 *
 * @see sdJwt
 * @see buildSdObject
 */
typealias SdObjectBuilder = (@SdElementDsl MutableMap)
typealias SdOrPlainJsonObjectBuilder = (@SdElementDsl JsonObjectBuilder)

//
// Methods for building sd arrays
//
/**
 * A convenient method for building a [SdArray] given a [builderAction]
 * ```
 * val arr = buildSdArray{
 *    // adds non-selectively disclosable primitive
 *    plain("DE")
 *    // adds selectively disclosable primitive
 *    sd("GR")
 *    // add selectively disclosable object
 *    sd {
 *     put("over_18", true)
 *     put("over_25", false)
 *    }
 * }
 * ```
 *
 * @return the [SdArray] described by the [builderAction]
 */
inline fun buildSdArray(
    minimumDigests: Int?,
    builderAction: SdArrayBuilder.() -> Unit,
): SdArray = SdArray(buildList(builderAction), minimumDigests.atLeastDigests())
fun SdArrayBuilder.plain(value: String) = plain(JsonPrimitive(value))
fun SdArrayBuilder.plain(value: Number) = plain(JsonPrimitive(value))
fun SdArrayBuilder.plain(value: Boolean) = plain(JsonPrimitive(value))
fun SdArrayBuilder.plain(value: JsonElement) = add(SdArrayElement.plain(value))

/**
 * Adds a plain claim to a [SdArray] using KotlinX Serialization DSL
 *
 * ```
 * sdJwt {
 *   // GR is plain, DE is selectively disclosable
 *   sdArray("nationalities) {
 *      plain("GR")
 *      sd("DE")
 *   }
 *   // work phone is plain, home phone is selectively disclosable
 *   sdArray("phone_numbers") {
 *      plain {
 *          put("number", "+30 12345667")
 *          put("type", "work")
 *      }
 *      sd {
 *          put("number", "+30 55555555")
 *          put("type", "home")
 *      }
 *   }
 * }
 * ```
 */
fun SdArrayBuilder.plain(action: SdOrPlainJsonObjectBuilder.() -> Unit) = plain(buildJsonObject(action))

/**
 * Adds into an [SdArray] an element [claims] that will be translated into a
 * set of claims, in plain, using KotlinX Serialization
 *
 * @param claims an instance of a kotlin class serializable via KotlinX Serialization
 * @receiver the builder to which the [claims] will be added
 */
inline fun  SdArrayBuilder.plain(claims: E) {
    plain(Json.encodeToJsonElement(claims))
}

fun SdArrayBuilder.sd(value: String) = sd(JsonPrimitive(value))
fun SdArrayBuilder.sd(value: Number) = sd(JsonPrimitive(value))
fun SdArrayBuilder.sd(value: Boolean) = sd(JsonPrimitive(value))
fun SdArrayBuilder.sd(value: JsonElement) = add(SdArrayElement.sd(value))

/**
 * Adds a selectively disclosable claim to a [SdArray] using KotlinX Serialization DSL
 *
 * ```
 * sdJwt {
 *   // GR is plain, DE is selectively disclosable
 *   sdArray("nationalities) {
 *      plain("GR")
 *      sd("DE")
 *   }
 *   // work phone is plain, home phone is selectively disclosable
 *   sdArray("phone_numbers") {
 *      plain {
 *          put("number", "+30 12345667")
 *          put("type", "work")
 *      }
 *      sd {
 *          put("number", "+30 55555555")
 *          put("type", "home")
 *      }
 *   }
 * }
 * ```
 */
fun SdArrayBuilder.sd(action: SdOrPlainJsonObjectBuilder.() -> Unit) = sd(buildJsonObject(action))
fun SdArrayBuilder.buildSdObject(minimumDigests: Int? = null, action: SdObjectBuilder.() -> Unit) {
    add(SdArrayElement.sd(eu.europa.ec.eudi.sdjwt.buildSdObject(minimumDigests, action)))
}

/**
 * Adds into an [SdArray] an element [claims] that will be translated into a
 * set of claims, all of them individually selectively disclosable, using KotlinX Serialization
 *
 * @param claims an instance of a kotlin class serializable via KotlinX Serialization
 * @receiver the builder to which the [claims] will be added
 */
inline fun  SdArrayBuilder.sd(claims: E) {
    sd(Json.encodeToJsonElement(claims))
}

//
// Methods for building sd arrays
//

/**
 * Factory method for creating a [SdObject] using the [SdObjectBuilder]
 * @param minimumDigests check [SdObject.minimumDigests]
 * @param builderAction some usage/action of the [SdObjectBuilder]
 * @return the [SdObject]
 */
inline fun sdJwt(
    minimumDigests: Int? = null,
    builderAction: SdObjectBuilder.() -> Unit,
): SdObject = buildSdObject(minimumDigests, builderAction)

/**
 * Factory method for creating a [SdObject] using the [SdObjectBuilder]
 * @param minimumDigests check [SdObject.minimumDigests]
 * @param builderAction some usage/action of the [SdObjectBuilder]
 * @return the [SdObject]
 */
inline fun buildSdObject(
    minimumDigests: Int? = null,
    builderAction: SdObjectBuilder.() -> Unit,
): SdObject = SdObject(buildMap(builderAction), minimumDigests.atLeastDigests())

fun SdObjectBuilder.plain(name: String, element: JsonElement) = put(name, SdObjectElement.plain(element))
fun SdObjectBuilder.sd(name: String, element: JsonElement) = put(name, SdObjectElement.sd(element))
fun SdObjectBuilder.sd(name: String, element: SdObjectElement) = put(name, element)
fun SdObjectBuilder.sd(name: String, value: String) = sd(name, JsonPrimitive(value))
fun SdObjectBuilder.sd(name: String, value: Number) = sd(name, JsonPrimitive(value))
fun SdObjectBuilder.sd(name: String, value: Boolean) = sd(name, JsonPrimitive(value))
fun SdObjectBuilder.sd(obj: Claims) = obj.forEach { (k, v) -> sd(k, v) }

/**
 * Adds into an [SdObject] an element [claims] that will be translated into a
 * set of claims, each of them selectively disclosable individually, using KotlinX Serialization
 *
 * ```
 * @Serializable
 * data class Address(@SerialName("street_address") val streetAddress: String, @SerialName("postal_code") val postalCode: String)
 * val myAddress = Address("street", "15235")
 *
 * sdJwt {
 *    sd(myAddress)
 *    // is equivalent to
 *    sd {
 *       put("street_address", "street")
 *       put("postal_code", "15235")
 *    }
 * }
 * ```
 *
 * @param claims an instance of a kotlin class serializable via KotlinX Serialization
 * @receiver the builder to which the [claims] will be added
 */
inline fun  SdObjectBuilder.sd(claims: E) {
    sd(Json.encodeToJsonElement(claims).jsonObject)
}

/**
 * Marks a set of claims expressed using KotlinX Serialization builder
 * as selectively disclosable
 *
 * ```
 * sdJwt {
 *   sd {
 *       put("given_name", "John")
 *       put("family_name", "Doe")
 *       put("email", "[email protected]")
 *       put("phone_number", "+1-202-555-0101")
 *       put("phone_number_verified", true)
 *       putJsonObject("address") {
 *           put("street_address", "123 Main St")
 *           put("locality", "Any town")
 *           put("region", "Any state")
 *           put("country", "US")
 *       }
 *   }
 * }
 * ```
 */
fun SdObjectBuilder.sd(action: SdOrPlainJsonObjectBuilder.() -> Unit) = sd(buildJsonObject(action))
fun SdObjectBuilder.plain(name: String, value: String) = plain(name, JsonPrimitive(value))
fun SdObjectBuilder.plain(name: String, value: Number) = plain(name, JsonPrimitive(value))
fun SdObjectBuilder.plain(name: String, value: Boolean) = plain(name, JsonPrimitive(value))
fun SdObjectBuilder.plain(obj: Claims) = obj.forEach { (k, v) -> plain(k, v) }

/**
 * Adds into an [SdObject] an element [claims] that will be translated into a
 * set of claims, in plain, using KotlinX Serialization
 *
 * ```
 * @Serializable
 * data class Address(@SerialName("street_address") val streetAddress: String, @SerialName("postal_code") val postalCode: String)
 * val myAddress = Address("street", "15235")
 *
 * sdJwt {
 *    plain(myAddress)
 *    // is equivalent to
 *    plain {
 *       put("street_address", "street")
 *       put("postal_code", "15235")
 *    }
 * }
 * ```
 *
 * @param claims an instance of a kotlin class serializable via KotlinX Serialization
 * @receiver the builder to which the [claims] will be added
 */
inline fun  SdObjectBuilder.plain(claims: E) {
    plain(Json.encodeToJsonElement(claims).jsonObject)
}

/**
 * Marks a set of claims expressed using KotlinX Serialization builder
 * as disclosable (non-selectively)
 *
 * ```
 * sdJwt {
 *   plain {
 *       put("given_name", "John")
 *       put("family_name", "Doe")
 *       put("email", "[email protected]")
 *       put("phone_number", "+1-202-555-0101")
 *       put("phone_number_verified", true)
 *       putJsonObject("address") {
 *           put("street_address", "123 Main St")
 *           put("locality", "Any town")
 *           put("region", "Any state")
 *           put("country", "US")
 *       }
 *   }
 * }
 * ```
 */
fun SdObjectBuilder.plain(action: SdOrPlainJsonObjectBuilder.() -> Unit) = plain(buildJsonObject(action))
fun SdObjectBuilder.sdArray(name: String, minimumDigests: Int? = null, action: SdArrayBuilder.() -> Unit) {
    sd(name, buildSdArray(minimumDigests, action))
}

fun SdObjectBuilder.structured(name: String, minimumDigests: Int? = null, action: (SdObjectBuilder).() -> Unit) {
    val obj = buildSdObject(minimumDigests, action)
    sd(name, StructuredSdObject(obj))
}

fun SdObjectBuilder.recursiveArray(name: String, minimumDigests: Int? = null, action: SdArrayBuilder.() -> Unit) {
    val arr = buildSdArray(minimumDigests, action)
    sd(name, RecursiveSdArray(arr))
}

fun SdObjectBuilder.recursive(name: String, minimumDigests: Int? = null, action: (SdObjectBuilder).() -> Unit) {
    val obj = buildSdObject(minimumDigests, action)
    sd(name, RecursiveSdObject(obj))
}

//
// JWT registered claims
//

/**
 * Represents a build action that puts a claim into a container
 * such as a [JsonObject] or [SdObject].
 *
 * Thus, this alias represent an action of [JsonObjectBuilder] or/and
 * [SdObjectBuilder] respectively
 */
private typealias BuilderAction = (String, V) -> Unit

private fun sub(value: String, action: BuilderAction) = action("sub", value)
private fun iss(value: String, action: BuilderAction) = action("iss", value)
private fun iat(value: Long, action: BuilderAction) = action("iat", value)
private fun exp(value: Long, action: BuilderAction) = action("exp", value)
private fun jti(value: String, action: BuilderAction) = action("jti", value)
private fun nbf(value: Long, action: BuilderAction) = action("nbf", value)
private fun aud(aud: List, action: BuilderAction) = when (aud.size) {
    0 -> Unit
    1 -> action("aud", JsonPrimitive(aud[0]))
    else -> action("aud", JsonArray(aud.map { JsonPrimitive(it) }))
}

/**
 * Adds the JWT publicly registered subclaim (Subject)
 */
fun JsonObjectBuilder.sub(value: String) = sub(value, this::put)

/**
 * Adds the JWT publicly registered ISS claim (Issuer)
 */
fun JsonObjectBuilder.iss(value: String) = iss(value, this::put)

/**
 *  Adds the JWT publicly registered IAT claim (Issued At)
 */
fun JsonObjectBuilder.iat(value: Long) = iat(value, this::put)

/**
 *  Adds the JWT publicly registered EXP claim (Expires)
 */
fun JsonObjectBuilder.exp(value: Long) = exp(value, this::put)

/**
 * Adds the JWT publicly registered JTI claim
 */
fun JsonObjectBuilder.jti(value: String) = jti(value, this::put)

/**
 *  Adds the JWT publicly registered NBF claim (Not before)
 */
fun JsonObjectBuilder.nbf(value: Long) = nbf(value, this::put)

/**
 * Adds the JWT publicly registered AUD claim (single Audience)
 */
fun JsonObjectBuilder.aud(vararg value: String) = aud(value.asList(), this::put)

/**
 * Adds the JWT publicly registered subclaim (Subject), in plain
 */
fun SdObjectBuilder.sub(value: String) = sub(value, this::plain)

/**
 * Adds the JWT publicly registered ISS claim (Issuer), in plain
 */
fun SdObjectBuilder.iss(value: String) = iss(value, this::plain)

/**
 *  Adds the JWT publicly registered IAT claim (Issued At), in plain
 */
fun SdObjectBuilder.iat(value: Long) = iat(value, this::plain)

/**
 *  Adds the JWT publicly registered EXP claim (Expires), in plain
 */
fun SdObjectBuilder.exp(value: Long) = exp(value, this::plain)

/**
 * Adds the JWT publicly registered JTI claim, in plain
 */
fun SdObjectBuilder.jti(value: String) = jti(value, this::plain)

/**
 *  Adds the JWT publicly registered NBE claim (Not before), in plain
 */
fun SdObjectBuilder.nbf(value: Long) = nbf(value, this::plain)

/**
 * Adds the JWT publicly registered AUD claim (single Audience), in plain
 */
fun SdObjectBuilder.aud(vararg value: String) = aud(value.asList(), this::plain)

/**
 * Adds the confirmation claim (cnf) as a plain (always disclosable) which
 * contains the [jwk]
 *
 * No checks are performed for the [jwk]
 *
 * @param jwk the key to put in confirmation claim.
 */
fun SdObjectBuilder.cnf(jwk: JsonObject) =
    plain("cnf", buildJsonObject { put("jwk", jwk) })




© 2015 - 2024 Weber Informatics LLC | Privacy Policy