Bảo mật code Android

I. Giới thiệu

1. Tại sao phải bảo mật

Android shield

  • Nếu bạn không bảo mật code của bạn thì khi bạn đưa thư viện (aar, jar) cho người khác (bên thứ 3) hoặc publish ứng dụng lên store thì hacker (một dev android khác thôi ^^) có thể lấy apk của bạn và giải mã ra toàn bộ nhằm những mục đích xấu (clone ứng dụng để bán, xem cấu trúc, source code, xem api, các key bảo liên lạc api server của bạn chẳng hạn)

  • Đảm bảo sự riêng tư và toàn vẹn thông tin cá nhân của bạn

  • Để thể hiện bạn là một lập trình viên cẩn thận, đáng tin

Và còn rất rất nhiều lý do khác nữa, mà bạn có thể xem thêm ở đây. http://www.infoworld.com/article/2610857/security/protect-your-source-code-before-it-s-too-late.html

2. Làm thế nào để bảo mật

How to

Có rất nhiều các để bảo vệ code của bạn khỏi các mối xâm hại bên ngoài như mã hóa code (proguard - obfuscate, code = NDK, sử dụng các thuật toán để mã hóa các chuỗi string bí mật trong code...)

Ở trong bài này mình sẽ giới thiệu với các bạn một vài cách để bảo vệ code của bạn từ cơ bản nhất đến nâng cao.

Có nhiều bạn hỏi làm điều này có khó không, dung lượng ứng dụng sau khi mã hóa có bị tăng lên không?
Mình xin trả lời rằng: tùy từng trường hợp mà khó hay dễ, còn dung lượng thì chắc chắn là tăng, dùng mã hóa = java thì tăng ~ 100kb, dùng openssl thì tăng ~3mb cho một thư viện nhé :D

II. Mã hóa

1. Sử dụng proguard có sẵn trong android

Với cách này thì bạn chỉ cần enable chức năng này và định nghĩa các file cần proguard obfuscation mà thôi.

Eclipse thì bạn thêm dòng này vào file project.properties: proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt

Eclipse

Android studio thì trong file build.gradle: bạn enable dòng minifyEnabled false -> minifyEnabled true

Android studio

Sau đó thì bạn định nghĩa quy tắc proguard trong file: proguard-project.txt hoặc proguard-rules.pro ... trong ứng dụng của bạn nhé.

Đây là file mẫu trong ứng dụng demo này:

-printmapping mapping.txt
-verbose
-dontoptimize
-dontpreverify
-dontshrink
-dontskipnonpubliclibraryclassmembers
-dontusemixedcaseclassnames
-keepparameternames
-renamesourcefileattribute SourceFile
-keepattributes *Annotation*
-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod

-keep class * extends android.app.Activity
-assumenosideeffects class android.util.Log {
    public static *** d(...);
    public static *** v(...);
}

-keep class com.facebook.** { *; }
-keep class com.androidquery.** { *; }
-keep class com.google.** { *; }
-keep class org.acra.** { *; }
-keep class org.apache.** { *; }
-keep class com.mobileapptracker.** { *; }
-keep class com.nostra13.** { *; }
-keep class net.simonvt.** { *; }
-keep class android.support.** { *; }
-keep class com.nnacres.app.model.** { *; }
-keep class com.facebook.** { *; }
-keep class com.astuetz.** { *; }
-keep class twitter4j.** { *; }
-keep class com.actionbarsherlock.** { *; }
-keep class com.dg.libs.** { *; }
-keep class android.support.v4.** { *; }
-keep class com.bluetapestudio.templateproject.** { *; }
-keep class com.yourideatoreality.model.** { *; }
-keep interface com.yourideatoreality.model.** { *; }
-keep class com.bluetapestudio.** { *; }
-keep interface com.bluetapestudio.** { *; }
# Suppress warnings if you are NOT using IAP:
-dontwarn com.nnacres.app.**
-dontwarn com.androidquery.**
-dontwarn com.google.**
-dontwarn org.acra.**
-dontwarn org.apache.**
-dontwarn com.mobileapptracker.**
-dontwarn com.nostra13.**
-dontwarn net.simonvt.**
-dontwarn android.support.**
-dontwarn com.facebook.**
-dontwarn twitter4j.**
-dontwarn com.astuetz.**
-dontwarn com.actionbarsherlock.**
-dontwarn com.dg.libs.**
-dontwarn  com.bluetapestudio.templateproject.**

-keepattributes Signature

# For using GSON @Expose annotation
-keepattributes *Annotation*

# Gson specific classes
-keep class sun.misc.Unsafe { *; }
#-keep class com.google.gson.stream.** { *; }

# The official support library.
-keep class android.support.v4.app.** { *; }
-keep interface android.support.v4.app.** { *; }

#  Library JARs.
#-keep class de.greenrobot.dao.** { *; }
#-keep interface de.greenrobot.dao.** { *; }
# Library projects.
-keep class com.actionbarsherlock.** { *; }
-keep interface com.actionbarsherlock.** { *; }
#Keep native
-keepclasseswithmembernames class * {
    native <methods>;
}

Với cách này thì những class, method cần được proguard sẽ bị chuyển tên thành a, b, d, aa... -> để undex java đọc code thì sẽ rất tốn time (với những thanh niên lầy level max ngồi decompile ra thành file đọc hiểu được thì mình cũng bó tay luôn)

Các bạn xem thêm:

http://proguard.sourceforge.net/

http://proguard.sourceforge.net/manual/examples.html

2. Sử dụng AES để cipher hoặc decipher theo keystore

Cipher

Thường thì khi chúng ta build debug hay sign apk đều cần đến 1 cái keystore (thường là default.keystore apt tự generate lúc mới cài đặt và nằm trong ~home/.android/debug.keystore).
Còn khi chúng ta sign apk thì cần đến key mà chúng ta đã tạo ra ở đây https://developer.android.com/studio/publish/app-signing.html

Và Key này là duy nhất đi theo máy lúc tạo ra, nghĩa là key của bạn sẽ không trùng với bất kỳ người nào khác nữa -> dựa vào điều này chúng ta có thể dùng để bảo vệ các đoạn mã bí mật trong app của mình.

     /**
     * Encrypt data
     *
     * @param secretKey
     *            - a secret key used for encryption
     * @param data
     *            - data to encrypt
     * @return Encrypted data
     * @throws Exception
     */
    @SuppressLint("TrulyRandom")
    public String cipher(Context context, String data) throws Exception {
        String secretKey = SecureUtil.getHashKey(context);
        SecretKeyFactory factory = SecretKeyFactory.getInstance(secure.secretType());
        KeySpec spec = new PBEKeySpec(secretKey.toCharArray(), secretKey.getBytes(), 128, 256);
        SecretKey tmp = factory.generateSecret(spec);
        SecretKey key = new SecretKeySpec(tmp.getEncoded(), secure.cipher());
        Cipher cipher = Cipher.getInstance(secure.cipher());
        cipher.init(Cipher.ENCRYPT_MODE, key);
        String cipherKey = SecureUtil.toHex(secure.hex(), cipher.doFinal(data.getBytes()));
        byte[] encoded = secure.aes(cipherKey.getBytes(SecureUtil.UTF_8), AESMode.CIPHER.getValue());
        return SecureUtil.bytes2HexStr(encoded);
    }

    /**
     * Decrypt data
     *
     * @param data
     *            - data to decrypt
     * @return Decrypted data
     * @throws Exception
     */
    public String decipher(Context context, String data) throws Exception {
        byte[] encoded = SecureUtil.hexStr2Bytes(data);
        encoded = secure.aes(encoded, AESMode.DECIPHER.getValue());
        data = new String(encoded, SecureUtil.UTF_8);
        SecretKeyFactory factory = SecretKeyFactory.getInstance(secure.secretType());
        String secretKey = SecureUtil.getHashKey(context);
        KeySpec spec = new PBEKeySpec(secretKey.toCharArray(), secretKey.getBytes(), 128, 256);
        SecretKey tmp = factory.generateSecret(spec);
        SecretKey key = new SecretKeySpec(tmp.getEncoded(), secure.cipher());

        Cipher cipher = Cipher.getInstance(secure.cipher());

        cipher.init(Cipher.DECRYPT_MODE, key);

        return new String(cipher.doFinal(SecureUtil.toByte(data)));
    }

