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
.
安全性库实现了用于存储静态数据的加密安全性最佳实践,并且当前提供了EncryptedSharedPreferences
和EncryptedFile
类。
EncryptedSharedPreferences (EncryptedSharedPreferences)
EncryptedSharedPreferences
is an implementation of SharedPreferences
where the keys and values are both encrypted.
EncryptedSharedPreferences
是SharedPreferences
的实现,其中密钥和值均被加密。
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 RobolectricRobolectric不支持
"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 。 因此, KeyStoreSpi
为KeyStore
类定义了服务提供者接口。
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使用