แนวทางของการเขียน android app ให้ปลอดภัย - Android App security best practices [2020]


Enforce secure communication

Use implicit intents and non-exported content providers


Show an app chooser

ถ้า implicit intent สามารถเรียกเปิด แอพที่2
บนอุปกรณ์ของผู้ใช้
ให้ทำ app chooser วิธีนี้ช่วยให้ผู้ใช้โอนถ่าย sensitive information ไปยัง appที่เขาไว้วางใจ


val intent = Intent(ACTION_SEND)
val possibleActivitiesList: List<ResolveInfo> = queryIntentActivities(intent, PackageManager.MATCH_ALL)


// Verify that an activity in at least two apps on the user's device
// can handle the intent. Otherwise, start the intent only if an app
// on the user's device can handle the intent.


if (possibleActivitiesList.size > 1) {

    // Create intent to show chooser.
    // Title is something similar to "Share this photo with".

    val chooser = resources.getString(R.string.chooser_title).let { title ->
        Intent.createChooser(intent, title)
    }
    startActivity(chooser)
} else if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
}





Apply signature-based permissions


<permission    
   android:name="com.example.ak1.PREM"    
   android:protectionLevel="signature" />  // กำหนดให้สามารถเข้าถึงได้เฉพาะ App ที่สร้างโดย Keystore เดียวกัน

เพิ่มเติม : android:protectionLevel


ตัวอย่างการนำมาใช้กับ broadcast receiver

<receiver    
  android:name=".OnWifiEnabled"    
  android:permission="com.example.ak1.PERM">    
     <intent-filter>        
       <action android:name="android.net.wifi.STATE_CHANGE" />    
     </intent-filter>
</receiver>


อันตรายจากการไม่ใช้  Custom Permission
https://iak1.blogspot.com/2015/09/android-intent-spoofing.html



Disallow access to your app's content providers

ถ้าคุณตั้งใจจะส่งข้อมูลจากแอพคุณไปยังแอพอื่น (ที่ไม่ใช่แอพของคุณ)
ไม่ควรอนุญาติให้ app ตัวอื่นเข้าถึง contentProvider ที่แอพคุณมี
หากแอพของคุณสามารถติดตั้งบน android 4.1.1 (api level 16)
หรือต่ำกว่านั้น android:exported attribute จะเป็น "true" by default

<provider
  android:name="android.support.v4.content.FileProvider"
  android:authorities="com.example.myapp.fileprovider"
       ...
  android:exported="false">
  <!-- Place child elements of <provider> here. -->
</provider>

แล้วถ้าไม่ประกาศ exported=false ก็จะเจอกับ Exploiting Content Provider 
(SQL Injection)




Ask for credentials before showing sensitive information

เมื่อขอข้อมูลประจำตัวจากผู้ใช้เพื่อให้สามารถเข้าถึง sensitive information หรือ premium content ในแอพ ให้ขออย่างใดอย่างนึงเช่น Pin / Password / Pattern หรือ biometric credential เช่น fingerprint

ตัวอย่างการทำ Biometric Authen ทั้งแบบ kotlin และ javaได้ที่ https://developer.android.com/topic/security/best-practices#ask-for-credentials


System dialog requesting biometric authentication



Apply network security measures


Use SSL traffic

ถ้า app ของคุณมีการติดต่อกับ web server ที่มี certificateที่ออกโดย บริษัทที่มีชื่อเสียงและเชื่อถือได้
ใช้ https request แบบ simple simple แบบนี้

val url = URL("https://www.google.com")
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.connect()
urlConnection.inputStream.use {
    ...
}



Add a network security configuration

ถ้า app ของคุณใช้ custom CAs
ก็ยังสามารถทำการตั้งค่า network's security settings ได้ใน configuration file

วิธีการสร้าง configuration โดยไม่ต้องแก้ไขโค้ดในแอพ

1. Declare the configuration in your app's manifest:

<manifest ... >
    <application
        android:networkSecurityConfig="@xml/network_security_config"
        ... >
        <!-- Place child elements of <application> element here. -->
    </application>
</manifest>


