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

io.provenance.p8e.shared.service.AffiliateService.kt Maven / Gradle / Ivy

package io.provenance.p8e.shared.service

import io.p8e.crypto.SignerFactory
import io.p8e.crypto.SignerFactoryParam
import io.p8e.crypto.SignerImpl
import io.p8e.crypto.Hash
import io.p8e.proto.Affiliate.AffiliateContractWhitelist
import io.p8e.proto.Affiliate.AffiliateWhitelist
import io.p8e.util.*
import io.p8e.util.auditedProv
import io.p8e.util.toProtoTimestampProv
import io.provenance.engine.crypto.Bech32
import io.provenance.engine.crypto.toBech32Data
import io.provenance.p8e.encryption.ecies.ECUtils
import io.provenance.p8e.shared.extension.isActive
import io.provenance.os.client.OsClient
import io.provenance.p8e.encryption.model.ExternalKeyRef
import io.provenance.p8e.encryption.model.KeyProviders.DATABASE
import io.provenance.p8e.encryption.model.KeyProviders.SMARTKEY
import io.provenance.p8e.encryption.model.KeyRef
import io.provenance.p8e.shared.domain.*
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.elasticsearch.client.RequestOptions
import org.elasticsearch.client.RestHighLevelClient
import org.elasticsearch.client.indices.CreateIndexRequest
import org.elasticsearch.client.indices.GetIndexRequest
import org.springframework.cache.CacheManager
import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.Cacheable
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.security.KeyPair
import java.security.PublicKey
import java.time.OffsetDateTime
import java.util.*

