Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 9c375660 authored by DaVinci9196's avatar DaVinci9196 Committed by GitHub
Browse files

PI: New parameter content (#3039)



Co-authored-by: default avatarMarvin W <git@larma.de>
parent 965368c6
Loading
Loading
Loading
Loading
+124 −7
Original line number Diff line number Diff line
@@ -8,9 +8,11 @@ package com.google.android.finsky
import android.accounts.AccountManager
import android.accounts.AccountManagerFuture
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.net.ConnectivityManager
import android.os.Bundle
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
@@ -21,6 +23,7 @@ import com.android.vending.buildRequestHeaders
import com.android.vending.makeTimestamp
import com.google.android.finsky.expressintegrityservice.ExpressIntegritySession
import com.google.android.finsky.expressintegrityservice.IntermediateIntegrityResponseData
import com.google.android.finsky.expressintegrityservice.PackageInformation
import com.google.android.gms.droidguard.DroidGuard
import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest
import com.google.android.gms.tasks.await
@@ -35,7 +38,9 @@ import kotlinx.coroutines.withContext
import okio.ByteString
import okio.ByteString.Companion.encode
import okio.ByteString.Companion.toByteString
import org.microg.gms.common.Constants
import org.microg.gms.profile.Build
import org.microg.gms.profile.ProfileManager
import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE
import org.microg.vending.billing.GServices
import org.microg.vending.billing.core.HttpClient
@@ -49,6 +54,7 @@ import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.ProviderException
import java.security.spec.ECGenParameterSpec
import kotlin.coroutines.resume
@@ -111,6 +117,15 @@ private fun Context.getProtoFile(): File {
    return file
}

fun Context.isNetworkConnected(): Boolean {
    return try {
        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        connectivityManager.activeNetworkInfo?.isConnected == true
    } catch (_: RuntimeException) {
        false
    }
}

private fun getExpressFilePB(context: Context): ExpressFilePB {
    return runCatching { FileInputStream(context.getProtoFile()).use { input -> ExpressFilePB.ADAPTER.decode(input) } }
        .onFailure { Log.w(TAG, "Failed to read express cache ", it) }
@@ -155,6 +170,15 @@ fun ByteArray.encodeBase64(noPadding: Boolean, noWrap: Boolean = true, urlSafe:
    return Base64.encodeToString(this, flags)
}

fun ByteArray.md5(): ByteArray? {
    return try {
        val md5 = MessageDigest.getInstance("MD5")
        md5.digest(this)
    } catch (e: NoSuchAlgorithmException) {
        null
    }
}

fun ByteArray.sha256(): ByteArray {
    return MessageDigest.getInstance("SHA-256").digest(this)
}
@@ -163,6 +187,11 @@ fun Bundle.getPlayCoreVersion() = PlayCoreVersion(
    major = getInt(KEY_VERSION_MAJOR, 0), minor = getInt(KEY_VERSION_MINOR, 0), patch = getInt(KEY_VERSION_PATCH, 0)
)

fun List<AdviceType>?.ensureContainsLockBootloader(): List<AdviceType> {
    if (isNullOrEmpty()) return listOf(AdviceType.LOCK_BOOTLOADER)
    return if (contains(AdviceType.LOCK_BOOTLOADER)) this else listOf(AdviceType.LOCK_BOOTLOADER) + this
}

fun readAes128GcmBuilderFromClientKey(clientKey: ClientKey?): Aead? {
    if (clientKey == null) {
        return null
@@ -249,7 +278,6 @@ fun fetchCertificateChain(context: Context, attestationChallenge: ByteArray?): L
}

suspend fun updateLocalExpressFilePB(context: Context, intermediateIntegrityResponseData: IntermediateIntegrityResponseData) = withContext(Dispatchers.IO) {
    Log.d(TAG, "Writing AAR to express cache")
    val intermediateIntegrity = intermediateIntegrityResponseData.intermediateIntegrity
    val expressFilePB = getExpressFilePB(context)

@@ -258,17 +286,13 @@ suspend fun updateLocalExpressFilePB(context: Context, intermediateIntegrityResp
        packageName = intermediateIntegrity.packageName
        cloudProjectNumber = intermediateIntegrity.cloudProjectNumber
        callerKey = intermediateIntegrity.callerKey
        webViewRequestMode = intermediateIntegrity.webViewRequestMode.let {
            when (it) {
                in 0..2 -> it + 1
                else -> 1
            }
        } - 1
        webViewRequestMode = intermediateIntegrity.webViewRequestMode.takeIf { it in 0..2 } ?: 0
        deviceIntegrityWrapper = DeviceIntegrityWrapper.Builder().apply {
            creationTime = intermediateIntegrity.callerKey.generated
            serverGenerated = intermediateIntegrity.serverGenerated
            deviceIntegrityToken = intermediateIntegrity.intermediateToken
        }.build()
        intermediateIntegrity.integrityAdvice?.let { advice = it }
    }.build()

    val requestList = expressFilePB.integrityRequestWrapper.toMutableList()
@@ -508,3 +532,96 @@ suspend fun requestIntermediateIntegrity(
        adapter = IntermediateIntegrityResponseWrapperExtend.ADAPTER
    )
}

fun buildClientKeyExtend(
    context: Context,
    session: ExpressIntegritySession,
    packageInformation: PackageInformation,
    clientKey: ClientKey
): ClientKeyExtend {
    return ClientKeyExtend.Builder().apply {
        cloudProjectNumber = session.cloudProjectNumber
        keySetHandle = clientKey.keySetHandle
        if (session.webViewRequestMode == 2) {
            this.optPackageName = KEY_OPT_PACKAGE
            this.versionCode = 0
        } else {
            this.optPackageName = session.packageName
            this.versionCode = packageInformation.versionCode
            this.certificateSha256Hashes = packageInformation.certificateSha256Hashes
        }
        this.deviceSerialHash = ProfileManager.getSerial(context).toByteArray().sha256().toByteString()
    }.build()
}

fun buildInstallSourceMetaData(
    context: Context,
    packageName: String
): InstallSourceMetaData {
    fun resolveInstallerType(name: String?): InstallerType = when {
        name.isNullOrEmpty() -> InstallerType.UNSPECIFIED_INSTALLER
        name == Constants.VENDING_PACKAGE_NAME -> InstallerType.PHONESKY_INSTALLER
        else -> InstallerType.OTHER_INSTALLER
    }

    fun resolvePackageSourceType(type: Int): PackageSourceType = when (type) {
        1 -> PackageSourceType.PACKAGE_SOURCE_OTHER
        2 -> PackageSourceType.PACKAGE_SOURCE_STORE
        3 -> PackageSourceType.PACKAGE_SOURCE_LOCAL_FILE
        4 -> PackageSourceType.PACKAGE_SOURCE_DOWNLOADED_FILE
        else -> PackageSourceType.PACKAGE_SOURCE_UNSPECIFIED
    }

    val builder = InstallSourceMetaData.Builder().apply {
        installingPackageName = InstallerType.UNSPECIFIED_INSTALLER
        initiatingPackageName = InstallerType.UNSPECIFIED_INSTALLER
        originatingPackageName = InstallerType.UNSPECIFIED_INSTALLER
        updateOwnerPackageName = InstallerType.UNSPECIFIED_INSTALLER
        packageSourceType = PackageSourceType.PACKAGE_SOURCE_UNSPECIFIED
    }

    val applicationInfo = runCatching {
        context.packageManager.getApplicationInfo(packageName, 0)
    }.getOrNull()

    if (Build.VERSION.SDK_INT >= 30) {
        runCatching {
            val info = context.packageManager.getInstallSourceInfo(packageName)
            builder.apply {
                initiatingPackageName = resolveInstallerType(info.initiatingPackageName)
                installingPackageName = resolveInstallerType(info.installingPackageName)
                originatingPackageName = resolveInstallerType(info.originatingPackageName)

                if (Build.VERSION.SDK_INT >= 34) {
                    updateOwnerPackageName = resolveInstallerType(info.updateOwnerPackageName)
                }
                if (Build.VERSION.SDK_INT >= 33) {
                    packageSourceType = resolvePackageSourceType(info.packageSource)
                }
            }
        }
    } else {
        builder.installingPackageName = runCatching {
            resolveInstallerType(context.packageManager.getInstallerPackageName(packageName))
        }.getOrElse { InstallerType.UNSPECIFIED_INSTALLER }
    }

    builder.appFlags = applicationInfo?.let { info ->
        buildList {
            if (info.flags and ApplicationInfo.FLAG_SYSTEM != 0) add(SystemAppFlag.FLAG_SYSTEM)
            if (info.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0) {
                add(SystemAppFlag.FLAG_UPDATED_SYSTEM_APP)
            }
        }.ifEmpty { listOf(SystemAppFlag.SYSTEM_APP_INFO_UNSPECIFIED) }
    } ?: listOf(SystemAppFlag.SYSTEM_APP_INFO_UNSPECIFIED)

    return builder.build()
}

fun validateIntermediateIntegrityResponse(intermediateIntegrityResponse: IntermediateIntegrityResponseData) {
    val intermediateIntegrity = intermediateIntegrityResponse.intermediateIntegrity

    requireNotNull(intermediateIntegrity.intermediateToken) { "Null intermediateToken" }
    requireNotNull(intermediateIntegrity.serverGenerated) { "Null serverGenerated" }
}
+62 −41
Original line number Diff line number Diff line
@@ -26,8 +26,10 @@ import com.google.android.finsky.ClientKey
import com.google.android.finsky.ClientKeyExtend
import com.google.android.finsky.DeviceIntegrityWrapper
import com.google.android.finsky.ExpressIntegrityResponse
import com.google.android.finsky.IntegrityAdvice
import com.google.android.finsky.INTERMEDIATE_INTEGRITY_HARD_EXPIRATION
import com.google.android.finsky.IntermediateIntegrityRequest
import com.google.android.finsky.IntermediateIntegrityResponse
import com.google.android.finsky.IntermediateIntegritySession
import com.google.android.finsky.KEY_CLOUD_PROJECT
import com.google.android.finsky.KEY_NONCE
@@ -43,13 +45,20 @@ import com.google.android.finsky.PlayProtectDetails
import com.google.android.finsky.PlayProtectState
import com.google.android.finsky.RESULT_UN_AUTH
import com.google.android.finsky.RequestMode
import com.google.android.finsky.TestErrorType
import com.google.android.finsky.buildClientKeyExtend
import com.google.android.finsky.buildInstallSourceMetaData
import com.google.android.finsky.getPlayCoreVersion
import com.google.android.finsky.encodeBase64
import com.google.android.finsky.ensureContainsLockBootloader
import com.google.android.finsky.getAuthToken
import com.google.android.finsky.getExpirationTime
import com.google.android.finsky.getIntegrityRequestWrapper
import com.google.android.finsky.getPackageInfoCompat
import com.google.android.finsky.isNetworkConnected
import com.google.android.finsky.md5
import com.google.android.finsky.model.IntegrityErrorCode
import com.google.android.finsky.model.StandardIntegrityException
import com.google.android.finsky.readAes128GcmBuilderFromClientKey
import com.google.android.finsky.requestIntermediateIntegrity
import com.google.android.finsky.sha256
@@ -58,6 +67,7 @@ import com.google.android.finsky.updateExpressAuthTokenWrapper
import com.google.android.finsky.updateExpressClientKey
import com.google.android.finsky.updateExpressSessionTime
import com.google.android.finsky.updateLocalExpressFilePB
import com.google.android.finsky.validateIntermediateIntegrityResponse
import com.google.android.play.core.integrity.protocol.IExpressIntegrityService
import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback
import com.google.android.play.core.integrity.protocol.IRequestDialogCallback
@@ -91,11 +101,9 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override
    override fun warmUpIntegrityToken(bundle: Bundle, callback: IExpressIntegrityServiceCallback?) {
        lifecycleScope.launchWhenCreated {
            runCatching {
                val authToken = getAuthToken(context, AUTH_TOKEN_SCOPE)
                if (TextUtils.isEmpty(authToken)) {
                    Log.w(TAG, "warmUpIntegrityToken: Got null auth token for type: $AUTH_TOKEN_SCOPE")
                if (!context.isNetworkConnected()) {
                    throw StandardIntegrityException(IntegrityErrorCode.NETWORK_ERROR, "No network is available")
                }
                Log.d(TAG, "warmUpIntegrityToken authToken: $authToken")

                val expressIntegritySession = ExpressIntegritySession(
                    packageName = bundle.getString(KEY_PACKAGE_NAME) ?: "",
@@ -106,9 +114,18 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override
                    null,
                    webViewRequestMode = bundle.getInt(KEY_REQUEST_MODE, 0)
                )
                Log.d(TAG, "warmUpIntegrityToken session:$expressIntegritySession}")

                updateExpressSessionTime(context, expressIntegritySession, refreshWarmUpMethodTime = true, refreshRequestMethodTime = false)

                val clientKey = updateExpressClientKey(context)

                val authToken = getAuthToken(context, AUTH_TOKEN_SCOPE)
                if (TextUtils.isEmpty(authToken)) {
                    Log.w(TAG, "warmUpIntegrityToken: Got null auth token for type: $AUTH_TOKEN_SCOPE")
                }
                Log.d(TAG, "warmUpIntegrityToken authToken: $authToken")

                val expressFilePB = updateExpressAuthTokenWrapper(context, expressIntegritySession, authToken, clientKey)

                val tokenWrapper = expressFilePB.tokenWrapper ?: AuthTokenWrapper()
@@ -125,14 +142,14 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override

                val deviceIntegrity = deviceIntegrityAndExpiredKey.deviceIntegrity
                if (deviceIntegrity.deviceIntegrityToken?.size == 0 || deviceIntegrity.clientKey?.keySetHandle?.size == 0) {
                    throw RuntimeException("DroidGuard token is empty.")
                    throw StandardIntegrityException("DroidGuard token is empty.")
                }

                val deviceKeyMd5 = Base64.encodeToString(
                    deviceIntegrity.clientKey?.keySetHandle?.md5()?.toByteArray(), Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE
                )
                if (deviceKeyMd5.isNullOrEmpty()) {
                    throw RuntimeException("Null deviceKeyMd5.")
                    throw StandardIntegrityException("Null deviceKeyMd5.")
                }

                val deviceIntegrityResponse = DeviceIntegrityResponse(
@@ -147,35 +164,16 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override
                }

                val packageInformation = PackageInformation(certificateSha256Hashes, packageInfo.versionCode)

                val clientKeyExtend = ClientKeyExtend.Builder().apply {
                    cloudProjectNumber = expressIntegritySession.cloudProjectNumber
                    keySetHandle = clientKey.keySetHandle
                    if (expressIntegritySession.webViewRequestMode == 2) {
                        this.optPackageName = KEY_OPT_PACKAGE
                        this.versionCode = 0
                    } else {
                        this.optPackageName = expressIntegritySession.packageName
                        this.versionCode = packageInformation.versionCode
                        this.certificateSha256Hashes = packageInformation.certificateSha256Hashes
                    }
                }.build()

//                val certificateChainList = fetchCertificateChain(context, clientKeyExtend.keySetHandle?.sha256()?.toByteArray())

                val sessionId = expressIntegritySession.sessionId
                val playCoreVersion = bundle.getPlayCoreVersion()

                Log.d(TAG, "warmUpIntegrityToken sessionId:$sessionId")

                val clientKeyExtend = buildClientKeyExtend(context, expressIntegritySession, packageInformation, clientKey)
                val intermediateIntegrityRequest = IntermediateIntegrityRequest.Builder().apply {
                    deviceIntegrityToken(deviceIntegrityResponse.deviceIntegrity.deviceIntegrityToken)
                    readAes128GcmBuilderFromClientKey(deviceIntegrityResponse.deviceIntegrity.clientKey)?.let {
                        clientKeyExtendBytes(it.encrypt(clientKeyExtend.encode(), null).toByteString())
                    }
                    playCoreVersion(playCoreVersion)
                    sessionId(sessionId)
//                    certificateChainWrapper(IntermediateIntegrityRequest.CertificateChainWrapper(certificateChainList))
                    playCoreVersion(bundle.getPlayCoreVersion())
                    sessionId(expressIntegritySession.sessionId)
                    installSourceMetaData(buildInstallSourceMetaData(context, expressIntegritySession.packageName))
                    cloudProjectNumber(expressIntegritySession.cloudProjectNumber)
                    playProtectDetails(PlayProtectDetails(PlayProtectState.PLAY_PROTECT_STATE_NONE))
                    if (expressIntegritySession.webViewRequestMode != 0) {
                        requestMode(RequestMode.Builder().mode(expressIntegritySession.webViewRequestMode.takeIf { it in 0..2 } ?: 0).build())
@@ -185,9 +183,19 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override
                Log.d(TAG, "intermediateIntegrityRequest: $intermediateIntegrityRequest")

                val intermediateIntegrityResponse = requestIntermediateIntegrity(context, authToken, intermediateIntegrityRequest).intermediateIntegrityResponseWrapper?.intermediateIntegrityResponse
                    ?: throw RuntimeException("intermediateIntegrityResponse is null.")

                Log.d(TAG, "requestIntermediateIntegrity: $intermediateIntegrityResponse")
                    ?: IntermediateIntegrityResponse()

                Log.d(TAG, "requestIntermediateIntegrity response: ${intermediateIntegrityResponse.encode().encodeBase64(true)}")

                val errorCode = intermediateIntegrityResponse.errorInfo?.let { error ->
                    if (error.errorCode == null) {
                        null
                    } else if (error.testErrorType == TestErrorType.REQUEST_EXPRESS) {
                        error.errorCode
                    } else if (error.testErrorType == TestErrorType.WARMUP) {
                        throw StandardIntegrityException(error.errorCode, "Server-specified exception")
                    } else null
                }

                val defaultAccountName: String = runCatching {
                    if (expressIntegritySession.webViewRequestMode != 0) {
@@ -197,9 +205,13 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override
                    }
                }.getOrDefault(RESULT_UN_AUTH)

                val callerKeyMd5 = clientKey.encode().md5() ?: throw StandardIntegrityException("Null callerKeyMd5")
                val refreshClientKey = clientKey.newBuilder()
                    .generated(makeTimestamp(System.currentTimeMillis()))
                    .build()
                val fixedAdvice = IntegrityAdvice.Builder()
                    .advices(intermediateIntegrityResponse.integrityAdvice?.advices.ensureContainsLockBootloader())
                    .build()
                val intermediateIntegrityResponseData = IntermediateIntegrityResponseData(
                    intermediateIntegrity = IntermediateIntegrity(
                        expressIntegritySession.packageName,
@@ -209,21 +221,24 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override
                        intermediateIntegrityResponse.intermediateToken,
                        intermediateIntegrityResponse.serverGenerated,
                        expressIntegritySession.webViewRequestMode,
                        0
                    ),
                    callerKeyMd5 = Base64.encodeToString(
                        refreshClientKey.encode(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
                        errorCode,
                        fixedAdvice
                    ),
                    callerKeyMd5 = callerKeyMd5.encodeBase64(noPadding = true),
                    appVersionCode = packageInformation.versionCode,
                    deviceIntegrityResponse = deviceIntegrityResponse,
                    appAccessRiskVerdictEnabled = intermediateIntegrityResponse.appAccessRiskVerdictEnabled
                )

                validateIntermediateIntegrityResponse(intermediateIntegrityResponseData)

                updateLocalExpressFilePB(context, intermediateIntegrityResponseData)

                callback?.onWarmResult(bundleOf(KEY_WARM_UP_SID to sessionId))
                callback?.onWarmResult(bundleOf(KEY_WARM_UP_SID to expressIntegritySession.sessionId))
            }.onFailure {
                callback?.onWarmResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID))
                val exception = it as? StandardIntegrityException ?: StandardIntegrityException(it.message)
                Log.w(TAG, "warm up has failed: code=${exception.code}, message=${exception.message}", exception)
                callback?.onWarmResult(bundleOf(KEY_ERROR to exception.code))
            }
        }
    }
@@ -242,6 +257,8 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override
                    webViewRequestMode = bundle.getInt(KEY_REQUEST_MODE, 0)
                )

                Log.d(TAG, "requestExpressIntegrityToken session:$expressIntegritySession}")

                if (TextUtils.isEmpty(expressIntegritySession.packageName)) {
                    Log.w(TAG, "packageName is empty.")
                    callback?.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTERNAL_ERROR))
@@ -277,6 +294,9 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override
                    return@launchWhenCreated
                }

                integrityRequestWrapper.deviceIntegrityWrapper?.errorCode?.let {
                    throw StandardIntegrityException(it, "Server-specified exception")
                }
                val expirationTime = integrityRequestWrapper.getExpirationTime()

                if (expirationTime > INTERMEDIATE_INTEGRITY_HARD_EXPIRATION * 1000) {
@@ -309,8 +329,9 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override
                    )
                )
            }.onFailure {
                Log.e(TAG, "requesting token has failed for ${bundle.getString(KEY_PACKAGE_NAME)}.")
                callback?.onRequestResult(bundleOf(KEY_ERROR to IntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID))
                val exception = it as? StandardIntegrityException ?: StandardIntegrityException(it.message)
                Log.w(TAG, "requesting token has failed: code=${exception.code}, message=${exception.message}", exception)
                callback?.onRequestResult(bundleOf(KEY_ERROR to exception.code))
            }
        }
    }
+9 −1
Original line number Diff line number Diff line
@@ -12,4 +12,12 @@ data class ExpressIntegritySession(
    var originatingWarmUpSessionId: Long,
    var verdictOptOut: List<Int>?,
    var webViewRequestMode: Int
) {
    override fun toString(): String {
        return "ExpressIntegritySession(packageName='$packageName', cloudProjectNumber=$cloudProjectNumber, sessionId=$sessionId, requestHash=$requestHash, originatingWarmUpSessionId=$originatingWarmUpSessionId, verdictOptOut=${
            verdictOptOut?.joinToString(
                prefix = "[", postfix = "]"
            )
        }, webViewRequestMode=$webViewRequestMode)"
    }
}
 No newline at end of file
+3 −1
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@
package com.google.android.finsky.expressintegrityservice

import com.google.android.finsky.ClientKey
import com.google.android.finsky.IntegrityAdvice
import okio.ByteString
import org.microg.vending.proto.Timestamp

@@ -17,5 +18,6 @@ data class IntermediateIntegrity(
    var intermediateToken: ByteString?,
    var serverGenerated: Timestamp?,
    var webViewRequestMode: Int,
    var testErrorCode: Int
    var testErrorCode: Int?,
    var integrityAdvice: IntegrityAdvice?
)
 No newline at end of file
+18 −0
Original line number Diff line number Diff line
/**
 * SPDX-FileCopyrightText: 2025 microG Project Team
 * SPDX-License-Identifier: Apache-2.0
 */

package com.google.android.finsky.model

class StandardIntegrityException : Exception {
    val code: Int

    constructor(code: Int, message: String) : super(message) {
        this.code = code
    }

    constructor(cause: String?) : super(cause) {
        this.code = IntegrityErrorCode.INTERNAL_ERROR
    }
}
 No newline at end of file
Loading