DEV Community

dss99911
dss99911

Posted on • Originally published at dss99911.github.io

Android 보안 베스트 프랙티스

Android 보안 베스트 프랙티스

Android 앱의 보안을 강화하는 다양한 방법을 알아봅니다.

네트워크 보안

HTTPS 사용

모든 네트워크 통신은 HTTPS를 사용해야 합니다.

val url = URL("https://api.example.com/data")
val connection = url.openConnection() as HttpsURLConnection
connection.connect()
val inputStream = connection.inputStream
Enter fullscreen mode Exit fullscreen mode

Network Security Config

Android 7.0(API 24)부터 지원되는 네트워크 보안 설정입니다.

<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <certificates src="system" />
        </trust-anchors>
    </base-config>

    <!-- 특정 도메인만 HTTP 허용 -->
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">legacy.example.com</domain>
    </domain-config>
</network-security-config>
Enter fullscreen mode Exit fullscreen mode
<!-- AndroidManifest.xml -->
<application
    android:networkSecurityConfig="@xml/network_security_config">
</application>
Enter fullscreen mode Exit fullscreen mode

SSL Pinning

중간자 공격(MITM)을 방지하기 위해 인증서를 고정합니다.

인증서 고정 대상

  • 서버 인증서: 가장 확실하지만, 인증서 갱신 시 앱 업데이트 필요
  • 중간 CA 인증서: 같은 CA에서 발급받으면 앱 업데이트 불필요
  • Public Key: 인증서가 갱신되어도 Public Key는 유지될 수 있음

OkHttp에서 SSL Pinning

val certificatePinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()
Enter fullscreen mode Exit fullscreen mode

Network Security Config으로 SSL Pinning

<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <pin-set expiration="2025-12-31">
            <pin digest="SHA-256">base64EncodedHash==</pin>
            <!-- 백업 핀 -->
            <pin digest="SHA-256">backupHash==</pin>
        </pin-set>
    </domain-config>
</network-security-config>
Enter fullscreen mode Exit fullscreen mode

앱 무결성 검증

APK 서명 검증

앱이 변조되었는지 확인합니다.

fun hasValidSignature(context: Context): Boolean {
    return try {
        val pm = context.packageManager
        val packageInfo = pm.getPackageInfo(
            context.packageName,
            PackageManager.GET_SIGNATURES
        )
        val cert = packageInfo.signatures[0].toByteArray()
        val digest = MessageDigest.getInstance("SHA1")
        val hash = digest.digest(cert)

        val calculated = hash.joinToString("") {
            Integer.toString(it.toInt() and 0xff, 16).lowercase()
        }

        val expected = context.getString(R.string.expected_fingerprint)
        calculated == expected
    } catch (e: Exception) {
        false
    }
}
Enter fullscreen mode Exit fullscreen mode

SafetyNet Attestation

루팅 기기 및 변조된 환경을 감지합니다.

SafetyNet.getClient(context)
    .attest(nonce, apiKey)
    .addOnSuccessListener { response ->
        val jwsResult = response.jwsResult
        // 서버에서 검증
    }
    .addOnFailureListener { e ->
        // 실패 처리
    }
Enter fullscreen mode Exit fullscreen mode

데이터 보호

Provider 보안

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
Enter fullscreen mode Exit fullscreen mode

Signature 기반 권한

같은 서명을 가진 앱끼리만 데이터 공유:

<manifest>
    <permission
        android:name="com.example.MY_PERMISSION"
        android:protectionLevel="signature" />

    <application>
        <provider
            android:name=".MyProvider"
            android:permission="com.example.MY_PERMISSION"
            android:exported="true" />
    </application>
</manifest>
Enter fullscreen mode Exit fullscreen mode

코드 난독화

ProGuard/R8 설정

# proguard-rules.pro

# 모델 클래스 유지 (JSON 파싱용)
-keep class com.example.model.** { *; }

# 리플렉션 사용 클래스 유지
-keep class com.example.reflect.** { *; }

# 스택 트레이스 가독성
-keepattributes SourceFile,LineNumberTable
Enter fullscreen mode Exit fullscreen mode

리소스 난독화

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

암호화

Android Keystore

민감한 키를 안전하게 저장합니다.

val keyGenerator = KeyGenerator.getInstance(
    KeyProperties.KEY_ALGORITHM_AES,
    "AndroidKeyStore"
)

val keySpec = KeyGenParameterSpec.Builder(
    "myKeyAlias",
    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
    .setUserAuthenticationRequired(true)
    .build()

keyGenerator.init(keySpec)
val secretKey = keyGenerator.generateKey()
Enter fullscreen mode Exit fullscreen mode

EncryptedSharedPreferences

val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val sharedPreferences = EncryptedSharedPreferences.create(
    context,
    "secret_shared_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
Enter fullscreen mode Exit fullscreen mode

보안 체크리스트

  1. 네트워크

    • [ ] HTTPS만 사용
    • [ ] SSL Pinning 구현
    • [ ] Network Security Config 설정
  2. 데이터

    • [ ] 민감한 데이터 암호화
    • [ ] SharedPreferences 대신 EncryptedSharedPreferences 사용
    • [ ] 로그에 민감한 정보 제외
  3. 코드

    • [ ] ProGuard/R8 난독화 적용
    • [ ] 앱 서명 검증
    • [ ] 디버그 빌드에서만 로깅
  4. 인증

    • [ ] 토큰 안전하게 저장
    • [ ] 세션 타임아웃 구현
    • [ ] 생체 인증 고려

보안 도구

  • Stetho: 디버그 시 DB 및 네트워크 검사 (디버그 빌드만)
  • FindBugs: 보안 취약점 정적 분석
  • OWASP ZAP: 동적 보안 테스트

결론

앱 보안은 여러 계층에서 적용해야 합니다. 네트워크 통신 암호화, 로컬 데이터 보호, 코드 난독화를 함께 적용하고, 정기적으로 보안 취약점을 점검하세요.


Originally published at https://dss99911.github.io

Top comments (0)