@Component
class AffiliateService(
    private val cacheManager: CacheManager,
    private val osClient: OsClient,
    private val keystoneService: KeystoneService,
    private val esClient: RestHighLevelClient,
    private val signerFactory: SignerFactory,
) {

    companion object {
        const val AFFILIATE = "affiliate"
        const val AFFILIATE_FIRST = "affiliate_first"
        const val AFFILIATE_KEY_PAIR = "affiliate_key_pair"
        const val AFFILIATE_SIGNING_KEY_PAIR = "affiliate_signing_key_pair"
        const val AFFILIATE_PEN = "affiliate_pen"
        const val AFFILIATES = "affiliates"
        const val AFFILIATES_ENCRYPTION_KEYS = "affiliates_encryption_keys"
        const val AFFILIATES_SIGNING_KEYS = "affiliates_signing_keys"
        const val AFFILIATE_INDEX_NAMES = "affiliate_index_names"
        const val AFFILIATE_INDEX_NAME = "affiliate_index_name"
        const val AFFILIATE_ENCRYPTION_KEY_PAIR = "affiliate_encryption_key_pair"
        const val AFFILIATE_BECH32_LOOKUP = "affiliate_bech32_lookup"
        const val PUBLIC_KEY_TO_ADDRESS = "public_key_to_address"
        const val AFFILIATE_SIGNING_KID = "affiliate_signing_key_id"
        const val AFFILIATE_ENCRYPTION_PUBLIC_KEY = "affiliate_encryption_public_key"
    }

    /**
     *  Return the signer to be used
     *
     *  [publicKey] publicKey used to validate
     *  [signer] The specific signer used to sign contracts
     */
    fun getSigner(publicKey: PublicKey): SignerImpl {
        val affiliateRecord = get(publicKey)
            .orThrowNotFound("Affiliate Record Not found")

        return when(affiliateRecord.keyType) {
            SMARTKEY -> signerFactory.getSigner(SignerFactoryParam.SmartKeyParam(affiliateRecord.signingKeyUuid.toString(), affiliateRecord.publicKey.value.toJavaPublicKey()))
            DATABASE -> signerFactory.getSigner(SignerFactoryParam.PenParam(KeyPair(affiliateRecord.publicKey.value.toJavaPublicKey(), affiliateRecord.privateKey!!.toJavaPrivateKey())))
        }
    }

    fun getEncryptionKeyRef(publicKey: PublicKey): KeyRef {
        val affiliateRecord = getFirst(publicKey)
        return KeyRef(
            affiliateRecord.encryptionPublicKey.toJavaPublicKey(),
            affiliateRecord.encryptionPrivateKey?.toJavaPrivateKey(),
            affiliateRecord.encryptionKeyUuid,
            affiliateRecord.keyType
        )
    }

    /**
     * Get affiliate by pk.
     *
     * @param [publicKey] primary key
     * @return the [AffiliateRecord] found or null
     */
    @Cacheable(AFFILIATE)
    fun get(publicKey: PublicKey): AffiliateRecord? = AffiliateRecord.findById(publicKey.toHex())
            ?: AffiliateRecord.findByEncryptionPublicKey(publicKey)
            ?: AffiliateRecord.findByAuthenticationPublicKey(publicKey)

    /**
     * Get affiliate by UUID.
     *
     * @param [uuid] keypair identifier.
     * @return the [AffiliateRecord] found or null
     */
    @Cacheable(AFFILIATE)
    fun get(uuid: String): AffiliateRecord? = AffiliateRecord.findById(uuid)

    /**
     * Get affiliate by pk, not nullable.
     *
     * @param [uuid] primary key
     * @throws [NotFoundException] If null, exceptions
     * @return the [AffiliateRecord] found
     */
    @Cacheable(AFFILIATE_FIRST)
    internal fun getFirst(publicKey: PublicKey): AffiliateRecord = get(publicKey)
        ?: throw NotFoundException("Public Key is not a valid signing or encryption key: ${publicKey.toHex()}")

    /**
     * Get affiliate by pk, not nullable.
     *
     * @param [uuid] primary key
     * @throws [NotFoundException] If null, exceptions
     * @return the [AffiliateRecord] found
     */
    @Cacheable(AFFILIATE_FIRST)
    internal fun getFirst(uuid: String): AffiliateRecord = get(uuid)
        ?: throw NotFoundException("Key UUID is not found: $uuid")

    /**
     * Get encryption key pair for an affiliate.
     *
     * @param [uuid] primary key
     * @param [certificateUuid] certificate to use
     * @throws [NotFoundException] If null, exceptions
     * @return the [KeyPair] derived from affiliate encryption
     */
    @Cacheable(AFFILIATE_KEY_PAIR)
    fun getSigningKeyPair(publicKey: PublicKey): KeyPair {
        val affiliateRecord = getFirst(publicKey)
        return KeyPair(affiliateRecord.publicKey.value.toJavaPublicKey(), affiliateRecord.privateKey?.toJavaPrivateKey())
    }

    @Cacheable(AFFILIATE_ENCRYPTION_KEY_PAIR)
    fun getEncryptionKeyPair(publicKey: PublicKey): KeyPair {
        val affiliateRecord = getFirst(publicKey)
        return KeyPair(affiliateRecord.encryptionPublicKey.toJavaPublicKey(), affiliateRecord.encryptionPrivateKey?.toJavaPrivateKey())
    }
    
    @Cacheable(AFFILIATE_KEY_PAIR)
    fun getSigningKeyPair(uuid: String): KeyPair{
        val affiliateRecord = getFirst(uuid)
        return KeyPair(affiliateRecord.publicKey.value.toJavaPublicKey(), affiliateRecord.privateKey?.toJavaPrivateKey())
    }

    fun getSigningPublicKey(publicKey: PublicKey): PublicKey = getFirst(publicKey).publicKey.value.toJavaPublicKey()

    /**
     * Validate that the public key is a valid affiliate against the database.
     *
     * @param [publicKey] Public key of the contract being executed.
     * @return [boolean] return true if the public key exists in the affiliate table.
     */
    @Cacheable(AFFILIATES_ENCRYPTION_KEYS)
    fun getEncryptionPublicKey(publicKey: PublicKey): PublicKey {
        val affiliateRecord = getFirst(publicKey)
        return affiliateRecord.encryptionPublicKey.toJavaPublicKey()
    }

    @Cacheable(AFFILIATE_BECH32_LOOKUP)
    fun getAffiliateFromBech32Address(bech32Address: String): AffiliateRecord? {
        val mainNet = bech32Address.startsWith(Bech32.PROVENANCE_MAINNET_ACCOUNT_PREFIX)
        return getAll()
            .find { affiliate ->
                getAddress(affiliate.publicKey.value.toJavaPublicKey(), mainNet) == bech32Address || getAddress(affiliate.encryptionPublicKey.toJavaPublicKey(), mainNet) == bech32Address
            }
    }

    @Cacheable(PUBLIC_KEY_TO_ADDRESS)
    private fun getAddress(publicKey: PublicKey, mainNet: Boolean): String =
        publicKey.let {
            (it as BCECPublicKey).q.getEncoded(true)
        }.let {
            Hash.sha256hash160(it)
        }.let {
            val prefix = if (mainNet) Bech32.PROVENANCE_MAINNET_ACCOUNT_PREFIX else Bech32.PROVENANCE_TESTNET_ACCOUNT_PREFIX
            it.toBech32Data(prefix).address
        }

    /**
     * Get all affiliates.
     *
     * @return the [AffiliateRecord] list
     */
    @Cacheable(AFFILIATES)
    fun getAll(): List = AffiliateRecord.all().toList()

    /**
     * Get all distinct index names
     *
     * @return the list of index names
     */
    @Cacheable(AFFILIATE_INDEX_NAMES)
    fun getAllIndexNames() = AffiliateRecord.getDistinctIndexNames()

    /**
     * Get index name from public key
     *
     * @return the index name for the specified public key
     */
    @Cacheable(AFFILIATE_INDEX_NAME)
    fun getIndexNameByPublicKey(publicKey: PublicKey) = AffiliateRecord.findByPublicKey(publicKey)?.indexName ?: DEFAULT_INDEX_NAME

    /**
     * Save or update an affiliate.
     *
     * @param [signingKeyPair] The signing key pair for data signing
     * @param [encryptionKeyPair] The encryption used for affiliate auth
     * @param [indexName] Name of index for doc storage.
     * @param [alias] alias used to describe an affiliate.
     * @param [jwt] token for webservice authentication.
     * @return [AffiliateRecord] The affiliate record
     */
    @CacheEvict(cacheNames = [
        AFFILIATE,
        AFFILIATE_FIRST,
        AFFILIATE_KEY_PAIR,
        AFFILIATE_PEN,
        AFFILIATES,
        AFFILIATES_ENCRYPTION_KEYS,
        AFFILIATES_SIGNING_KEYS,
        AFFILIATE_INDEX_NAMES,
        AFFILIATE_INDEX_NAME,
        AFFILIATE_BECH32_LOOKUP,
    ])

    fun save(signingKeyPair: KeyPair, encryptionKeyPair: KeyPair, authPublicKey: PublicKey, indexName: String? = null, alias: String? = null): AffiliateRecord =
        AffiliateRecord.insert(signingKeyPair, encryptionKeyPair, authPublicKey, indexName, alias)
            .also {
                // Register the key with object store so that it monitors for replication.
                osClient.createPublicKey(encryptionKeyPair.public)

                // create index in ES if it doesn't already exist
                indexName?.let {
                    if (!esClient.indices().exists(GetIndexRequest(it), RequestOptions.DEFAULT)) {
                        val response = esClient.indices().create(CreateIndexRequest(it), RequestOptions.DEFAULT)
                        require (response.isAcknowledged) { "ES index creation of $it was not successful" }
                    }
                }
            }


    /**
     * Save or update an affiliate with a signing public key from a key management system.
     *
     * @param [signingPublicKey] The provided signing public key from the key management system.
     * @param [encryptionKeyPair] The encryption used for affiliate auth
     * @param [indexName] Name of index for doc storage.
     * @param [alias] alias used to describe an affiliate.
     * @param [jwt] token for webservice authentication.
     * @return [AffiliateRecord] The affiliate record
     */
    @CacheEvict(cacheNames = [
        AFFILIATE,
        AFFILIATE_FIRST,
        AFFILIATE_KEY_PAIR,
        AFFILIATE_PEN,
        AFFILIATES,
        AFFILIATES_ENCRYPTION_KEYS,
        AFFILIATES_SIGNING_KEYS,
        AFFILIATE_INDEX_NAMES,
        AFFILIATE_INDEX_NAME,
        AFFILIATE_BECH32_LOOKUP,
    ])
    fun save(signingPublicKey: ExternalKeyRef, encryptionPublicKey: ExternalKeyRef, authPublicKey: PublicKey, indexName: String? = null, alias: String?, jwt: String? = null, identityUuid: UUID? = null): AffiliateRecord =
        AffiliateRecord.insert(signingPublicKey, encryptionPublicKey, authPublicKey, indexName, alias)
            .also {
                // Register the key with object store so that it monitors for replication.
                osClient.createPublicKey(encryptionPublicKey.publicKey)

                // create index in ES if it doesn't already exist
                indexName?.let {
                    if (!esClient.indices().exists(GetIndexRequest(it), RequestOptions.DEFAULT)) {
                        val response = esClient.indices().create(CreateIndexRequest(it), RequestOptions.DEFAULT)
                        require (response.isAcknowledged) { "ES index creation of $it was not successful" }
                    }
                }
            }

    fun attachServiceKeys(affiliatePublicKey: PublicKey, servicePublicKeys: List) = servicePublicKeys.map { serviceKey ->
        AffiliateToServiceRecord.new(affiliatePublicKey.toHex()) {
            servicePublicKey = serviceKey.toHex()
        }
    }.let {
        ServiceAccountRecord.findByPublicKeys(servicePublicKeys).toList()
    }

    fun removeServiceKeys(affiliatePublicKey: PublicKey, servicePublicKeys: List) = AffiliateToServiceTable.deleteWhere {
        (AffiliateToServiceTable.affiliatePublicKey eq affiliatePublicKey.toHex()) and (AffiliateToServiceTable.servicePublicKey inList servicePublicKeys.map { it.toHex() })
    }

    /**
     * Get all encryption key pairs mapped by public key.
     *
     * @return the [AffiliateKeyPair] map.
     */
    @Cacheable(AFFILIATES_ENCRYPTION_KEYS)
    fun getEncryptionKeyPairs(): Map = getAll()
        .map {
            // We need to handle nullable keys
            it.encryptionPublicKey to
                    KeyPair(it.encryptionPublicKey.toJavaPublicKey(), it.encryptionPrivateKey?.toJavaPrivateKey())
        }.toMap()

    /**
     * Get whitelists data for an affiliate.
     *
     * @param [uuid] primary key
     * @return the [AffiliateWhitelist] or default
     */
    fun getWhitelists(publicKey: PublicKey): AffiliateWhitelist = getFirst(publicKey).whitelistData ?: AffiliateWhitelist.getDefaultInstance()

    /**
     * Get whitelist contract for an affiliate if its active.
     *
     * @param [uuid] primary key
     * @return the active [AffiliateContractWhitelist]
     */
    fun getWhitelistContractByClassNameActive(publicKey: PublicKey, className: String): AffiliateContractWhitelist? = getWhitelists(publicKey)
        .contractWhitelistsList
        .firstOrNull { it.classname == className && it.isActive() }

    /**
     * Save class to affiliate class whitelist.
     *
     * @param [uuid] primary key
     * @param [affiliateContractWhitelist] The contract to add to whitelist
     */
    @CacheEvict(cacheNames = [
        AFFILIATE,
        AFFILIATE_FIRST,
        AFFILIATES
    ])
    fun addWhitelistClass(publicKey: PublicKey, affiliateContractWhitelist: AffiliateContractWhitelist): AffiliateWhitelist? {
        val signingPublicKey = get(publicKey)!!.publicKey.value.toJavaPublicKey()
        return AffiliateRecord.findForUpdate(signingPublicKey)!!
            .also {
                val builder = it.whitelistData?.toBuilder() ?: AffiliateWhitelist.newBuilder()

                if (builder.contractWhitelistsBuilderList.none { whitelist ->
                        whitelist.classname == affiliateContractWhitelist.classname && whitelist.build().isActive()
                    }) {
                    it.whitelistData = builder
                        .addContractWhitelists(
                            affiliateContractWhitelist.toBuilder().setStartTime(OffsetDateTime.now().toProtoTimestampProv())
                        )
                        .auditedProv()
                        .build()
                }
            }
            .whitelistData
    }

    fun getSharePublicKeys(publicKeys: Collection): AffiliateSharePublicKeys =
        AffiliateShareRecord.findByAffiliates(publicKeys)
            .map { it.typedPublicKey() }
            .toSet()
            .let(::AffiliateSharePublicKeys)

    fun getShares(affiliatePublicKey: PublicKey): List = AffiliateShareRecord.findByAffiliate(affiliatePublicKey).toList()

    fun addShare(affiliatePublicKey: PublicKey, publicKey: PublicKey) = AffiliateShareRecord.insert(affiliatePublicKey, publicKey)

    fun removeShare(affiliatePublicKey: PublicKey, publicKey: PublicKey) = AffiliateShareRecord.findByAffiliateAndPublicKey(affiliatePublicKey, publicKey)?.delete()

    /**
     * Eviction of caches on a timer.
     */
    @Scheduled(fixedRate = 120000)
    fun evictCachesAtIntervals() {
        // Not needed but a just in case someone modifies affiliates outside of this service scope
        cacheManager.cacheNames.forEach { cacheManager.getCache(it)?.clear() }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy