Android

안드로이드의 암호화

최데브 2024. 10. 12. 14:38

안드로이드 개발을 하다보면 암호화에 대한 이야기가 종종 나온다.

 

민감한 정보를 사용하지 않는 앱이라면 굳이 할 필요가 없을 수 있지만 개인정보나 중요한 key 를 다뤄야할 일이 생기는데

이것들이 악의를 가진 사람들에 의해 외부로 노출되면 심각한 문제가 될 수 있다.

 

하지만 개발자들은 바보가 아니지.

시도할 수 있는 방법들이 이미 여럿 만들어져 있는데 대표적인 것들을 알아보고 필요할때 적용해보도록 하자.

 

암호화의 기본적인 개념

암호화에 대해서 설명하기전 아래 개념을 알면 좀 더 이해가 편하다. 

  • 평문(Plaintext) : 해독 가능한 형태의 메시지(암호화전 메시지)
  • 암호문(Cipertext) : 해독 불가능한 형태의 메시지(암호화된 메시지)
  • 암호화(Encryption) : 평문을 암호문으로 변환하는 과정
  • 복호화(Decryption) : 암호문을 평문으로 변환하는 과정

그럼 암호화하는 방식은 어떤게 있을까

 

크게 양방향 암호화, 단방향 암호화로 분류할 수 있는데 !

솔직히 내가 적는것보다 훨씬 멋진 정리 글 있어서 첨부했다. 

 

https://velog.io/@inyong_pang/Programming-암호화-알고리즘-종류와-분류

 

[Programming] 암호화 알고리즘 종류와 분류

평문(Plaintext) : 해독 가능한 형태의 메시지(암호화전 메시지)암호문(Cipertext) : 해독 불가능한 형태의 메시지(암호화된 메시지암호화(Encryption) : 평문을 암호문으로 변환하는 과정복호화(Decryption)

velog.io

 

하지만 간단하게 이야기 하자면

 

양방향 암호화는 복호화를 통해 원본 데이터 복원이 가능하다.

Key 의 값이 변하지 않았을때 입력값이 같으면 매번 동일하게 암호화가 된다.

 

단방향의 경우 대표적으로 해시를 쓸 수 있는데 복호화가 불가능하다. 즉 원본 데이터 복원이 불가능

입력값이 같으면 해시 함수를 적용할때 매번 동일한 해시값이 나온다.

보통 비밀번호 같이 입력 값이 동일할때 같은 해시값을 반환하는지 검증에 쓰일 수 있다.

 

그래서 결국 암호화를 어떻게 하면 될까. 

각각의 상황에 맞게 적합한 암호화 알고리즘을 고르는건 개발자의 판단이지만

많이 쓰이거나 권장하는 것들로 찍먹해보자.

 

일단 Java 에는 JCE, JCA 라는게 있다.

JCE 는 Java 플랫폼에 있는 표준 암호화 확장인데 JDK 1.4 부터 제공되고 있다.

대표적인 클래스로는 Cipher, KeyGenerator 가 있다.

 

JCA 는 JAVA 의 암호화 프레임워크인데 KeyStore , Message Digest 같은 클래스들이 있다.

코틀린에서는 직접적으로 제공한는 암호화 라이브러리가 따로 없기 떄문에 JCE 에서 제공하는 Cipher 를 사용하라고

안드로이드에서도 권장하고 있다.

 

근데 또 Cipher 를 열어보면 엄청나게 많은 암호화 알고리즘이 있다.

하지만 이걸 다 알 필요는 없지! 우리는 필요한것만 그때 그때 찾아서 잘 써먹기만 하면 된다.

잘 써먹기 위해서는 대충 알아두면 좋은 개념이 있다.

 

위에 첨부했던 링크에도 설명이 있지만 대칭 키 암호화, 공개 키(비대칭 키) 암호화라는 개념인데

간략하게 말하면 대칭 키는 암복호화에 쓰이는 key가 같으며 속도는 빠르지만 보안강도는 상대적으로 낮고

공개 키 암호화는 암호화 복호화에 쓰이는 key가 다르며 속도는 느리지만 보안강도는 높다는게 특징이다.

 

대칭 키 암호화에는 AES,DES 알고리즘이 있고 공개 키 암호화에는 RSA,ECC 알고리즘이 있다.

안드로이드에서는 AES/CBC , AES/GCM 방식을 추천하고 있고 일반적으로는 AES-256을 추천한다.

 

여기선 AES/GCM 에 대해서 알아보자.

 

AES 는 뭘까?

고급 암호화 표준 (Advanced Encryption Standard)

128비트의 블록 단위로 암호화를 수행하는 대칭 키 알고리즘이다.

128비트라는 값은 암호화 키 사이즈를 의미하는데 128비트 말고도 192비트, 256비트를 지원한다.

AES-256 이라는건 256비트의 암호화 키를 사용하는 AES 알고리즘이라는 뜻이다.

 

GCM 은 갈루와/카운터모드 라는 뜻인데 미안하지만 이건 진짜 뭔소린지 모르겠어서 정리를 못했다.

 

 

이제 한번 코드를 봐보자.

object AESGCMEncryption {

    private const val AES_KEY_SIZE = 128 // 호환성을 위해 128비트 키 사용
    private const val GCM_NONCE_LENGTH = 12 // GCM nonce(IV)의 권장 길이
    private const val GCM_TAG_LENGTH = 128 // 인증 태그 길이(비트 단위)

    // AES SecretKey 생성
    fun generateKey(): SecretKey {
        val keyGenerator = KeyGenerator.getInstance("AES")
        keyGenerator.init(AES_KEY_SIZE)
        return keyGenerator.generateKey()
    }

    // 암호화 함수
    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    fun encrypt(plainText: ByteArray, secretKey: SecretKey): Pair<ByteArray, ByteArray> {
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")

        // 랜덤 IV 생성
        val iv = ByteArray(GCM_NONCE_LENGTH)
        SecureRandom().nextBytes(iv)

        val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec)

        val cipherText = cipher.doFinal(plainText)
        return Pair(iv, cipherText)
    }

    // 복호화 함수
    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    fun decrypt(iv: ByteArray, cipherText: ByteArray, secretKey: SecretKey): ByteArray {
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
        cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)

        return cipher.doFinal(cipherText)
    }
}

 

 

import android.os.Build
import androidx.annotation.RequiresApi

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun main() {
    // 비밀 키 생성
    val secretKey = AESGCMEncryption.generateKey()

    // 평문 정의
    val plainText = "안녕하세요, 세계!".toByteArray(Charset.forName("UTF-8"))

    // 암호화
    val (iv, cipherText) = AESGCMEncryption.encrypt(plainText, secretKey)
    println("IV: ${iv.toHexString()}")
    println("암호문: ${cipherText.toHexString()}")

    // 복호화
    val decryptedText = AESGCMEncryption.decrypt(iv, cipherText, secretKey)
    println("복호화된 텍스트: ${String(decryptedText, Charset.forName("UTF-8"))}")
}

// ByteArray를 16진수 문자열로 변환하는 확장 함수
fun ByteArray.toHexString(): String = joinToString(separator = "") { "%02x".format(it) }

 

이때 고려하면 좋은 점은 IV나 SecretKey 를 여러곳에서 재사용하는건 좋지 않다고 한다.

최대한 이 값들을 숨기는게 베스트다. 그리고 데이터마다 고유한 IV 와 SecretKey를 사용해주자.

 

자자 이대로 끝났어요 라고하면 좀 속상하다.

방금 말한 값들을 숨겨주는 방법도 알면 최고지 않을까.

 

이때 우리는 Android KeySotre 를 쓸 수 있다. 이건 이미 머쨍이 구글이 만들어놨다.

위에 제공했던 코드를 조금 뜯어 고쳐서 다시 적어본다.

 

object AESGCMEncryption {

    private const val KEY_ALIAS = "my_aes_key" // 키 식별자
    private const val ANDROID_KEYSTORE = "AndroidKeyStore"

    // KeyStore에서 키 생성
    fun generateKeyStoreKey() {
        val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES,
            ANDROID_KEYSTORE
        )

        val keyGenParameterSpec = KeyGenParameterSpec.Builder(
            KEY_ALIAS,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        ).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setKeySize(128) // 키 사이즈 설정
            .build()

        keyGenerator.init(keyGenParameterSpec)
        keyGenerator.generateKey()
    }

    // KeyStore에서 키 로드
    fun getKeyStoreKey(): SecretKey {
        val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
        keyStore.load(null)
        val secretKeyEntry = keyStore.getEntry(KEY_ALIAS, null) as KeyStore.SecretKeyEntry
        return secretKeyEntry.secretKey
    }

    // 암호화 함수
    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    fun encrypt(plainText: ByteArray, secretKey: SecretKey): Pair<ByteArray, ByteArray> {
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")

        // 랜덤 IV 생성
        val iv = ByteArray(GCM_NONCE_LENGTH)
        SecureRandom().nextBytes(iv)

        val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec)

        val cipherText = cipher.doFinal(plainText)
        return Pair(iv, cipherText)
    }

    // 복호화 함수
    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    fun decrypt(iv: ByteArray, cipherText: ByteArray, secretKey: SecretKey): ByteArray {
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
        cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)

        return cipher.doFinal(cipherText)
    }
}

 

import android.os.Build
import androidx.annotation.RequiresApi

@RequiresApi(Build.VERSION_CODES.M) // API 레벨 변경
fun main() {
    // KeyStore를 이용하여 키 생성
    AESGCMEncryption.generateKeyStoreKey()

    // 키 로드
    val secretKey = AESGCMEncryption.getKeyStoreKey()

    // 평문 정의
    val plainText = "안녕하세요, 세계!".toByteArray(Charsets.UTF_8)

    // 암호화
    val (iv, cipherText) = AESGCMEncryption.encrypt(plainText, secretKey)
    println("IV: ${iv.toHexString()}")
    println("암호문: ${cipherText.toHexString()}")

    // 복호화
    val decryptedText = AESGCMEncryption.decrypt(iv, cipherText, secretKey)
    println("복호화된 텍스트: ${String(decryptedText, Charsets.UTF_8)}")
}

// ByteArray를 16진수 문자열로 변환하는 확장 함수
fun ByteArray.toHexString(): String = joinToString(separator = "") { "%02x".format(it) }

 

뚝딱

 

위는 대칭키를 기준으로 설명했는데 비대칭키를 사용하고 싶다면 key를 생성할때

KeyGenerator 대신 KeyPairGenerator 를 사용해보자. 다른 구현 부분은 바뀌는지 안바뀌는진 모르겠다.

그건 각자가 알아보자! 파이팅!

 

 

 

Reference

https://velog.io/@inyong_pang/Programming-%EC%95%94%ED%98%B8%ED%99%94-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%A2%85%EB%A5%98%EC%99%80-%EB%B6%84%EB%A5%98

반응형