2.เพิ่ม XML resource file ไว้ใน res/xml/network_security_config.xml
ระบุว่าให้ traffic ทั้งหมดอยู่บนโดเมนเฉพาะ ควรใช้ HTTPS มาปิดการใช้งาน clear-text


<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">secure.example.com</domain>
        ...
    </domain-config>
</network-security-config>


3.ระบุใน network security configuration XML file:

<network-security-config>
    <debug-overrides>
        <trust-anchors>
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

อ่านแบบละเอียดๆ - Network security configuration
https://developer.android.com/training/articles/security-config.html



Create your own trust manager

SSL ของคุณไม่ควรยอมรับทุกใบรับรอง
คุณอาจต้องการ setup trust manager และ handle ssl warning ที่เกิดขึ้น
หากมีเงื่อนไขใดๆต่อไปนี้ใน use case

- คุณกำลังติดต่อกับ web server ที่มี certificate ที่ signed โดย custom CA
- CA ที่ไม่น่าเชื่อถือจาก device ที่คุณกำลังใช้
- คุณไม่สามารถใช้ network security configuration.

To learn more about how to complete these steps, see discussion about handling an : unknown cerificate authority.

ข้อมูลที่เกี่ยวข้อง :





Provide the right permissions


Use intents to defer permissions

เป็นไปได้คุณไม่ควรเพิ่ม permission
เพื่อดำเนินการอะไรบางอย่างให้เสร็จสมบูรณ์ในแอพอื่น
ใช้ intent เพื่อ defer request ไปยังแอพที่ต่างกันที่มี permission อยู่แล้ว

ตัวอย่างการใช้ intent เพื่อพาผู้ใช้ไปยัง Contacts app
แทนการร้องขอ READ_CONTACTS and WRITE_CONTACTS permissions:

// Delegates the responsibility of creating the contact to a contacts app,
// which has already been granted the appropriate WRITE_CONTACTS permission.

Intent(Intent.ACTION_INSERT).apply {
    type = ContactsContract.Contacts.CONTENT_TYPE
}.also { intent ->
    // Make sure that the user has a contacts app installed on their device.
    intent.resolveActivity(packageManager)?.run {
        startActivity(intent)
    }
}


นอกจากนี้, ถ้า app ของคุณต้องการดำเนินการ file-based I/O เช่นการเข้าถึงที่เก็บข้อมูล
หรือการเลือกไฟล์ต่างๆ, มันไม่ต้องการ Special permissions เพราะว่าระบบสามารถดำเนินการให้เสร็จสมบูรณ์อยู่แล้ว


Share data securely across apps

  • การ share app content กับแอพอื่นๆในแบบที่ปลอดภัย 
  • ใช้ read-only หรือ write-only permissions ตามความจำเป็น
  • จัดการ one-time access to data โดยใช้ FLAG_GRANT_READ_URI_PERMISSION และ FLAG_GRANT_WRITE_URI_PERMISSION flags.



Store data safely

Store private data within internal storage

เก็บข้อมูลส่วนตัวของผู้ใช้ทั้งหมดไว้ใน
devicei internalstorage ที่มี sanboxed แต่ละ app.

แอพของคุณไม่ต้องการ request permission เพื่อดูไฟล์เหล่านี้, และแอพอื่นๆไม่สามารถเข้าถึงไฟล์ เป็นการรักษาความปลอดภัยที่เพิ่มเข้ามา

เมื่อ user uninstall app ... device จะลบทุกไฟล์ที่แอพเซฟไว้ใน internal storage

// Creates a file with this name, or replaces an existing file that has the same name. 
// Note that the file name cannot contain path separators.

val FILE_NAME = "sensitive_info.txt"
val fileContents = "This is some top-secret information!"

openFileOutput(FILE_NAME, Context.MODE_PRIVATE).use { fos ->
    fos.write(fileContents.toByteArray())
}


Use external storage cautiously

โดย default, android system ไม่บังคับใช้ข้อจำกัดด้านความปลอดภัยบนข้อมูลที่อยู่ใน external storage, และการจัดเก็บข้อมูลไม่รับประกัณว่าจะสามารถเชื่อมต่อกับอุปกรณ์ได้ (External storage directory แต่ละเครื่องไม่เหมือนกัน), คุณควรใช้มาตรการความปลอดภัยต่อไปนี้ เพื่อให้การเข้าถึงข้อมูลอย่างปลอดภัยใน external storage

        > Use scoped directory access
