Java Cryptography Extension no Android

ava 的设计几乎总是考虑到其功能的扩展。例如,定义 JAX-RS(用于 RESTful Web 服务的 Java API)和 JAXB(用于 XML 绑定的 Java 架构)注解等 API 接口。因此,出现了在标准规范要求集之上提供几个额外功能的框架。

Java SE 功能的扩展点之一是称为JCE  (Java Cryptography Extension)的加密算法提供程序规范 ——该扩展为我们提供了一种添加不同加密提供程序的方法。这样,我们就可以使用 JRE(Java 运行时环境)标准中没有的算法,并且我们还可以通过该架构更改算法强度限制设置。

也许这里有一个问题:但我们为什么要这样做?一个原因是使用比平台提供的更强大的加密。一个很好的例子是对称块加密高级加密标准 (AES) 算法。默认情况下,  Java 的密钥大小限制为 128 位。. 可以使用默认加密提供程序扩展此最小值,但为此必须通过安装额外的“策略”文件(也称为“无限强度”)来自定义 JRE。这有几个原因:上限不是 Sun 或 Oracle 从他们的头脑中创造出来的,而是美国机构强加的对消费产品加密强度的上限。我不打算参与这个问题的政治讨论,但由于这种限制并非在世界各地都存在,因此可以选择超越这一限制。

无论出于何种原因,能够添加另一个加密提供程序并自定义 JCA(Java 加密体系结构)边界总是好的。原因之一是可以比较这些敏感算法的实现,也就是说,可以测试不属于 JRE 的不太传统的算法。

按照这些思路,Android 决定默认使用不同的提供程序(此外,当然,它不能使用 JRE 默认值,因为它是专有的)。被选为标准的项目是 Bouncy Castle,它目前已有 15 年历史,由一个名为 Legion of the Bouncy Castle Inc. 的非营利组织资助。

此解决方案对 Android 开发人员有利有弊。这种方式的最大优势,前面已经提到过,是存在更多的算法通过JCE来保持与Java API的兼容性,也就是说,当我们想要生成一个AES密钥时,我们可以像下面这样初始化一个KeyGenerator的实例:

try {
   final KeyGenerator keyGen = KeyGenerator.getInstance("AES", "BC");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
   // do something
}

在这段代码中,我们为 AES 算法请求一个 KeyGenerator 实例,并将我们想要的提供者——在本例中,“BC”传递给 Bouncy Castle。因此,我们使用 javax.crypto 包中的 API,但我们使用 Bouncy Castle 实现。

在这种情况下,我们可以使用“keyGen”变量根据我们想要的密钥大小初始化其内部状态:

keyGen.init(256);

这是我们可能开始陷入困境的地方。还记得 JRE 带来的默认限制吗?上面的代码可以在 Android 模拟器(没有这个限制)中顺利运行,但不能在标准的 Oracle JRE 中运行。然后就会出现各种各样的问题……你打算如何测试你的加密代码,通常是本地测试,因为它使用的业务逻辑多于交互逻辑?如果您尝试使用 Robolectric 或类似的东西,由于 Oracle JVM 和 Android 平台的默认 JCE 提供程序之间的差异,您仍然会遇到问题。

在平台上使用默认提供程序的另一个有问题的缺点是它的升级。例如,在 Android 4.0(API 14)版本中,发布的 Bouncy Castle 是 1.46 版本(根据 本期),但在撰写本文时,最新版本是 1.56。如果我们查看更改日志,我们会看到在不同版本中实施了几个修复程序。因此,尽管我们在下面的示例中使用了 Bouncy Castle,但值得记住的是,生产中的错误可能与 Bouncy Castle 的版本有关。

如果您的应用程序依赖于加密算法,则需要大量考虑您的架构决策以避免安全问题。解决升级问题的一种方法是提供第三方 JCE 提供程序。例如, 海绵城堡。这个提供者是 Bouncy Castle 的一个分支,但是包和提供者的名字发生了变化,所以我们在 Android 应用程序中没有类冲突。如果我们尝试简单地放置一个装有最新版本的 Bouncy Castle 的 jar,我们将遇到 ClassLoader 问题,因为 Bouncy Castle 在应用程序的加载路径上可用。但是,这种情况仅适用于 Android,因为 JCE 提供程序必须由 Oracle 签名才能在其 JVM 中被接受为实现。

有了这个,我们是否能够直接在 JVM 中测试我们的代码?让我们看看下面的例子:

public static void main(String... args) throws Exception {
    final KeyGenerator keyGen = KeyGenerator.getInstance("AES");
    System.out.println("O provider que usamos é " + keyGen.getProvider().getName());
    keyGen.init(256);
    final SecretKey secretKey = keyGen.generateKey();
    System.out.println("O tamanho da chave é " + (secretKey.getEncoded().length * 8));
}

如果您没有更改 JRE 中的任何内容,那么您应该会收到如下异常:

java.security.InvalidKeyException:非法密钥大小

但是现在呢?我是否将 Bouncy Castle 安装为 JRE 的加密提供程序?嗯……在 Android 上它已经在我们的加载路径上,所以我只需要一个依赖项来进行我们的测试:

// Dependência de teste local
    testCompile 'org.bouncycastle:bcprov-jdk16:1.46'

 现在我们可以在我们的测试中安装提供程序:

public class CryptoTest {

    static {
        Security.insertProviderAt(new BouncyCastleProvider(), 1);
    }

