Robolectric 3.x编写屏幕分辨率/多语言/资源文件相关测试用例

在编写 Android 测试用例的时候,有时候我们需要涉及到屏幕分辨率相关测试用例。

比如不同分辨率得到不同的像素数值,可以参考如下:

比如不同语言得到不同的字符串,可以参考如下:

其他相关的测试参数,参考 Device Configuration

注意需要在 build.gradle 中增加资源包含信息,否则在测试的时候会找不到指定的资源文件,默认只测试代码,被测试的资源文件不打包进入应用。

参考如下:

参考链接


Android单元测试--使用DummyActivity

在编写Android测试用例的时候,有时候我们需要测试与Activity相关的功能,同时又没办法直接调用被测试代码中的Activity的时候,我们需要创建DummyActivity的方式来进行。

我们希望这个DummyActivity只在测试代码中存在,相关的资源也只存在于测试代码里面使用,不侵入主代码。

可以参考如下布局进行项目的处理。

上述代码同样适用于Android资源文件相关的测试逻辑。

只是需要注意的是,当引入资源的时候,我们需要使用 {module_package}.test.R 的方式进行引入,否则代码中会提示找不到资源文件。

参考链接


测试Activity的onSaveInstanceState/onRestoreInstanceState

最近遇到一个应用崩溃问题,这个问题是由于在 ActivityonSaveInstanceState 中进行了数据的保存,然后在 onRestoreInstanceState 进行解析的时候出现崩溃。

实际测试的时候,发现当内存充足的时候,非常难稳定的诱发 ActivityonSaveInstanceState 事件。

早期的版本,可以通过 ActivityManagerNative.getDefault().setAlwaysFinish 来强制系统在 Activity 切换到后台之后,立即触发 onSaveInstanceState

参考代码如下:

但是遗憾的是,新系统比如 Android 8 等系统上,在真机环境中已经没办法通过上述的方法进行诱发了。系统会直接抛出异常,或者设置无效。

真机环境,可以尝试在 开发人员选项 中设置开启 “不保留活动” 按钮,如下图所示:

继续阅读测试Activity的onSaveInstanceState/onRestoreInstanceState

Android获取导航栏/状态栏/键盘的高度和状态

最近 Android 11 系统兼容性测试的时候,发现界面适配异常(标题栏沉浸式部分),已经无法通过网上流行的,通过反射方法获取状态栏的高度了。

于是翻了一下以前公共库里的代码,发现是使用如下代码获取状态栏高度的:

其实从 Android 9 开始,就已经对通过反射调用非公开 API 的方式进行警告了。

Android 11 以前的系统,只是给出警告, Android 11 直接抛出了调用异常。

于是搜索一下,发现网上已经更改成如下方法了:

仔细观察两个方法,会发现,其实两者都是获取了系统里的状态栏使用的某个资源的高度信息,然后作为状态栏的高度信息

这样就引发一个问题,那就是如果非官方的系统UI,比如小米,华为等自定义的UI,不使用这个资源文件,或者根本就没有这个资源文件,那么获取到的高度信息不就是不正确的了吗?

继续阅读Android获取导航栏/状态栏/键盘的高度和状态

动态Java代码注入(JDK/Android)

JDK下使用javax.tools.JavaCompiler进行动态代码的生成,编译,执行。

在本文中,我们将研究如何将Java代码动态加载到正在运行的jvm中。 该代码可能是全新的,或者我们可能想更改程序中某些现有代码的功能。

