8

用户可以购买我的应用程序的“专业”版本。当他们这样做时,我按如下方式存储并验证他们的购买。

  • 结合用户的 UUID 和另一个唯一字符串。
  • 然后使用静态种子对生成的字符串进行加密。我这样做是使用SecureRandom.getInstance("SHA1PRNG", "Crypto")- 这就是问题所在!
  • 生成的加密字符串就是“解锁码”。
  • 因此,我始终知道用户预期的唯一解锁码值。
  • 当用户购买“Pro”时,我将“解锁码”存储在数据库中。
  • 我通过查看数据库中存储的“解锁代码”是否与基于其唯一信息的预期代码匹配来检查用户是否拥有“Pro”。

所以,不是最好的系统,但对于我不起眼的应用程序来说,一切都已经足够模糊了。

问题是SecureRandom.getInstance("SHA1PRNG", "Crypto")在 N 上失败,因为不支持“加密”。我了解到,依赖特定的提供者是不好的做法,并且 Crypto 不受 N 支持。哎呀。

所以我有一个问题:我依靠价值种子对的加密来始终具有相同的输出。Android N 不支持我使用的加密提供程序,所以我不知道如何确保 N 上的加密输出与其他设备上的相同。

我的问题:

  1. 是否可以在我的 APK 中包含“加密”以便它始终可用?
  2. 在 Android N 上加密值-种子对时,我能否以其他方式确保相同的输出?

我的代码:

public static String encrypt(String seed, String cleartext) throws Exception {
    byte[] rawKey = getRawKey(seed.getBytes(), seed);
    byte[] result = encrypt(rawKey, cleartext.getBytes());
    return toHex(result); // "unlock code" which must always be the same for the same seed and clearText accross android versions
}

private static byte[] getRawKey(byte[] seed, String seedStr) throws Exception {
    SecureRandom sr;
    sr = SecureRandom.getInstance("SHA1PRNG", "Crypto");  // what used to work
    KeyGenerator kgen = KeyGenerator.getInstance("AES");
    sr.setSeed(seed);
    kgen.init(128, sr); 
    SecretKey skey = kgen.generateKey();
    byte[] raw = skey.getEncoded();
    return raw;
}

private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {
    SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
    byte[] encrypted = cipher.doFinal(clear);
    return encrypted;
}

public static String toHex(byte[] buf) {
    if (buf == null)
        return "";
    StringBuffer result = new StringBuffer(2 * buf.length);
    for (int i = 0; i < buf.length; i++) {
        appendHex(result, buf[i]);
    }
    return result.toString();
}
4

3 回答 3

6

我最近与 Android 安全团队讨论了这个问题。

在 Android N 中,SHA1PRNG 已被移除,因为我们没有对其进行安全实现。具体来说,.setSeed(long)在请求 PRNG 输出之前调用会替换 SecureRandom 实例中的所有熵。

这种行为长期以来一直被认为是安全故障(阅读:经常导致应用程序中的细微错误),因此我们选择在替换 SecureRandom 提供程序时不复制它。

如果您需要 PRNG,则只需使用new SecureRandom().

也就是说... SecureRandom() 并非旨在用作密钥派生函数,就像您在示例中所做的那样。请不要这样做!相反,请使用 PBKDF2 等算法,可通过SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").

一段时间以来,我们一直在警告开发人员。请看这些帖子:

如果您真的需要 SHA1PRNG,即使在所有这些之后......那么解决方法是从 Android 源代码中复制实现,就像他的回答中提到的@artjom-b 一样。

但是,请仅当您在迁移到 PBKDF2 或类似软件时需要兼容性时才这样做。

于 2016-05-18T03:02:21.157 回答
5

使用诸如 SecureRandom 之类的 PRNG 来确定性地导出数据通常是一个坏主意,因为有破坏性更改的历史。使用特定的实现并将其包含在您的应用程序中始终是一个好主意。在您的情况下,可以只复制实现代码。

SecureRandom.getInstance("SHA1PRNG", "Crypto");查找org.apache.harmony.security.provider.crypto.CryptoProviderAndroid 5.1.1 中的“加密”提供程序。它重定向到org.apache.harmony.security.provider.crypto.SHA1PRNG_SecureRandomImpl作为实际实现。您可以轻松地将代码复制到不同包下的项目中,并确保遵守代码许可。

然后你可以像这样使用它:

sr = new SecureRandom(new your.pkg.SHA1PRNG_SecureRandomImpl(), null);

根据代码不使用第二个提供程序参数,但您可以创建一个虚拟提供程序。


从某个种子生成密钥的正确方法是使用密钥派生函数 (KDF)。如果seed是类似密码的,那么当指定大量迭代时,PBKDF2 是一个很好的 KDF。如果seed是 key-like,那么推荐使用类似 HKDF 的 KBKDF。

于 2016-04-23T19:05:04.503 回答
4

我为 CryptoProvider 添加了一个类,您可以替换 SecureRandom.getInstance("SHA1PRNG", "Crypto"); SecureRandom.getInstance("SHA1PRNG", new CryptoProvider());

您可以参考以下链接以获取解决方案,它对我有用;

Android N 中已弃用安全“加密”提供程序

于 2017-02-20T06:39:04.037 回答