保护android项目中的秘密

顾正初
2023-12-01

When developing apps we will often need to use secret values that we don’t want anyone to get access to, such as tokens, IDs and API keys. There are many reasons they may be needed in our source code and in Gradle scripts, the most common being when we are asked to provide one to authenticate with a third-party API.

在开发应用程序时,我们经常需要使用我们不希望任何人访问的秘密值,例如令牌,ID和API密钥。 在我们的源代码和Gradle脚本中可能有很多原因,最常见的原因是当我们被要求提供一个使用第三方API进行身份验证的信息时。

We will examine a selection of techniques that we can apply, providing protection for our secrets and preventing them from sitting in plaintext, in plain sight! 

我们将研究一些可以应用的技术,以保护我们的秘密,并防止它们在明文中以明文形式出现! 

Before we continue: Please check out the article on my blog, Lord Codes, you will find code snippets with themed syntax highlighting and much more, it is definitely my preferred way to read it! 

在继续之前:查看我博客上的文章Lord Codes ,您将找到突出显示主题语法的代码片段以及更多内容,这绝对是我首选的阅读方式! 

为什么 (Why)

When following the setup instructions to integrate a new library, we are usually told to put the API key in the AndroidManifest.xml, in the source code or in a Gradle file. These suggestions will result in the secrets being added to source control and to be easily obtainable in plaintext by decompiling our app.

按照设置说明集成新库时,通常会告诉我们将API密钥放入源代码或Gradle文件中的AndroidManifest.xml中。 这些建议将导致秘密被添加到源代码控制中,并且可以通过反编译我们的应用轻松以纯文本形式获得。

There are more secure ways of managing our secrets and through these tips, we can make them significantly harder to obtain. It is worth remembering that our app is published and installed, meaning people will be able to take it apart and try and find secret values within it. All we can do as developers is to apply an appropriate level of security and do our best to keep these secrets safe. When it comes to API keys and tokens, there are also techniques that can be applied on the backend-side to detect fraudulent use and block access using those credentials.

有更安全的方式来管理我们的秘密,通过这些技巧,我们可以使秘密变得更难获得。 值得记住的是,我们的应用已发布并安装,这意味着人们将能够拆开它并尝试在其中找到秘密价值。 作为开发人员,我们所能做的就是应用适当级别的安全性,并尽最大努力确保这些秘密的安全。 对于API密钥和令牌,还可以在后端应用一些技术来检测欺诈性使用并使用这些凭据阻止访问。

存放在哪里 (Where to store them)

Gradle allows values to be passed in via Gradle properties, these can be passed on the command line or stored in a project-level or user-level properties file. A great way to handle our secrets is to use the user-level file on our filesystem, keeping them out of the project and out of source control. If we were to remove the project from our system and then re-clone it the secrets would still be there and we also have the possibility to include the same secrets into multiple projects without requiring extra set up.

Gradle允许通过Gradle属性传递值,这些值可以在命令行中传递或存储在项目级别或用户级别的属性文件中。 处理机密的一种好方法是在文件系统上使用用户级别的文件,使它们不受项目和源代码控制。 如果我们要从系统中删除项目,然后将其重新克隆,则秘密仍然存在,并且我们还可以将相同的秘密包含在多个项目中,而无需进行额外的设置。

By storing them at a user-level and keeping them out of Git, it means everyone with read-access to the source code doesn’t automatically receive the secrets and if our source control system was compromised the attackers wouldn’t obtain all of our secrets alongside the source code.

通过将它们存储在用户级别并将其保留在Git之外,这意味着具有对源代码的读取权限的每个人都不会自动接收机密,并且如果我们的源代码控制系统受到攻击,攻击者将无法获得我们所有的机密。与源代码一起的秘密。

The file is stored within our user directory:

该文件存储在我们的用户目录中:

  • On Mac or Linux: /Users/<you>/.gradle/gradle.properties

    在Mac或Linux上:/ /Users/<you>/.gradle/gradle.properties

  • On Windows: C:\Users\<you>\.gradle\gradle.properties

    在Windows上: C:\Users\<you>\.gradle\gradle.properties

We add a property for each secret to the file, keeping in mind it is user-level and so we will want something to signify which app or project the secret corresponds to.

我们会为文件中的每个机密添加一个属性,请记住它是用户级别的,因此我们需要一些东西来表示该机密对应的应用程序或项目。

GameCatalogueApp_UploadKeystore_KeyPassword=aaaabbbbcccc
GameCatalogueApp_AuthClientSecret=123456789
GameCatalogueApp_Pusher_APIKey=ksldjalksdjskald

使用值 (Using values)

Accessing the values within our Android project is as simple as reading them as a Gradle property and using them how we wish. If the value is needed within a Gradle script, such as to pass in a keystore password, it can just be read and used as it is. We are using a handy extension to get the property and return an empty string if it isn’t present, to avoid the null value. If our secrets are set up correctly, they won’t be missing.

访问我们的Android项目中的值就像读取Gradle属性一样简单,并按我们期望的方式使用它们。 如果在Gradle脚本中需要该值(例如传递密钥库密码),则可以按原样读取和使用它。 我们正在使用一个方便的扩展程序来获取属性,如果不存在该属性,则返回一个空字符串,以避免使用null值。 如果我们的机密设置正确,它们将不会丢失。

signingConfigs {
  create("upload") {
    storePassword = propertyOrEmpty(
      "GameCatalogueApp_UploadKeystore_KeyPassword"
    )
  }
}


fun Project.propertyOrEmpty(name: String): String {
  val property = findProperty(name) as String?
  return property ?: ""
}

Using the values from our source code requires them to be passed through using either resValue or buildConfigField, depending on whether we want them as an Android resource or as a property on the BuildConfig object. One quirk with buildConfigField is the String containing the property value needs to have quotes within it, in order for BuildConfig to be correctly generated.

使用源代码中的值要求使用resValuebuildConfigField传递它们,具体取决于我们希望将它们用作Android资源还是BuildConfig对象的属性。 buildConfigField一个怪癖是包含属性值的字符串,该属性值需要在其中包含引号,以便正确生成BuildConfig

defaultConfig {
  buildConfigField(
    "String", 
    "AUTH_CLIENT_SECRET", 
    buildConfigProperty("GameCatalogueApp_AuthClientSecret")
  )


  resValue(
    "string", 
    "pusher_key", 
    propertyOrEmpty("GameCatalogueApp_Pusher_APIKey")
  )
}


fun Project.buildConfigProperty(name: String) = "\"${propertyOrEmpty(name)}\""

那CI呢 (What about CI)

It would be very common for our project to be built on a continuous integration (CI) system or services, such as Bitrise or Jenkins. If this is the case we will need our secrets to be available on CI and passed through to the build environment. A handy trick here is that we can set the secrets as environment variables that use the same names as the Gradle properties. The functions we use to read their values within Gradle can then check for both a Gradle property or an environment variable and use whichever is found.

将我们的项目构建在持续集成(CI)系统或服务(例如Bitrise或Jenkins)上非常普遍。 如果是这种情况,我们将需要我们的机密信息在CI上可用并传递到构建环境。 一个方便的技巧是,我们可以将秘密设置为使用与Gradle属性相同名称的环境变量。 然后,我们用于在Gradle中读取其值的函数可以检查Gradle属性或环境变量并使用找到的任何一个。

fun Project.propertyOrEmpty(name: String): String {
    val property = findProperty(name) as String?
    return property ?: environmentVariable(name)
}


fun environmentVariable(name: String) = System.getenv(name) ?: ""

Of course, this means environment variables can also be used locally if we would prefer, however, using Gradle properties is a very simple process.

当然,这意味着如果我们愿意,也可以在本地使用环境变量,但是,使用Gradle属性是一个非常简单的过程。

加密它们 (Encrypt them)

Even though our secrets are now usable and kept separate from the source code, it would still be fairly simple to decompile the app and extract our secrets in plain text. We can take our solution further by encrypting the values before they are stored and then decrypting them at runtime.

即使我们的秘密现在可以使用并且与源代码分开了,但是反编译应用程序并以纯文本形式提取我们的秘密仍然非常简单。 我们可以通过在存储值之前对其进行加密,然后在运行时对其进行解密来进一步解决方案。

There are various options for encryption, including built-in options or third-party libraries. One possibility is Themis, which is easy to use and provides strong cryptographic techniques, along with a unified API between Android and iOS. Unless there is a skilled cryptographer working on the project, it may be a good idea to use a higher-level encryption API to avoid mistakes being made which weaken applied security measures.

有多种加密选项,包括内置选项或第三方库。 一种可能性是Themis ,它易于使用并提供强大的加密技术,以及Android和iOS之间的统一API。 除非有一个熟练的密码学家从事该项目,否则最好使用更高级别的加密API来避免犯下会削弱所应用的安全措施的错误。

In order to encrypt our data, we will need an encryption key.

为了加密我们的数据,我们将需要一个加密密钥。

GameCatalogueApp_EncryptionKey=super_secret_key


buildConfigField(
  "String", 
  "ENCRYPTION_KEY", 
  buildConfigProperty("GameCatalogueApp_EncryptionKey")
)

We can provide some obfuscation and protection to our key by applying runtime transformations to it, resulting in the “real” key we will actually use. By doing this an attacker would need to decompile the source code and work out from the obfuscated code which operations were applied to the key.

我们可以通过对其进行运行时转换来对密钥进行混淆和保护,从而产生我们将实际使用的“真实”密钥。 通过这样做,攻击者将需要反编译源代码,并从混淆后的代码中找出对密钥进行了哪些操作。

fun generateKey(): ByteArray {
  val rawKey = buildString(5) {
    append(byteArrayOf(0x12, 0x27, 0x42).base64EncodedString())
    append(500 + 6 / 7 * 89)
    append(BuildConfig.ENCRYPTION_KEY)
    append("pghy^%£aft")
  }
  return rawKey.toByteArray()
}


fun ByteArray.base64EncodedString() = Base64.encodeToString(this, Base64.NO_WRAP)