(在开始之前,您可能想知道为什么到底有人会这样做。显而易见的示例是规则引擎之类的东西。规则引擎希望为用户提供添加或更改规则的能力,而不必重新启动规则。您可以通过将DSL脚本作为规则注入,由规则引擎调用此方法,这种方法的真正问题是必须对DSL脚本进行解释,使其运行起来非常缓慢。然后可以像程序中的任何其他代码一样编译和运行该程序,效率将提高几个数量级。

我们将要使用的库是Chronicle开源库Java-Runtime-Compiler 。

从下面的代码中您将看到,该库的使用极其简单-实际上,它实际上只需要几行。 创建一个CachedCompiler,然后调用loadFromJava。 (有关实际最简单的用例,请参见此处的文档。)

下面列出的程序执行以下操作:

  1. 创建一个线程,该线程每秒调用一次Strategy。 该战略的投入为10和20。
  2. 加载将两个数字相加的策略
  3. 等待3秒
  4. 加载从另一个数中减去一个数的策略

这是完整的代码清单:

这是输出:

请注意,在每次加载策略时,在代码中我们都创建了一个新的ClassLoader和一个CachedCompiler。 这样做的原因是,ClassLoader一次只能加载一个特定类的一个实例。

如果仅使用该库来加载新代码,则可以这样做,而无需创建ClassLoader(即使用默认的ClassLoader)和CachedCompiler。

Android下使用由于无法使用javax.tools.JavaCompiler,因此使用 linkedin/dexmaker 进行动态代码的生成,编译,执行

参考链接


[翻译]OWASP 安卓测试指南(v1.2 - 14 May 2020)节选

Android Keystore对称-非对称加密防止关键数据被克隆

背景

最近需要解决用户使用手机克隆进行手机备份,某些设备相关的数据,比如设备ID的的存储文件也被克隆,导致用户更换设备之后,从本地读取到的设备ID信息依旧是上个手机的(Android Q以及以上的设备,有时候没办法获取设备ID,只能给一个随机数,因此只能存储在应用本地),这样会导致安全问题。

KeyStore简介

利用 Android KeyStore System,您可以在容器中存储加密密钥,从而提高从设备中提取密钥的难度。在密钥进入密钥库后,可以将它们用于加密操作,而密钥材料仍不可导出。此外,它提供了密钥使用的时间和方式限制措施,例如要求进行用户身份验证才能使用密钥,或者限制为只能在某些加密模式中使用。

密钥库系统由 KeyChain API 以及在 Android 4.3API 级别 18)中引入的 Android 密钥库提供程序功能使用。本文说明了何时以及如何使用 Android 密钥库提供程序。

应用场景

1、 存储密匙:Android提供的这个KeyStore最大的作用就是不需要开发者去维护这个密匙的存储问题,相比起存储在用户的数据空间或者是外部存储器都更加安全。注意的是这个密匙随着用户清除数据或者卸载应用都会被清除掉。
2、得益于Android独立的一套密匙库系统,可以提高安全性

安全功能

Android 密钥库系统可以保护密钥材料免遭未经授权的使用。首先,Android 密钥库可以防止从应用进程和 Android 设备中整体提取密钥材料,从而避免了在 Android 设备之外以未经授权的方式使用密钥材料。其次,Android 密钥库可以让应用指定密钥的授权使用方式,并在应用进程之外强制实施这些限制,从而避免了在 Android 设备上以未经授权的方式使用密钥材料。

提取防范

Android 密钥库密钥使用两项安全措施来避免密钥材料被提取:

  • 密钥材料永不进入应用进程。通过 Android 密钥库密钥执行加密操作时,应用会将待签署或验证的明文、密文和消息馈送到执行加密操作的系统进程。如果应用进程受攻击,攻击者也许能使用应用密钥,但无法提取密钥材料(例如,在 Android 设备以外使用)。
  • 您可以将密钥材料绑定至 Android 设备的安全硬件,例如可信执行环境 (TEE) 和安全元素 (SE)。为密钥启用此功能时,其密钥材料永远不会暴露于安全硬件之外。如果 Android 操作系统受到攻击或者攻击者可以读取设备内部存储空间,攻击者也许能在 Android 设备上使用应用的 Android 密钥库,但无法从设备上提取这些数据。只有设备的安全硬件支持密钥算法、区块模式、填充方案和密钥有权使用的摘要的特定组合时,才可启用此功能。要检查是否为密钥启用了此功能,请获取密钥的 KeyInfo 并检查 KeyInfo.isInsideSecurityHardware() 的返回值。
