Android Keystore의 키로 암호화된 설정 저장소.
왜 평문이 위험한가
-
/data/data/<package>/shared_prefs/auth.xml
-
루팅된 기기에서는 그대로 읽힌다
-
adb backup 이 허용돼 있으면 통째로 빠져나간다
-
다른 앱의 취약점을 통해 노출될 수 있다
-
"어차피 샌드박스니 안전하다" 는 루팅 앞에서 무너진다
val key = MasterKey.Builder(ctx)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val prefs = EncryptedSharedPreferences.create(
ctx, "auth", key,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
prefs.edit().putString("refresh_token", token).apply()
Keystore가 실제로 하는 일
키가 앱 메모리에 노출되지 않는다
- 하드웨어 보안 모듈(TEE/StrongBox) 안에 있고, 앱은 '이 키로 암호화해 줘' 라고 요청만 한다
- 앱 데이터를 통째로 복사해도 다른 기기에서 복호화할 수 없다
생체 인증과 묶을 수도 있다
- setUserAuthenticationRequired(true)
- 지문을 대야만 키가 열린다
무엇을 저장하나
- 저장해도 되는 것 — 리프레시 토큰 · 세션 식별자 · 사용자 설정 저장하면 안 되는 것 비밀번호 원문 · 카드 번호 · 서버 비밀 키
"기기에 두지 않는 것" 이 가장 안전한 선택이라는 원칙은 그대로다
API 키를 코드에 넣으면
APK 를 풀면 문자열이 그대로 나온다
- unzip app.apk && strings classes.dex | grep -i "api"
BuildConfig · local.properties · NDK · 난독화 — 전부 늦출 뿐 막지 못한다
진짜 해법
- 비밀은 서버에 두고 앱은 자기 토큰으로 서버에 묻는다
- 꼭 필요하면 서버에서 단기 토큰을 받아 쓴다
백업에서 제외하기
<application android:dataExtractionRules="@xml/data_rules">
<!-- data_rules.xml -->
<data-extraction-rules>
<cloud-backup><exclude domain="sharedpref" path="auth.xml"/></cloud-backup>
</data-extraction-rules>
면접 함정
- ❌ "암호화했으니 루팅 기기에서도 안전하다" → 앱이 실행 중이면 복호화된 값을 메모리에서 읽힐 수 있다. 위험을 줄일 뿐이다.
- ❌ "난독화가 API 키를 보호한다" → 문자열 상수는 그대로 남는다.
무엇으로 대체되고 있나
androidx.security-crypto 의 EncryptedSharedPreferences 는 1.1.0-alpha 이후 정체돼 있고 deprecated 경로에 있다
대안
- DataStore + 직접 암호화 (Keystore 키로 값만 암호화)
- 아예 저장하지 않는다 (서버에서 짧은 수명 토큰을 매번 받는다)
- 생체 인증으로 감싼 Keystore 키
면접에서는 "무엇을 쓰는가" 보다 "왜 평문이 위험하고 Keystore 가 무엇을 보장하는가" 가 중요하다
Keystore로 직접 암호화
val keyGen = KeyGenerator.getInstance("AES", "AndroidKeyStore")
keyGen.init(KeyGenParameterSpec.Builder("my_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true) // 지문을 대야 열린다
.build())
keyGen.generateKey()
GCM 은 IV 를 매번 새로 만들어 함께 저장해야 한다 (비밀이 아니다) 같은 IV 를 재사용하면 GCM 의 안전성이 무너진다 — 흔한 구현 실수
루팅 탐지의 한계
SafetyNet 은 종료됐고 Play Integrity API 가 후속이다
- 기기 무결성 · 앱 무결성 · 라이선스를 서버에서 검증한다
중요한 것은 '서버가 판단한다' 는 점이다
- 앱 안에서 루팅을 탐지해 분기하면 그 분기를 패치로 지울 수 있다