본문 바로가기
모바일 보안

안전한 토큰 저장: 모바일 개발자를 위한 베스트 프랙티스

모바일 개발자를 위한 안전한 토큰 저장 베스트 프랙티스: Keychain, Keystore 기반 DataStore, refresh token rotation, EncryptedSharedPreferences에서 마이그레이션.

안전한 토큰 저장: 모바일 개발자를 위한 베스트 프랙티스

인증 토큰을 WebView localStorage나 일반 SharedPreferences가 아닌, 플랫폼 기반 암호화 저장소에 저장하세요. iOS에서는 Keychain을 사용하세요. Android에서는 키를 보호할 때 Android Keystore (기기에 따라 software, TEE 또는 StrongBox)를 사용하고, 암호문은 다음을 사용해 저장하세요: Jetpack DataStore + Tink. EncryptedSharedPreferences 사용 중단 예정입니다 에서 androidx.security.crypto; 지금 마이그레이션을 계획하세요. Capacitor 앱의 경우, JavaScript에서 암호화 로직을 중복 구현하는 대신 전송 보안(HTTPS, pinning)과 네이티브 기반 스토리지 플러그인을 결합하여 사용하세요.

스토리지 패턴 선정 기준

이곳의 모든 권장 사항은 네 가지 필터를 거칩니다. 첫째, 하드웨어 지원: 비밀 데이터는 Keychain/Keystore에 저장되거나 이를 지원하는 키로 암호화되어야 합니다. 둘째, 공식 지원 마이그레이션 경로: Google이나 Apple이 해당 방식을 지원 중단했다면, 새로운 code에 권장하지 않습니다. 셋째, Capacitor 호환성: 플러그인 경계에서 원시 키를 JavaScript에 노출하지 않고도 작동해야 합니다. 넷째, 취소 가능성: 유출된 자격 증명은 서버 측에서 무효화할 수 있어야 합니다.

마지막 필터 때문에 이 글 전반에 걸쳐 refresh token rotation이 등장합니다. Chainalysis가 수집한 2024년 사고 데이터를 검토한 결과, 개인 키 및 자격 증명 유출이 탈취된 암호화폐의 대부분을 차지했습니다(일본 DMM Bitcoin에서 3억 500만 달러 이상, 인도의 WazirX에서 2억 3,500만 달러 탈취 포함). 주요 실패 원인은 단일하고 지속적으로 유효한 자격 증명을 무조건 신뢰한 것이었습니다(Chainalysis 2024 범죄 보고서). 모바일 인증 토큰도 마찬가지입니다. 평문 스토리지에 저장된 장기 bearer token은 해킹을 방치하는 것과 같습니다. 저희는 자격 증명을 교체(rotation)하지 않고 무기한 유지하는 아키텍처는 더 이상 권장하지 않습니다.

모든 플랫폼에 적용되는 원칙

아래 표는 스냅샷으로 요약됩니다. 핵심 로직은 access/refresh token 분리입니다. 액세스 토큰 은 수명이 짧고(며칠이 아니라 몇 분), API가 전송하는 값입니다. 영향 범위가 작고 금방 만료되므로 메모리에만 두고, 앱을 실행할 때마다 다시 만들어야 합니다. 리프레시 토큰 은 새 access token을 발급할 수 있는 오래 유지되는 비밀 값이라, 하드웨어 기반의 암호화된 영구 저장소에 보관해야 합니다. 수명이 다르면 보관 위치도 달라야 하며, 두 토큰을 같은 방식으로 저장하는 것이 대부분의 유출이 시작되는 지점입니다.

런타임 전체 흐름은 다음과 같습니다. 앱이 access token 없이 시작해 저장된 refresh token으로 인증 서버를 호출하고, 새 access token을 메모리로 받은 뒤, 서버가 사용할 때마다 rotate하므로 이전 refresh token을 폐기합니다. 서버가 이미 사용된 refresh token이 다시 제출되는 것을 보면, 이를 탈취 증거로 간주하고 해당 token family 전체를 무효화합니다. 로그아웃 시 클라이언트는 로컬 저장소를 비우고 서버는 grant를 취소합니다. 서버 취소 없이 로컬 데이터만 지우는 것은 보안이 아니라 요행을 바라는 것입니다.