Và lấy key SHA theo keystore của app:

        static String getHashKey(Context context) {
        Context mAppContext = context.getApplicationContext();
        try {
            PackageInfo info = mAppContext.getPackageManager().getPackageInfo(mAppContext.getPackageName(),
                    PackageManager.GET_SIGNATURES);
            String sig = null;
            Signature signature = info.signatures[0];
            MessageDigest md = MessageDigest.getInstance("SHA");
            md.update(signature.toByteArray());
            sig = Base64.encodeToString(md.digest(), Base64.DEFAULT);
            Toast.makeText(context, sig, Toast.LENGTH_LONG).show();
            return sig;
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

Vậy là các bạn đã được một chuỗi key bí mật từ chính keystore của mình, người khác chỉ giải mã được đoạn key này khi họ có keystore, alias, password của bạn mà thôi.

3. Mã hóa bằng NDK

Nếu sau khi proguard và dùng keystore ở trên vẫn chưa thỏa mãn được yêu cầu bảo mật của bạn, vì thực ra SHA của app cũng khá là dễ lấy với những người biết dexJava và có một chút hiểu biết về keystore thì bạn có thể tham khảo cách này nữa: Dùng C, C++ từ android NDK để mã hóa key bí mật của bạn

Ở đây mình dùng thuật toán cipher, decipher AES

JNIEXPORT jbyteArray JNICALL Java_com_manhnv_hidepassword_Secure_aes(
        JNIEnv *env, jobject javaThis, jbyteArray jarray, jint jmode) {
    //check input data
    unsigned int len = (unsigned int) env->GetArrayLength(jarray);
    if (len <= 0 || len >= MAX_LEN) {
        return NULL;
    }

    unsigned char *data = (unsigned char*) env->GetByteArrayElements(jarray,
    NULL);
    if (!data) {
        return NULL;
    }

    //(DESede/CBC/PKCS5Padding)
    unsigned int mode = (unsigned int) jmode;
    unsigned int rest_len = len % AES_BLOCK_SIZE;
    unsigned int padding_len = (
            (ENCRYPT == mode) ? (AES_BLOCK_SIZE - rest_len) : 0);
    unsigned int src_len = len + padding_len;

    unsigned char *input = (unsigned char *) malloc(src_len);
    memset(input, 0, src_len);
    memcpy(input, data, len);
    if (padding_len > 0) {
        memset(input + len, (unsigned char) padding_len, padding_len);
    }

    //env->ReleaseByteArrayElements(jarray, data, 0);

    unsigned char * buff = (unsigned char*) malloc(src_len);
    if (!buff) {
        free(input);
        return NULL;
    }
    memset(buff, src_len, 0);

    //set key & iv
    unsigned int key_schedule[AES_BLOCK_SIZE * 4] = { 0 }; //>=53(这里取64)
    aes_key_setup(AES_KEY, key_schedule, AES_KEY_SIZE);

    if (mode == ENCRYPT) {
        aes_encrypt_cbc(input, src_len, buff, key_schedule, AES_KEY_SIZE,
                AES_IV);
    } else {
        aes_decrypt_cbc(input, src_len, buff, key_schedule, AES_KEY_SIZE,
                AES_IV);
    }

    if (ENCRYPT != mode) {
        unsigned char * ptr = buff;
        ptr += (src_len - 1);
        padding_len = (unsigned int) *ptr;
        if (padding_len > 0 && padding_len <= AES_BLOCK_SIZE) {
            src_len -= padding_len;
        }
        ptr = NULL;
    }

    jbyteArray bytes = env->NewByteArray(src_len);
    env->SetByteArrayRegion(bytes, 0, src_len, (jbyte*) buff);

    free(input);
    free(buff);

    return bytes;
}

Như ở đây mình sẽ dùng SHA làm key để mã hóa chuổi bảo mật của mình sau đó chuỗi này sẽ được mã hóa lần thứ 2 ở dưới C = method eas.

Mình nghĩ code ở dạng này đã được coi là bảo mật 70% rồi ạ. Còn các bạn mà vẫn chưa yên tâm thì tiếp tục coi bên dưới nhé.

4. Mã hóa chuỗi bằng OpenSSL và NDK

OpenSSL

Còn 1 cách bảo mật hơn nữa là kết hợp NDK ở trên + 1 key session lấy qua API.

Phần liên quan đến OpenSSL thì được mô tả khá là chi tiết ở kipablog vậy các bạn vô đây xem luôn nhé:

http://kipalog.com/posts/Tim-hieu-ve-android-ndk-va-openssl--phan-1

http://kipalog.com/posts/Android-NDK-va-OpenSSL-Phan-2

Thư viện prebuilt openssl android thì bạn có thể download ở đây:

https://github.com/emileb/OpenSSL-for-Android-Prebuilt

Note:

  • Với 2 phương pháp đầu thì dung lượng APK gần như không thay đổi
  • Với phương pháp thứ 3, dùng NDK thì APK size tăng ~100kb
  • Phương pháp openssl để mã hóa thì dung lượng sẽ tăng ~3mb (@@)
  • Các bạn nên tạo 2 thư viện: Locker & Key, trong đó locker chính là app cần mã hóa, còn key thì được tạo ra trên 1 app khác, khi tạo được key rồi thì lưu lại và dùng để mở app locker.
    Trong thư viện demo mình viết thì để tiện mình đã gộp chung tất cả với nhau nhưng đã được đặt tên để dễ tách rồi nhé.

III. Tổng kết

Theo những tài liệu mình đã đọc và nghiên cứu thì không có cách nào bảo mật source code android hoàn hảo cả mà mọi phương pháp của các developer hay ngay cả chính google là bảo mật tối đa, làm giảm việc lộ thông tin một cách tối thiểu nhất mà thôi.

Source code tham khảo: GITHUB Link

App để các bạn decompile thử: APP tham khảo