안드로이드 앱은 난독화 처리를 하지 않으면 디컴파일러로 소스 코드 내용이 그대로 유출될 수 있다.
예를 들어 특정 라이브러리들의 소스 코드를 보면 난독화 처리가 적용되어 있는 것을 확인할 수 있다.
안드로이드에서는 이런 난독화 처리를 위해 Proguard라는 기능을 제공하고 있다.
운영 중인 서비스에 아직 적용이 되지 않아 뒤늦게 적용을 진행했다..
(프로가드 적용이 필요하거나 필요하지 않은 부분에 대한 조사가 시간이 걸렸다. 앱 개발 시 초기부터 신경 쓰자..)
Proguard의 주요 기능은 다음과 같다.
코드 축소 (Shrinking): 사용되지 않는 코드와 리소스를 제거하여 APK 크기를 줄입니다. 이는 애플리케이션을 더 가볍게 만들고 다운로드 및 설치 시간을 단축하는 데 도움이 됩니다.
코드 최적화 (Optimization): 실행 속도를 향상시키기 위해 코드를 최적화합니다. 불필요한 명령문이나 리소스를 제거하여 애플리케이션의 성능을 최적화합니다.
코드 난독화 (Obfuscation): 클래스, 메서드, 변수의 이름을 난독화하여 코드를 이해하기 어렵게 만듭니다. 이는 앱의 소스 코드를 분석하거나 디컴파일하는 시도를 어렵게 하여 보안을 강화합니다.
프로가드 사용을 위해서 build.gradle을 다음과 같이 수정한다.
buildTypes {
release {
...
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
...
}
}
여기서 proguardFiles 설정은 프로가드를 사용하도록 하는데, proguard-android-optimize.txt 파일은 안드로이드에서 제공하는
최적화된 프로가드 규칙이 포함된 파일이며, proguard-rules.pro 파일은 사용자 정의 프로가드 규칙을 추가하는 파일이다.
사용자 정의 규칙은 주로 특정 클래스나 패키지를 난독화하지 않거나 예외적인 규칙을 적용할 때 사용된다.
이 파일을 사용하면 프로가드가 어떤 부분을 어떻게 처리할지에 대한 세부적인 제어를 할 수 있다.
특히 Retrofit에서는 라이브러리에서 안내하는 rules를 기입하면 되는데, 이때, 서버로 보내는 Request 또는 수신하는 Response 처리를 위한 data class들을 직접 rules로 keep (프로가드 예외 처리) 해주어야 했다.
따라서 다음과 같이 data class 파일을 폴더로 정리하여 예외 처리에 용이하도록 수정했다.
# Data Model
-keep class com.test.main.api.model.** { *; }
Proguard-rules에 사용되는 명령어의 종류에는 대표적으로 다음과 같다.
# 클래스와 멤버 보호 예제
-keep class com.example.MyClass { *; }
# 경고 무시 예제
-dontwarn com.example.**
# 클래스 이름 보호, 멤버 난독화 예제
-keepclassmembers class com.example.MyClass { *; }
# 디버깅 정보 속성 보존
-keepattributes LineNumberTable, SourceFile
또한 앱에서 사용하는 라이브러리들의 홈페이지, 깃허브를 확인하여 rules가 정의되어 있는지 확인하고 기입해야 한다.
이후 플레이스토어 업로드를 위한 제출 시에는 app - build - outputs - mapping - buildVersion - mapping.txt 파일을 추가적으로 업로드해주어야 한다.
최종적으로 필자가 정리한 proguard-rules 파일은 다음과 같다.
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Android\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Crash report
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
-dontwarn com.google.errorprone.annotations.*
# Crashlytics
# In order to provide the most meaningful crash reports
-keepattributes SourceFile,LineNumberTable
# If you're using custom Eception
-keep public class * extends java.lang.Exception
-keep class com.crashlytics.** { *; }
-dontwarn com.crashlytics.**
-keep class com.google.firebase.crashlytics.** { *; }
-dontwarn com.google.firebase.crashlytics.**
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
# EnclosingMethod is required to use InnerClasses.
-keepattributes Signature, InnerClasses, EnclosingMethod
# Retrofit does reflection on method and parameter annotations.
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
# Keep annotation default values (e.g., retrofit2.http.Field.encoded).
-keepattributes AnnotationDefault
# Retain service method parameters when optimizing.
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
# Ignore JSR 305 annotations for embedding nullability information. And For Guava
-dontwarn javax.annotation.**
-dontwarn javax.inject.**
# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
-dontwarn kotlin.Unit
# Top-level functions that can only be used by Kotlin.
-dontwarn retrofit2.KotlinExtensions
-dontwarn retrofit2.KotlinExtensions$*
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1>
# Keep inherited services.
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface * extends <1>
# With R8 full mode generic signatures are stripped for classes that are not
# kept. Suspend functions are wrapped in continuations where the type argument
# is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
# R8 full mode strips generic signatures from return types if not kept.
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
# With R8 full mode generic signatures are stripped for classes that are not kept.
-keep,allowobfuscation,allowshrinking class retrofit2.Response
# Gson
-keep class com.google.gson.** { *; }
-keep class sun.misc.Unsafe { *; } # Gson uses Unsafe
-keep class com.google.gson.stream.** { *; }
-keep class okio.** { *; }
-keep class com.google.gson.reflect.TypeToken
-keep class * extends com.google.gson.reflect.TypeToken
-keep public class * implements java.lang.reflect.Type
-keep class **$GsonConverter { *; }
-keepattributes Signature
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
# Jsoup
-keep public class org.jsoup.** {
public *;
}
# OkHttp3
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
# Data Model
-keep class com.api.model.** { *; }
# RxJava, RxAndroid (https://gist.github.com/kosiara/487868792fbd3214f9c9)
-dontwarn org.reactivestreams.FlowAdapters
-dontwarn org.reactivestreams.**
-dontwarn java.util.concurrent.Flow*
-dontwarn java.util.concurrent.flow.**
-dontwarn java.util.concurrent.**
# LeakCanary
-keep class org.eclipse.mat.** { *; }
-keep class com.squareup.leakcanary.** { *; }
# Stetho, Stetho Realm plugin
-keep class com.facebook.stetho.** {
*;
}
-dontwarn com.facebook.stetho.**
-keep class com.uphyca.** { *; }
# Glide, Glide Okttp Module, Glide Transformations
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# -keepresourcexmlelements manifest/application/meta-data@value=GlideModule 3 For dexguard
-dontwarn jp.co.cyberagent.android.gpuimage.**
# Viewpager indicator
-dontwarn com.viewpagerindicator.**
# Support v7, Design
# http://stackoverflow.com/questions/29679177/cardview-shadow-not-appearing-in-lollipop-after-obfuscate-with-proguard/29698051
-keep public class android.support.v7.widget.** { *; }
-keep public class android.support.v7.internal.widget.** { *; }
-keep public class android.support.v7.internal.view.menu.** { *; }
-dontwarn android.support.**
-dontwarn android.support.design.**
-keep class android.support.design.** { *; }
-keep interface android.support.design.** { *; }
-keep public class android.support.design.R$* { *; }
# Retrolambda
# as per official recommendation: https://github.com/evant/gradle-retrolambda#proguard
-dontwarn java.lang.invoke.*
# Keep class TypeToken (respectively its generic signature) if present
-if class com.google.gson.reflect.TypeToken
-keep,allowobfuscation class com.google.gson.reflect.TypeToken
# Keep any (anonymous) classes extending TypeToken
-keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken
# Keep classes with @JsonAdapter annotation
-keep,allowobfuscation,allowoptimization @com.google.gson.annotations.JsonAdapter class *
# Keep fields with any other Gson annotation
# Also allow obfuscation, assuming that users will additionally use @SerializedName or
# other means to preserve the field names
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.Expose <fields>;
@com.google.gson.annotations.JsonAdapter <fields>;
@com.google.gson.annotations.Since <fields>;
@com.google.gson.annotations.Until <fields>;
}
# Keep no-args constructor of classes which can be used with @JsonAdapter
# By default their no-args constructor is invoked to create an adapter instance
-keepclassmembers class * extends com.google.gson.TypeAdapter {
<init>();
}
-keepclassmembers class * implements com.google.gson.TypeAdapterFactory {
<init>();
}
-keepclassmembers class * implements com.google.gson.JsonSerializer {
<init>();
}
-keepclassmembers class * implements com.google.gson.JsonDeserializer {
<init>();
}
# Keep fields annotated with @SerializedName for classes which are referenced.
# If classes with fields annotated with @SerializedName have a no-args
# constructor keep that as well. Based on
# https://issuetracker.google.com/issues/150189783#comment11.
# See also https://github.com/google/gson/pull/2420#discussion_r1241813541
# for a more detailed explanation.
-if class *
-keepclasseswithmembers,allowobfuscation class <1> {
@com.google.gson.annotations.SerializedName <fields>;
}
-if class * {
@com.google.gson.annotations.SerializedName <fields>;
}
-keepclassmembers,allowobfuscation,allowoptimization class <1> {
<init>();
}
## Android Advertiser ID
-keep class com.google.android.gms.common.GooglePlayServicesUtil {*;}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient {*;}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info {*;}
### Exoplayer2
-dontwarn com.google.android.exoplayer2.**
## Databinding or library depends on databinding
-dontwarn android.databinding.**
-keep class android.databinding.** { *; }
### Kotlin Coroutine
# https://github.com/Kotlin/kotlinx.coroutines/blob/master/README.md
# ServiceLoader support
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {}
-keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {}
# Most of volatile fields are updated with AFU and should not be mangled
-keepclassmembernames class kotlinx.** {
volatile <fields>;
}
# Same story for the standard library's SafeContinuation that also uses AtomicReferenceFieldUpdater
-keepclassmembernames class kotlin.coroutines.SafeContinuation {
volatile <fields>;
}
-dontwarn kotlinx.atomicfu.**
-dontwarn kotlinx.coroutines.flow.**
# Kotlin
-keepclassmembers class **$WhenMappings {
<fields>;
}
-keep class kotlin.Metadata { *; }
-keepclassmembers class kotlin.Metadata {
public <methods>;
}
# Google Play Billing
-keep class com.android.vending.billing.**
# Adjust SDK, android.installreferrer
-keep public class com.adjust.sdk.** { *; }
-keep class com.google.android.gms.common.ConnectionResult {
int SUCCESS;
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient {
com.google.android.gms.ads.identifier.AdvertisingIdClient$Info getAdvertisingIdInfo(android.content.Context);
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info {
java.lang.String getId();
boolean isLimitAdTrackingEnabled();
}
-keep public class com.android.installreferrer.** { *; }
-keep public class com.miui.referrer.** {*;}
# Android Iconics
-keep class .R
-keep class **.R$* {
<fields>;
}
# Appsflyer
-keep class com.appsflyer.** { *; }
-keep public class com.android.installreferrer.** { *; }
-keep public class com.miui.referrer.** {*;}
# uninstall tracking (FirebaseMessagingService)
-dontwarn com.appsflyer.**
-keep public class com.google.firebase.messaging.FirebaseMessagingService {
public *;
}
# Huawei HMS core
-keepattributes *Annotation*
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable
-keep class com.hianalytics.android.**{*;}
-keep class com.huawei.updatesdk.**{*;}
-keep class com.huawei.hms.**{*;}
# Huawei HMS ads
-keep class com.huawei.openalliance.ad.** { *; }
-keep class com.huawei.hms.ads.** { *; }
-keep interface com.huawei.hms.ads.** { *; }
# Huawei App Metrics
-keep class com.huawei.agconnect.**{*;}
-dontwarn com.huawei.agconnect.**
-keep class com.hianalytics.android.**{*;}
-keep class com.huawei.updatesdk.**{*;}
-keep class com.huawei.hms.**{*;}
-keep interface com.huawei.hms.analytics.type.HAEventType{*;}
-keep interface com.huawei.hms.analytics.type.HAParamType{*;}
-keepattributes Exceptions, Signature, InnerClasses, LineNumberTable
# AdPopcorn
-keep class com.igaworks.** { *; }
-dontwarn com.igaworks.**
# Class names are needed in reflection
-keepnames class com.amazonaws.**
-keepnames class com.amazon.**
# Enums are not obfuscated correctly in combination with Gson
-keepclassmembers enum * { *; }
# Request handlers defined in request.handlers
-keep class com.amazonaws.services.**.*Handler
# The following are referenced but aren't required to run
-dontwarn com.fasterxml.jackson.**
# Android 6.0 release removes support for the Apache HTTP client
-dontwarn org.apache.commons.logging.**
-dontwarn org.apache.http.**
# The SDK has several references of Apache HTTP client
-dontwarn com.amazonaws.http.**
-dontwarn com.amazonaws.metrics.**
-dontwarn com.amazonaws.mobileconnectors.cognitoauth.** -dontwarn com.amazonaws.mobile.auth.**
# BlurView
-keep class android.support.v8.renderscript.** { *; }
-keep class androidx.renderscript.** { *; }
# Firebase
-keep public class com.google.firebase.analytics.FirebaseAnalytics {
public *;
}
-keep public class com.google.android.gms.measurement.AppMeasurement {
public *;
}
# Realm Data Class
-keep class io.realm.internal.Keep
-keep,includedescriptorclasses @io.realm.internal.Keep class * { *; }