실습 왜 중요한가
수명이 짧은 access token 토큰 유출 시 피해 최소화
Refresh token rotation 정상 사용 후 탈취된 refresh token 무효화
HTTPS 필수 사용 단순 네트워크 가로채기 방지
Certificate pinning (필요 시) 부적절한 인증서 또는 커스텀 루트 위험 감소 (OWASP certificate pinning 가이드 참조)
로그, URL, 분석 도구에 토큰 포함 금지 Observability 도구에 노출된 토큰은 보안 침해로 이어짐
로그아웃 시 토큰 만료 클라이언트의 데이터 삭제를 기다리기보다 서버 측 무효화가 확실함

@capgo/capacitor-ssl-pinning 더 엄격한 전송 제어가 필요한 경우 iOS 및 Android에서 Capacitor HTTP용 TLS를 pin할 수 있습니다.

Android: Keystore 및 DataStore (EncryptedSharedPreferences 지양)

Google에서 다음을 deprecated함 androidx.security.crypto 다음과 같은 헬퍼를 포함하여, EncryptedSharedPreferences 및 MasterKey. 패키지 요약에는 개발자가 다음을 사용하도록 안내합니다: KeyGenerator AndroidKeyStore 프로바이더와 함께 사용 새 키 자료를 위해.

Tink 기반의 엔드 투 엔드 파이프라인을 사용하세요:

  1. 다음으로 Tink keyset을 빌드합니다: AndroidKeysetManager, 마스터 키를 Android Keystore에 저장하며, android-keystore:// URI를 사용합니다 (다음을 참조하세요: datastore-tink 릴리스 노트).
  2. 다음 항목을 가져오세요: Aead 키셋에서 해당 프리미티브를 가져옵니다.
  3. 추가 androidx.datastore:datastore-tink (다음에서 사용 가능) DataStore 1.3.0-rc01; stable 1.2.x 버전에는 이 아티팩트가 포함되어 있지 않음), 귀하의 Serializer 로 AeadSerializer, 그리고 이를 DataStore로 전달합니다. 여러 스토어가 하나의 Aead 키를 공유하는 경우, 각 스토어에 고유한 associatedData를 부여하세요. 파일명에서 유도한 안정적인 값 등이 좋습니다.

Google은 이를 지원 중단된 기능을 대체하는 공식 경로로 문서화했습니다. androidx.security.crypto helpers. 단독 JCA를 혼용하지 마세요. SecretKey 함께 AeadSerializer; serializer는 Tink Aead 인스턴스를 기대합니다.

버전 관리에 관한 참고 사항: 해당 datastore-tink 아티팩트는 1.3.0-rc01 버전부터 존재하므로, 안정적인 1.2.x 버전을 고정해서 사용하는 팀은 문서화된 대체 항목이 의존성 트리에서 보이지 않을 수 있습니다. 이 격차는 이미 여러 릴리스 전부터 발생했으며, 저희는 이것이 마이그레이션 계획이 지연되는 주요 원인이라고 판단합니다. 실수로 버전이 꼬이지 않도록, 릴리스 후보(RC) 버전을 사용할지 아니면 기다릴지 의도적으로 결정하십시오.

알아두면 유용한 Keystore 옵션

  • setIsStrongBoxBacked(true) 할 FEATURE_STRONGBOX_KEYSTORE 더 강력한 키 격리를 위해 사용 가능합니다.
  • 사용자 인증 바인딩 (setUserAuthenticationRequired) 생체 인식이나 기기 인증 후에만 토큰에 접근해야 하는 경우에 사용합니다.
  • Auto Backup 사용 금지 복구 동작을 완벽히 이해하지 못했다면 키셋이나 암호문을 저장하는 파일에 주의해야 합니다. Google의 EncryptedSharedPreferences 문서는 키 없이 암호문만 백업되어 복구되는 위험성을 경고한 바 있습니다.