密钥使用授权

为了避免在 Android 设备上以未经授权的方式使用密钥材料,在生成或导入密钥时 Android 密钥库会让应用指定密钥的授权使用方式。一旦生成或导入密钥,其授权将无法更改。然后,每次使用密钥时,都会由 Android 密钥库强制执行授权。这是一项高级安全功能,通常仅用于有以下要求的情形:在生成/导入密钥后(而不是之前或当中),应用进程受到攻击不会导致密钥以未经授权的方式使用。

支持的密钥使用授权可归为以下几个类别:

  • 加密:授权密钥算法、运算或目的(加密、解密、签署、验证)、填充方案、区块模式以及可与密钥搭配使用的摘要;
  • 时间有效性间隔:密钥获得使用授权的时间间隔;
  • 用户身份验证:密钥只能在用户最近进行身份验证时使用。请参阅要求进行用户身份验证才能使用密钥。

作为一项额外的安全措施,对于密钥材料位于安全硬件内部的密钥(请参阅 KeyInfo.isInsideSecurityHardware()),某些密钥使用授权可能由安全硬件实施,具体取决于 Android 设备。加密和用户身份验证授权可能由安全硬件实施。由于安全硬件一般不具备独立的安全实时时钟,时间有效性间隔授权不可能由其实施。

您可以使用 KeyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware() 查询密钥的用户身份验证授权是否由安全硬件实施。

选择密钥链或 Android 密钥库提供程序

在需要系统级凭据时请使用 KeyChain API。在应用通过 KeyChain API 请求使用任何凭据时,用户需要通过系统提供的 UI 选择应用可以访问已安装的哪些凭据。因此,在用户同意的情况下多个应用可以使用同一套凭据。

使用 Android 密钥库提供程序让各个应用存储自己的凭据,并且只允许应用自身访问。这样,应用可以管理仅能由自己使用的凭据,同时又可以提供等同于 KeyChain API 为系统级凭据提供的安全优势。这一方法不需要用户选择凭据。

使用 Android 密钥库提供程序

要使用此功能,请使用标准的 KeyStoreKeyPairGeneratorKeyGenerator 类,以及在 Android 4.3API 级别 18)中引入的 AndroidKeyStore 提供程序。

AndroidKeyStore 注册为 KeyStore 类型以用于 KeyStore.getInstance(type) 方法,而在用于 KeyPairGenerator.getInstance(algorithm, provider)KeyGenerator.getInstance(algorithm, provider) 方法时则注册为提供程序。

生成新私钥

生成新的 PrivateKey 要求您同时指定自签署证书具备的初始 X.509 属性。之后,您可以使用 KeyStore.setKeyEntry 将证书替换为由证书颁发机构 (CA) 签署的证书。

要生成密钥,则使用 KeyPairGeneratorKeyPairGeneratorSpec

生成新密钥

要生成密钥,请使用 KeyGeneratorKeyGenParameterSpec

使用密钥库条目

AndroidKeyStore 提供程序的使用通过所有的标准 KeyStore API 加以实现。

列出条目

通过调用 aliases() 方法列出密钥库中的条目:

签署和验证数据

通过从密钥库提取 KeyStore.Entry 并使用 Signature API(例如 sign())签署数据:

类似地,请使用 verify(byte[]) 方法验证数据:

要求进行用户身份验证才能使用密钥

生成密钥或将密钥导入到 AndroidKeyStore 时,您可以指定密钥仅授权给经过身份验证的用户使用。用户使用安全锁定屏幕凭据(模式/PIN/密码、指纹)的子集进行身份验证。

这是一项高级安全功能,通常仅用于有以下要求的情形:在生成/导入密钥后(而不是之前或当中),应用进程受到攻击不会导致密钥被未经身份验证的用户使用。

