토큰·키·PII를 위한 Kotlin 보안 저장 체크리스트
Kotlin 보안 저장 체크리스트: Android Keystore, EncryptedSharedPreferences, 데이터베이스 암호화 중 토큰·키·PII에 적합한 선택을 하는 방법.

보호하려는 항목(간단히 설명)
비즈니스 앱에서의 보안 저장은 한 가지 의미를 갖습니다: 누군가 기기(또는 앱 파일들)를 얻더라도 저장한 내용을 읽거나 재사용할 수 없어야 합니다. 여기에는 디스크에 있는 데이터(휴지 상태)뿐 아니라 백업, 로그, 크래시 리포트, 디버그 도구를 통해 비밀이 유출되는 상황도 포함됩니다.
간단한 사고 실험: 낯선 사람이 당신의 앱 저장 폴더를 열면 무엇을 할 수 있을까요? 많은 앱에서 가장 가치 있는 항목은 사진이나 설정이 아닙니다. 접근을 풀어주는 작은 문자열들입니다.
디바이스 내부 저장에는 보통 세션 토큰(사용자 로그인 유지용), 리프레시 토큰, API 키, 암호화 키, 이름·이메일 같은 개인 데이터(PII), 그리고 오프라인에서 쓰는 캐시된 비즈니스 기록(주문, 티켓, 고객 메모) 등이 포함됩니다.
실제에서 흔한 실패 유형은 다음과 같습니다:
- 분실 또는 도난된 기기에서 토큰을 복사해 사용자 사칭에 사용됨.
- 루팅된 기기나 접근성 트릭을 통해 악성 앱 또는 "도움" 앱이 로컬 파일을 읽음.
- 자동 디바이스 백업으로 앱 데이터가 의도치 않은 곳으로 옮겨짐.
- 디버그 빌드가 토큰을 로그에 남기거나, 크래시 리포트에 기록하거나, 보안 검사를 비활성화함.
그래서 "그냥 SharedPreferences에 저장하면 되지"는 접근 권한을 주거나 사용자와 회사에 피해를 줄 수 있는 항목(토큰, PII)에 대해서는 적절하지 않습니다. 평문 SharedPreferences는 앱 내부에 비밀을 붙여둔 포스트잇과 같습니다: 편리하지만 누군가 기회를 얻으면 쉽게 읽힙니다.
가장 유용한 출발점은 저장 항목마다 이름을 붙이고 두 가지 질문을 하는 것입니다: 이것이 어떤 접근 권한을 열어주는가? 공개되면 문제가 되는가? 나머지(Keystore, 암호화된 환경설정, 암호화 데이터베이스)는 그 답에 따라 결정됩니다.
데이터 분류: 토큰, 키, PII
모든 "민감한 데이터"를 동일하게 취급하지 않으면 보안 저장이 더 쉬워집니다. 먼저 앱이 저장하는 항목과 그것이 유출되면 어떤 일이 벌어지는지 목록을 만드세요.
토큰은 비밀번호와 동일하지 않습니다. 액세스 토큰과 리프레시 토큰은 사용자가 로그인 상태를 유지하도록 저장되지만 여전히 높은 가치의 비밀입니다. 비밀번호는 절대 저장하면 안 됩니다. 로그인이 필요하면 세션을 유지하는 데 필요한 것(대개 토큰)만 저장하고 비밀번호 검증은 서버에 의존하세요.
키는 다른 범주입니다. API 키, 서명 키, 암호화 키는 단일 사용자 계정을 넘어서 시스템 전체를 열 수 있습니다. 누군가 기기에서 키를 추출하면 대규모 남용을 자동화할 수 있습니다. 규칙 한 가지: 값이 앱 외부에서 앱을 사칭하거나 데이터를 복호화하는 데 사용될 수 있다면 사용자 토큰보다 높은 위험으로 취급하세요.
PII는 사람을 식별할 수 있는 모든 것: 이메일, 전화번호, 주소, 고객 메모, 신분증, 건강 관련 데이터 등입니다. 겉보기엔 무해해 보이는 필드도 결합되면 민감해집니다.
실무에서 잘 통하는 빠른 분류 체계:
- 세션 비밀: 액세스 토큰, 리프레시 토큰, 세션 쿠키
- 앱 비밀: API 키, 서명 키, 암호화 키(가능하면 기기에 두지 마세요)
- 사용자 데이터(PII): 프로필 정보, 식별자, 문서, 의료·금융 정보
- 기기 및 애널리틱스 ID: 광고 ID, 디바이스 ID, 설치 ID(여전히 많은 정책에서 민감 정보로 간주)
Android Keystore: 언제 사용해야 하나
Android Keystore는 평문 형태로 기기를 떠나면 안 되는 비밀을 보호할 때 가장 적합합니다. 이것은 암호화 키를 보관하는 금고이지 실제 데이터베이스가 아닙니다.
적합한 용도: 암호화·복호화, 서명·검증에 쓰일 키를 생성하고 보관하는 것입니다. 일반적으로 토큰이나 오프라인 데이터를 다른 곳에서 암호화하고, Keystore 키로 그것을 잠금 해제합니다.
하드웨어 백업 키: 실제 의미
많은 기기에서 Keystore 키는 하드웨어 백업을 지원할 수 있습니다. 이는 키 연산이 보호된 환경에서 발생하고 키 재료를 추출할 수 없음을 의미합니다. 이렇게 하면 앱 파일을 읽을 수 있는 악성 코드로부터의 위험이 줄어듭니다.
단, 하드웨어 백업이 모든 기기에서 보장되는 것은 아니며 동작은 기기 모델 및 Android 버전에 따라 다릅니다. 키 연산이 실패할 수 있다고 가정하고 설계하세요.
사용자 인증 게이트
Keystore는 키 사용 전에 사용자 존재 확인을 요구할 수 있습니다. 이렇게 해서 생체인식이나 기기 자격 증명과 접근을 연결할 수 있습니다. 예를 들어 내보내기 토큰을 암호화해두고 지문 또는 PIN으로 확인해야만 복호화하도록 할 수 있습니다.
Keystore는 내보내기 불가능한 키가 필요할 때, 민감 동작에 대해 생체 인증이나 기기 자격 증명을 요구할 때, 동기화나 백업과 함께 이동하면 안 되는 디바이스별 비밀을 원할 때 강력한 선택입니다.
함정에 대비하세요: 잠금 화면 변경, 생체 정보 변경, 보안 이벤트 후에 키가 무효화될 수 있습니다. 실패를 예상하고 명확한 폴백을 구현하세요: 키 무효화를 감지하면 암호화된 블롭을 삭제하고 사용자가 다시 로그인하도록 유도하세요.
EncryptedSharedPreferences: 언제 충분한가
EncryptedSharedPreferences는 키-값 형태의 작은 비밀 몇 개에 대한 안전한 기본값으로 좋습니다. "SharedPreferences이지만 암호화된" 형태라서 누군가 파일을 열어 값을 바로 읽을 수 없습니다.
내부적으로는 값을 암호화·복호화하기 위한 마스터 키를 사용합니다. 그 마스터 키는 Android Keystore로 보호되어 앱이 원시 암호화 키를 평문으로 저장하지 않습니다.
자주 읽는 소량 항목(예: 액세스/리프레시 토큰, 세션 ID, 디바이스 ID, 환경 플래그, 마지막 동기화 시간 등)에는 보통 충분합니다. 정말로 저장해야 하는 아주 작은 사용자 데이터에도 괜찮지만, PII 버리는 장소가 되게 해서는 안 됩니다.
대량이거나 구조화된 데이터에는 적합하지 않습니다. 오프라인 목록, 검색, 필드별 쿼리가 필요하면 EncryptedSharedPreferences는 느리고 불편합니다. 그럴 때는 암호화된 데이터베이스로 넘어가야 합니다.
간단한 규칙: 저장된 키를 한 화면에 모두 나열할 수 있다면 EncryptedSharedPreferences로 충분할 가능성이 큽니다. 행과 쿼리가 필요하면 다른 선택을 검토하세요.
데이터베이스 암호화: 언제 필요한가
데이터베이스 암호화는 작은 설정이나 한 토큰 이상을 저장할 때 필요합니다. 앱이 기기에 비즈니스 데이터를 유지하면 분실된 전화기에서 추출될 수 있다고 가정하고 보호해야 합니다.
오프라인으로 레코드에 접근해야 하거나, 성능을 위한 로컬 캐시, 이력/감사 기록, 긴 노트나 첨부파일을 저장해야 할 때 데이터베이스가 필요합니다.
두 가지 일반적 접근법
전체 데이터베이스 암호화(종종 SQLCipher 스타일)는 파일 전체를 휴지 상태에서 암호화합니다. 앱은 키로 열고 사용합니다. 어떤 컬럼이 보호되는지 기억할 필요가 없어 이해하기 쉽습니다.
앱 레이어 필드 암호화는 쓰기 전에 특정 필드만 암호화하고 읽은 뒤 복호화합니다. 대부분의 레코드는 민감하지 않거나 파일 형식을 바꾸지 않고자 할 때 유용할 수 있습니다.
기밀성 vs 검색·정렬의 트레이드오프
전체 데이터베이스 암호화는 디스크상의 모든 것을 숨기지만 데이터베이스가 잠금 해제되면 앱은 정상적으로 쿼리할 수 있습니다.
필드 암호화는 특정 컬럼을 보호하지만 암호화된 값에 대한 검색·정렬이 어렵습니다. 암호화된 성을 기준으로 정렬할 수 없고, 검색은 "복호화 후 검색"(느림) 또는 "추가 인덱스 저장"(복잡성 및 잠재적 유출) 중 하나를 선택해야 합니다.
키 관리 기본
데이터베이스 키는 절대 하드코딩하거나 앱과 함께 배포하면 안 됩니다. 일반적인 패턴은 무작위 데이터베이스 키를 생성한 뒤, Android Keystore에 보관된 키로 감싸(wrap)서 저장하는 것입니다. 로그아웃 시에는 감싼 키를 삭제해 로컬 데이터베이스를 일회용으로 취급하거나, 앱이 세션 간 오프라인 동작을 유지해야 하면 이를 보존하는 방식을 선택합니다.
선택 방법: 실무 비교
일반적으로 "가장 안전한" 옵션을 고르는 것이 아니라 앱의 데이터 사용 방식에 맞는 가장 안전한 옵션을 고릅니다.
실제 결정을 이끄는 질문들:
- 데이터는 얼마나 자주 읽히나(매 실행 시 또는 드물게)?
- 데이터 양은 얼마나 되는가(몇 바이트 또는 수천 레코드)?
- 유출되면 어떤 일이 생기나(성가심, 비용 발생, 법적 보고 필요)?
- 오프라인 접근, 검색, 정렬이 필요한가?
- 준수 요구사항(보존, 감사, 암호화 규칙)이 있는가?
실용적 매핑 예:
- 토큰(OAuth 액세스, 리프레시 토큰)은 작고 자주 읽히므로 보통
EncryptedSharedPreferences에 저장됩니다. - 키 재료는 가능한 경우 Android Keystore에 보관해 기기에서 복사될 가능성을 줄이세요.
- PII와 오프라인 비즈니스 데이터는 몇 필드 이상이거나 오프라인 목록·필터링이 필요하면 보통 데이터베이스 암호화가 필요합니다.
혼합된 데이터는 비즈니스 앱에서 정상입니다. 실용적 패턴은 로컬 데이터베이스나 파일을 위한 무작위 데이터 암호화 키(DEK)를 생성하고, 그 DEK만 Keystore 기반 키로 감싸서 저장하며 필요 시 교체하는 것입니다.
확신이 서지 않으면 더 안전한 단순 경로를 선택하세요: 저장을 줄이세요. 오프라인 PII는 정말 필요할 때만 보관하고 키는 Keystore에 두세요.
단계별: Kotlin 앱에서 안전한 저장 구현하기
먼저 기기에 저장할 모든 값을 적고 그 값을 왜 보관해야 하는지 정확히 적으세요. 이것이 "혹시 몰라" 저장을 방지하는 가장 빠른 방법입니다.
코드를 쓰기 전에 규칙을 정하세요: 각 항목의 수명은 얼마인지, 언제 교체할지, 로그아웃이 정확히 무엇을 의미하는지. 액세스 토큰은 15분으로 짧게, 리프레시 토큰은 더 길게, 오프라인 PII는 "30일 후 삭제" 같은 확고한 규칙이 필요할 수 있습니다.
유지보수하기 쉬운 구현 지침:
- 앱의 나머지 부분이 직접 SharedPreferences, Keystore, 데이터베이스를 건드리지 않도록 단일
SecureStorage래퍼를 만드세요. - 각 항목을 적절한 장소에 두세요: 토큰은
EncryptedSharedPreferences, 암호화 키는 Android Keystore로 보호, 큰 오프라인 데이터셋은 암호화된 데이터베이스. - 실패를 의도적으로 처리하세요. 보안 저장이 실패하면 닫힌 상태로 실패(fail closed)하세요. 평문 저장으로 조용히 폴백하지 마세요.
- 데이터 유출이 되지 않도록 진단 로그를 추가하세요: 이벤트 타입과 오류 코드만 기록하고 토큰, 키, 사용자 세부정보는 절대 기록하지 마세요.
- 로그아웃, 계정 삭제, "앱 데이터 지우기"가 동일한 삭제 루틴으로 흐르도록 경로를 연결하세요.
그런 다음 프로덕션에서 보안 저장을 깨뜨리는 지루하지만 중요한 경우들을 테스트하세요: 백업에서 복원, 이전 앱 버전에서 업그레이드, 기기 잠금 설정 변경, 새 폰으로의 마이그레이션. 앱이 복호화할 수 없는 상태에 빠지는데 계속 재시도만 하는 루프에 빠지지 않도록 하세요.
마지막으로 팀 전체가 따를 수 있도록 한 페이지 분량으로 결정 내용을 적으세요: 무엇을 어디에 저장하는지, 보존 기간, 복호화 실패 시 어떤 일이 일어나는지.
보안 저장을 망치는 흔한 실수들
대부분 실패는 잘못된 라이브러리 선택 때문이 아닙니다. 한 작은 지름길이 비밀을 의도치 않은 곳에 복사하는 상황에서 조용히 발생합니다.
가장 큰 적색 신호는 리프레시 토큰(또는 장기 세션 토큰)을 평문으로 저장하는 것입니다: SharedPreferences, 파일, "임시" 캐시, 로컬 DB 컬럼 어디든지 해당됩니다. 백업, 루팅된 디바이스 덤프, 디버그 빌드 아티팩트에 노출되면 그 토큰은 비밀번호보다 더 오래 유효할 수 있습니다.
비밀은 저장 위치가 아니라 가시성을 통해서도 유출됩니다. 전체 요청 헤더를 로깅하거나 디버깅 중 토큰을 출력하거나 크래시 리포트·애널리틱스에 문맥을 붙이면 자격 증명이 앱 밖으로 노출될 수 있습니다. 로그는 공용으로 취급하세요.
키 처리도 흔한 취약점입니다. 모든 것에 하나의 키를 사용하면 피해 범위가 커집니다. 키를 전혀 교체하지 않으면 과거의 침해가 계속 유효합니다. 키 버전 관리, 교체 계획, 오래된 암호화 데이터에 대한 처리 계획을 포함하세요.
"금고 밖" 경로도 잊지 마세요
암호화는 클라우드 백업이 로컬 앱 데이터를 복사하는 것을 막지 못합니다. 스크린샷이나 화면 녹화가 PII를 캡처하는 것도 막지 않습니다. 디버그 빌드의 완화된 설정이나 CSV/공유 시트 같은 내보내기 기능도 민감 필드를 유출할 수 있습니다. 클립보드 사용도 일회성 코드나 계좌번호를 유출할 수 있습니다.
또한 암호화가 권한 부여 문제를 해결해주지는 않습니다. 로그아웃 후에도 앱이 PII를 표시하거나 캐시를 그대로 두는 것은 접근 제어 버그입니다. UI를 잠그고 로그아웃 시 민감 캐시를 삭제하며 보호된 데이터를 표시하기 전에 권한을 다시 확인하세요.
운영적 세부사항: 수명 주기, 로그아웃, 엣지 케이스
보안 저장은 비밀을 어디에 두느냐만이 아닙니다. 앱이 잠들 때, 사용자가 로그아웃할 때, 기기가 잠길 때 그들이 어떻게 동작하는지도 포함됩니다.
토큰에 대해 전체 수명 주기를 계획하세요. 액세스 토큰은 단명으로 두세요. 리프레시 토큰은 비밀번호처럼 취급하세요. 토큰이 만료되면 조용히 갱신하세요. 리프레시가 실패(토큰 취소, 비밀번호 변경, 기기 제거)하면 재시도 루프를 멈추고 강제 재로그인을 시키세요. 서버 측에서의 취소 지원도 필요합니다. 로컬 저장만으로 훔친 자격 증명을 무효화할 수는 없습니다.
생체인식은 재인증 수단으로 사용하되 모든 곳에 쓰지는 마세요. PII 보기, 데이터 내보내기, 정산 정보 변경, 일회성 키 보기 같은 실제 위험이 있는 동작에서만 요청하세요. 앱 실행마다 요청하면 안 됩니다.
로그아웃 시에는 엄격하고 예측 가능하게 동작하세요:
- 먼저 메모리 내 복사본을 지우세요(싱글턴, 인터셉터, ViewModel 등에 캐시된 토큰).
- 저장된 토큰과 세션 상태(리프레시 토큰 포함)를 삭제하세요.
- 설계가 허용하면 로컬 암호화 키를 제거하거나 무효화하세요.
- 오프라인 PII와 캐시된 API 응답을 삭제하세요.
- 백그라운드 작업이 데이터를 다시 가져오지 않도록 비활성화하세요.
비즈니스 앱에서는 엣지 케이스가 중요합니다: 한 기기에서 여러 계정, 워크 프로필, 백업/복원, 디바이스 간 전송, 부분 로그아웃(회사/워크스페이스 전환) 등. 강제 중지, OS 업그레이드, 시계 변경도 테스트하세요. 시간 차이가 만료 로직을 깨뜨릴 수 있습니다.
변조 감지는 트레이드오프입니다. 기본 검사(디버그 가능 빌드, 에뮬레이터 플래그, 간단한 루트 신호, Play Integrity 결과)는 우발적 악용을 줄이지만 결심한 공격자는 우회할 수 있습니다. 변조 신호는 위험 입력으로 취급하세요: 오프라인 접근을 제한하고 재인증을 요구하며 이벤트를 기록하세요.
출시 전 빠른 체크리스트
출시 전에 이 리스트를 확인하세요. 실제 비즈니스 앱에서 보안 저장이 실패하는 지점을 겨냥합니다.
- 기기가 적대적일 수 있음을 가정하세요. 공격자가 루팅된 기기나 전체 디바이스 이미지를 가지고 있다면 앱 파일, 환경설정, 로그, 스크린샷에서 토큰, 키, PII를 읽을 수 있나요? 답이 "아마도"면 Keystore 기반 보호로 비밀을 옮기고 페이로드를 암호화하세요.
- 백업과 디바이스 전송을 확인하세요. 민감 파일은 Android Auto Backup, 클라우드 백업, 기기 간 전송에서 제외하세요. 복원 시 키가 없어서 복호화가 불가능하면 복구 흐름을 계획하세요(복호화 시도 대신 재인증하고 재다운로드하도록).
- 디스크에 평문이 있는지 찾아내세요. 임시 파일, HTTP 캐시, 크래시 리포트, 애널리틱스 이벤트, 이미지 캐시에서 PII나 토큰이 들어가 있지 않은지 검사하세요. 디버그 로깅과 JSON 덤프도 확인하세요.
- 만료와 교체(rotate)를 정하세요. 액세스 토큰은 짧게, 리프레시 토큰은 보호하고 서버 측 세션은 취소 가능하게 하세요. 키 교체 정책과 토큰이 거부될 때 앱이 어떻게 행동할지 정의하세요(삭제, 재인증, 한 번의 재시도 등).
- 재설치 및 기기 변경 행동을 테스트하세요. 삭제 후 재설치하고 오프라인으로 열어보세요. Keystore 키가 사라지면 앱은 안전하게 실패해야 합니다(암호화 데이터 삭제, 로그인 화면 표시, 상태를 부분적으로 읽어 상태를 손상시키지 않음).
빠른 검증 방법은 "나쁜 날" 테스트입니다: 사용자가 로그아웃하고, 비밀번호를 변경하고, 백업을 새 폰으로 복원한 뒤 비행기에서 앱을 열어봅니다. 결과는 예측 가능해야 합니다: 올바른 사용자라면 데이터가 복호화되거나, 아니면 삭제되어 로그인 후 다시 가져와야 합니다.
예시 시나리오: 오프라인에 PII를 저장하는 비즈니스 앱
신호가 약한 지역에서 쓰는 필드 영업 앱을 상상해보세요. 담당자는 아침에 한 번 로그인하고 오프라인에서 할당된 고객을 보고, 미팅 메모를 추가한 뒤 나중에 동기화합니다. 이럴 때 체크리스트가 이론이 아니라 실제 유출을 막습니다.
실용적 분리 예:
- 액세스 토큰: 수명이 짧고
EncryptedSharedPreferences에 보관. - 리프레시 토큰: 더 엄격히 보호하고 Android Keystore로 접근을 제한.
- 고객 PII(이름, 전화, 주소): 암호화된 로컬 데이터베이스에 저장.
- 오프라인 노트와 첨부파일: 암호화된 데이터베이스에 저장하되 내보내기·공유에 각별히 주의.
여기에 두 가지 기능을 추가하면 위험이 달라집니다.
"나를 기억하기(remember me)" 기능을 추가하면 리프레시 토큰이 계정으로 다시 들어가는 주된 출입구가 됩니다. 이를 비밀번호처럼 취급하세요. 사용자에 따라 기기 잠금(PIN/패턴/생체)으로 해제하도록 요구할 수 있습니다.
오프라인 모드를 추가하면 더 이상 세션만 보호하는 것이 아닙니다. 전체 고객 목록 자체가 가치가 있으므로 보통 데이터베이스 암호화와 명확한 로그아웃 규칙(로컬 PII 삭제, 다음 로그인에 필요한 최소한만 보관, 백그라운드 동기화 취소)을 적용하게 됩니다.
에뮬레이터뿐 아니라 실제 기기에서 테스트하세요. 최소한 잠금/잠금 해제 동작, 재설치 행동, 백업/복원, 다중 사용자 또는 워크 프로필 분리를 확인하세요.
다음 단계: 팀 습관으로 만들기
보안 저장은 습관일 때만 효과가 있습니다. 팀이 따를 수 있는 짧은 저장 정책을 작성하세요: 무엇을 어디에(Keystore, EncryptedSharedPreferences, 암호화 데이터베이스) 두는지, 절대 저장하면 안 되는 것, 로그아웃 시 무엇을 삭제해야 하는지.
이를 일상 배달 프로세스에 포함하세요: 완료 기준(definition of done), 코드 리뷰, 릴리스 체크에 포함시키세요.
가벼운 리뷰어 체크리스트:
- 모든 저장 항목이 라벨링되어 있는가(토큰, 키 재료, 또는 PII).
- 스토리지 선택이 코드 주석에 정당화되어 있는가.
- 로그아웃과 계정 전환이 올바른 데이터를 제거하는가(그리고 정확히 그 데이터만).
- 오류와 로그에 비밀이나 전체 PII가 출력되지 않는가.
- 정책을 소유하고 최신 상태로 유지하는 사람이 있는가.
팀이 AppMaster(appmaster.io)를 사용해 비즈니스 앱을 빌드하고 Android 클라이언트용 Kotlin 소스를 내보낸다면, 생성된 코드와 커스텀 코드가 동일한 정책을 따르도록 SecureStorage 래퍼 접근법을 유지하세요.
작은 POC로 시작하세요
인증 토큰 하나와 PII 레코드 하나(예: 오프라인에 필요한 고객 전화번호)를 저장하는 작은 POC를 만드세요. 그다음 새 설치, 업그레이드, 로그아웃, 잠금 화면 변경, 앱 데이터 지우기를 테스트하세요. 삭제 동작이 올바르고 반복 가능할 때만 확장하세요.
자주 묻는 질문
먼저 정확히 무엇을 저장하는지, 그리고 왜 저장해야 하는지 적어보세요. 작은 세션 비밀(예: 접근 토큰, 리프레시 토큰)은 EncryptedSharedPreferences에 두고, 암호화 키 재료는 Android Keystore에 보관하며, 오프라인 비즈니스 레코드나 PII가 몇 필드 이상이거나 쿼리가 필요하면 암호화된 데이터베이스를 사용하세요.
일반 SharedPreferences는 값을 평문 파일로 보관하는 경우가 많아 디바이스 백업, 루팅된 기기 파일 접근, 디버깅 아티팩트로부터 읽힐 수 있습니다. 값이 토큰이나 PII라면 일반 설정처럼 취급하면 복사되어 앱 외부에서 재사용되기 쉬워 위험합니다.
추출 불가능해야 하는 암호화 키를 생성하고 보관할 때 Android Keystore를 사용하세요. 보통 Keystore 키로 다른 데이터(토큰, 데이터베이스 키, 파일)를 암호화하거나, 사용자가 인증(생체인식 또는 기기 자격 증명)을 해야 키를 사용하도록 요구할 때 활용합니다.
하드웨어 백업은 키 연산이 보호된 하드웨어에서 이루어져 키 재료를 추출하기 어렵게 만든다는 뜻입니다. 앱 파일을 읽을 수 있는 공격자라도 키 재료를 쉽게 얻지 못하게 해주지만, 항상 사용 가능하거나 동작 방식이 기기마다 동일하다고 가정하면 안 됩니다. 실패에 대비한 복구 흐름을 설계하세요.
EncryptedSharedPreferences는 접근/리프레시 토큰, 세션 ID, 작은 상태 값처럼 자주 읽는 소량의 키-값 비밀에 대해 보통 충분합니다. 대량 데이터, 구조화된 오프라인 레코드, 또는 고객/티켓/주문처럼 필터링·검색이 필요한 경우에는 적합하지 않습니다.
오프라인 비즈니스 데이터나 PII를 대규모로 저장하거나, 검색/정렬/쿼리가 필요하거나, 오프라인 이력을 유지해야 하면 암호화된 데이터베이스를 선택하세요. 이렇게 하면 분실된 기기로 전체 고객 목록이나 노트가 노출될 위험을 줄이면서도 오프라인 동작을 유지할 수 있습니다.
전체 데이터베이스 암호화는 파일 전체를 보호해서 어떤 컬럼이 민감한지 추적할 필요가 없어 관리하기 쉽습니다. 필드 단위 암호화는 일부 컬럼에만 적용할 때 가능하지만 검색·정렬이 어려워지고 색인이나 파생 필드를 통해 실수로 유출할 수 있습니다.
무작위 데이터베이스 키(DEK)를 생성한 다음, Keystore 기반 키로 감싼(wrapped) 형태로만 저장하는 패턴이 일반적입니다. 키를 하드코딩하거나 앱에 포함시키지 마세요. 로그아웃이나 키 무효화 시에는 감싼 키를 삭제하고 로컬 데이터를 폐기하도록 설계하세요.
잠금화면 설정 변경, 생체 정보 변경, 복원·마이그레이션 상황 등으로 키가 무효화되어 복호화가 실패할 수 있습니다. 이를 명시적으로 처리하세요: 복호화 실패를 감지하면 암호화된 블롭이나 로컬 DB를 안전하게 삭제하고 사용자가 다시 로그인하도록 유도하세요. 반복 시도하거나 평문으로 대체하지 마세요.
암호화만으로는 유출을 막지 못하는 경로들이 많습니다: 로그, 크래시 리포트, 애널리틱스 이벤트, 디버그 출력, HTTP 캐시, 스크린샷, 클립보드 등입니다. 로그를 공용으로 취급하고 토큰이나 전체 PII는 절대 기록하지 마세요. 내보내기 기능이나 임시 파일도 주의하세요.


