jetpack使用_使用Robolectric测试Jetpack安全性

jetpack使用

When updating my Android caching library, layercache, with built-in support for the new Jetpack SecurityEncryptedSharedPreferences I started to write unit tests using Robolectric, but soon came across the exception java.security.KeyStoreException: AndroidKeyStore not found.

当更新Android缓存库layercache时 ,它具有对新的Jetpack Security EncryptedSharedPreferences内置支持,我开始使用Robolectric编写单元测试,但很快遇到异常java.security.KeyStoreException: AndroidKeyStore not found

Usually, shadows come to mind when faced with issues like this, but in this article, we will discuss why this won’t work and how we can get around the problem to continue to write unit tests without requiring a test device.

通常,遇到诸如此类的问题时,阴影会浮现,但是在本文中,我们将讨论为什么这种方法行不通,以及如何解决该问题以继续编写单元测试而不需要测试设备。

什么是Jetpack安全 (What is Jetpack Security)

The Security library implements crypto security best practices for storing data at rest and currently provides the classes EncryptedSharedPreferences and EncryptedFile.

安全性库实现了用于存储静态数据的加密安全性最佳实践,并且当前提供了EncryptedSharedPreferencesEncryptedFile类。

EncryptedSharedPreferences (EncryptedSharedPreferences)

EncryptedSharedPreferences is an implementation of SharedPreferences where the keys and values are both encrypted.

EncryptedSharedPreferencesSharedPreferences的实现,其中密钥和值均被加密。

To use, add the following dependency into yourbuild.gradle file:

要使用,请将以下依赖项添加到build.gradle文件中:

dependencies {
implementation("androidx.security:security-crypto:1.1.0-alpha01")
}

To create EncryptedSharedPreferences, you first create a MasterKey, default settings can be achieved with:

要创建EncryptedSharedPreferences ,首先要创建一个MasterKey ,可以使用以下方法实现默认设置:

val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()

The MasterKey can also be configured to ensure it is hardware backed as well as requiring user authentication. The documentation contains all the options.

还可以配置MasterKey以确保它是硬件支持的,并且需要用户验证。 该文档包含所有选项。

Then you can create the EncryptedSharedPreferences as follows:

然后,您可以如下创建EncryptedSharedPreferences

val sharedPreferences = EncryptedSharedPreferences.create(
context
,
"preference file name",
masterKey,
PrefKeyEncryptionScheme.AES256_SIV,
PrefValueEncryptionScheme.AES256_GCM
)

You can then read and write encrypted data seamlessly as if it were a normal SharedPreferences file.

然后,您可以无缝地读写加密数据,就好像它是普通的SharedPreferences文件一样。

sharedPreferences.edit().apply {
putString("key", "value")
}
.apply()
sharedPreferences
.getString("key", "default") // returns "value"

编写单元测试 (Writing a unit test)

We can start to write a simple test to ensure reading and writing to this SharedPreferences works as expected using Robolectric.

我们可以开始编写一个简单的测试,以确保使用Robolectric可以对SharedPreferences进行读写操作

@RunWith(RobolectricTestRunner::class)@Config(manifest = Config.NONE)
class
EncryptedSharedPreferencesTest {
private val context
=
ApplicationProvider.getApplicationContext<Context>()
private val masterKey
= MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val sharedPreferences
=
EncryptedSharedPreferences.create(
context
,
"testPrefs",
masterKey,
PrefKeyEncryptionScheme.AES256_SIV,
PrefValueEncryptionScheme.AES256_GCM
)
@Test
fun `verify string storage`() {
sharedPreferences
.edit().apply {
putString("key", "value")
}
.apply()
assertEquals(
"value"
,
sharedPreferences.getString("key", "default")
)
}
}

Running the test as-is results in a dreaded exception:

按原样运行测试会导致可怕的异常:

java.security.KeyStoreException: AndroidKeyStore not found
at java.security.KeyStore.getInstance
at androidx.security.crypto.MasterKeys.keyExists
at androidx.security.crypto.MasterKeys.getOrCreate
at androidx.security.crypto.MasterKey$Builder.buildOnM
at androidx.security.crypto.MasterKey$Builder.build