Capacitor의 Native Kotlin 또는 Java 모듈은 플러그인 내부에서 이 패턴을 구현해야 합니다. JavaScript는 원본 키가 아닌 불투명한 핸들이나 세션 상태만 전달받아야 합니다.

iOS: Keychain 및 Secure Enclave

이 Keychain은 iOS에서 토큰을 저장하기 위한 가장 적합한 장소입니다. 항목은 시스템에 의해 암호화되고 보호됩니다. 범위를 좁게 설정한 accessibility 상수를 사용하고, 액세스 제어 Face ID, Touch ID 또는 암호 입력이 필요한 경우를 고려하세요.

accessibility 선택은 단순히 기본값을 복사하는 것이 아니라 신중히 결정해야 할 문제입니다. 다음 중 하나를 선택하세요. kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 푸시 트리거 리프레시나 background fetch처럼 백그라운드 작업에서 토큰을 사용해야 하지만, 백업을 통해 새 기기로 이전되어서는 안 되는 경우를 위해 선택하세요. kSecAttrAccessibleWhenUnlockedThisDeviceOnly 제약 사항을 감수하더라도 더 엄격한 보안이 필요한 포그라운드 전용 토큰 사용의 경우를 위해 선택하세요. 두 경우 모두, kSecUseDataProtectionKeychain 최신 타겟에서 활성화하여 레거시 파일 기반 방식이 아닌 데이터 보호 Keychain을 사용하도록 하세요.

CryptoKit는 helps when you encrypt payloads yourself, but for standard OAuth-style bearer tokens, storing the token string directly in Keychain with correct accessibility is usually enough. Secure Enclave-backed keys (a P-256 key generated with SecureEnclave in CryptoKit) only pay off when the key itself must never leave the secure hardware — think signing operations or device attestation. They can’t hold a token string for you; they protect your own envelope key. If you’re not rotating envelopes and managing that lifecycle, plain Keychain storage is simpler and no less safe. Extra app-layer encryption only helps when you also rotate keys and manage envelopes carefully.

Capacitor의 @capacitor/preferences 쓰기 권한이 있는 UserDefaults에 는 보안 저장소가 아닙니다. 토큰을 여기에 저장하지 마세요.

Capacitor: Capgo 조직 내 보안 저장 옵션

Capgo는 는 “secure storage”라는 패키지를 출시하지만, 일반적인 Capacitor 요구사항은 다음 두 플러그인이 해결합니다:

플러그인 가장 적합한 용도 Native 백엔드
@capgo/capacitor-native-biometric setData / getSecureData 소규모 보안 데이터 (세션 토큰, API 키) iOS Keychain; Android Keystore 기반 SharedPreferences 암호화
@capgo/capacitor-data-storage-sqlite 암호화 옵션 포함 대규모 또는 구조화된 로컬 데이터 SQLCipher 옵션이 포함된 SQLite

이 Key-value 스토리지 가이드 Preferences, SQLite 스토리지 및 NativeBiometric.setData 비밀 정보 저장용.

다음 버전이 필요합니다: @capgo/capacitor-native-biometric 8.6.0 이상 (플러그인 메이저 버전을 Capacitor 메이저 버전과 맞추세요). 생체 인증 기반 읽기 예시:

import {
  AccessControl,
  NativeBiometric,
} from '@capgo/capacitor-native-biometric';

const TOKEN_KEY = 'auth.refresh_token';

export async function saveRefreshToken(token: string) {
  await NativeBiometric.setData({
    key: TOKEN_KEY,
    value: token,
    accessControl: AccessControl.BIOMETRY_CURRENT_SET,
  });
}

export async function loadRefreshToken(): Promise<string | null> {
  try {
    const { value } = await NativeBiometric.getSecureData({
      key: TOKEN_KEY,
      reason: 'Unlock your session',
      title: 'Authenticate',
    });
    return value ?? null;
  } catch {
    return null;
  }
}

getData 저장된 값만 반환합니다 없이 accessControl. 생체 인증 보호를 위해 항상 BIOMETRY_CURRENT_SET 또는 BIOMETRY_ANY 와 함께 사용하세요 getSecureData.

