android安全开发_现代android安全开发

android安全开发

第一次(错误)练习 (1st (Mistake) Practice)

Given the “find out how to en-decrypt data in Android” requirement, what would you do?

考虑到“查找如何在Android中加密数据”的要求,您会怎么做?

Unless you are a security expert or developer with a security career dedication that writing the cryptographic code from scratch is just a trivial task, it’s very common for us to start “Googling”, deep dive in “Stack Overflow” to find security-related implementation answers until we probably find below similar snippet:

除非您是安全专家或致力于安全事业的开发人员,否则从头开始编写密码代码只是一件微不足道的工作,否则我们通常会开始“ Google搜寻”,深入研究“ Stack Overflow”以查找与安全相关的实现答案,直到我们可能在下面找到相似的代码段为止:

问题: (Problem:)

So what‘s wrong with the snippet? The encrypt and decrypt APIs look solid and have our work done, don’t they?

那么这段代码怎么了? encryptdecrypt API看起来很可靠,我们的工作完成了,不是吗?

Well, there’s absolutely nothing wrong with that argument. The interchangeability between the plain and the cipher is proven goes smoothly. But the such old, common, and unsafe to use of the snippet is the most concerning issue we’re really facing now. That issue factors could be the reason that our thought-have-secured assets might be available or easily accessible to an attacker.

好吧,这种说法绝对没有错。 证明了普通密码和密码之间的互换性进行得很顺利。 但是,使用如此古老,常见且不安全的代码段是我们现在真正面临的最令人关注的问题。 这个问题因素可能是我们的思想安全资产可能可供攻击者使用或容易获得的原因。

第二次(错误)练习 (2nd (Mistake) Practice)

Let’s have a short scenario:

让我们有一个简短的场景:

Backend engineer: “Keep this JWT token, will you? We need it for some later authorizations.”

后端工程师:“保留这个JWT令牌,好吗? 我们需要它以备日后使用。”

Probably us: “No problem (better say hi to SharedPreferences now).”

可能是我们:“没问题(现在最好向SharedPreferences打个招呼)。”

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="user_token">
        eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
        eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
        SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    </string>
</map>

问题: (Problem:)

By having a rooted phone/device, please note that everyone literally could access our app XML preference file, located inside the internal storage. Talking about JWT token, the token is literally composed of algorithm-token header type, payload, and verify signature parts, each concatenated by . . And based on this recommended writing,

请注意,通过扎根手机/设备,每个人都可以访问内部存储内的我们的应用XML首选项文件。 谈到JWT令牌,令牌实际上是由算法令牌头类型, 有效负载验证签名部分组成的,每个部分由组成. 。 根据这份推荐的文字,

You might want to read off what are some potential vulnerabilities, how the attackers can forge, manipulate the token parts, and log in as someone else. The only moral story here we can learn to help to prevent such an issue is to not store the token in raw.

您可能想要阅读一些潜在的漏洞,攻击者如何伪造,操纵令牌部分以及以其他人身份登录。 在这里,我们唯一可以防止发生此类问题的道德故事就是不要将令牌原始存储。

FAQ:

常问问题:

“But wait a minute… how are we supposed to act now? We understand if we could encrypt the token before storing it in the preference file or somewhere else should satisfy the issue prevention. But the API we use for the encryption scheme is issued on its own as well at the 1st problem.”

“但是等一下……我们现在应该怎么做? 我们知道在将令牌存储到首选项文件之前是否可以对令牌进行加密,或者应该满足防止问题的其他地方。 但是,我们用于加密方案的API也是在第一个问题上自行发布的。”

Well, if we could have our own set of security arsenal that we can rely on, I personally can state, just go ahead. However, if we have less knowledge of any robust cryptographic algorithm or looking for an official and standardized solution, we may want to refer to the next section.

好吧,如果我们可以拥有一套可以依靠的安全工具库,我个人可以声明,那就继续吧。 但是,如果我们对任何健壮的密码算法了解较少,或者正在寻找正式的标准化解决方案,则可能需要参考下一部分。

Jetpack安全 (Jetpack Security)

Or “JetSec” for short, introduced at last Android Dev Summit 2019 provides us a high-level abstraction to allow encrypting data, file, until shared preferences easily without having to really understand the ins and outs of security.

