问题描述
用户可以购买我的应用的专业版"版本.当他们这样做时,我按如下方式存储并验证他们的购买.
- 结合用户的 UUID 和另一个唯一字符串.
- 然后使用静态种子对生成的字符串进行加密.我使用
SecureRandom.getInstance("SHA1PRNG", "Crypto")
- 这就是问题所在! - 生成的加密字符串就是解锁码".
- 因此,我始终知道预期用户的唯一解锁码值.
- 当用户购买Pro"时,我将解锁码"存储在数据库中.
- 我通过查看存储在数据库中的解锁码"是否与基于他们的唯一信息的预期码匹配来检查用户是否拥有Pro".
所以,这不是最好的系统,但对于我不起眼的应用来说,一切都已经足够模糊了.
问题是 SecureRandom.getInstance("SHA1PRNG", "Crypto")
在 N 上失败,因为不支持Crypto".我了解到依赖特定提供商是不好的做法,并且 N 不支持 Crypto .哎呀.
所以我有一个问题:我依靠值种子对的加密来始终具有相同的输出. Android N 不支持我使用的加密提供程序,所以我不支持知道如何确保 N 上的加密输出与其他设备上的相同.
我的问题:
- 是否可以在我的 APK 中包含加密"以便它始终可用?
- 在 Android N 上加密值种子对时,我能否以其他方式确保相同的输出?
我的代码:
public static String encrypt(String seed, String cleartext) throws Exception {byte[] rawKey = getRawKey(seed.getBytes(), seed);byte[] result = encrypt(rawKey, cleartext.getBytes());返回到十六进制(结果);//解锁代码"对于相同的种子和 clearText 必须始终相同,跨 android 版本}私有静态字节 [] getRawKey(字节 [] 种子,字符串 seedStr) 抛出异常 {SecureRandom sr;sr = SecureRandom.getInstance("SHA1PRNG", "Crypto");//过去的工作KeyGenerator kgen = KeyGenerator.getInstance("AES");sr.setSeed(种子);kgen.init(128, sr);SecretKey skey = kgen.generateKey();byte[] raw = skey.getEncoded();返回原始;}私有静态字节 [] 加密(字节 [] 原始,字节 [] 清除)抛出异常 {SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");密码密码 = Cipher.getInstance("AES");cipher.init(Cipher.ENCRYPT_MODE, skeySpec);byte[] 加密 = cipher.doFinal(clear);返回加密;}公共静态字符串 toHex(byte[] buf) {如果(buf == null)返回 "";StringBuffer 结果 = new StringBuffer(2 * buf.length);for (int i = 0; i < buf.length; i++) {appendHex(结果, buf[i]);}返回结果.toString();}
我最近和 Android 安全团队讨论过这个问题.
在 Android N 中,SHA1PRNG 已被移除,因为我们没有对其进行安全实现.具体来说,在请求 PRNG 输出之前调用 .setSeed(long)
会替换 SecureRandom 实例中的所有熵.
这种行为长期以来一直被认为是一种安全故障(阅读:经常会导致应用程序中出现细微的错误),因此我们选择在替换 SecureRandom 提供程序时不复制它.
如果您需要 PRNG,则只需使用 new SecureRandom()
.
也就是说... SecureRandom() 并非旨在用作密钥派生函数,就像您在示例中所做的那样.请不要这样做!相反,请使用 PBKDF2 等算法,可通过 SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
获得.
一段时间以来,我们一直在警告开发者.请参阅以下帖子:
如果您真的需要 SHA1PRNG,即使在所有这些之后......那么解决方法是从 Android 源代码中复制实现,就像他的回答中提到的 @artjom-b 一样.p>
但请仅当您在迁移到 PBKDF2 或类似软件时需要兼容性时才这样做.
Users can purchase a "Pro" version of my app. When they do, I store and verify their purchase as follows.
- Combine the user's UUID and another unique string.
- The resulting string is then encrypted using a static seed. I do this using
SecureRandom.getInstance("SHA1PRNG", "Crypto")
- This is the problem! - The resulting encrypted string is then the "unlock code".
- Therefore, I always know the expected unique unlock code value for the user.
- When the user purchases "Pro", I store the "unlock code" in the database.
- I check to see whether the user has "Pro" by seeing if the stored "unlock code" in the database matches the expected code based on their unique info.
So, not the best system, but everything is obfuscated enough for my humble app.
The problem is that SecureRandom.getInstance("SHA1PRNG", "Crypto")
fails on N because "Crypto" is not supported. I have learned that relying on specific providers is bad practice and Crypto is not supported on N. Oops.
So I have a problem: I rely on the encryption of a value-seed pair to always have the same output. Android N does not support the encryption provider I use, so I don't know how to ensure that the encryption output will be the same on N as it is on other devices.
My questions:
- Is it possible to include "Crypto" in my APK so that it is always available?
- Can I otherwise ensure the same output when encrypting a value-seed pair on Android N?
My code:
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();
}
I had a discussion with the Android Security team about this recently.
In Android N, SHA1PRNG was removed because we don't have a secure implementation of it. Specifically, calling .setSeed(long)
before requesting output from the PRNG replaces all of the entropy in the SecureRandom instance.
This behavior has long been pointed to as a security failure (read: frequently causes subtle bugs in apps), so we chose not to replicate it when the SecureRandom provider was replaced.
If you need a PRNG, then just use new SecureRandom()
.
That said... SecureRandom() is not designed to be used as a key derivation function, as you've done in your example. Please don't do this! Instead, use an algorithm such as PBKDF2, available via SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
.
We've been warning developers about this for a while. Please see these posts:
- Android Developers Blog: Using Cryptography to Store Credentials Safely
- Android 4.2 broke my encrypt/decrypt code and the provided solutions don't work
IF YOU REALLY NEED SHA1PRNG, EVEN AFTER ALL OF THAT... then the workaround is to copy the implementation out of the Android source, like @artjom-b mentioned in his answer.
But please, only do this if you need compatibility while migrating to PBKDF2 or similar.
这篇关于SecureRandom 提供者“加密"在 Android N 中无法确定性地生成密钥的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!