웹 빌드의 경우, 프로덕션 토큰을 절대 localStorage. 대신 백엔드의 웹 세션 모델을 사용하세요.

서버와 클라이언트의 토큰 생명주기

네, 모바일 앱은 refresh token을 저장해야 합니다. 그렇지 않으면 앱을 실행할 때마다 사용자가 다시 인증해야 하는데, 이는 실제 보안보다는 보안을 하는 척하는 것에 불과합니다. 핵심은 어디에 및 어떻게: refresh token만 Keychain 또는 Keystore 기반 저장소에 저장하고, 반드시 rotation 및 서버 측 revocation 기능과 함께 사용해야 한다는 점입니다.

Rotation은 저장을 안전하게 만드는 핵심입니다. refresh token을 일회용 자격 증명으로 발급하세요. 즉, 토큰을 사용할 때마다 새로운 refresh token을 반환하고 기존 토큰은 무효화합니다. 서버는 짧은 재사용 감지(reuse-detection) 윈도우를 유지해야 합니다. 이미 사용된 토큰이 다시 제출되면 해당 토큰 패밀리가 탈취된 것으로 간주하여 전체 체인을 무효화하고, 해당 토큰을 공유하던 모든 기기에서 다시 로그인하도록 강제합니다. 다만, 레이스 컨디션이나 오프라인 재연결 상황을 고려해 약간의 유예 기간(몇 초에서 1분 내외)을 두는 것이 좋습니다. 정당한 클라이언트가 새 토큰을 받기 전 마지막으로 이전 토큰을 제출할 수 있기 때문입니다. 유예 기간이 전혀 없으면 불안정한 네트워크 환경에서 사용자가 차단될 수 있고, 너무 길면 공격 표면이 넓어집니다.

각 토큰의 저장 위치는 수명에 따라 결정됩니다:

  • 발급된 액세스 토큰 TTL을 짧게(일 단위가 아닌 분 단위) 설정하여 메모리에만 유지하며 세션 동안만 사용합니다.
  • 다음은 리프레시 토큰 Keychain 또는 Keystore 기반 암호화 저장소에 저장하고, 위와 같이 주기적으로 교체하세요.
  • 비밀번호 변경, 로그아웃 또는 기기 분실 시에는 서버 측에서 refresh tokens를 무효화해야 합니다. 클라이언트에서 로컬 데이터를 삭제하는 것은 무효화가 아닙니다.
  • 가벼운 토큰 바인딩 전략(디바이스 키 증명 또는 앱 인스턴스 ID)을 고려하세요. 위협 모델에 따라 필요할 수 있습니다.

Capacitor 앱의 토큰 처리 보안 패치를 배포할 때, Capgo Live Update 사용하기 스토어 정책을 준수한다면 웹 레이어 변경 사항을 빠르게 적용할 수 있습니다. 단, Native storage 변경은 스토어 빌드가 필요합니다.

스토리지 옵션 비교

메서드 보안 Capacitor 적합성
In-memory JS 변수 실행 중에는 높으나, 프로세스 종료 시 유실됨 세션 동안 access token에만 적합
localStorage / 설정 낮음 설정값 전용, 비밀 정보 저장 금지
iOS Keychain / Android Keystore 높음 네이티브 code 또는 Capgo 플러그인 사용
DataStore + Tink (Android 네이티브) 높음 커스텀 Capacitor 플러그인 또는 네이티브 인증 SDK
JS 내 커스텀 암호화 가변적 피해야 합니다: JS 키는 추출될 수 있습니다

마지막 줄에서 주목할 점은, JavaScript crypto의 실패 원인은 취약한 알고리즘이 아니라 위치라는 것입니다. JS 런타임이 읽을 수 있는 모든 키는 런타임 내의 다른 요소에 의해 유출될 수 있으므로, 어떤 라이브러리를 선택하더라도 해당 레이어에서는 구조적으로 '높은 보안'을 달성할 수 없습니다.

EncryptedSharedPreferences 마이그레이션 체크리스트

  1. 어떤 키가 비밀 정보를 담고 있고, 어떤 키가 공개 설정인지 분류하세요.
  2. DataStore + 구현 AeadSerializer Keystore 기반 Tink keyset 사용
  3. 다음 방식으로 마이그레이션: SharedPreferencesMigration 레거시 인스턴스를 여는 프로듀서를 사용하여 EncryptedSharedPreferences 인스턴스를 가져오되(평문이 아닌 SharedPreferences), 읽기에 성공하면 레거시 파일을 삭제합니다.
  4. 스토어 릴리스에 네이티브 변경 사항을 포함하세요. 실제 기기에서 백업 및 복원을 테스트해야 합니다.
  5. 삭제 androidx.security.crypto 참조하는 곳이 없는 의존성은 제거하세요.

자주 묻는 질문

Keychain에 저장하기 전에 토큰을 암호화해야 하나요?

Keychain 접근 권한이 위협 모델과 일치한다면, 일반적인 bearer 토큰에는 별도의 암호화 계층이 필요하지 않은 경우가 많습니다. 규정이나 키 로테이션 정책이 필요한 경우에는 envelope encryption을 추가하세요.

Capacitor 플러그인의 SQLCipher가 Keystore를 대체할 수 있나요?

SQLCipher는 데이터베이스 파일을 보호하지만, 데이터베이스를 복호화하려면 Keystore나 Keychain에 강력한 키가 여전히 필요합니다. 암호화 패스프레이즈 처리를 보안 스토리지 설계의 일부로 다루세요.

Capacitor Preferences에 난독화하여 토큰을 저장해도 되나요?

난독화는 암호화가 아닙니다. Preferences는 하드웨어 기반 보호 기능 없이 UserDefaults 및 SharedPreferences로 매핑됩니다.

모바일 앱에서 refresh token을 저장해도 되나요?

네, 하지만 반드시 Keychain이나 Keystore 기반의 암호화된 스토리지에 저장해야 하며, 서버 측의 로테이션 및 취소 기능과 병행해야 합니다. 저장하지 않으면 앱을 실행할 때마다 재인증이 필요하며, 이는 사용자 경험을 해쳐 보안 요구 사항을 낮추게 만듭니다. 절대 해서는 안 될 행동은 refresh token을 Preferences, localStorage 또는 일반 텍스트 파일에 저장하는 것입니다.

최신 앱에서 access token을 저장하는 가장 안전한 방법은 무엇인가요?

토큰을 아예 저장하지 마세요. 세션 동안에는 수명이 짧은 access token을 메모리에만 유지하고, 앱을 실행할 때마다 refresh token을 사용해 다시 생성하세요. refresh token만 iOS Keychain이나 Android Keystore 기반의 암호화된 저장소에 저장하세요. 이렇게 하면 기기가 탈취되더라도 토큰 rotation과 revocation을 통해 피해를 최소화할 수 있습니다.

Secure Token Storage: Best Practices for Mobile Developers 시리즈 계속 보기

사용 중인 경우 안전한 토큰 저장: 모바일 개발자를 위한 베스트 프랙티스 보안 및 컴플라이언스 계획을 세우려면, 다음 항목과 연결하세요 암호화 암호화 구현 세부 사항은, 컴플라이언스 컴플라이언스 구현 세부 사항은, Capgo 보안 스캐너 Capgo Security Scanner의 제품 워크플로우는, Capgo 보안 Capgo Security의 제품 워크플로우와, Capgo Trust Center Capgo Trust Center의 제품 워크플로우는.

Capacitor 앱을 위한 Live Updates

웹 레이어 버그가 라이브 상태일 때, 앱 스토어 승인을 며칠씩 기다리지 말고 Capgo를 통해 수정 사항을 배포하세요. 네이티브 변경 사항은 정상적인 리뷰 절차를 따르는 동안, 사용자는 백그라운드에서 업데이트를 받습니다.

블로그 최신 글

Capgo는 진정한 프로급 모바일 앱을 만드는 데 필요한 최고의 인사이트를 제공합니다.