或在上次Android Dev Summit 2019上推出的简称“ JetSec”为我们提供了一个高级抽象,允许您轻松加密数据,文件,直到共享首选项,而无需真正了解安全的来龙去脉。

JetSec features Android KeyStore¹ which is the mastermind of every cryptographic operation and we may assume all data secured is done via it. Of course, every secured data associates with a private key which is a primary material used for any cryptographic op. In JetSec, these private keys called keyset. Android KeyStore stores these keyset materials in a container hardware-backed which makes accessing them very hard and it’s not exportable.

JetSec具有Android KeyStore¹ ,这是每个加密操作的策划者,我们可以假设所有受保护的数据都是通过它完成的。 当然,每个受保护的数据都与一个私钥相关联,这是用于任何加密操作的主要材料。 在JetSec,这些私有密钥称为密钥集 。 Android KeyStore将这些密钥集材料存储在硬件支持的容器中,这使得访问它们非常困难且不可导出。

Note: JetSec also employs Tink, a cross-platform, open-source library from Google to leverage the MasterKey concept for securing all keysets.

注意:JetSec还采用了Tink (一种来自Google的跨平台开源库)来利用MasterKey概念来保护所有密钥集。

Gradle Import

Gradle导入

dependencies {
    implementation "androidx.security:security-crypto:$latest_version"
}

Now let’s jump into practical data encryption with JetSec.

现在,让我们进入使用JetSec进行实用的数据加密。

Note: The library requires API 23/Marshmallow at minimum.

注意:该库至少需要API 23 /棉花糖。

I personally wouldn’t mind raising the minSdkVersion bar as a trade-off since some of my projects hold some critical sensitive information.

我个人不介意提高minSdkVersion标准,因为我的一些项目包含一些关键的敏感信息。

MasterKeyBefore leveraging any toolbox from JetSec, the generation of the MasterKey is a prerequisite. Since the minSdkVersion has been set to 23, we’re allowed to use the default AES-based spec for the key generation:

MasterKey在利用JetSec的任何工具箱之前,必须先生成MasterKey 。 由于minSdkVersion已设置为23,因此我们允许使用默认的基于AES的规范来生成密钥:

fun generateMasterKey(context: Context): MasterKey {
    return MasterKey.Builder(context)
        .setKeyGenParameterSpec(AES256_GCM_SPEC)
        .build()
}

Though it’s fine for most use cases, we’re also allowed to customize our own spec to secure some of the sensitive data clusters in advance, in case.

尽管对于大多数用例来说都很好,但也允许我们自定义自己的规范,以防万一提前保护一些敏感数据集群。

fun generateMasterKey(context: Context): MasterKey {
    val advancedSpec = KeyGenParameterSpec.Builder(
        "master_key_name",
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
    ).apply {
          setBlockModes(KeyProperties.BLOCK_MODE_GCM)
          setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
          setKeySize(256)
          setUserAuthenticationRequired(true)
          setUserAuthenticationValidityDurationSeconds(60)
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
              setUnlockDeviceRequired(true)
              setIsStrongBoxBacked(true)
          }
    }.build()
    
    return MasterKey.Builder(context)
        .setKeyGenParameterSpec(advancedSpec)
        .build()
}

EncryptedFile & EncryptedSharedPreferencesI wouldn’t write too far all about these tools, while we can find their functionalities and implementation details here². Here are some results of the encrypted content along the generated keyset stored inside files:

EncryptedFile和EncryptedSharedPreferences这些工具我不会写得太多,尽管我们可以在此处找到它们的功能和实现细节。 这是存储在文件中的加密内容以及生成的密钥集的一些结果:

Image for post
EncryptedFile - “Hello World” in ciphertext
EncryptedFile-密文中的“ Hello World”
Image for post
EncryptedFile - Generated keyset
EncryptedFile-生成的密钥集
Image for post
EncryptedSharedPreferences - JWT “user_token” in ciphertext
EncryptedSharedPreferences-密文中的JWT“ user_token”
Image for post
EncryptedSharedPreferences - Generated keysets
EncryptedSharedPreferences-生成的密钥集