ถ้า app ของคุณต้องการ เข้าถึงเฉพาะ directory ที่กำหนดใน external storage ของอุปกรณ์,
คุณสามารถใช้ scoped directory access เพื่อจำกัด app ที่จะเข้าถึงที่เก็บข้อมูล external storage ของ device ....เพื่อความสะดวกแก่ผู้ใช้ app คุณควร save directory access URI เพื่อให้ผู้ใช้ไม่จำเป็นต้องอนุมัติการเข้าถึง directory ทุกครั้งที่แอพของคุณพยาม access มัน

Note* ถ้าคุณใช้ scoped directory access กับ directory ที่เฉพาะเจาะจงใน external storage, ผู้ใช้อาจจะลบกันได้ดังนั้นควรจะมี logic ที่มาควบคุมโดยใช้ Environment.getExternalStorageState()
จะ return value ที่เป็น พฤติกรรมของผู้ใช้

The following code snippet uses scoped directory access with the pictures directory
within a device's primary shared storage:


private const val PICTURES_DIR_ACCESS_REQUEST_CODE = 42

...

private fun accessExternalPicturesDirectory() {
    val intent: Intent = (getSystemService(Context.STORAGE_SERVICE) as StorageManager)
            .primaryStorageVolume.createAccessIntent(Environment.DIRECTORY_PICTURES)
    startActivityForResult(intent, PICTURES_DIR_ACCESS_REQUEST_CODE)
}

...

override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
    if (requestCode == PICTURES_DIR_ACCESS_REQUEST_CODE && resultCode == Activity.RESULT_OK) {

        // User approved access to scoped directory.
        if (resultData != null) {
            val picturesDirUri: Uri = resultData.data

            // Save user's approval for accessing this directory
            // in your app.
            contentResolver.takePersistableUriPermission(
                    picturesDirUri,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION
            )
        }
    }
}

คำเตือน !! อย่าส่งค่า null เข้าไปใน createAccessIntent() เกินความจำเป็นเพราะนี้จะทำให้แอปของคุณเข้าถึงปริมาณทั้งหมดที่ StorageManager ค้นหาแอปของคุณ



      > Check validity of data

ถ้าแอปของคุณใช้ข้อมูลจาก external storage, ต้อง make sure ว่า contents ของข้อมูล
ไม่ได้รับความเสียหาย หรือ modified แอปของคุณควรจะมี logic เพื่อจัดการความถูกต้องด้วย

The following example shows the permission and logic that check a file's validity:

<manifest ... >

    <!-- Apps on devices running Android 4.4 (API level 19) or higher cannot
         access external storage outside their own "sandboxed" directory, so
         the READ_EXTERNAL_STORAGE (and WRITE_EXTERNAL_STORAGE) permissions
         aren't necessary. -->

    <uses-permission
          android:name="android.permission.READ_EXTERNAL_STORAGE"
          android:maxSdkVersion="18" />
    ...
</manifest>



MyFileValidityChecker

private val UNAVAILABLE_STORAGE_STATES: Set<String> =
        setOf(MEDIA_REMOVED, MEDIA_UNMOUNTED, MEDIA_BAD_REMOVAL, MEDIA_UNMOUNTABLE)
...
val ringtone = File(getExternalFilesDir(DIRECTORY_RINGTONES), "my_awesome_new_ringtone.m4a")
when {
    isExternalStorageEmulated(ringtone) -> {
        Log.e(TAG, "External storage is not present")
    }
    UNAVAILABLE_STORAGE_STATES.contains(getExternalStorageState(ringtone)) -> {
        Log.e(TAG, "External storage is not available")
    }
    else -> {
        val fis = FileInputStream(ringtone)

        // available() determines the approximate number of bytes that
        // can be read without blocking.

        val bytesAvailable: Int = fis.available()
        val fileBuffer = ByteArray(bytesAvailable)
        StringBuilder(bytesAvailable).apply {
            while (fis.read(fileBuffer) != -1) {
                append(fileBuffer)
            }

            // Implement appropriate logic for checking a file's validity.

            checkFileValidity(this)
        }
    }
}



