[Sample] 카드 인식 모듈화 작업(3)

수호·2025년 11월 20일
post-thumbnail

Payon Moduel Flow

class PayonReader @Inject constructor(
    private val nfcAdapter: PayonNfcAdapter,
    private val payonApiService: RetrofitPayonApi
) : ReaderStrategy {

    companion object {
        // Payon SDK 접근
        const val clientType = "****"
        const val osVer = "**
        const val osName = ""****"
        const val appId = ""*********"
        const val validKey = ""*************"
        const val envCode = ""****"
        const val deviceId = ""*************"
    }

    override suspend fun readCard(tag: Tag): Flow<CardResult> = flow {

        emit(CardResult.Loading(true))

        val result = runCatching {

            // 인증 요청 + 실패 처리
            val auth = safeAuthRequest()

            // keySeed 생성 및 salt, vi, passphrase 생성
            TagCryptoConverter().extractCryptoParamsFromKeySeed(auth.key)

            // cid 추출
            val cid = nfcAdapter.getMifareCid(tag) ?: throw CardException.CidError("CID is null")

            // sector0 key 요청 데이터 만들기
            val sector0KeyRequestData = TagCryptoConverter().encryptMifareCid(cid)

            // sector0 key 요청 + 실패 처리
            val sector0Key = sector0KeyRequest(auth.trId ?: "", sector0KeyRequestData)

            // certifyCode 추출
            val certifyCode = nfcAdapter.getCertifyCode(tag, sector0Key) ?: throw CardException.CertifyCodeError("CertifyCode is null")

            // sector12 key 요청 데이터 만들기
            val sector12KeyRequestData = TagCryptoConverter().encryptCertifyCode(sector0KeyRequestData.payonCard.chipSerialNumber, certifyCode)

            // sector12 key 요청 + 실패 처리
            val sector12Key = sector12KeyRequest(auth.trId ?: "", sector12KeyRequestData)

            // encrypt card data 추출
            val encCardData = nfcAdapter.getEncCardData(tag, sector12Key) ?: throw CardException.CidError("Enc Card Data is null")

            CardResult.Success(CardData(encCardData,byteArrayOf(),byteArrayOf()))

        }.getOrElse { exception ->
            handleCardException(exception)
        }

        emit(CardResult.Loading(false))
        emit(result)
    }

    /**
     * 인증 요청 (예외 → CardException.NetworkError)
     */
    private suspend fun safeAuthRequest(): PayonAuthResponse {

        val response = payonApiService.auth(
            clientType = clientType,
            osVer = osVer,
            osName = osName,
            appId = appId,
            validKey = validKey,
            envCode = envCode,
            deviceId = deviceId
        )

        if (!response.isSuccessful) {
            val error = parseError(response)
            throw CardException.NetworkError(
                code = error?.code ?: response.code().toString(),
                message = error?.message ?: "Unknown server error"
            )
        }

        return response.body() ?: throw CardException.AuthError("Auth body null")

    }

    /**
     * Sector0Key 요청 (예외 → CardException.NetworkError)
     */
    private suspend fun sector0KeyRequest(transactionId: String, sector0KeyRequestData: SectorKeyDataRequest): String {

        val response = payonApiService.getSector0Key(
            clientType = clientType,
            osVer = osVer,
            osName = osName,
            appId = appId,
            validKey = validKey,
            envCode = envCode,
            deviceId = deviceId,
            trId = transactionId,
            body = sector0KeyRequestData
        )

        if (!response.isSuccessful) {
            val error = parseError(response)
            throw CardException.NetworkError(
                code = error?.code ?: response.code().toString(),
                message = error?.message ?: "Unknown server error"
            )
        }

        return TagCryptoConverter().decryptSector0Key(response.body()?.payonCard?.sector0KeyA ?: "")
    }

    /**
     * Sector0Key 요청 (예외 → CardException.NetworkError)
     */
    private suspend fun sector12KeyRequest(transactionId: String, sector12KeyRequestData: SectorKeyDataRequest): String {

        val response = payonApiService.getSector12Key(
            clientType = clientType,
            osVer = osVer,
            osName = osName,
            appId = appId,
            validKey = validKey,
            envCode = envCode,
            deviceId = deviceId,
            trId = transactionId,
            body = sector12KeyRequestData
        )

        if (!response.isSuccessful) {
            val error = parseError(response)
            throw CardException.NetworkError(
                code = error?.code ?: response.code().toString(),
                message = error?.message ?: "Unknown server error"
            )
        }

        return TagCryptoConverter().decryptSector12Key(response.body()?.payonCard?.sector12KeyA ?: "")
    }

}
class PayonNfcAdapter @Inject constructor(
    private val context: Context
) {
    private val nfcAdapter: NfcAdapter = NfcAdapter.getDefaultAdapter(context)

    fun getMifareCid(tag: Tag): ByteArray? {

        val mifare = MifareClassic.get(tag)

        return try {
            if(!mifare.isConnected){
                mifare.connect()
            }
            var id = mifare.tag.id
            if(id.isEmpty()){
                id = mifare.readBlock(0)
            }
            id
        } catch (e: Exception) {
            Timber.d("getMifareCid Exception : $e")
            null
        } finally {
            try {
                mifare.close()
            } catch (e: Exception) {
                Timber.d("mifare close Exception : $e")
            }
        }

    }

    fun getCertifyCode(tag: Tag, sector0Key: String): ByteArray?{

        val mifare = MifareClassic.get(tag)

        return try {
            if(!mifare.isConnected){
                mifare.connect()
            }

            val authenticateSector0 = mifare.authenticateSectorWithKeyA(0, Util().hexToByte(sector0Key))
            Timber.d("authenticateSector0 : $authenticateSector0")
            var certifyCode: ByteArray? = null
            if(authenticateSector0){
                certifyCode = mifare.readBlock(2)
                Timber.d("certifyCode : $certifyCode")
            }
            certifyCode

        } catch (e: Exception) {
            Timber.d("getCertifyCode Exception : $e")
            null
        } finally {
            try {
                mifare.close()
            } catch (e: Exception) {
                Timber.d("mifare close Exception : $e")
            }
        }
    }

    fun getEncCardData(tag: Tag, sector12Key: String): ByteArray?{

        val mifare = MifareClassic.get(tag)

        return try {

            if(!mifare.isConnected){
                mifare.connect()
            }

            val authenticateSector12 = mifare.authenticateSectorWithKeyA(12, Util().hexToByte(sector12Key))
            Timber.d("authenticateSector12 : $authenticateSector12")

            var encCardData: ByteArray? = null
            if(authenticateSector12){
                encCardData = mifare.readBlock( 48)
                Timber.d("encCardData : $encCardData")
            }
            encCardData

        } catch (e: Exception) {
            Timber.d("getEncCardData Exception : $e")
            null
        } finally {
            try {
                mifare.close()
            } catch (e: Exception) {
                Timber.d("mifare close Exception : $e")
            }
        }
    }

}
class TagCryptoConverter {

    companion object {
        private var keySeed: ByteArray?= null
        private var passphrase: String?= null
        private var salt: ByteArray?= null
        private var iv: ByteArray?= null
    }

    fun extractCryptoParamsFromKeySeed(key: String?){
        try {
            keySeed = Base64.decode(key, Base64.NO_WRAP)
            keySeed?.let { seed ->
                passphrase = CipherUtil.getPassphraseFromKeySeed(seed)
                salt = CipherUtil.getSaltFromKeySeed(seed)
                iv = CipherUtil.getIvFromKeySeed(seed)
            }
        } catch (e: Exception){
            CardException.TagCryptoError(
                message = "extractCryptoParamsFromKeySeed Error : ${e.message}"
            )
        }
    }

    fun encryptMifareCid(
        cid: ByteArray
    ): SectorKeyDataRequest {

        try {
            val encryptCid = CipherUtil.encrypt(Util().bytesToHex(cid).toByteArray(), passphrase, salt, iv)
            val base64Encoder = Base64.encodeToString(encryptCid, Base64.NO_WRAP)
            Timber.d("encryptMifareCid : $base64Encoder")

            return SectorKeyDataRequest(SectorPayonData(base64Encoder,null),"K1")
        } catch (e: Exception){
            throw CardException.TagCryptoError(
                message = "encryptMifareCid Error : ${e.message}"
            )
        }
    }

    fun decryptSector0Key(
        encryptSector0Key: String
    ): String {
        try {
            val base64Sector0Key = Base64.decode(encryptSector0Key, Base64.NO_WRAP)
            val sector0Key = String(CipherUtil.decrypt(base64Sector0Key, passphrase!!, salt, iv), StandardCharsets.UTF_8)

            return sector0Key
        } catch (e: Exception){
            throw CardException.TagCryptoError(
                message = "decryptSector0Key Error : ${e.message}"
            )
        }
    }

    fun encryptCertifyCode(
        base64Cid: String,
        certifyCode: ByteArray
    ): SectorKeyDataRequest{
        try {
            val encryptCertifyCode = CipherUtil.encrypt(Util().bytesToHex(certifyCode).toByteArray(), passphrase!!, salt, iv)
            val base64Encoder = Base64.encodeToString(encryptCertifyCode, Base64.NO_WRAP)

            return SectorKeyDataRequest(SectorPayonData(base64Cid,base64Encoder),"K1")
        } catch (e: Exception){
            throw CardException.TagCryptoError(
                message = "encryptCertifyCode Error : ${e.message}"
            )
        }
    }

    fun decryptSector12Key(
        encryptSector12Key: String
    ): String {
        try {
            val base64Sector12Key = Base64.decode(encryptSector12Key, Base64.NO_WRAP)
            val sector12Key = String(CipherUtil.decrypt(base64Sector12Key, passphrase!!, salt, iv), StandardCharsets.UTF_8)

            return sector12Key
        } catch (e: Exception){
            throw CardException.TagCryptoError(
                message = "decryptSector0Key Error : ${e.message}"
            )
        }
    }
}
/**
 * 공통 에러 처리 → CardResult.Error 변환
 */
fun handleCardException(exception: Throwable): CardResult.Error {
    return when (exception) {
        is CardException.NetworkError ->
            CardResult.Error(
                code = exception.code,
                message = exception.message ?: "Network error",
                throwable = exception
            )

        is CardException.AuthError ->
            CardResult.Error(
                code = "AUTH_ERROR",
                message = exception.message ?: "Auth error",
                throwable = exception
            )

        is CardException.CidError ->
            CardResult.Error(
                code = "CID_ERROR",
                message = exception.message ?: "CID read fail",
                throwable = exception
            )

        is CardException.CertifyCodeError ->
            CardResult.Error(
                code = "CERTIFY_CODE_ERROR",
                message = exception.message ?: "Certify Code read fail",
                throwable = exception
            )

        is CardException.EncCardDataError ->
            CardResult.Error(
                code = "ENC_CARD_DATA_ERROR",
                message = exception.message ?: "Enc Card Data read fail",
                throwable = exception
            )

        else ->
            CardResult.Error(
                code = "UNKNOWN",
                message = exception.message ?: "Unknown error",
                throwable = exception
            )
    }
}

sealed class CardException(message: String) : Exception(message) {
    class NetworkError(val code: String, message: String) : CardException(message)
    class AuthError(message: String) : CardException(message)
    class CidError(message: String) : CardException(message)
    class CertifyCodeError(message: String) : CardException(message)
    class EncCardDataError(message: String) : CardException(message)
    class TagCryptoError(message: String) : CardException(message)
    class UnknownError(message: String) : CardException(message)
}
  1. 코드를 작성하면서 생각한 것 → 확장성을 생각한 공통 cardData, errorData 정의
  2. 중복코드 제거 목표
profile
처음부터 다시 시작!!

0개의 댓글