안드로이드/개발관련(Kotlin)

안드로이드에서 AES256알고리즘을 사용하여 암/복호화

닉네임못짓는사람 2023. 2. 27. 20:05
반응형

이번 글에서는 안드로이드에서 AES256알고리즘으로 메세지를 암호화하는 방법에 대해서 알아보자.

AES256은 공통키 암호화 알고리즘으로, 256bit의 공통된 Key를 A와 B가 가지고 있어야 암호화가 가능하다.

이를 위해 따로 gradle에 추가시켜줄 코드는 없으니 바로 코드를 확인해보자.

 

가장 먼저, A와 B가 사용할 공통키 K를 생성해야한다.

이때 키 교환 과정은 생략하고, 두 사용자가 동일한 K를 공유한다고 가정하자.

val keygen = KeyGenerator.getInstance("AES")
keygen.init(256)
val key: SecretKey = keygen.generateKey()

K는 안드로이드의 KeyGenerator를 사용하여 생성하는데, 안드로이드에서는

이 Key가 정해진 알고리즘에서만 사용될 수 있도록 지정할 수 있다.

 

위에서처럼 getInstance에 "AES"라고 알고리즘을 기입해주면, 해당 키는 AES알고리즘에서만 사용할 수 있다.

또한 우리는 AES-256을 사용할 예정이니 keygen.init(256)으로 256bit Key를 생성해주자.

 

이렇게 Key를 생성했으면 다음은 암호화를 살펴보자.

    fun encrypt(input: String, key: SecretKey): String {
        var arr = ByteArray(16)
        var str = ""
        while(str.length != 16){
            SecureRandom().nextBytes(arr)
            str = String(arr)
        }
        val iv = getIv(str)
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        cipher.init(Cipher.ENCRYPT_MODE, key, iv)
        val encrypt = cipher.doFinal(input.toByteArray())
        return str + Base64Utils.encode(encrypt)
    }

암호화를 위해선 평문(PlainText)와 암호키K를 함수에 전달해주어야 한다.

또한 우리가 이번 글에서 사용해볼 암호화는 GCM모드이다.

GCM모드는 갈루와 카운터 모드를 말하는데, 이 GCM모드는 암호화 과정에서 Hash값을 추가하여 암호문의 무결성 검사가 가능하다.

 

때문에 본래 이런 과정이 없는 암호화 모드의 경우 잘못된 비밀키를 사용했을 때, 이게 잘못된 평문인지

실제로 나한테 보내진 평문인지 확인할 방법이 없는반면에,

GCM모드의 경우 Hash를 통한 무결성 검사를 통해 이런 취약점을 방지할 수 있다.

 

안드로이드에서 GCM를 통한 무결성 검사를 통과하지 못 할 경우 아래와 같은 에러가 발생하기때문에

이를 try~catch를 사용하여 예외처리를 해주면 잘못된 메세지들을 걸러줄 수 있다.

(에러 메세지 : javax.crypto.AEADBadTagException: mac check in GCM faild)

 

잠깐 이야기가 다른곳으로 새었는데, GCM모드를 사용할 때는 IV라는 초기 백터값이 필요하다.

GCM모드에선 IV값+카운터값을 평문 블록과 XOR하는 과정을 거치게되는데, 이를 위해 IV라는 임의의 값이 필요하다.

여기서 IV값은 16Byte(128bit)로 정했다.

 

위 코드에선 ByteArray를 만들어서 이를 문자열로 변경한 뒤, 아래의 getIV함수에 넣어서 Hash화 한 뒤

이 값에서 16Byte를 잘라서 IV값으로 만드는데, 굳이 이렇게 할 필요는 없으니 이부분은 따라하지 않아도 된다.

    fun getIv(input: String): IvParameterSpec {
        var ivHash = MessageDigest.getInstance("SHA1")
                .digest(input.toByteArray())
        var ivBytes = Arrays.copyOf(ivHash, 16)
        var iv = IvParameterSpec(ivBytes)
        return iv
    }

Hash값은 SHA1알고리즘을 사용하여 구했다.

 

다시 암호화쪽 코드로 넘어가서, 이렇게 생성한 IV값은 복호화를 할 때에도 동일한 값이 필요하다.

때문에 A에서 B로 데이터를 보낸다고 하면, A에서 생성한 IV값을 B에게 그대로 전달해 주어야 하는데

여기선 arr -> str순으로 만들어낸 16글자 문자열을 암호문 앞에 붙여서 전송해주도록 하자.

 

Hash알고리즘의 경우 동일한 값을 넣었을 때, 결과값도 언제나 동일하기 때문에

A와 B에서 동일한 문자열로 Hash값을 생성해서 IV값을 잘라내면, 두 값은 동일하다.

    fun decrypt(input: String, key: SecretKey): String {
        var iv = getIv(input.substring(0, 16))
        var cryptText = input.substring(16, input.length)
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        cipher.init(Cipher.DECRYPT_MODE, key, iv)
        val decrypt = cipher.doFinal(Base64Utils.decode(cryptText))
        return String(decrypt)
    }

이렇게 데이터를 잘 보냈다면, B에서 이 암호문을 복호화 시켜주어야 한다.

복호화 시에는 먼저 받은 데이터에서 앞 16자리, 즉 IV값을 먼저 때어낸다.

그리고 때어낸 IV와 사전에 공유한 K를 사용하여 암호문을 복호화하면 암호화 데이터 교환이 가능하다.

위 사진이 실제로 암/복호화한 데이터들을 Log로 찍은 사진이다.

암호화 메세지의 앞 16글자가 IV값을 계산하기위해 사용되는 문자열이다.

반응형