Store only non-sensitive data in cache files

เพื่อให้เข้าถึงได้เร็วขึ้น non-sensitive app data ควรเก็บมันไว้ใน device's cache.
สำหรับ cache ที่ใหญ่กว่า 1MB ใช้ getExternalCacheDir() หรือใช้ getCacheDir()


The following code snippet shows how to cache a file that your app recently downloaded:

val cacheFile = File(myDownloadedFileUri).let { fileToCache ->
    File(cacheDir.path, fileToCache.name)
}



Use SharedPreferences in private mode

อันนี้คือ basic อยู่แล้วขอข้ามคำอธิบายไป แต่แนะนำว่า
ถ้าต้องการแชร์ data ข้าม app ห้ามใช้ SharedPreferences
ให้ย้อนไปอ่าน share data securely across apps.

เพิ่มเติม : http://trymydroid.blogspot.com/2017/06/hack-android-hack-sharedpreference.html

Update*
https://trymydroid.blogspot.com/2020/01/encryptedsharedpreferences-security.html



Keep services and dependencies up-to-date


Check the Google Play services security provider
section นี้ใช้กับ app ที่มีการใช้งาน google play service เท่านั้น


Update all app dependencies
ก่อนการ deploy app ต้อง make sure ก่อนว่า  libraries, SDKs, และ dependencies อื่นๆ อัพเดทเป็นเวอร์ชั่นล่าสุด

ตัวอย่างเช่น Android SDK ใช้ tool บน Android Studio อย่าง SDK Manager ที่มา update ให้
หรืออีกตัวอย่าง third-party dependencies ให้เช็คที่ website ของ libraries ที่ app เพื่ออัพเดท security patches



...........................SUMMARY............................


-  Signature-basedpermissions

- Disallow access to your app's content providers

- ทำ app chooser ช่วยให้ผู้ใช้สามารถเลือกโอนถ่าย sensitive information  ไปยัง appที่เขาไว้วางใจ

- เพื่อให้สามารถเข้าถึง sensitive information หรือ premium content ในแอพ ให้สอบถาม
Password / Pattern หรือ biometric credential เช่น fingerprint


- Use SSL traffic

  • ถ้า app ของคุณมีการติดต่อกับ web server ที่มี certificateที่ออกโดย บริษัทที่มีชื่อเสียงและเชื่อถือได้ 
  • ถ้า app ของคุณใช้ custom CAsก็ยังสามารถทำการตั้งค่า network's security settings ได้ใน configuration file

การใช้ intent เพื่อพาผู้ใช้ไปยัง Contacts app แทน READ_CONTACTS and WRITE_CONTACTS permissions

  • การ share app content กับแอพอื่นๆในแบบที่ปลอดภัย 
  • ใช้ read-only หรือ write-only permissions ตามความจำเป็น
  • จัดการ one-time access to data โดยใช้ FLAG_GRANT_READ_URI_PERMISSION และ FLAG_GRANT_WRITE_URI_PERMISSION flags.

- เก็บข้อมูลส่วนตัวของผู้ใช้ทั้งหมดไว้ใน device internal storage ที่มี sanboxed แต่ละ app.

  • การเข้าถึงข้อมูลอย่างปลอดภัยใน external storage
  • Use scoped directory access
  • Environment.getExternalStorageState() จะ return value ที่เป็น พฤติกรรมของผู้ใช้

- ถ้าแอปของคุณใช้ข้อมูลจาก external storage,
ต้อง make sure ว่า contents ของข้อมูลไม่ได้รับความเสียหาย หรือ modified แอปของคุณควรจะมี logic เพื่อจัดการความถูกต้องด้วย

- non-sensitive app data ควรเก็บมันไว้ใน device's cache. สำหรับ cache ที่ใหญ่กว่า 1MB ใช้ getExternalCacheDir() หรือใช้ getCacheDir()

  • Check the Google Play services security provider
  • Update all app dependencies


Ref: https://developer.android.com/topic/security/best-practices