    @Test
    public void canSetProvider() throws Exception {
        // passando o provider como segundo parâmetro
        final KeyGenerator keyGen = KeyGenerator.getInstance("AES", "BC");
        Assert.assertEquals(keyGen.getProvider().getName(), "BC");
        keyGen.init(256);
        final SecretKey secretKey = keyGen.generateKey();
        // Tudo ok com o tamanho!
        Assert.assertEquals(secretKey.getEncoded().length * 8, 256);
    }
}

 

这几乎是一个出路,但我们有一些问题。

首先,我们还是要安装JRE的无限强度策略。此限制独立于 JCE 提供程序。

另一个问题是,我们没有使用平台默认值,而是在 getInstance() 方法中显式传递提供者。因此,如果我们想要更改默认提供程序,或者更确切地说,如果 Android 将默认提供程序从一个版本更改为另一个版本,我们就会遇到问题。

我们回到最初的斯诺克。但是让我们了解在这种情况下我们想要发生什么:

我们希望 在 Android 上使用javax.crypto包中的 API, 以便我们可以在 JVM 中使用本地测试进行测试,而无需更改我们的代码。

我们的示例有一个简单的解决方案:安装无限强度策略。这是因为我们使用了比默认允许的更大的密钥大小。

要增加默认加密强度限制,只需从 https://goo.gl/dl1x7H(适用于 Java 8)下载 zip,解压并将 local_policy.jar 和 US_export_policy.jar 移动到 $JAVA_HOME/jre/lib 文件夹 /security (建议先备份被覆盖的文件)。准备好了!

如果我们再次运行我们的示例,测试将通过。但是,默认的 Oracle JVM 提供程序不称为“BC”,而是“SunJCE”。因此,让我们更改代码以使用默认提供程序:

public class CryptoTest {

    @Test
    public void canSetProvider() throws Exception {
        // passando o provider como segundo parâmetro
        final KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        Assert.assertEquals(keyGen.getProvider().getName(), "SunJCE");
        keyGen.init(256);
        final SecretKey secretKey = keyGen.generateKey();
        // Tudo ok com o tamanho!
        Assert.assertEquals(secretKey.getEncoded().length * 8, 256);
    }
}

 

因此,我们的代码将在 Android 模拟器和设备上与 BouncyCastle 一起运行,但将在本地测试中使用默认的 JVM 提供程序。这是可以接受的吗?这取决于。

默认提供程序和 Bouncy Castle 都是受尊重的加密算法实现。因此,如果您的应用程序仅依赖于两个实现都包含的算法,那么应该不会有任何问题。

然而,读者应该不会再感到惊讶了,这种方法存在一个大问题:加密模式的解释。理论上大家对PKCS(Password-based Cryptography Specification)中定义的标准的理解是一样的吧?毕竟,规范不应留有解释的余地​​。嗯……不幸的是,它不是那样的。

在JCA(Java Cryptography Architecture)算法规范中,给padding起了“PKCS5Padding”的名字,其实就是“PKCS7Padding”(虽然规范对padding有不同的定义,但可以说PKCS5Padding是PKCS7Padding的子集)。如果您在 Internet 上快速搜索“Java PCKS7 padding”,您会发现许多与此问题类似的问题。

那么我们真的没有办法解决 Java 和 Android 加密提供商的问题吗?僵局?没有出口的街道?还没有。

回顾一下我们目前所见:Android 提供了一个不同于标准 Oracle JVM 的默认加密算法提供程序。我们希望使用 javax.crypto 包中的 API,并且能够在模拟器和 JVM 中的本地测试中测试我们的代码。为此,我们安装了加密强度扩展文件并使我们的代码不依赖于特定的提供者。我们现在的问题是 Android 和 JVM 中填充规范的名称不同。

还有最后一种更彻底的方法可以完全解决这个问题:更改整个 JRE 的默认加密提供程序。因此,当我们不在 KeyGenerator.getInstance() 之类的方法中传递提供者时,我们实际上将使用 Bouncy Castle,而不是 Sun JCE。

请记住,这种方法可能会以相反的方式使我们复杂化:如果某些代码假定默认提供程序是 SunJCE 并且没有在方法中传递提供程序,那么它会因对规范的不同解释而出现问题。

要更改 JRE 默认使用的提供程序,我们必须更改 $JAVA_HOME/jre/lib/security/java.security 文件,找到声明提供程序的部分(它们都以 security.provider.N 开头,其中 N是提供者的搜索顺序)并添加一行我们想要的提供者。在我们的例子中,它可能是:

security.provider.10=org.bouncycastle.jce.provider.BouncyCastleProvider

这里的顺序很重要。如果 security.provider.N=com.sun.crypto.provider.SunJCE 提供程序
具有优先权,则将首先使用它。在 Bouncy Castle 文档中,他们不建议将 Bouncy Castle 放在首位,因为这会破坏 JVM 本身。

如果我们这样做,我们的代码将不需要传递我们想要的提供者并且将使用 Bouncy Castle。这样,我们就可以避免诸如“PKCS7Padding”之类的问题,但我们需要知道任何假设相反的代码(例如,“PKCS5Padding”与“PKCS7Padding”是同一个东西)都可能有问题。

还有其他不遵循 JCA 架构的加密算法实现,因此不会面临这些环境奇偶校验问题。 Facebook 的隐藏库就是一个很好的例子 。请记住,决定使用非标准平台库进行加密是一个应该深思熟虑并与安全团队讨论的决定。如果有任何疑问,最好不要使用它。

我希望通过本文及其参考资料,读者将更好地了解加密提供程序的 Java 架构是什么,以及与 Android 有什么区别。

其他参考资料:https : //www.crypto101.io/  – 很棒的免费密码学书籍。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值