如果密钥仅授权给经过身份验证的用户使用,可以将其配置为以下列两种模式之一运行:

  • 经过身份验证的用户可以在一段时间内使用密钥。在用户解锁安全锁定屏幕或使用 KeyguardManager.createConfirmDeviceCredentialIntent 流程确认其安全锁定屏幕凭据后,即可使用此模式中的所有密钥。每个密钥的授权持续时间各不相同,并由 setUserAuthenticationValidityDurationSeconds 在密钥生成或导入时指定。此类密钥只能在启用安全锁定屏幕时生成或导入(请参阅 KeyguardManager.isDeviceSecure())。在安全锁定屏幕停用(重新配置为“无”、“滑动”或不验证用户身份的其他模式)或被强制重置(例如由设备管理员执行)时,这些密钥将永久失效。
  • 用户身份验证会授权与某一密钥关联的特定加密操作。在此模式中,涉及此类密钥的每个操作都需要用户单独授权。目前,此类授权的唯一方式是指纹身份验证:FingerprintManager.authenticate。此类密钥只能在至少注册一个指纹时生成或导入(请参阅 FingerprintManager.hasEnrolledFingerprints)。一旦注册新指纹或取消注册所有指纹,这些密钥将永久失效。

Android数据加密:

Android 提供了 KeyStore 等可以长期存储和检索加密密钥的机制,Android KeyStore 系统特别适合于存储加密密钥。

AndroidKeyStore” 是 KeyStore 的一个子集,存进 AndroidKeyStorekey 将受到签名保护,并且这些 key 是存在系统里的,而不是在 Appdata 目录下,依托于硬件的 KeyChain 存储,可以做到 private key 一旦存入就无法取出,

每个 App 自己创建的 key,别的应用是访问不到的。

它提供了限制何时以何种方式使用密钥的方法,比如使用密钥时需要用户认证或限制密钥只能在加密模式下使用。

一个应用程式只能编辑、保存、取出自己的密钥。

App可以生成或者接收一个公私密钥对,并存储在AndroidKeystore系统中。公钥可以用于在应用数据放置到特定文件夹前对数据进行加密,私钥可以在需要的时候解密相应的数据。

作用:

KeyStore 适用于生成和存储密钥,这些密钥可以用来加密运行时获取到的数据,比如运行时,用户输入的密码,或者服务端传下来的 token。

操作方式

建议做法

1. 使用对称式加解密,但只能在Api Level 23+使用

对称式加解密(AES)速度较快,但是对称式的Key若要存在KeyStore裡,Api level一定要在23以上才支持,23以下是无法存入KeyStore的,非对称式的Key則不在此限。

2. 想兼容各Api版本(23以下也能用)
  • 若要存取的東西不多、字串長度也不長:直接使用非对称式加解密即可
  • 若要存取的東西很多或字串長度很長:由於非对称式加解密速度较慢,使用非对称式+对称式加解密可以解決此問題。

考慮到加解密效能、版本兼容,下面會介紹用非对称式+对称式來加解密。

KeyStore非对称+对称式加解密流程
  1. 使用KeyStore产生随机的RSA Key
  2. 产生AES Key,并用RSA Public Key加密后存入SharedPrefs
  3. SharedPrefs取出AES Key,並用RSA Private Key解密,用這把AES Key來加解密信息;

主流的加密方式有:(对称加密)AES、DES        (非对称加密)RSA、DSA

工作模式:

DES一共有:

电子密码本模式(ECB)、加密分组链接模式(CBC)、加密反馈模式(CFB)、输出反馈模式(OFB);

AES一共有:

电子密码本模式(ECB)、加密分组链接模式(CBC)、加密反馈模式(CFB)、输出反馈模式(OFB)、计数器模式(CTR),伽罗瓦计数器模式(GCM

PKCS5Padding是填充模式,还有其它的填充模式;

对于初始化向量iv: 初始化向量参数,AES16bytes. DES8bytes

(1)产生随机的RSA Key

产生RSA Key会使用到KeyPairGenerator

其中KeyPairGeneratorSpec在Api 23以上已經Deprecated了;

Api level 23以上改使用KeyGenParameterSpec

api23 以上使用 KeyGenParameterSpec

api23 以下使用 KeyPairGeneratorSpec

注意,已知,在某些低端设备上,RSA密钥对生成的时间可能超过1S以上。尝试过异步子线程初始化,但是由KeyStore内部的函数在实现的时候,线程不安全,导致子线程初始化的时候诱发了异常行为,因此只能在主线程中进行操作。

(2)产生AES Key后,并用RSA Public Key加密后存入SharedPrefs

  1] 加密存储:使用RSA Public Key 加密 AES Key,存入缓存中。

  2]  解密使用:使用RSA Private Key 解密 得到 AES Key。

获取AES :

再使用AES 加解密内容:

对于:Cipher 初始化

具体使用:

iv 初始化向量

关于RSA:

使用RSA加解密时,在较低版本的手机上可能无法选择OAEP(最优非对称加密填充,RSA的加密解密是基于OAEP的)這個模式;

因此可以改使用RSA_PKCS1_PADDING模式,使用这个模式的話,输入必须比RSA的Key最大长度少11個字节,如果需要被加密的字串过长的话,可以在产生Key时指定Key Size长度,或是将字串分段加密。

以预设Key Size = 2048bit(256byte)來说,输入最长只能到256–11=245byte,我們可以透过setKeySize(int keySize)指定Key的长度,但是Key Size越大,加解密时速度就越慢。

需要注意,由于设备可能存储密钥到硬件设备(KeyInfo.isInsideSecurityHardware()),然而硬件设备不一定能保存我们手工指定的某些长度的密钥。导致如果我们设置了指定长度,可能由于硬件设备不支持,反而只能存储到系统中,造成密钥存储的安全性反而下降了。

我们希望厂家设置的默认值是硬件能支持的最大安全性,并且尽量存储到硬件中。尽管密钥安全性可能下降了,但是存储安全性反而上升了。

或者从高到低重试,选择当前设备支持的最高的硬件存储的长度。究竟是存储位置重要还是加密级别更重要,需要权衡。

判断生成的密钥是否由硬件存储,参考如下代码:

获取设备生成的密钥的长度,参考如下代码:

需要解决的一个疑惑就是,既然可以通过

的方式获得RSA私钥,那么,我们能不导出这个私钥呢?

答案显然是不能的,原因在于系统给出的私钥只是一个代理,并没有实际的私钥数据,私钥数据被存储在相关的硬件或者系统内核中,主要证据就是privateKey.getEncoded()返回了null,这样就实现了关键的私钥数据都无法获得。

参考如下测试代码:

需要注意的一个问题在于,由于RSA的大素数搜索机制,导致每次生成密钥的时间可能会超过预期,最长的可能会耗时1-2S以上。因此,如果设备支持,我们直接申请硬件AES密钥的方式来进行数据的加解密操作,达到更高的安全程度。

可用的参考代码如下:

参考链接


ubuntu 20.04系统AOSP(Android 11)集成Frida

参考 ubuntu 20.04编译Android 11源代码&模拟器 完成Android 11源代码的编译工作,保证能编译通过。

想自己手工编译Frida源代码的话,请参照下面:

如果想直接下载对应版本的Frida库并存放到已经编译过的库位置,由于64位系统需要兼容32位应用,因此需要安装两个版本的动态库:

创建Frida Gadget库的配置文件

里面的配置内容如下:

观察Frida源代码,发现在 frida-core/lib/gadget/gadget-glue.c中配置了lib库的入口函数

这就意味着只要使用dlopen加载frida-gadget,我们就能实现对于指定应用的Hook

我们只需要监听子进程,不需要在Zygote中加载,因此只需要在源代码 frameworks/base/core/jni/com_android_internal_os_Zygote.cppcom_android_internal_os_Zygote_nativeForkAndSpecialize函数中增加加载代码:

具体添加位置如下:

编译并重新生成系统镜像:

运行镜像

选择system-qemu.img和vendor-qemu.img,这两个镜像是专门为qemu运行制作的,如果选择system.img 和vendor.img,则avd运行失败。

上面运行起来的镜像是从~/AndSrc/aosp/out/debug/target/product/generic/hardware-qemu.ini即可读取配置信息的,但是这个文件直接修改无效,我们如果需要修改参数,只能从启动参数中设置。
比如我们如果需要增大内存,开启GPU的支持,则执行如下命令:

参考链接


Android SSL证书设置和锁定(SSL/TLS Pinning)

我们认识到在移动端开发中安全性设置非常重要,尤其是目前非常流程H5混合式开发APP,在Android开发中,我们可以通过证书锁定的方式来增加客户端与服务端的安全保障《证书锁定SSL Pinning简介及用途》,本文主要介绍 SSL数字证书在Android开发中的证书锁定(SSL/TLS Pinning)

1. 常规SSL证书设置

通常由CA权威机构签发的证书,其根证书都内置在最新的Android操作系统中,因此默认情况下可不进行SSL证书锁定,开发APP时也就变得非常简单,以infinisign.com为例,摘自App security best practices

1.1 JAVA方法

1.2 KOTLIN方法

2. 网络安全性设置

本方案是官方提供,但需要依赖Android N(Android 7.0 API 24)及以后版本,可在APP开发阶段在APP中内置安全性设置,以达到防止中间人攻击(MITM)的目的,此方法只限制在Android 7.0 API 24以后版本,因此该版本之前的安全性设置仍然需要使用证书锁定方法,本文以infinisign.com为例。

2.1 创建配置文件

创建文件res/xml/network_security_config.xml,需要注意的是,使用证书锁定,需要配置一个备份密钥,假如证书到期或更换了CA品牌后,不至于重新发行APP,这个备份密码可以是中级证书或根证书。

通俗的说,如果系统检测到签发证书过期了,则自动使用其中级或才根级证书作为验证,因为通常中级机构、根机构的证书到期时间非常长。

但实际情况是,infinisign.com所售的CA签发证书有效期都是一年,而现在发行APP或更新APP通常在一年都会有更新重新上架操作。

2.2 引入配置文件

Androidmanifest.xml引入配置文件android:networkSecurityConfig="@xml/network_security_config"

3. okHttp锁定

okHttp是一个用于Android处理网络请求的开源项目,是安卓端最流行的轻量级的网络框架,其主要用来替代HttpUrlConnection处理方式

不过需要注意的是,okHttp锁定证书方式不适用于公钥锁定方式,必须以证书锁定方式内置SSL数字证书。

4. TrustManager锁定

TrustManager是一个比较老的证书锁定方法,主要用于早期的Android版本或者用于一些CA根机构在Android系统中缺失根证书的情形下,当然也适用于自签名证书的锁定,通常我们不建议这样做,因为TrustManager的缺点是中间人仍然可以使用成熟的绕过方案来实现截持。

在SSL普及的今天,let's encrypt的开源免费解决方案,和一些入门的便宜DV域名型SSL证书(见PositiveSSL ¥39/年)足以媲美自签名方案。

详细请参考javax.net.ssl.TrustManager接口实现,简单步骤如下

  1. 在APP源码中内置证书

  2. 使用KeyStore加载证书

  3. TrustManagerFactory实例化证书

  4. 创建SSLContext实例,与TrustManager进行绑定。

总结

在多数移动操作系统中有大大小小几十个CA机构内置的根证书,但也不排除已经不被信任的CA机构存在的旧根证书,还有一些例如国内的一些基于Android老版本的操作系统仍然面临着安全风险,所以使用SSL数字证书锁定(SSL/TLS Pinning)的目标是缩小可信CA的范围,让APP客户端传送数据更安全,更切实地保障用户数据。

参考链接