AES-256 在 iOS 和 Node.js 中使用相同的密码生成不同的密文
AES-256 produces different ciphertexts in iOS and Node.js with same password
所以我有我的 iOS 代码:
#import <CommonCrypto/CommonCrypto.h>
NSString* password = @"1234567890123456";
NSString* salt = @"gettingsaltyfoo!";
-(NSString *)decrypt:(NSString*)encrypted64{
NSMutableData* hash = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
NSMutableData* key = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CC_SHA256(salt.UTF8String, (CC_LONG)strlen(salt.UTF8String), hash.mutableBytes);
CCKeyDerivationPBKDF(kCCPBKDF2, password.UTF8String, strlen(password.UTF8String), hash.bytes, hash.length, kCCPRFHmacAlgSHA1, 1000, key.mutableBytes, key.length);
NSLog(@"Hash : %@",[hash base64EncodedStringWithOptions:0]);
NSLog(@"Key : %@",[key base64EncodedStringWithOptions:0]);
NSData* encryptedWithout64 = [[NSData alloc] initWithBase64EncodedString:encrypted64 options:0];
NSMutableData* decrypted = [NSMutableData dataWithLength:encryptedWithout64.length + kCCBlockSizeAES128];
size_t bytesDecrypted = 0;
CCCrypt(kCCDecrypt,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding,
key.bytes,
key.length,
NULL,
encryptedWithout64.bytes, encryptedWithout64.length,
decrypted.mutableBytes, decrypted.length, &bytesDecrypted);
NSData* outputMessage = [NSMutableData dataWithBytes:decrypted.mutableBytes length:bytesDecrypted];
NSString* outputString = [[NSString alloc] initWithData:outputMessage encoding:NSUTF8StringEncoding];
NSLog(@"Decrypted : %@",outputString);
return outputString;
}
-(NSString *)encrypt:(NSString *)toEncrypt{
NSMutableData* hash = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
NSMutableData* key = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CC_SHA256(salt.UTF8String, (CC_LONG)strlen(salt.UTF8String), hash.mutableBytes);
CCKeyDerivationPBKDF(kCCPBKDF2, password.UTF8String, strlen(password.UTF8String), hash.bytes, hash.length, kCCPRFHmacAlgSHA1, 1000, key.mutableBytes, key.length);
NSData* message = [toEncrypt dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData* encrypted = [NSMutableData dataWithLength:message.length + kCCBlockSizeAES128];
size_t bytesEncrypted = 0;
CCCrypt(kCCEncrypt,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding,
key.bytes,
key.length,
NULL,
message.bytes, message.length,
encrypted.mutableBytes, encrypted.length, &bytesEncrypted);
NSString* encrypted64 = [[NSMutableData dataWithBytes:encrypted.mutableBytes length:bytesEncrypted] base64EncodedStringWithOptions:0];
NSLog(@"Encrypted : %@",encrypted64);
return encrypted64;
}
我有我的 node.js 代码:
var CryptoJS = require("crypto-js");
var crypto = require('crypto');
var password = "1234567890123456";
var salt = "gettingsaltyfoo!";
var hash = CryptoJS.SHA256(salt);
var key = CryptoJS.PBKDF2(password, hash, { keySize: 256/32, iterations: 1000 });
var algorithm = 'aes128';
function encrypt(text){
var cipher = crypto.createCipher(algorithm,key.toString(CryptoJS.enc.Base64));
var crypted = cipher.update(text,'utf8','hex');
crypted += cipher.final('hex');
console.log(crypted);
console.log(hash.toString(CryptoJS.enc.Base64));
console.log(key.toString(CryptoJS.enc.Base64));
return crypted;
}
function decrypt(text){
var decipher = crypto.createDecipher(algorithm,key.toString(CryptoJS.enc.Base64));
var dec = decipher.update(text,'hex','utf8');
dec += decipher.final('utf8');
console.log(dec);
return dec;
}
问题:不幸的是,虽然我有相同的散列、密钥和最终解密值(意味着它们可以独立工作),但我得到不同的加密值。因此,在一个代码中,如果我采用加密值并尝试在另一个代码中对其进行解密,则会出现错误。当我从 iOS 转到节点时,出现此错误:
ERROR:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length CODE: decrypt('vfOzya0yV9G5hLHeSh3R1g==');
我还得到了字符串 "Hello World":
这些不同的加密值
IOS: vfOzya0yV9G5hLHeSh3R1g==
NODE: 13b51a6785f47d8601c3a612d41b9a8b
我该如何解决这个问题,以便我可以互操作我的 iOS 和 Node.js,以及将来的 Android。我知道我的哈希算法适合生成 SHA256 和 PBDKF2,因为我得到了相同的哈希和密钥。这意味着在加密我的密码时,我的 AES128 实现在某处是错误的。很可能是我的 iOS 代码。请让我知道我的错误在哪里。
你不需要使用 CryptoJS,因为 node.js' crypto module 提供了你需要的一切。 CryptoJS 与 node.js' 原生 Buffer 具有不同的二进制表示,因此将两者结合使用会出现问题。
问题:
- 您正在使用
crypto.createCipher()
,它将以 OpenSSL 兼容格式自行从密码派生密钥。你想使用 crypto.createCipheriv()
.
- 您没有将 IV 传递给 in Objective-C,它默认为零填充 IV。您需要通过初始化零填充缓冲区在 node.js 中执行相同的操作。
- 您在 node.js 中以 Base64 编码形式提供密钥,但您必须提供字节 (Buffer)。
- 由于密钥大小是 256 位,因此您实际上使用的是 AES-256 而不是 AES-128。尽管指定了 128 位,CommonCrypto 代码似乎会自动更改为 256 位,但 node.js 要求您明确指定 256 位。另外,"aes128"或"aes256"会在node.js中默认为ECB模式,而CommonCrypto默认为CBC模式,所以需要明确指定。
完整的工作代码:
var crypto = require('crypto');
var password = "1234567890123456";
var salt = "gettingsaltyfoo!";
var sha256 = crypto.createHash("sha256");
sha256.update(salt);
var hash = sha256.digest();
var key = crypto.pbkdf2Sync(password, hash, 1000, 32, "sha1");
var iv = new Buffer(16);
iv.fill(0);
var algorithm = 'aes-256-cbc';
function encrypt(text){
var cipher = crypto.createCipheriv(algorithm, key, iv);
var crypted = cipher.update(text,'utf8','base64');
crypted += cipher.final('base64');
return crypted;
}
function decrypt(text){
var decipher = crypto.createDecipheriv(algorithm, key, iv);
var dec = decipher.update(text,'base64','utf8');
dec += decipher.final('utf8');
return dec;
}
console.log(encrypt("Hello World"));
输出:
vfOzya0yV9G5hLHeSh3R1g==
其他注意事项:
您需要为每次加密生成一个随机 IV。如果您不这样做,那么如果您每次都使用相同的密钥,那么攻击者可能会看到您对同一消息进行了多次加密,而实际上并没有对其进行解密。由于您是从密码派生密钥,因此您可以通过生成随机盐并从 PBKDF2 派生 384 位(48 字节)来更好地做到这一点。使用前 32 个字节作为密钥,其余的作为 IV。
您需要验证密文。如果您不这样做,那么攻击者可能会在您的系统上安装填充 oracle 攻击。您可以通过 运行 密文上的 HMAC 轻松完成此操作,并将生成的标签与其一起发送。然后,您可以在解密之前通过 运行 HMAC 在接收到的密文上再次验证标签,以检查操纵。
或者您可以使用像 GCM 这样的身份验证模式。
所以我有我的 iOS 代码:
#import <CommonCrypto/CommonCrypto.h>
NSString* password = @"1234567890123456";
NSString* salt = @"gettingsaltyfoo!";
-(NSString *)decrypt:(NSString*)encrypted64{
NSMutableData* hash = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
NSMutableData* key = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CC_SHA256(salt.UTF8String, (CC_LONG)strlen(salt.UTF8String), hash.mutableBytes);
CCKeyDerivationPBKDF(kCCPBKDF2, password.UTF8String, strlen(password.UTF8String), hash.bytes, hash.length, kCCPRFHmacAlgSHA1, 1000, key.mutableBytes, key.length);
NSLog(@"Hash : %@",[hash base64EncodedStringWithOptions:0]);
NSLog(@"Key : %@",[key base64EncodedStringWithOptions:0]);
NSData* encryptedWithout64 = [[NSData alloc] initWithBase64EncodedString:encrypted64 options:0];
NSMutableData* decrypted = [NSMutableData dataWithLength:encryptedWithout64.length + kCCBlockSizeAES128];
size_t bytesDecrypted = 0;
CCCrypt(kCCDecrypt,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding,
key.bytes,
key.length,
NULL,
encryptedWithout64.bytes, encryptedWithout64.length,
decrypted.mutableBytes, decrypted.length, &bytesDecrypted);
NSData* outputMessage = [NSMutableData dataWithBytes:decrypted.mutableBytes length:bytesDecrypted];
NSString* outputString = [[NSString alloc] initWithData:outputMessage encoding:NSUTF8StringEncoding];
NSLog(@"Decrypted : %@",outputString);
return outputString;
}
-(NSString *)encrypt:(NSString *)toEncrypt{
NSMutableData* hash = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
NSMutableData* key = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CC_SHA256(salt.UTF8String, (CC_LONG)strlen(salt.UTF8String), hash.mutableBytes);
CCKeyDerivationPBKDF(kCCPBKDF2, password.UTF8String, strlen(password.UTF8String), hash.bytes, hash.length, kCCPRFHmacAlgSHA1, 1000, key.mutableBytes, key.length);
NSData* message = [toEncrypt dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData* encrypted = [NSMutableData dataWithLength:message.length + kCCBlockSizeAES128];
size_t bytesEncrypted = 0;
CCCrypt(kCCEncrypt,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding,
key.bytes,
key.length,
NULL,
message.bytes, message.length,
encrypted.mutableBytes, encrypted.length, &bytesEncrypted);
NSString* encrypted64 = [[NSMutableData dataWithBytes:encrypted.mutableBytes length:bytesEncrypted] base64EncodedStringWithOptions:0];
NSLog(@"Encrypted : %@",encrypted64);
return encrypted64;
}
我有我的 node.js 代码:
var CryptoJS = require("crypto-js");
var crypto = require('crypto');
var password = "1234567890123456";
var salt = "gettingsaltyfoo!";
var hash = CryptoJS.SHA256(salt);
var key = CryptoJS.PBKDF2(password, hash, { keySize: 256/32, iterations: 1000 });
var algorithm = 'aes128';
function encrypt(text){
var cipher = crypto.createCipher(algorithm,key.toString(CryptoJS.enc.Base64));
var crypted = cipher.update(text,'utf8','hex');
crypted += cipher.final('hex');
console.log(crypted);
console.log(hash.toString(CryptoJS.enc.Base64));
console.log(key.toString(CryptoJS.enc.Base64));
return crypted;
}
function decrypt(text){
var decipher = crypto.createDecipher(algorithm,key.toString(CryptoJS.enc.Base64));
var dec = decipher.update(text,'hex','utf8');
dec += decipher.final('utf8');
console.log(dec);
return dec;
}
问题:不幸的是,虽然我有相同的散列、密钥和最终解密值(意味着它们可以独立工作),但我得到不同的加密值。因此,在一个代码中,如果我采用加密值并尝试在另一个代码中对其进行解密,则会出现错误。当我从 iOS 转到节点时,出现此错误:
ERROR:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length CODE: decrypt('vfOzya0yV9G5hLHeSh3R1g==');
我还得到了字符串 "Hello World":
这些不同的加密值IOS: vfOzya0yV9G5hLHeSh3R1g==
NODE: 13b51a6785f47d8601c3a612d41b9a8b
我该如何解决这个问题,以便我可以互操作我的 iOS 和 Node.js,以及将来的 Android。我知道我的哈希算法适合生成 SHA256 和 PBDKF2,因为我得到了相同的哈希和密钥。这意味着在加密我的密码时,我的 AES128 实现在某处是错误的。很可能是我的 iOS 代码。请让我知道我的错误在哪里。
你不需要使用 CryptoJS,因为 node.js' crypto module 提供了你需要的一切。 CryptoJS 与 node.js' 原生 Buffer 具有不同的二进制表示,因此将两者结合使用会出现问题。
问题:
- 您正在使用
crypto.createCipher()
,它将以 OpenSSL 兼容格式自行从密码派生密钥。你想使用crypto.createCipheriv()
. - 您没有将 IV 传递给 in Objective-C,它默认为零填充 IV。您需要通过初始化零填充缓冲区在 node.js 中执行相同的操作。
- 您在 node.js 中以 Base64 编码形式提供密钥,但您必须提供字节 (Buffer)。
- 由于密钥大小是 256 位,因此您实际上使用的是 AES-256 而不是 AES-128。尽管指定了 128 位,CommonCrypto 代码似乎会自动更改为 256 位,但 node.js 要求您明确指定 256 位。另外,"aes128"或"aes256"会在node.js中默认为ECB模式,而CommonCrypto默认为CBC模式,所以需要明确指定。
完整的工作代码:
var crypto = require('crypto');
var password = "1234567890123456";
var salt = "gettingsaltyfoo!";
var sha256 = crypto.createHash("sha256");
sha256.update(salt);
var hash = sha256.digest();
var key = crypto.pbkdf2Sync(password, hash, 1000, 32, "sha1");
var iv = new Buffer(16);
iv.fill(0);
var algorithm = 'aes-256-cbc';
function encrypt(text){
var cipher = crypto.createCipheriv(algorithm, key, iv);
var crypted = cipher.update(text,'utf8','base64');
crypted += cipher.final('base64');
return crypted;
}
function decrypt(text){
var decipher = crypto.createDecipheriv(algorithm, key, iv);
var dec = decipher.update(text,'base64','utf8');
dec += decipher.final('utf8');
return dec;
}
console.log(encrypt("Hello World"));
输出:
vfOzya0yV9G5hLHeSh3R1g==
其他注意事项:
您需要为每次加密生成一个随机 IV。如果您不这样做,那么如果您每次都使用相同的密钥,那么攻击者可能会看到您对同一消息进行了多次加密,而实际上并没有对其进行解密。由于您是从密码派生密钥,因此您可以通过生成随机盐并从 PBKDF2 派生 384 位(48 字节)来更好地做到这一点。使用前 32 个字节作为密钥,其余的作为 IV。
您需要验证密文。如果您不这样做,那么攻击者可能会在您的系统上安装填充 oracle 攻击。您可以通过 运行 密文上的 HMAC 轻松完成此操作,并将生成的标签与其一起发送。然后,您可以在解密之前通过 运行 HMAC 在接收到的密文上再次验证标签,以检查操纵。
或者您可以使用像 GCM 这样的身份验证模式。