안드로이드 AES256 암호화 "Unsupported key size: 43 bytes" 에러 완벽 해결 가이드
TL;DR — AES256 암호화에서 키 길이가 32바이트가 아닐 때 발생하는 InvalidKeyException의 원인과, Base64 디코딩 누락·키 파생 누락 같은 실수를 바로잡는 단계별 해결법을 정리했습니다.
안드로이드에서 데이터를 AES256으로 암호화하다 보면 다음과 같은 예외를 만나 빌드는 멀쩡한데 런타임에서 앱이 죽는 경험을 하게 됩니다.
java.security.InvalidKeyException: Unsupported key size: 43 bytes
이 에러는 처음 보면 당황스럽지만, 원인은 의외로 단순합니다. 핵심은 "AES256은 키가 반드시 32바이트여야 한다"는 한 문장입니다. 이 글에서는 왜 하필 43바이트라는 숫자가 튀어나오는지, 그리고 실무에서 가장 흔하게 저지르는 실수가 무엇인지부터 안전한 키 생성 패턴까지 차근차근 풀어보겠습니다.
1. 문제 상황: 왜 런타임에서만 터지는가
Cipher.getInstance("AES/CBC/PKCS5Padding")까지는 컴파일러가 잡아주지 않습니다. 알고리즘 문자열이 문법적으로 올바르기 때문입니다. 문제는 cipher.init()을 호출하는 순간, JCE(Java Cryptography Extension)가 전달받은 SecretKey의 실제 바이트 길이를 검사하면서 발생합니다.
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); // ← 여기서 InvalidKeyException
즉 이 에러는 "코드 작성 단계"가 아니라 "실행 단계"에서 키의 길이가 규격과 다를 때만 드러납니다. 그래서 테스트 데이터로는 통과하다가 특정 키 값에서만 크래시가 나는 것처럼 보이기도 합니다.
2. 핵심 개념: AES 키 길이와 비트/바이트 관계
AES는 키 길이에 따라 세 종류로 나뉩니다.
| 명칭 | 비트 | 바이트 |
|---|---|---|
| AES128 | 128bit | 16byte |
| AES192 | 192bit | 24byte |
| AES256 | 256bit | 32byte |
여기서 반드시 기억할 공식은 바이트 = 비트 ÷ 8 입니다. AES256은 256 ÷ 8 = 32바이트가 정확히 맞아야 하며, 31바이트나 33바이트는 물론이고 32바이트가 아닌 어떤 값도 받지 않습니다. JCE는 이 길이를 융통성 없이 엄격하게 검사합니다.
3. "43바이트"의 정체 — 가장 흔한 원인 3가지
43이라는 숫자는 사실 매우 중요한 단서입니다. 32바이트짜리 데이터를 Base64로 인코딩하면 약 44자(패딩 포함)의 문자열이 됩니다. 그리고 그 문자열을 그대로 바이트 배열로 변환하면 대략 43~44바이트가 됩니다. 즉 43바이트라는 숫자는 "디코딩되지 않은 Base64 문자열을 키로 넘겼다"는 강력한 신호입니다.
대표적인 원인을 정리하면 다음과 같습니다.
원인 1 — Base64 디코딩 누락 (가장 흔함)
서버나 설정 파일에서 키를 Base64 문자열로 받아온 뒤, Base64.decode()를 거치지 않고 key.toByteArray() 또는 getBytes()로 바로 변환한 경우입니다. 32바이트 키가 Base64 문자열이 되면서 길이가 부풀려져 43바이트가 됩니다.
원인 2 — 문자열을 그대로 키로 사용
사람이 외우기 좋은 평문 비밀번호(예: "myProjectSecretPassword2026!")를 그대로 getBytes() 해서 키로 쓰는 경우입니다. 문자열 길이에 따라 바이트 수가 제각각이 되어 32바이트가 맞을 확률이 거의 없습니다.
원인 3 — 인코딩 방식 불일치 같은 키라도 UTF-8과 다른 인코딩으로 변환하면 바이트 길이가 달라질 수 있습니다. 한글이나 특수문자가 섞인 키 문자열에서 자주 발생합니다.
4. 단계별 해결법
- 키의 실제 바이트 길이부터 로그로 찍는다.
Log.d("AES", "key length=${keyBytes.size}")로 확인하면 16/24/32 중 어디에 해당하는지 즉시 보입니다. - 키가 Base64 문자열이라면 반드시 디코딩한다.
Base64.decode(keyString, Base64.NO_WRAP)를 거쳐 원래 바이트로 복원합니다. - 평문 비밀번호를 쓰고 있다면 키 파생(KDF)을 적용한다. SHA-256 해시나 PBKDF2로 임의 길이 문자열을 32바이트 고정 길이로 변환합니다.
- 키를 새로 만들어야 한다면
SecureRandom으로 32바이트를 생성한다. - 암호화/복호화 양쪽이 동일한 키 처리 경로를 쓰는지 확인한다. 한쪽만 디코딩하면 키 불일치로 또 다른 에러가 납니다.
5. 동작하는 예제 코드
Kotlin — SecureRandom으로 안전한 32바이트 키 생성
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom
// 256bit(32byte) 키를 난수로 생성
fun generateAes256Key(): ByteArray {
val keyBytes = ByteArray(32) // 정확히 32바이트 확보
SecureRandom().nextBytes(keyBytes) // 암호학적 난수로 채움
return keyBytes
}
fun encrypt(plainText: String, keyBytes: ByteArray): ByteArray {
require(keyBytes.size == 32) { "AES256 키는 32바이트여야 합니다 (현재 ${keyBytes.size})" }
val secretKey = SecretKeySpec(keyBytes, "AES")
val iv = ByteArray(16).also { SecureRandom().nextBytes(it) } // IV는 16바이트
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
return cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
}
Kotlin — Base64 문자열 키를 안전하게 받아오기
import android.util.Base64
// 서버가 내려준 Base64 키 문자열을 실제 바이트로 복원
fun decodeKeyFromBase64(base64Key: String): ByteArray {
// NO_WRAP: 줄바꿈 문자가 섞이지 않도록 처리
val decoded = Base64.decode(base64Key, Base64.NO_WRAP)
check(decoded.size == 32) {
"디코딩 후 길이가 ${decoded.size}바이트입니다. 32바이트가 아니면 키 형식을 다시 확인하세요."
}
return decoded
}
Java — 평문 비밀번호를 32바이트로 파생
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
// 임의 길이 비밀번호를 SHA-256으로 항상 32바이트 키로 변환
public static byte[] deriveKey(String password) throws Exception {
MessageDigest sha = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = sha.digest(password.getBytes(StandardCharsets.UTF_8));
// SHA-256 출력은 언제나 32바이트 → AES256에 그대로 사용 가능
return keyBytes;
}
참고: 단순 SHA-256 파생은 빠르지만 무차별 대입에 상대적으로 약합니다. 보안 요구 수준이 높다면 솔트와 반복 횟수를 포함하는
PBKDF2WithHmacSHA256을 사용하는 것이 좋습니다.
6. 자주 하는 실수와 엣지 케이스
- IV 길이 혼동: AES의 키는 32바이트지만 CBC 모드의 IV는 블록 크기인 16바이트입니다. IV에 32바이트를 넣으면 또 다른 예외가 납니다.
- 암호화·복호화 IV 불일치: IV는 비밀이 아니어도 되지만 암복호화 양쪽에서 같은 값을 써야 합니다. 보통 암호문 앞에 IV를 붙여 함께 전달합니다.
Base64.DEFAULT로 인한 줄바꿈 오염: 안드로이드 기본 Base64는 줄바꿈을 넣습니다. 키나 암호문 전송에는Base64.NO_WRAP을 권장합니다.- 하드코딩된 키: 소스 코드에 키를 박아두면 디컴파일로 쉽게 노출됩니다. Android Keystore 시스템 사용을 검토하세요.
- 구버전 JDK의 정책 제한: 아주 오래된 환경에서는 강한 암호화에 JCE Unlimited Strength 정책 파일이 필요했지만, 최신 안드로이드/JDK에서는 기본 활성화되어 있습니다.
7. 요약
Unsupported key size: 43 bytes는 결국 "AES256에 32바이트가 아닌 키가 들어왔다"는 뜻입니다. 43이라는 숫자를 보면 거의 확실하게 Base64 디코딩 누락을 의심하세요. 해결의 핵심은 세 가지입니다. (1) 키의 실제 바이트 길이를 먼저 로그로 확인하고, (2) Base64 문자열이면 반드시 디코딩하며, (3) 임의 길이 비밀번호라면 SHA-256/PBKDF2로 32바이트로 파생하는 것입니다.
AI에게 물어볼 때 (프롬프트 팁)
암호화 에러는 맥락 정보를 얼마나 정확히 주느냐에 따라 AI의 답변 품질이 크게 갈립니다. ChatGPT나 Claude에게 물을 때는 다음처럼 구체적으로 질문하세요.
프롬프트 예시 1 — 원인 진단형
"안드로이드에서 AES/CBC/PKCS5Padding으로 암호화 중
InvalidKeyException: Unsupported key size: 43 bytes가 발생합니다. 키는 서버에서 Base64 문자열로 받습니다. 43바이트라는 숫자가 의미하는 바와 가장 가능성 높은 원인 3가지를 우선순위대로 알려주고, 각각의 확인 방법을 코드와 함께 제시해줘."
프롬프트 예시 2 — 코드 리뷰형
"다음 Kotlin 암호화 코드를 붙여넣겠습니다. 키 처리 경로에서 길이가 32바이트가 보장되는지 검증하고, Base64 디코딩 누락이나 인코딩 불일치 같은 잠재 버그가 있으면 수정안을 diff 형태로 제안해줘. [코드 첨부]"
프롬프트 예시 3 — 보안 강화형
"현재 평문 비밀번호를 SHA-256으로 해시해 AES256 키로 쓰고 있습니다. 이 방식의 보안 약점을 설명하고, PBKDF2WithHmacSHA256으로 전환하는 안드로이드 예제 코드와 권장 반복 횟수, 솔트 관리 방법을 알려줘."
이렇게 "에러 메시지 전문 + 키의 출처와 형식 + 사용 중인 알고리즘 문자열"을 함께 제공하면, AI가 추측하지 않고 정확한 원인을 짚어줍니다. 프롬프트를 더 정교하게 다듬고 싶다면 Prompt Architect의 프롬프트 분석기로 질문의 명확성과 맥락 충실도를 점검해보세요.