Note: It’s quite interesting when we find that we literally don’t need to provide our own custom private key string either in a master key generation or when taking both EncryptedFile and EncryptedSharedPreferences in some actions. Hence, we won’t be concerned with another issue about how to secure the private key per se, declared inside the source code. Thanks to the Android KeyStore.

注意:当我们发现实际上不需要在主密钥生成中或在某些操作中同时使用EncryptedFileEncryptedSharedPreferences时,不需要提供自己的自定义私钥字符串时,这是非常有趣的。 因此,我们不会担心在源代码中声明的如何保护私钥本身的另一个问题。 多亏了Android KeyStore。

In addition to JetSec, below are other security tools compliment which I equally highly recommend to strengthen the security factors:

除了JetSec,以下是其他安全工具,我同样强烈建议您加强安全因素:

密封对象 (SealedObject)

At some point, we may want to have our serialized objects resided in some untrusted mediums³. JavaX’s SealedObject encapsulates the original object, in serialized format, and encrypts the content to protect its confidentiality.

在某些时候,我们可能希望序列化的对象驻留在某些不受信任的介质 ³中。 JavaX的SealedObject以序列化的格式封装原始对象,并加密内容以保护其机密性。

SealedObject will cover us in order to prevent someone from tampering with our serialized object, but the reconstituted object may be lacking transient fields or other information.

SealedObject将覆盖我们,以防止有人篡改我们的序列化对象,但是重构的对象可能缺少瞬态字段或其他信息。

class ObjectEncryptor {
    private val scheme: String = "AES / CBC / PKCS7Pdding / Whatever..."
    private val cipher: Cipher by lazy { Cipher.getInstance(scheme) }


    fun sealObject(`object`: Serializable, privateKey: String): Serializable {
        val secretKey = SecretKeySpec(privateKey.toByteArray(), scheme)
        cipher.init(ENCRYPT_MODE, secretKey)
      
        return SealedObject(`object`, cipher)
    }
  
    @Throws(IOException::class)
    fun unsealObject(`object`: Serializable, privateKey: String): Serializable {
        val secretKey = SecretKeySpec(privateKey.toByteArray(), scheme)
        val sealedObject = `object` as SealedObject
      
        return sealedObject.getObject(secretKey) as Serializable
    }
}

Let’s break-down:

让我们细分一下:

  • The sealObject API is for encapsulating the original object as the SealedObject contract states. We then finally store the encapsulated Serializable result in a somewhere medium.

    sealObject API用于将原始对象封装为SealedObject合同状态。 然后,我们最终将封装的Serializable结果存储在某个地方的介质中。

  • We decide to retrieve back the serialized object. To verify if the serialization is modified, we testify it via unsealObject API. The API should unwrap back to the original one if the object is proven the same still (of course with the corresponding secret key), else an exception will be thrown.

    我们决定取回序列化的对象。 为了验证序列化是否被修改,我们通过unsealObject API对其进行unsealObject 。 如果该对象仍然被证明是相同的(当然带有相应的密钥),则API应该解开为原始对象,否则将引发异常。

Note: We may consider SignedObject and GuardedObject as part of the compliments.

注意:我们可以将SignedObjectGuardedObject视为补充。

R8 (R8)

“One Shot Two Kills”

“一枪两杀”

It isn’t only reducing the APK/AAB size significantly, R8 contributes to a matter of security context as well. Today, cracking the source code along with embedded resources/assets can be done easily with some simple tools, like the dex2jar converter, Java Decompiler (JD) & friends, even the Android Studio (3.6+) facilitates the APK/AAB analyzer.

R8不仅显着减小了APK / AAB的大小,而且还促进了安全性。 如今,使用一些简单的工具(例如dex2jar转换器,Java Decompiler(JD)和朋友)可以轻松地将源代码与嵌入式资源/资产一起破解,甚至Android Studio(3.6+)都可以简化APK / AAB分析器。

To reduce risk at a minimum of attackers from stealing, recompiling, until publishing back to the store again but with extra profit elements, such as ads, R8’s obfuscation feature does its best to transform any reverse engineering activity much more difficult starting at a point. We can enable R8 via app-level build.gradle file:

为了降低攻击者从窃取,重新编译到再次发布回商店的风险,同时又增加了广告等额外的利润,R8的混淆功能尽其所能地改变了任何逆向工程活动,从一开始就更加困难。 我们可以通过应用程序级别的build.gradle文件启用R8:

android {
    ...
    
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true // optional
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

Now an example, let’s compare the decompiled source code between the one which doesn’t benefit the R8 and the one with R8 enabled:

现在来看一个示例,让我们在不使R8受益的代码和启用R8的代码之间进行反编译的源代码:

package com.modern.security.practice


import android.content.Context
import androidx.security.crypto.EncryptedFile
import androidx.security.crypto.EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
import androidx.security.crypto.MasterKey
import androidx.security.crypto.MasterKeys.AES256_GCM_SPEC
import java.io.File


class FileEncryptor(private val context: Context) {


    fun createMasterKey(): MasterKey {
        return MasterKey.Builder(context)
            .setKeyGenParameterSpec(AES256_GCM_SPEC)
            .build()
    }


    fun createEncryptedFile(fileName: String, masterKey: MasterKey): EncryptedFile {
        val file = File(context.filesDir, fileName)
        return EncryptedFile.Builder(context, file, masterKey, AES256_GCM_HKDF_4KB).build()
    }


    fun readFile(encryptedFile: EncryptedFile, callback: (String) -> Unit) {
        encryptedFile.openFileInput().use { stream ->
            val plainText = stream.bufferedReader().readText()
            callback(plainText)
        }
    }


    fun writeFile(encryptedFile: EncryptedFile, plainText: String) {
        encryptedFile.openFileOutput().use { stream ->
            stream.write(plainText.toByteArray())
        }
    }
}
package com.modern.security.practice;


import android.content.Context;
import androidx.security.crypto.EncryptedFile;
import androidx.security.crypto.MasterKey;
import androidx.security.crypto.MasterKeys;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import kotlin.Metadata;
import kotlin.Unit;
import kotlin.io.TextStreamsKt;
import kotlin.jvm.functions.Function1;
import kotlin.jvm.internal.Intrinsics;
import kotlin.text.Charsets;


@Metadata(bv = {1, 0, 3}, d1 = {"\0006\n\002\030\002\n\002\020\000\n\000\n\002\030\002\n\002\b\002\n\002\030\002\n\000\n\002\020\016\n\000\n\002\030\002\n\002\b\002\n\002\020\002\n\002\b\002\n\002\030\002\n\002\b\003\030\0002\0020\001B\r\022\006\020\002\032\0020\003\006\002\020\004J\026\020\005\032\0020\0062\006\020\007\032\0020\b2\006\020\t\032\0020\nJ\006\020\013\032\0020\nJ\"\020\f\032\0020\r2\006\020\016\032\0020\0062\022\020\017\032\016\022\004\022\0020\b\022\004\022\0020\r0\020J\026\020\021\032\0020\r2\006\020\016\032\0020\0062\006\020\022\032\0020\bR\016\020\002\032\0020\003X\004\006\002\n\000\006\023"}, d2 = {"Lcom/modern/security/practice/FileEncryptor;", "", "context", "Landroid/content/Context;", "(Landroid/content/Context;)V", "createEncryptedFile", "Landroidx/security/crypto/EncryptedFile;", "fileName", "", "masterKey", "Landroidx/security/crypto/MasterKey;", "createMasterKey", "readFile", "", "encryptedFile", "callback", "Lkotlin/Function1;", "writeFile", "plainText", "app_release"}, k = 1, mv = {1, 1, 16})
public final class FileEncryptor {
  private final Context context;
  
  public FileEncryptor(Context paramContext) {
    this.context = paramContext;
  }
  
  public final EncryptedFile createEncryptedFile(String paramString, MasterKey paramMasterKey) {
    Intrinsics.checkParameterIsNotNull(paramString, "fileName");
    Intrinsics.checkParameterIsNotNull(paramMasterKey, "masterKey");
    File file = new File(this.context.getFilesDir(), paramString);
    EncryptedFile encryptedFile = (new EncryptedFile.Builder(this.context, file, paramMasterKey, EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB)).build();
    Intrinsics.checkExpressionValueIsNotNull(encryptedFile, "EncryptedFile.Builder(co);
    return encryptedFile;
  }
  
  public final MasterKey createMasterKey() {
    MasterKey masterKey = (new MasterKey.Builder(this.context)).setKeyGenParameterSpec(MasterKeys.AES256_GCM_SPEC).build();
    Intrinsics.checkExpressionValueIsNotNull(masterKey, "MasterKey.Builder(contex\n            .build()");
    return masterKey;
  }
  
  public final void readFile(EncryptedFile paramEncryptedFile, Function1<? super String, Unit> paramFunction1) {
    Intrinsics.checkParameterIsNotNull(paramEncryptedFile, "encryptedFile");
    Intrinsics.checkParameterIsNotNull(paramFunction1, "callback");
    FileInputStream fileInputStream = paramEncryptedFile.openFileInput();
    Throwable throwable = (Throwable)null;
    try {
      BufferedReader bufferedReader;
      FileInputStream fileInputStream1 = fileInputStream;
      Intrinsics.checkExpressionValueIsNotNull(fileInputStream1, "stream");
      InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream1, Charsets.UTF_8);
      if (inputStreamReader instanceof BufferedReader) {
        bufferedReader = (BufferedReader)inputStreamReader;
      } else {
        bufferedReader = new BufferedReader(bufferedReader, 8192);
      } 
      paramFunction1.invoke(TextStreamsKt.readText(bufferedReader));
      Unit unit = Unit.INSTANCE;
      return;
    } finally {
      paramEncryptedFile = null;
    } 
  }
  
  public final void writeFile(EncryptedFile paramEncryptedFile, String paramString) {
    Intrinsics.checkParameterIsNotNull(paramEncryptedFile, "encryptedFile");
    Intrinsics.checkParameterIsNotNull(paramString, "plainText");
    FileOutputStream fileOutputStream = paramEncryptedFile.openFileOutput();
    Throwable throwable = (Throwable)null;
    try {
      FileOutputStream fileOutputStream1 = fileOutputStream;
      byte[] arrayOfByte = paramString.getBytes(Charsets.UTF_8);
      Intrinsics.checkExpressionValueIsNotNull(arrayOfByte, "(this as java.lang.String).getBytes(charset)");
      fileOutputStream1.write(arrayOfByte);
      Unit unit = Unit.INSTANCE;
      return;
    } finally {
      paramString = null;
    } 
  }
}

Though it’s harder to read now, still, the essences of the source code, like the security schemes that we’re using, AES256_GCM_SPEC or AES256_GCM_HKDF_4KB can still be clearly spotted at the respective 31st and 37th lines which is not good enough. Now let’s look over the one with R8 enabled:

尽管现在很难读懂,但是仍然可以在第31行和第37行分别清楚地看到源代码的本质,例如我们正在使用的安全性方案AES256_GCM_SPECAES256_GCM_HKDF_4KB 。 现在让我们看一下启用了R8的那一个:

No, I’m not pasting the wrong gist. What we’re seeing is valid and I do not say the code is really getting deleted by the R8 but more like it’s obfuscated and inlined to somewhere entry point classes where the invocation of the instance happens, e.g., MainActivity.

不,我没有粘贴错误的要点。 我们所看到的是有效的,我并不是说代码确实被R8删除了,而是更像是将其混淆并内联到某个实例调用发生的入口点类中,例如MainActivity

This new decompiled project structure should make any tracing-based process from a given APK/AAB becomes one step harder. Strictly speaking,

这种新的反编译项目结构应该使给定APK / AAB中任何基于跟踪的过程变得更加困难。 严格来说,

Image for post
Boromir from LOTR
LOTR的Boromir

Note: dex2jar and JD-GUI tools are used to generate above decompiled source code.

注意:dex2jar和JD-GUI工具用于生成上述反编译的源代码。

用例 (Use Case)

Last but not least, this writing also brings some daily use cases (based on experience) to the surface that we might want to put great attention on securing them.

最后但并非最不重要的一点是,本文还将一些日常用例(基于经验)浮出水面,我们可能希望在确保它们的使用上投入大量精力。

Google Cloud API密钥 (Google Cloud API Keys)

AIzaSyD1mil8-JekjpFcsiZnvEUPhAOmUWLabS1

If we integrate some google cloud services, then we probably have this secret API key, and no matter what, we will want to protect the key in a VERY secure way, else the billing will just blow up.

如果我们集成了一些Google云服务,那么我们可能拥有这个秘密的API密钥,无论如何,我们都想以一种非常安全的方式来保护密钥,否则账单就会爆炸。

There are numerous ways we can store the key, like storing it in a C/C++ file using NDK⁴ in the first place or hosting own service provider to keep it. The rest is optional if we welcome JetSec’s EncryptedSharedPreferences for an easier accessing benefit with some security guarantees. We could also add an extra security layer by taking advantage of the GCP’s key restrictions feature.

我们可以通过多种方式存储密钥,例如首先使用NDK⁴将密钥存储在C / C ++文件中,或者托管自己的服务提供商以保留密钥。 如果我们欢迎JetSec的EncryptedSharedPreferences以便在获得一些安全保证的情况下获得更轻松的访问权限,则其余部分是可选的。 我们还可以利用GCP的密钥限制功能来添加额外的安全层。

Image for post
Google Cloud Platform (GCP) key restrictions
Google Cloud Platform(GCP)的主要限制

Lock which platforms can make the requests, fill up the forms, and we can sit relaxed for a moment in case the key leaks.

锁定哪些平台可以提出请求,填写表格,然后我们可以暂时放松一下,以防密钥泄漏。

Google云端服务帐户 (Google Cloud Service Accounts)

{
  "type": "service_account",
  "project_id": "modern-security-practice-123456",
  "private_key_id": "w1854831785d1t4c1949b83dkk0309a5f1ge47e9Z",
  "private_key": "-----BEGIN PRIVATE KEY-----\nIAMEvgIBADBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCN7lqkiKLQsNGh\n2JsafrhzwbBq47OALyegH5LF/uxiy5LWbF3vNN+lm6IcTIpRPDp24r7OlxiJbwHa\n2GvwdrqcTD4dS/PKvkOJtJPV+Lp1Mj+uQmxOW5niazA7Ko4OmFochNbq/ZRG6Dph\nxjslKtd5pG15kaa4WC6EhVnt6QNdIonqzQoUzv4TsaCVa7uSe/JiOfB9qTQVe3Wt\nPU+ucm3hzA9TApc5yMpiPcodCzca2jtQ3jLYNv0ziTOgiIyCYZHqjYlTvFnSjez2\nHGZ6t20jAgMBccECggEAFiKJ+GZ9Klphn8KmCEhaYWeEqe8wqF3yarVx8WKUoMYK\ndVn6fpv7A7vV2V8QtaHUpoUv6E3FnG3NVsZ/ZXxZR9aSub/ypb3E/5kCOdI+Yv9Z\nzX57iOuhsgl9X7Wfj7Bd7HSQVcPmP0GvBGgz0ZHbAhScHXqin+zz4K7i3d0Iy81a\nizGusddwEacItysQtaXjRWS2PB+0oOFkbki418fPsx/HVMoy2DVcjknhxOaBAUDt\nWTAFmQKBLn2h/OJR7dr6yfPOJOJPhDHUxYDl5iGjL0Wynk7wjwcLop7MCq16F2tqI\noeGH1Y+Gk+vbp967xdgH46S+mEiYa9SGfQQarlj7bQKBgQDC8q+bXnfcBLdWToJK\np1oT2o8Sv/8Knef0KzAu2RCH87eWYc4eXg/Udhdp3khkAJZ/MySYuszHAPD5zvN+\njfeLQo9bQR0TZDrzxjYKgf4TJZm6yMvEs/5tJMuJhO6ybACz0xsS+SS4PGn1yGC/\nZUET7cR6v0qLhx/bHyONIYTCPQKBgQC6XiySXGUT20utyiaBxeykOGsP/x3TUOqa\nMrNFzRT5+5NkaK8q36S2SB9rYHRB2T/AYVfAm0LwlWF0e+b7CBNuwHLqZnVFUAhl\n0PeffV4SLGzPk9N7B47r4e1D7Z+jF03AiLP96G3k2wKmIIZhiuFPRhB+F9b1Xfow\nm1ZEfdjC3wKBgQCUrX3cYrnK5RXmDs4znVqHlcJULHWYZUGH6IB22m9PIEK9wC3w\n3w/3m3DT3yEak5Azxo98o8qVY1RlUL5HoIJQhRzMHebM6T5EuMByVx2tAJoz5/vw\nMs4x80P/x5A8Jz6J5tDZMYYdtIWjW4Tds51kU4vriYSS0SUsep0kf5G+iQKBgQCa\n7M6AnO4po5M11WZNWwttOLlHgvx4PjvIE32T+PgHF2tffOag7WkvXqU/zd7XHpIw\nirs4EdmDxapN+vH4nK4K01C0KpgyFmXkveIbY0xgLy4FIQ4cqBvXWuiyH0clfKnV\n884stJsmNCnvj/ol/B1wmP381DSNpKHm5jrAlR0sYQKBgA0gQA9Zdp1MKW2YmzW+\ninPgHexqi4tmD+42sln6eIKA0DGcuWrKrJU3Cgmb8nsCXLbpx7d8YDpLqeWfX2OJ\n8Zi+m67opfKFPFURThKXiySX\n-----END PRIVATE KEY-----\n",
  "client_email": "modern-security-practice@modern-security-practice-123456.iam.gserviceaccount.com",
  "client_id": "12345678901234567890",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/modern-security-practice-139%40modern-security-practice-123456.iam.gserviceaccount.com"
}

This credential is literally an alternative to the previous GCP API keys. As far I can state, certain GCP services, like Translation API, require this credential parameter for some advanced auth ops.

从字面上看,该凭证是先前GCP API密钥的替代。 据我所知,某些GCP服务(例如Translation API)对于某些高级身份验证操作需要此凭据参数。

Same rule as previous, we want to store the JSON first to some trusted mediums where we favor one and having the JetSec’s EncryptedFile which is a better candidate for securing this credential type in advance.

与以前的规则相同,我们希望首先将JSON存储到某些我们希望使用的可信介质中,并拥有JetSec的EncryptedFile ,它是预先保护此凭证类型的更好的选择。

Firebase的google-services.json (Firebase’s google-services.json)

{
  "project_info": {
    "project_number": "123456789012",
    "firebase_url": "https://modern-security-practice-123456.firebaseio.com",
    "project_id": "modern-security-practice-123456",
    "storage_bucket": "modern-security-practice-123456.appspot.com"
  },
  "client": [
    {
      "client_info": {
        "mobilesdk_app_id": "1:123456789012:android:10jlbbg720e1daed160f40",
        "android_client_info": {
          "package_name": "com.bael.modern.security.practice"
        }
      },
      "oauth_client": [
        {
          "client_id": "123456789012-ablgbvb1234gtg7sma3ft9n0j568xx55.apps.googleusercontent.com",
          "client_type": 1,
          "android_info": {
            "package_name": "com.bael.modern.security.practice",
            "certificate_hash": "x88ab56a7c60ef8f1c3fabc8624692c63b86612a"
          }
        },
        {
          "client_id": "123456789012-wo9pmse25s09qpjti7qcf1e5g3q25v42.apps.googleusercontent.com",
          "client_type": 3
        }
      ],
      "api_key": [
        {
          "current_key": "AIzaSyKBlvz6JzP81LH1NOWPH6pJuE-AbcuNtxV"
        }
      ],
      "services": {
        "appinvite_service": {
          "other_platform_oauth_client": [
            {
              "client_id": "123456789012-wo9pmse25s09qpjti7qcf1e5g3q25v42.apps.googleusercontent.com",
              "client_type": 3
            }
          ]
        }
      }
    }
  ],
  "configuration_version": "1"
}

FAQ:

常问问题:

“Hold on a sec… Are you saying we could secure the JSON as well? We just follow every instruction as stated (below) and let the system do the rest for the Firebase initialization.“

“稍等……您是说我们也可以保护JSON吗? 我们只需按照下面所述的每条指令进行操作,然后让系统完成其余的Firebase初始化工作。”

Image for post
Firebase integration steps
Firebase集成步骤

Well, before I’m about to answer “Yes” o̶r̶ ̶”̶N̶o̶”̶, let me show you what could go wrong afterward by the above integration steps:

好吧,在我要回答“是” ̶ “̶N̶o̶” ̶之前,让我向您展示上述集成步骤之后可能出现的问题

Image for post
Exposed firebase project information
公开的Firebase项目信息

So when someone has the APK and decompiles back, we can see the resources.arsc file exposes every embedded resource including our firebase credential information. Judging by this issue, well, in all respects, I personally wouldn’t recommend by following the “official” documented for the firebase integration.

因此,当有人拥有APK并反编译时,我们可以看到resources.arsc文件公开了每个嵌入式资源,包括我们的Firebase凭证信息。 从这个问题来看,从各个方面来看,我个人都不建议遵循Firebase集成中记录的“官方”文档。

Instead, we could do some hacks by extracting the JSON fields, variablizing them, then we can initialize the Firebase manually. Here’s how:

相反,我们可以通过提取JSON字段,对其进行可变化来进行一些修改,然后我们可以手动初始化Firebase。 这是如何做:

  • Don’t remove com.google.gms.google-services plugin that has been set by us in app-level build.gradle. We need it to extract the fields.

    不要删除我们在应用程序级别build.gradle设置的com.google.gms.google-services插件。 我们需要它来提取字段。

  • Run ./gradlew :app:assembleDebug (at least in MacOs) in terminal or via Gradle tab > [project_name] > app > Tasks > other > assembleDebug to extract the fields.

    在终端中或通过Gradle选项卡> [project_name] > app > Tasks > other > assembleDebug运行./gradlew :app:assembleDebug (至少在MacO中),以提取字段。

  • Now we can remove the plugin along the classpath "com.google.gms:google-services:$whatever_version" set in project-level build.gradle.

    现在,我们可以沿着在项目级别build.gradle设置的classpath "com.google.gms:google-services:$whatever_version"删除该插件。

  • The extracted result is mapped to an XML file as shown below:

    提取的结果将映射到XML文件,如下所示:
Image for post
Located in: ../app/build/generated/res/google-services/debug/values/values.xml
位于:../ app / build /生成/res/google-services/debug/values/values.xml
  • Copy them and we can initialize the firebase manually.

    复制它们,我们可以手动初始化firebase。

Note: By owning these constants, now we can store somewhere and secure them with any fitted security tool we have mastered.

注意:通过拥有这些常量,现在我们可以将其存储在某个地方,并使用我们掌握的任何适合的安全工具对其进行保护。

闭幕 (Closing)

Thanks for reaching the potato. Few words left by this great said:

感谢您接触马铃薯。 这个伟大的人只剩下几句话:

“Security is always excessive until it’s not enough.”- Robbie Sinclair

“安全总是过分的,直到不够为止。”-罗比·辛克莱尔

In the security world, I believe, there’s no such single tool, silver bullet solution that automatically puts us in the green zone. Great security can only be achieved with a broad collection of complementary tools, every considered security element is taken into account, that forms a layered defense. A layered defense is the only viable defense.

我相信,在安全领域,没有任何一种工具可以自动将我们置于绿色区域。 只有使用广泛的补充工具才能实现强大的安全性,同时考虑到每个被考虑的安全元素,从而形成了分层防御。 分层防御是唯一可行的防御。

That’s all about. Hope this helps, thanks.

这就是全部。 希望这会有所帮助,谢谢。

External links:

外部链接:

  1. https://developer.android.com/training/articles/keystore

    https://developer.android.com/training/articles/keystore

  2. https://developer.android.com/topic/security/data

    https://developer.android.com/topic/security/data

  3. https://www.infoworld.com/article/2076237/signed-and-sealed-objects-deliver-secure-serialized-content.html

    https://www.infoworld.com/article/2076237/signed-and-sealed-objects-deliver-secure-serialized-content.html

  4. https://medium.com/programming-lite/securing-api-keys-in-android-app-using-ndk-native-development-kit-7aaa6c0176be

    https://medium.com/programming-lite/securing-api-keys-in-android-app-using-ndk-native-development-kit-7aaa6c0176be

翻译自: https://proandroiddev.com/modern-android-security-development-f84796824cea

android安全开发

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值