When you look at the stack trace for the exception there is no AndroidKeyStore class, just a call to java.security.KeyStore.getInstance, which brings us onto the first hurdle with Robolectric:

当您查看异常的堆栈跟踪时,没有AndroidKeyStore类,只有对java.security.KeyStore.getInstance的调用,这使我们进入了Robolectric的第一个障碍:

Robolectric doesn’t support shadows for classes under the "java." package unfortunately — Christian Williams, creator of Robolectric

Robolectric不支持"java."下的类的阴影"java." 不幸的是,包裹— Robolectric的创建者Christian Williams

Alternatively, can we utilise how KeyStore works in Java to provide our own AndroidKeyStore?

或者,我们可以利用KeyStore在Java中的工作方式来提供自己的AndroidKeyStore吗?

KeyStore如何工作? (How does KeyStore work?)

Java includes built-in providers that implement a basic set of security services but also allows the installation of custom providers. AndroidKeyStore is provided by one such provider. Providers contain a list of the services they offer by name, and we add them to the java.security.Security class which we can think of as a service locator.

Java包括内置的提供程序,这些提供程序实现一组基本的安全服务,但也允许安装自定义提供程序AndroidKeyStore类提供商之一提供。 提供程序包含按名称提供的服务列表,我们将它们添加到java.security.Security类中,我们可以将其视为服务定位器

What this means is we can add in our AndroidKeyStore implementation.

这意味着我们可以在我们的AndroidKeyStore实现中添加。

实施密钥库 (Implementing a KeyStore)

We do this by creating a class that extends java.security.Provider using the provider name AndroidKeyStore to match the Android platform implementation.

为此,我们使用提供程序名称AndroidKeyStore创建一个扩展java.security.Provider的类以匹配Android平台实现。

The Provider is a map of the services we support, where the keys to the map follow the pattern {engine class}.{algorithm name}, and values are the fully-qualified class name of the class that implements it.

Provider是我们支持的服务的映射,映射的键遵循模式{engine class}.{algorithm name} ,而值是实现它的类的标准类名。

For our use case, we need our Provider to contain a KeyStore for the AndroidKeyStore algorithm.

对于我们的用例,我们需要我们的Provider包含一个用于AndroidKeyStore算法的KeyStore

We then add this provider to the Java Security API before our unit test is executed.

然后,在执行单元测试之前,我们将此提供程序添加到Java Security API。

val provider = object : Provider("AndroidKeyStore", 1.0, "") {
init {
put("KeyStore.AndroidKeyStore",
FakeKeyStore::class.java.name)
}
}
Security.addProvider(provider)

To implement our FakeKeyStore, class must extend the abstract class KeyStoreSpi. As we will see throughout, the class we extend is always named, where SPI stands for Service Provider Interface. So, KeyStoreSpi defines the service provider interface for the KeyStore class.

要实现我们的FakeKeyStore,类必须扩展抽象类KeyStoreSpi 。 正如我们将始终看到的,我们扩展的类始终被命名,其中SPI表示Service Provider Interface 。 因此, KeyStoreSpiKeyStore类定义了服务提供者接口。

Of course, implementing our own KeyStore would be time-consuming involving the overriding of 21 functions, fortunately, of course, Java has a built-in implementation we can wrap around:

当然,实现我们自己的KeyStore会很耗时,涉及到覆盖21个功能,当然,幸运的是,Java有一个内置实现,我们可以包装一下:

class FakeKeyStore : KeyStoreSpi() {
private val wrapped
=
KeyStore.getInstance(KeyStore.getDefaultType())
override fun
engineIsKeyEntry(alias: String?) =
wrapped.isKeyEntry(alias)
override fun
engineIsCertificateEntry(alias: String?) =
wrapped.isCertificateEntry(alias) ... override fun engineGetKey(alias: String?, password: CharArray?)=
wrapped.getKey(alias, password)
}

If we run our unit test with our AndroidKeyStore provider set, the code now fails with the below exception:

如果我们使用AndroidKeyStore提供程序集来运行单元测试,则代码现在将失败,并带有以下异常:

java.security.NoSuchAlgorithmException: no such algorithm: AES for provider AndroidKeyStore
at javax.crypto.KeyGenerator.getInstance
at androidx.security.crypto.MasterKeys.generateKey
at androidx.security.crypto.MasterKeys.getOrCreate
at androidx.security.crypto.MasterKey$Builder.buildOnM
at androidx.security.crypto.MasterKey$Builder.build

实施AES (Implementing AES)

As with the KeyStore, we need to ensure our provider also contains a KeyGenerator by providing an implementation of KeyGeneratorSpi. Again we can rely on Java’s built-in AES algorithm for generating keys and ignore the engineInit functions that we are obliged to override as they’re abstract in the parent class.

KeyStore ,我们需要通过提供KeyGenerator的实现来确保提供者还包含KeyGeneratorSpi 。 同样,我们可以依靠Java的内置AES算法生成密钥,而忽略当它们在父类中是abstract ,我们不得不重写的engineInit函数。

class FakeAesKeyGenerator : KeyGeneratorSpi() {
private val wrapped
= KeyGenerator.getInstance("AES")
override fun
engineInit(random: SecureRandom?) = Unit
override fun engineInit(params: AlgorithmParameterSpec?,
random: SecureRandom?) = Unit
override fun engineInit(keysize: Int, random: SecureRandom?) =
Unit
override fun engineGenerateKey(): SecretKey =
wrapped.generateKey()
}

We add the KeyGenerator to the Provider with the following entry in our provider:

我们将KeyGenerator添加到Provider ,并在Provider添加以下条目:

put("KeyGenerator.AES", FakeAesKeyGenerator::class.java.name)

With this added, our unit test now passes.

有了这个功能,我们的单元测试现在可以通过了。

The gist below contains a working example of the code discussed in this article.

下面的要点包含本文中讨论的代码的有效示例。

/*
 * Copyright 2020 Appmattus Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package com.appmattus


import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import androidx.test.core.app.ApplicationProvider
import com.appmattus.FakeAndroidKeyStore
import org.junit.Assert.assertEquals
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config


@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, sdk = [22, 28])
class EncryptedSharedPreferencesTest {


    private val context = ApplicationProvider.getApplicationContext<Context>()


    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()


    private val sharedPreferences =
        EncryptedSharedPreferences.create(
            context,
            "testPrefs",
            masterKey,
            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        )


    @Test
    fun `verify string storage`() {
        sharedPreferences.edit().apply {
            putString("key", "value")
        }.apply()


        assertEquals("value", sharedPreferences.getString("key", "default"))
    }


    companion object {
        @JvmStatic
        @BeforeClass
        fun beforeClass() {
            FakeAndroidKeyStore.setup
        }
    }
}

结论 (Conclusions)

Hopefully, this has given a small insight into how the Java Security API works to enable you to write tests that contain code that utilizes the AndroidKeyStore, whether it is your own code or libraries such as Jetpack Security.

希望这对Java安全性API如何工作使您能够编写包含使用AndroidKeyStore代码的AndroidKeyStore (无论是您自己的代码还是库,例如Jetpack Security) AndroidKeyStore

Of course, in reality, setting up a mock AndroidKeyStore is far more complicated than I’ve shown. For example, the real implementation uses the AlgorithmParameterSpec provided in a KeyGenerator to store generated keys for later retrieval. For testing layercache, I implemented some of this complexity, so check out RobolectricKeyStore.kt for a more detailed example.

当然,实际上,设置模拟的AndroidKeyStore比我展示的要复杂得多。 例如,实际的实现使用KeyGenerator提供的AlgorithmParameterSpec来存储生成的密钥,以供以后检索。 为了测试layercache ,我实现了一些这种复杂性,因此请查看RobolectricKeyStore.kt以获得更详细的示例。

翻译自: https://proandroiddev.com/testing-jetpack-security-with-robolectric-9f9cf2aa4f61

jetpack使用

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值