Here we have only applied some fairly simple operations onto the key to demonstrate the idea, the concept could be taken much further and make the key harder to crack.

在这里,我们仅对密钥进行了一些相当简单的操作,以演示该想法,可以进一步扩展该概念并使密钥更难破解。

Now that we have our encryption key ready to go, we can use a Themis SecureCell to turn our raw String data into an encrypted byte string.

现在我们准备好了加密密钥,我们可以使用Themis SecureCell将原始String数据转换为加密的字节字符串。

fun encrypt(message: String): ByteArray {
  val encryptionKey = generateKey()
  val cell = SecureCell(encryptionKey, SecureCell.MODE_SEAL)
  val protectedData = cell.protect(
    encryptionContext, message.toByteArray()
  )
  return protectedData.protectedData
}


private val encryptionContext: ByteArray? = null

To store our encrypted secret we can encode the produced ByteArray into a base 64 encoded string. The same key generation code could be added to a script or we could just run the app, encrypt the secret and print out the value for us to copy.

为了存储我们的加密机密,我们可以将产生的ByteArray编码为以64为基数的字符串。 可以将相同的密钥生成代码添加到脚本中,或者我们可以仅运行该应用程序,加密机密并打印出要复制的值。

val encypted = EncryptionEngine().encrypt("raw_secret_value")
Log.d("ENCRYPTED", encypted.base64EncodedString())

The Gradle properties or our CI environment variables can now be replaced with encrypted versions.

现在可以将Gradle属性或我们的CI环境变量替换为加密版本。

解密它们 (Decrypt them)

At runtime, we will first need to convert the base 64 encoded version into an encrypted ByteArray.

在运行时,我们首先需要将base 64编码版本转换为加密的ByteArray

val encryptedDaya = Base64.decode(secretAsBase64, Base64.NO_WRAP)

The encrypted ByteArray now needs to be turned into the raw string versions, using the same encryption key as was used for the encryption process earlier. We will need to handle a failed decryption in some way, which would mean something had been set up incorrectly or an invalid value was passed in.

现在,需要使用与之前加密过程相同的加密密钥,将加密的ByteArray转换为原始字符串版本。 我们将需要以某种方式处理失败的解密,这意味着某些设置不正确或传入了无效值。

fun decrypt(encryptedData: ByteArray): String? {
  val encryptionKey = generateKey()
  val cell = SecureCell(encryptionKey, SecureCell.MODE_SEAL)
  return try {
    val cellData = SecureCellData(encryptedData, null)
    val decodedData = cell.unprotect(encryptionContext, cellData)
    String(decodedData)
  } catch (error: SecureCellException) {
    Log.e("EncryptionEngine", "Failed to decrypt message, error)
    null
  }
}

Our unencrypted secrets can now be used as they were before. It can be a good practice to decrypt them when they need to be used rather than storing them in an unencrypted state during an app session.

我们未加密的机密现在可以像以前一样使用。 最好在需要使用它们时对它们进行解密,而不是在应用程序会话期间将它们存储为未加密状态。

结语 (Wrap up)

Protecting our API keys and other secrets is a good practice to avoid someone accessing them and causing us harm. This is particularly true if the keys are used for authentication or for accessing our own API that serves up user data. Using various techniques, we have removed the secrets from source control, kept them out of the project, encrypted them and applied protection to our encryption key.

保护我们的API密钥和其他机密是一种很好的做法,可以避免他人访问它们并给我们造成伤害。 如果密钥用于身份验证或访问我们自己的提供用户数据的API,则尤其如此。 通过使用各种技术,我们从源代码管理中删除了秘密,将其排除在项目之外,对其进行了加密,并对我们的加密密钥进行了保护。

On top of what has been discussed, there are further practices that can be applied, some requiring more significant changes. It is best to evaluate the security needs of a particular application, based on what type of data it uses and the level of security a user would expect. Clearly a banking app will need to be much more security conscious than a timer app, however, a good level of security should be applied regardless to protect access to a user’s data.

除了已讨论的内容外,还可以应用其他实践,其中一些需要进行更重大的更改。 最好根据特定应用程序使用的数据类型和用户期望的安全级别来评估其安全性需求。 显然,与计时器应用程序相比,银行应用程序需要更加注重安全性,但是,无论保护用户数据的访问权限,都应该应用良好的安全性。

What do you think about handling secrets within your Android projects? Do you use something similar or another solution that has other advantages? Please feel free to put forward any thoughts or questions you have on Twitter @lordcodes.

您如何处理Android项目中的机密? 您是否使用类似的东西或其他具有其他优点的解决方案? 请随时在Twitter @lordcodes上提出您的任何想法或问题。

If you like what you have read, please don’t hesitate to share the article and subscribe to my feed if you are interested.

如果您喜欢阅读的内容,请随时分享文章订阅我的供稿

Thanks for reading and happy coding! 

感谢您的阅读和愉快的编码! 

翻译自: https://proandroiddev.com/protecting-secrets-in-an-android-project-ff99eaf7b9ec

 类似资料: