본문 바로가기

Android

[Android] AES 암호화

 

  양방향 단방향
특징 암호화 & 복호화가 가능 암호화만 가능.
종류 AES MD5, SHA-1, SHA-256
장점 동일 키 사용으로 빠르고 간단. Key와 DB가 털려도 복호화가 불가능
단점 Key유출되면 다 털림. 동일한 해시 값을 가지면 충돌 가능

Google Play Store에 앱을 올리기 위해선 앱 개발자가 누군지 나타내기 위해 자신의 '코드 서명 인증서'로 서명을 한 다음에 올려야 한다.

Update 버전을 배포하려할 땐, 첫 배포시 사용한 '코드 서명 인증서'로 서명을 해야만 배포가 가능하며,'코드 서명 인증서' '서명'을 한다는 것은 개인키(Private Key)와 공개키(Public Key)의 값을 비교하여 서명키를 검증하는 것이다.

 

Private Key가 해커에게 노출될 경우 악성 앱을 개발하여 악성 앱을 upgrade버전이라고 가장하여 배포할 수도 있어서,  이를 방지하기 위해 Private Key를 Android플랫폼에서 keyStore file에 안전하게 저장한다.

더욱 안전한 장비인 'HSM'을 사용하여 보관하는 것도 하나의 방법이다.

 

KeyStore?

  • Android KeyStore service에서 제공하는 keyStore에 암호키를 보관한다.
  • 외부로 유출되어서는 안되는 중요한 Data를 안전하게 암호화.
  • 특정 앱에서 다루는 Data는 다른 앱에서 접근할 순 없지만 'Rooting'을 통해 권한을 상승하면 모든앱에서 만든 Data를 접근할 수 있다.
  • 암호화를 할 때 사용한 암호화 키를 HardCoding 된 상태로 저장하는 경우는 앱을 Decompile하면 암호키가 바로 노출된다.

 

Android KeyStore?

  • AndroidOS에서 제공하는 Android KeyStore Service를 제공하기 위해 만들어진 H/W(HardWare)기반 secure key storage이며 Android Device에서 암호 키를 안전하게 저장한다.
  • Android  API18부터 지원하기 위한 interface로 java, security, keysore로 불리는 class를 제공한다.

 

Software 기반 Android Key Store

  • keystore driver 또는 Hardware가 인식이 안될시 작동.
  • 마시멜로 이전 버전은 keyStore이 rooting된 기기에서 키 유출이 가능하다.

Hardware 기반 Android Key Store

  • KeyStore은 암호화 키 뿐만 아닌 IV, AES, CBC등 연산 때 필요한 것들 또한 안전하게 보호.
  • Key를 사용한 암호 연산은 Application process가 아닌 System에서만 수행해야함. ( Key 정보를 안전하게 보호하기 위해 Application process에서는 접근 불가)

 

원본 소스코드 클릭하여 확인 

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
import java.io.IOException
import java.security.*
import java.security.cert.CertificateException
import java.security.spec.InvalidKeySpecException
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec


object AESHelper {
    /** 키를 외부에 저장할 경우 유출 위험이 있으니까 API키를 숨기는 방식과 같이 숨겨야 한다. 편의상 키를 그냥 상수 KEY_ALIAS로 선언하였다. 길이는 16자여야 한다. */
    private val KEY_ALIAS = "키스토어이름" // 키스토어 에서 사용할 별칭
    private const val AndroidKeyStore = "AndroidKeyStore"
    private const val CIPHER_TRANSFORMATION = "AES/CBC/PKCS7Padding"

    /*
    키스토어 암호화할때 map으로 return했기 떄문에 파라미터는 HashMap으로 해야한다.
    return 은 원래 String(kenyEncrypt파라미터는 String을 받아서 ByteArray로 변환했다.)값으로
    받도록 ByteArray로 해야한다.
    그리고 String으로 하지 않는 가장 큰 이유는 String은 Heap에 남기때문에 해킹으로부터 취약하기 때문이다.
    */
//    fun encrypt(dataToEncrypt : ByteArray) : HashMap<String, ByteArray>{
//        val map = HashMap<String, ByteArray>()
//        try {
//            //Get the key
//            val keyStore = KeyStore.getInstance(AndroidKeyStore)
//            keyStore.load(null)
//
//            val secretKeyEntry = keyStore.getEntry(KEY_ALIAS, null) as KeyStore.SecretKeyEntry
//            val secretKey = secretKeyEntry.secretKey
//
//            //Encrypt data
//            val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
//            cipher.init(Cipher.ENCRYPT_MODE, secretKey)
//
//            val ivBytes = cipher.iv
//
//            val encryptedBytes = cipher.doFinal(dataToEncrypt)
//
//            map["iv"] = ivBytes
//            map["encrypted"] = encryptedBytes
//        }catch (e: Exception){
//            Log.d("mException", "keystore 암호화 하는 도중 : ${e.message}")
//        }finally {
//            return map
//        }
//    }
//
//    fun decrpyt(map: HashMap<String, ByteArray>) : ByteArray?{
//        var decrypted: ByteArray? = null
//        try {
//            // 1
//            //Get the key
//            val keyStore = KeyStore.getInstance(AndroidKeyStore)
//            keyStore.load(null)
//
//            val secretKeyEntry = keyStore.getEntry(KEY_ALIAS, null) as KeyStore.SecretKeyEntry
//            val secretKey = secretKeyEntry.secretKey
//
//            // 2
//            //Extract info from map
//            val encryptedBytes = map["encrypted"]
//            val ivBytes = map["iv"]
//
//            // 3
//            //Decrypt data
//            val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
////            val spec = GCMParameterSpec(128, ivBytes)
//            val ivParams = IvParameterSpec(ivBytes)
////            cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
//            cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParams)
//            decrypted = cipher.doFinal(encryptedBytes)
//        } catch (e: Exception) {
//            Log.d("mException","AESHelper, keystore decrypt 중 Exception : ${e.message}")
//        } catch (e: Throwable) {
//            Log.d("mException","AESHelper, keystore decrypt 중 Throwable : ${e.message}")
//        }finally {
//            return decrypted
//        }
//    }


    fun encrypt(dataToEncrypt : ByteArray) : HashMap<String, ByteArray>{
        val map = HashMap<String, ByteArray>()
        try {
            //Get the key
            val keyStore = KeyStore.getInstance("AndroidKeyStore")
            keyStore.load(null)

            val secretKeyEntry = keyStore.getEntry("Sample Keystore Alias. 원하는 텍스트로 변환 가능.", null) as KeyStore.SecretKeyEntry
            val secretKey = secretKeyEntry.secretKey

            //Encrypt data
            val cipher = Cipher.getInstance("AES/GCM/NoPadding")
            cipher.init(Cipher.ENCRYPT_MODE, secretKey)
            val ivBytes = cipher.iv
            val encryptedBytes = cipher.doFinal(dataToEncrypt)

            map["iv"] = ivBytes
            map["encrypted"] = encryptedBytes
        }catch (e: Exception){
            Log.e("mException", "keystore 암호화 하는 도중 : ${e.message}")
        }finally {
            return map
        }
    }
    /*
     키스토어 암호화할때 map으로 return했기 떄문에 파라미터는 HashMap으로 해야한다.
     return 은 원래 String(kenyEncrypt파라미터는 String을 받아서 ByteArray로 변환했다.)값으로
     받도록 ByteArray로 해야한다.
     */
    fun decrypt(map: HashMap<String, ByteArray>) : ByteArray?{
        var decrypted: ByteArray? = null
        try {
            // 1
            //Get the key
            val keyStore = KeyStore.getInstance("AndroidKeyStore")
            keyStore.load(null)

            val secretKeyEntry = keyStore.getEntry("Sample Keystore Alias. 원하는 텍스트로 변환 가능.", null) as KeyStore.SecretKeyEntry
            val secretKey = secretKeyEntry.secretKey

            // 2
            //Extract info from map
            val encryptedBytes = map["encrypted"]
            val ivBytes = map["iv"]

            // 3
            //Decrypt data
            val cipher = Cipher.getInstance("AES/GCM/NoPadding")
            val spec = GCMParameterSpec(128, ivBytes)
            cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
            decrypted = cipher.doFinal(encryptedBytes)
        } catch (e: Exception) {
            Log.e("mException","AESHelper, keystore decrypt 중 Exception : ${e.message}")
        } catch (e: Throwable) {
            Log.e("mException","AESHelper, keystore decrypt 중 Throwable : ${e.message}")
        }finally {
            return decrypted
        }
    }



    fun keystoreTest(testMessage : String, afterFunc : (decrypted : String) -> Unit) {
        try {
            keystoreSetting()
            val map = encrypt(testMessage.toByteArray(Charsets.UTF_8))
            val decryptedData = decrypt(map)
            decryptedData?.let{
                val decryptedString = String(it, Charsets.UTF_8)
                afterFunc(decryptedString)
            }
        }catch (e: Exception){
            Log.e("mException", "keystore 테스트 하는 도중 : ${e.message}")
        }catch (e: InvalidKeySpecException) {
            Log.e("mException", "keystore 테스트 하는 도중 키를 못가져옴 : ${e.message}")
        }
    }

    fun keystoreSetting(){
        try {
            val prepareKey = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
            val keyCharacter = KeyGenParameterSpec.Builder("Sample Keystore Alias. 원하는 텍스트로 변환 가능.",
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .setRandomizedEncryptionRequired(true) //KeyStore에 매번 새로운 IV를 사용
//                .setUserAuthenticationRequired(true) -> 잠금화면 설정. 화면 잠금 요구사항을 활성화할때 사용자가 잠금화면, pin, 암호를 제거하거나 변경하면 키가 바로 취소된다.
//                .setUserAuthenticationValidityDurationSeconds(120) -> 장치 인증 후 120초 동안 키를 사용할 수 있다. 키에 엑세스 할 때마다 지문 인증이 필요하도록 -1을 전달한다.
                .build()

            // make Key
            prepareKey.init(keyCharacter)
            prepareKey.generateKey()
        }catch (e: Exception){
            Log.e("mException", "keystore 테스트 하는 도중 : ${e.message}")
        }
    }
}