안드로이드 개발을 하다보면 암호화에 대한 이야기가 종종 나온다.
민감한 정보를 사용하지 않는 앱이라면 굳이 할 필요가 없을 수 있지만 개인정보나 중요한 key 를 다뤄야할 일이 생기는데
이것들이 악의를 가진 사람들에 의해 외부로 노출되면 심각한 문제가 될 수 있다.
하지만 개발자들은 바보가 아니지.
시도할 수 있는 방법들이 이미 여럿 만들어져 있는데 대표적인 것들을 알아보고 필요할때 적용해보도록 하자.
암호화의 기본적인 개념
암호화에 대해서 설명하기전 아래 개념을 알면 좀 더 이해가 편하다.
- 평문(Plaintext) : 해독 가능한 형태의 메시지(암호화전 메시지)
- 암호문(Cipertext) : 해독 불가능한 형태의 메시지(암호화된 메시지)
- 암호화(Encryption) : 평문을 암호문으로 변환하는 과정
- 복호화(Decryption) : 암호문을 평문으로 변환하는 과정
그럼 암호화하는 방식은 어떤게 있을까
크게 양방향 암호화, 단방향 암호화로 분류할 수 있는데 !
솔직히 내가 적는것보다 훨씬 멋진 정리 글 있어서 첨부했다.
https://velog.io/@inyong_pang/Programming-암호화-알고리즘-종류와-분류
하지만 간단하게 이야기 하자면
양방향 암호화는 복호화를 통해 원본 데이터 복원이 가능하다.
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
'Android' 카테고리의 다른 글
MVI 를 찍먹해보자. (1) | 2025.01.19 |
---|---|
컴포즈 네비게이션 2.8 이후 (0) | 2024.12.17 |
사실 내 로망중엔 OpenGL 도 있었어 (1) | 2024.08.31 |
라이브러리를 배포해보자. (0) | 2024.08.24 |
코루틴의 Dispatcher 를 Hilt Singleton으로 주입해보자 (0) | 2024.08.17 |
[다시 만들어보는 클린아키텍쳐] 모듈구성 편 (0) | 2024.08.12 |
[다시 만들어보는 클린아키텍쳐] build-logic 편 (0) | 2024.07.31 |
[다시 만들어보는 클린아키텍쳐] 모듈분리 편 (1) | 2024.07.25 |