AES/GCM Kotlin 和 Dart 的 PBKDF2 加密
AES/GCM PBKDF2 Encryption for Kotlin and Dart
我正在尝试将 GCM 加密与 PBKDF2 一起使用,它可以跨 kotlin 和 dart 互操作。接下来是解密器。目前我正在使用一个“工作”的 kotlin 版本(下面),如果它是正确的,我想在 dart 中复制它(我在下面的尝试)。请参阅以下 Kotlin 版本(注意日志结果在各行下方提供。输出是可疑的。):
另请注意:这些示例现在使用相同的文本和 masterpass 输入。
KOTLIN:
fun encrypt(input: String, password: String): String {
val masterpw = getKey(password).toString(Charset.forName("UTF-8"))
val mastertest = getKey(password)
val random = SecureRandom()
Log.d("RANDOM", "${random}") //D/RANDOM (25834): java.security.SecureRandom@dc72083
val salt = ByteArray(8)
Log.d("SALT", "${salt}") //D/SALT (25834): [B@202f200
random.nextBytes(salt)
Log.d("SALT2", "${salt}") //D/SALT2 (25834): [B@202f200
val factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
Log.d("factory", "${factory}") //D/factory (25834): javax.crypto.SecretKeyFactory@cfd3a39
val spec: KeySpec = PBEKeySpec(masterpw.toString().toCharArray(), salt, 10000, 256)
Log.d("KeySpec", "${spec}") //D/KeySpec (25834): javax.crypto.spec.PBEKeySpec@a5587e
val tmp: SecretKey = factory.generateSecret(spec)
Log.d("SecretKey", "${tmp}") //D/SecretKey(25834): com.android.org.bouncycastle.jcajce.provider.symmetric.util.BCPBEKey@6d141df
val iv = ByteArray(12)
Log.d("IV", "${iv}") //D/IV (25834): [B@aa52e2c
random.nextBytes(iv)
Log.d("IV2", "${iv}") //D/IV2 (25834): [B@aa52e2c
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
Log.d("Cipher", "${cipher}") //D/Cipher (25834): javax.crypto.Cipher@e6e40f5
cipher.init(Cipher.ENCRYPT_MODE, tmp, IvParameterSpec(iv))
Log.d("Cipher2", "${cipher}") //D/Cipher2 (25834): javax.crypto.Cipher@e6e40f5
val cipherText: ByteArray = cipher.doFinal(input.toByteArray(Charset.forName("UTF-8")))
Log.d("cipherText", "${cipherText}") //D/cipherText(25834): [B@f3a7e8a
val ivstring: String = Base64.encodeToString(iv, Base64.NO_WRAP)
Log.d("ivstring", "${ivstring}") //D/ivstring(25834): D3tPtM6+WYnoSswE
val saltystring: String = Base64.encodeToString(salt, Base64.NO_WRAP)
Log.d("saltystring", "${saltystring}") //D/saltystring(25834): zbq9ZqJ9xiw=
val cipherstring: String = Base64.encodeToString(cipherText, Base64.NO_WRAP)
Log.d("cipherstring", "${cipherstring}") //D/cipherstring(25834): w/WqSqg++udXCLKE6ly765OWBHKt79Lw/g==
val returnstring: String = ivstring + "-" + saltystring + "-" + cipherstring
Log.d("returnstring", "${returnstring}") //D/returnstring(25834): D3tPtM6+WYnoSswE-zbq9ZqJ9xiw=-w/WqSqg++udXCLKE6ly765OWBHKt79Lw/g==
return returnstring
}
fun getKey(masterPass: String): ByteArray {
return masterPass.padEnd(32, '.').toByteArray(Charset.forName("UTF-8"))
}
飞镖:
dart 方法使用 cryptography.dart 包 v1.4.1。注意:由于应用程序中其他库的限制,我无法使用更新版本的加密包,我相信它会排除最近添加的 'AesGcm.with128bits' 函数。
当我尝试将加密的 cipherTextBytes 解码为字符串时,dart 版本在接近尾声时崩溃,如提供的日志结果所示。
encryptPassGCM(String text, String masterPass) async {
print("ENCRYPTPASSGCM STARTS WITH: " + "TEXT: " + text + " & " + "master: " + masterPass);
//trim key and convert
String keyString = masterPass;
if (keyString.length < 32) {
int count = 32 - keyString.length;
for (var i = 0; i < count; i++) {
keyString += ".";
}
}
Uint8List keyStringutf8 = utf8.encode(keyString);
print("keyStringutf8: " + keyStringutf8.toString());
//gen salt and iv
final salt = Nonce.randomBytes(8);
print("salt init: " + salt.bytes.toString());
//LOG salt init: [161, 50, 222, 98, 151, 225, 89, 65]
final iv = Nonce.randomBytes(12);
print("iv init: " + iv.bytes.toString());
//LOG iv init: [59, 188, 146, 172, 213, 13, 135, 35, 202, 220, 178, 190]
//create key
final pbkdf2 = Pbkdf2(
macAlgorithm: Hmac(sha1),
iterations: 10000,
bits: 256,
);
final keyBytes = await pbkdf2.deriveBits(
keyStringutf8,
nonce: salt,
);
print("keybytes: " + keyBytes.toString());
//LOG keybytes: [85, 204, 96, 108, 200, 21, 24, 115, 254, 104, 133, 81, 53, 126, 252, 161, 172, 193, 25, 177, 143, 69, 53, 35, 105, 144, 248, 6, 121, 106, 237, 142]
SecretKey secretKey = new SecretKey(keyBytes);
print("secretKey: " + secretKey.toString());
//LOG secretKey: SecretKey(...)
//create ciphertext and convert to string
List<int> textutf8 = utf8.encode(text);
Uint8List cipherTextBytes = await AesGcm().encrypt(textutf8, secretKey: secretKey, nonce: iv);
print("cipherTextBytes: " + cipherTextBytes.toString());
//LOG cipherTextBytes: [110, 3, 238, 169, 52, 125, 176, 200, 122, 142, 111, 75, 181, 248, 91, 57, 95, 131, 85, 223, 224, 73, 173, 39, 37]
var cipherText = utf8.decode(cipherTextBytes); //CRASHES HERE
print("cipherText: " + cipherText);
var cipherString = iv.toString() + "-" + salt.toString() + "-" + cipherText;
print("GCM CIPHER STRING COMPLETE: " + cipherString);
return cipherString;
}
- kotlin 实现是否正确?数据日志似乎有误。
- 我的 dart 实现有什么问题?
Utf8解码密文时抛出异常。像密文这样的任意二进制数据或像 salts 或 IVs 这样的伪随机数据不能用像 Utf8 这样的字符集编码来解码,因为数据会被破坏,见 here。相反,必须应用像 Base64 这样的二进制到文本的编码。
Kotlin 代码 Base64 对盐、IV 和密文进行编码,并用分隔符 (-) 连接这些部分。 Dart 的对应物是例如:
var cipherString = base64.encode(iv.bytes) + "-" + base64.encode(salt.bytes) + "-" + base64.encode(cipherTextBytes);
并且cipherTextBytes
的UTF8解码将被移除。
第二个问题是最初发布的代码意外地没有使用使用 PBKDF2 派生的密钥,而是随机生成的密钥(另请参阅 Rob Napier 的评论)。
当两个错误都被修复后,两个代码在功能上是相同的并且产生相同的密文(假设相同的盐和 IV)。
注意,在Dart和Kotlin代码中,GCM认证标签(默认16字节)会自动附加到密文中(即cipherTextBytes
不仅包含密文,还包含认证标签:ciphertext|tag
).不是所有的库都是这样的,所以在用这样的库解密的时候需要把tag(最后16字节)分开。
此外,Base64 分别编码 salt、IV 和 ciphertext/tag 并用分隔符连接这些部分的情况并不常见。相反,按照惯例,串联是在字节级别完成的:salt|IV|ciphertext|tag
。由于两边的盐、IV 和标签长度都是已知的,因此不需要分隔符。结果是 Base64 编码的。但如前所述,这只是一个约定。
关于 PBKDF2 的参数:尽管 SHA1 被归类为不安全(参见 here), its use as HMAC/SHA1 is not critical. However, a move to SHA256 would support the banishment of SHA1 from the ecosystem. The iteration count should slow down an attacker and should therefore be chosen as high as possible while maintaining acceptable performance (typically larger than 10,000). The recommended salt length is 16 bytes, see PBKDF2。
我正在尝试将 GCM 加密与 PBKDF2 一起使用,它可以跨 kotlin 和 dart 互操作。接下来是解密器。目前我正在使用一个“工作”的 kotlin 版本(下面),如果它是正确的,我想在 dart 中复制它(我在下面的尝试)。请参阅以下 Kotlin 版本(注意日志结果在各行下方提供。输出是可疑的。):
另请注意:这些示例现在使用相同的文本和 masterpass 输入。
KOTLIN:
fun encrypt(input: String, password: String): String {
val masterpw = getKey(password).toString(Charset.forName("UTF-8"))
val mastertest = getKey(password)
val random = SecureRandom()
Log.d("RANDOM", "${random}") //D/RANDOM (25834): java.security.SecureRandom@dc72083
val salt = ByteArray(8)
Log.d("SALT", "${salt}") //D/SALT (25834): [B@202f200
random.nextBytes(salt)
Log.d("SALT2", "${salt}") //D/SALT2 (25834): [B@202f200
val factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
Log.d("factory", "${factory}") //D/factory (25834): javax.crypto.SecretKeyFactory@cfd3a39
val spec: KeySpec = PBEKeySpec(masterpw.toString().toCharArray(), salt, 10000, 256)
Log.d("KeySpec", "${spec}") //D/KeySpec (25834): javax.crypto.spec.PBEKeySpec@a5587e
val tmp: SecretKey = factory.generateSecret(spec)
Log.d("SecretKey", "${tmp}") //D/SecretKey(25834): com.android.org.bouncycastle.jcajce.provider.symmetric.util.BCPBEKey@6d141df
val iv = ByteArray(12)
Log.d("IV", "${iv}") //D/IV (25834): [B@aa52e2c
random.nextBytes(iv)
Log.d("IV2", "${iv}") //D/IV2 (25834): [B@aa52e2c
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
Log.d("Cipher", "${cipher}") //D/Cipher (25834): javax.crypto.Cipher@e6e40f5
cipher.init(Cipher.ENCRYPT_MODE, tmp, IvParameterSpec(iv))
Log.d("Cipher2", "${cipher}") //D/Cipher2 (25834): javax.crypto.Cipher@e6e40f5
val cipherText: ByteArray = cipher.doFinal(input.toByteArray(Charset.forName("UTF-8")))
Log.d("cipherText", "${cipherText}") //D/cipherText(25834): [B@f3a7e8a
val ivstring: String = Base64.encodeToString(iv, Base64.NO_WRAP)
Log.d("ivstring", "${ivstring}") //D/ivstring(25834): D3tPtM6+WYnoSswE
val saltystring: String = Base64.encodeToString(salt, Base64.NO_WRAP)
Log.d("saltystring", "${saltystring}") //D/saltystring(25834): zbq9ZqJ9xiw=
val cipherstring: String = Base64.encodeToString(cipherText, Base64.NO_WRAP)
Log.d("cipherstring", "${cipherstring}") //D/cipherstring(25834): w/WqSqg++udXCLKE6ly765OWBHKt79Lw/g==
val returnstring: String = ivstring + "-" + saltystring + "-" + cipherstring
Log.d("returnstring", "${returnstring}") //D/returnstring(25834): D3tPtM6+WYnoSswE-zbq9ZqJ9xiw=-w/WqSqg++udXCLKE6ly765OWBHKt79Lw/g==
return returnstring
}
fun getKey(masterPass: String): ByteArray {
return masterPass.padEnd(32, '.').toByteArray(Charset.forName("UTF-8"))
}
飞镖:
dart 方法使用 cryptography.dart 包 v1.4.1。注意:由于应用程序中其他库的限制,我无法使用更新版本的加密包,我相信它会排除最近添加的 'AesGcm.with128bits' 函数。 当我尝试将加密的 cipherTextBytes 解码为字符串时,dart 版本在接近尾声时崩溃,如提供的日志结果所示。
encryptPassGCM(String text, String masterPass) async {
print("ENCRYPTPASSGCM STARTS WITH: " + "TEXT: " + text + " & " + "master: " + masterPass);
//trim key and convert
String keyString = masterPass;
if (keyString.length < 32) {
int count = 32 - keyString.length;
for (var i = 0; i < count; i++) {
keyString += ".";
}
}
Uint8List keyStringutf8 = utf8.encode(keyString);
print("keyStringutf8: " + keyStringutf8.toString());
//gen salt and iv
final salt = Nonce.randomBytes(8);
print("salt init: " + salt.bytes.toString());
//LOG salt init: [161, 50, 222, 98, 151, 225, 89, 65]
final iv = Nonce.randomBytes(12);
print("iv init: " + iv.bytes.toString());
//LOG iv init: [59, 188, 146, 172, 213, 13, 135, 35, 202, 220, 178, 190]
//create key
final pbkdf2 = Pbkdf2(
macAlgorithm: Hmac(sha1),
iterations: 10000,
bits: 256,
);
final keyBytes = await pbkdf2.deriveBits(
keyStringutf8,
nonce: salt,
);
print("keybytes: " + keyBytes.toString());
//LOG keybytes: [85, 204, 96, 108, 200, 21, 24, 115, 254, 104, 133, 81, 53, 126, 252, 161, 172, 193, 25, 177, 143, 69, 53, 35, 105, 144, 248, 6, 121, 106, 237, 142]
SecretKey secretKey = new SecretKey(keyBytes);
print("secretKey: " + secretKey.toString());
//LOG secretKey: SecretKey(...)
//create ciphertext and convert to string
List<int> textutf8 = utf8.encode(text);
Uint8List cipherTextBytes = await AesGcm().encrypt(textutf8, secretKey: secretKey, nonce: iv);
print("cipherTextBytes: " + cipherTextBytes.toString());
//LOG cipherTextBytes: [110, 3, 238, 169, 52, 125, 176, 200, 122, 142, 111, 75, 181, 248, 91, 57, 95, 131, 85, 223, 224, 73, 173, 39, 37]
var cipherText = utf8.decode(cipherTextBytes); //CRASHES HERE
print("cipherText: " + cipherText);
var cipherString = iv.toString() + "-" + salt.toString() + "-" + cipherText;
print("GCM CIPHER STRING COMPLETE: " + cipherString);
return cipherString;
}
- kotlin 实现是否正确?数据日志似乎有误。
- 我的 dart 实现有什么问题?
Utf8解码密文时抛出异常。像密文这样的任意二进制数据或像 salts 或 IVs 这样的伪随机数据不能用像 Utf8 这样的字符集编码来解码,因为数据会被破坏,见 here。相反,必须应用像 Base64 这样的二进制到文本的编码。
Kotlin 代码 Base64 对盐、IV 和密文进行编码,并用分隔符 (-) 连接这些部分。 Dart 的对应物是例如:
var cipherString = base64.encode(iv.bytes) + "-" + base64.encode(salt.bytes) + "-" + base64.encode(cipherTextBytes);
并且cipherTextBytes
的UTF8解码将被移除。
第二个问题是最初发布的代码意外地没有使用使用 PBKDF2 派生的密钥,而是随机生成的密钥(另请参阅 Rob Napier 的评论)。
当两个错误都被修复后,两个代码在功能上是相同的并且产生相同的密文(假设相同的盐和 IV)。
注意,在Dart和Kotlin代码中,GCM认证标签(默认16字节)会自动附加到密文中(即cipherTextBytes
不仅包含密文,还包含认证标签:ciphertext|tag
).不是所有的库都是这样的,所以在用这样的库解密的时候需要把tag(最后16字节)分开。
此外,Base64 分别编码 salt、IV 和 ciphertext/tag 并用分隔符连接这些部分的情况并不常见。相反,按照惯例,串联是在字节级别完成的:salt|IV|ciphertext|tag
。由于两边的盐、IV 和标签长度都是已知的,因此不需要分隔符。结果是 Base64 编码的。但如前所述,这只是一个约定。
关于 PBKDF2 的参数:尽管 SHA1 被归类为不安全(参见 here), its use as HMAC/SHA1 is not critical. However, a move to SHA256 would support the banishment of SHA1 from the ecosystem. The iteration count should slow down an attacker and should therefore be chosen as high as possible while maintaining acceptable performance (typically larger than 10,000). The recommended salt length is 16 bytes, see PBKDF2。