【问题标题】:Unable decrypt data in NodeJS which is encrypted using Swift CryptoKit - AES-256-GCM无法解密使用 Swift CryptoKit - AES-256-GCM 加密的 NodeJS 中的数据
【发布时间】:2021-03-05 06:59:19
【问题描述】:

我正在尝试在 NodeJS (Electron) 中创建一个应用程序作为跨平台桌面应用程序。这将与在 iOS 上使用 SWIFT 开发的移动应用程序配对。作为共享数据的一部分,它使用 AES-256-GCM 算法进行加密。我在 SWIFT 中有以下加密和解密方法:

//listItems is an array of the following structure:
// - id: Int, title: String, data: String, ldate: String
func encrypt(listItems: [ListItem], pass: String) -> String{
    let encoder = JSONEncoder()
    do{
        let data = try encoder.encode(listItems)
        let key = SymmetricKey(data: SHA256.hash(data: pass.data(using: .utf8)!))
        let iv = AES.GCM.Nonce()
        let sealedBox = try AES.GCM.seal(data, using: key, nonce: iv)
        return sealedBox.combined?.base64EncodedData()
    }catch{
        fatalError("Couldn't encrypt data\(error)")
    }
}

func decrypt(data: Data, pass: String) -> [ListItem]{
    do{
        let key = SymmetricKey(data: SHA256.hash(data: pass.data(using: .utf8)!))
        let mySealedBox = try AES.GCM.SealedBox(combined: Data(base64Encoded: data)!)
        let content = try AES.GCM.open(mySealedBox, using: key)
        return load(content)
    }catch{
        fatalError("Couldn't encrypt data\(error)")
    }
}

func load<T: Decodable>(_ data: Data) -> T{
    do{
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    }catch{
        fatalError("Could not parse the data")
    }
}

对于 NodeJS,我有以下功能:

const crypto = require('crypto')
module.exports = {
    encryptData(data,password){
        let password_hash = crypto.createHash('sha256').update(password, 'utf-8').digest('hex').slice(0,32).toLowerCase();
        let iv = crypto.pseudoRandomBytes(12);
        iv = Buffer.from('mBj0tzBUxDFmix1T', 'base64');

        let cipher = crypto.createCipheriv('aes-256-gcm', password_hash, iv);
        let encryptedData = Buffer.from(cipher.update(data, 'utf8', 'hex') + cipher.final('hex'), 'hex');
        console.log(' --------------- ENC BEGIN ---------------');
        console.log(`IV Length: ${iv.length}`);
        //console.log(`IV Base64 Length: ${iv.toString('base64').length}`);
        console.log(iv.toString('base64'));
        //console.log(`AuthTag Length: ${cipher.getAuthTag().length}`);
        //console.log(`AuthTag Base64 Length: ${cipher.getAuthTag().toString('base64').length}`);
        console.log(cipher.getAuthTag().toString('base64'));
        //console.log(`Encrypted Data Length: ${encryptedData.length}`)
        //console.log(`Encrypted Data Base64 Length: ${encryptedData.toString('base64').length}`)
        console.log(encryptedData.toString('base64'));
        console.log(' --------------- ENC END ---------------');
        console.log(Buffer.concat([cipher.getAuthTag(), encryptedData]).toString('base64'));
        console.log(Buffer.concat([encryptedData, cipher.getAuthTag()]).toString('base64'));
        //let encryptedBuffer = Buffer.concat([iv, cipher.getAuthTag(), encryptedData]);
        return iv.toString('base64') + cipher.getAuthTag().toString('base64') + encryptedData.toString('base64');
    },
    
    decryptData(data,password){
        let password_hash = crypto.createHash('sha256').update(password, 'utf-8').digest('hex').slice(0,32).toLowerCase();
        //let combinerBuffer = Buffer.from(data, 'base64');
        //let iv = combinerBuffer.slice(0,16);
        let iv = Buffer.from(data.slice(0,16), 'base64');
        console.log(' --------------- DEC BEGIN ---------------');
        console.log(iv.toString('base64'));
        let at = Buffer.from(data.slice(16,32), 'base64');
        console.log(at.toString('base64'));
        let enc_buffer = Buffer.from(data.slice(32), 'base64');
        console.log(enc_buffer.toString('base64'));
        console.log(' --------------- DEC END ---------------');
        let deciper = crypto.createDecipheriv('aes-256-gcm', password_hash, iv);
        deciper.setAuthTag(at)
        let dec_buf = deciper.update(enc_buffer, 'utf8') + deciper.final('utf8');
        return dec_buf.toString('utf8');
    }
}

被 SWIFT 加密的文件不能被 NodeJS 解密。在解密数据时,我收到错误: Unsupported state or unable to authenticate data

我在 Java 中有一个类似的代码,它也适用于 SWIFT 生成的加密数据,但 NodeJS 的代码根本不起作用。主要问题是如何从 SWIFT 生成的组合加密文本中获取 AAD 和 AuthTag。在 Java 中,我只需要提取 IV 的前 16 个字节,其余部分作为密文,其中也包括身份验证标签。但是,在 NodeJS 中,我需要手动提取 AuthTag 上的通行证。我尝试将来自 SWIFT 的组合数据分解为:

  • IV(16字节)+密文+标签(16字节) 以及:
  • IV(16字节)+标签(16字节)+密文

这两个都不起作用并产生与上述相同的错误。

以下是 Java 代码:

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;

class CryptoTest{
    public static void main(String[] args){
        try{
            String enc = Crypto.encrypt("This is data", "password");
            String dec = Crypto.decrypt(enc,"password");
            System.out.println(enc);
            System.out.println(dec);
        
        }catch(Exception ex){
            System.out.println("ERROR");
        }
    }    

    private static class Crypto {
        private static final int GCM_TAG_LENGTH = 16;
        private static final int GCM_IV_LENGTH = 12;
        private static final String ALGORITHM = "AES_256/GCM/NoPadding";
        private static final String ALGORITHM_SHORT_NAME = "AES";
        private static final String HASH_ALGORITHM = "SHA-256";
        
        public static String encrypt(String plaintext, String password) throws Exception
        {
            //Generate the key from password
            MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM);
            byte[] key = md.digest(password.getBytes(StandardCharsets.UTF_8));
            
            SecureRandom sr = new SecureRandom(password.getBytes(StandardCharsets.UTF_8));        
            byte[] iv = new byte[GCM_IV_LENGTH];
            // sr.nextBytes(iv);
            iv = Base64.getDecoder().decode("mBj0tzBUxDFmix1T");
    
            // Get Cipher Instance
            Cipher cipher = Cipher.getInstance(ALGORITHM);
    
            // Create SecretKeySpec
            SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM_SHORT_NAME);
    
            // Create GCMParameterSpec
            GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
    
            // Initialize Cipher for ENCRYPT_MODE
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec);
    
            // Perform Encryption
            byte[] cipherText = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
            
            //Return the IV and CipherText as Base64 encoded and appended strings
            System.out.println(Base64.getEncoder().encodeToString(iv));
            System.out.println(Base64.getEncoder().encodeToString(cipherText));

            return Base64.getEncoder().encodeToString(iv)+Base64.getEncoder().encodeToString(cipherText);
        }
    
        public static String decrypt(String sourceText, String password) throws Exception
        {
            //Get the IV from cipherText
            byte[] iv = Base64.getDecoder().decode(sourceText.substring(0,16));
            
            //Get the reminder of cipherText after the iv
            byte[] cipherText = Base64.getDecoder().decode(sourceText.substring(16));
            
            //Generate the key from password
            MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM);
            byte[] key = md.digest(password.getBytes(StandardCharsets.UTF_8));
            
            // Get Cipher Instance
            Cipher cipher = Cipher.getInstance(ALGORITHM);
    
            // Create SecretKeySpec
            SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM_SHORT_NAME);
    
            // Create GCMParameterSpec
            GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
    
            // Initialize Cipher for DECRYPT_MODE
            cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec);
    
            // Perform Decryption
            byte[] decryptedText = cipher.doFinal(cipherText);
    
            return new String(decryptedText);
        }
    }
}

数据:这是数据

密钥:密码

IV:mBj0tzBUxDFmix1T

Java 代码生成以下内容:

加密文本:UgNY3VAwNU07iEqU1Jq3m3Q+p6bDCZg6UI0h8w==

NodeJS 代码生成以下内容:

AuthTag:BXQ0vZH4HBBpGb7Y7R9iJw==

加密文本:O+wuVJB06JO6rPrc

根据我的阅读,Java 将生成包含 AuthTag 的加密文本。我尝试将 AuthTag 连接到加密文本,但输出永远不会等于 Java 生成的输出。

然而,Java 加密文本可以通过 SWIFT CryptoKit 代码解密而没有任何问题。

【问题讨论】:

  • 分享 Java 对我有很大帮助,因为我不熟悉 Swift 加密(但使用 Java 和 NodeJs)。额外的示例数据集也会有很大帮助(明文、密钥和密文)[十六进制或更好的 Base64 编码]。
  • @MichaelFehr 我已经用 Java 代码和示例数据集更新了这个问题。我还更新了 NodeJS 代码,因为我在查看 Java 代码时发现了问题,但两个代码的输出仍然完全不同。
  • 快速拍摄(今天早上我很忙):在 Java 端,AuthTag 被隐式添加到密文中,因此您应该将 NodeJs 方法从 cipher.getAuthTag().toString('base64 ') + encryptedData.toString('base64') 类似于 [pseudo code] "(encryptedData + cipher.getAuthTag()).toString('base64')" (意味着你必须连接来自 ciphertext | AuthTag 的字节,然后将其编码为 base64。
  • cipherText 和 authTag Buffer.concat([encryptedData, cipher.getAuthTag()]).toString('base64') 的连接返回 O+wuVJB06JO6rPrcBXQ0vZH4HBBpGb7Y7R9iJw==,而 Java 程序返回 UgNY3VAwNU07iEqU1Jq3m3Q+p6bDCZg6UI0h8w==。两者都使用相同的 IV 和 KEY,因此理想情况下两者都应该返回相同的加密文本。

标签: node.js swift encryption


【解决方案1】:

除了 Java 加密将 GCM authtag 放在密文末尾(在 cmets 中标识)之外,您的 Java 代码使用密码的 SHA256 作为密钥,而您的 nodejs 使用 ASCII 字符一半 SHA256 的十六进制表示;这是一个完全不同的值,对称(传统)密码学的重点是您必须在两端使用(完全)相同的密钥。此外,您转换为base64然后与字符串加号连接的方法,以及相反在解码之前对base64进行切片的方法,只有在数据和IV都是3的倍数时才有效:GCM IV/nonce是12,没关系,您的示例值“这是数据”也是如此,但大多数真实数据都不是。

以下修改后的 js 与您的 Java 匹配。我不做 SWIFT,但如果你说它匹配你的 Java,它也应该匹配这个 js。

const crypto = require('crypto')
function encryptData(data,password){
        //--let password_hash = crypto.createHash('sha256').update(password, 'utf-8').digest('hex').slice(0,32).toLowerCase();
        let password_hash = crypto.createHash('sha256').update(password, 'utf-8').digest();
        let iv = Buffer.from('mBj0tzBUxDFmix1T', 'base64'); // TEST ONLY SHOULD BE UNIQUE (such as random) 
        let cipher = crypto.createCipheriv('aes-256-gcm', password_hash, iv);
        //--let encryptedData = Buffer.from(cipher.update(data, 'utf8', 'hex') + cipher.final('hex'), 'hex');
        let encryptedData = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
        //--return iv.toString('base64') + cipher.getAuthTag().toString('base64') + encryptedData.toString('base64');
        return Buffer.concat([iv,encryptedData,cipher.getAuthTag()]).toString('base64');
        // or just concat([iv,cipher.update(data,'utf8'),cipher.final(),cipher.getAuthTag()]).toString('base64') 
    }
    
function decryptData(data,password){
        let password_hash = crypto.createHash('sha256').update(password, 'utf-8').digest(); //**
        let combinerBuffer = Buffer.from(data, 'base64'); //**
        let iv = combinerBuffer.slice(0,12); //**
        let deciper = crypto.createDecipheriv('aes-256-gcm', password_hash, iv);
        let temp = combinerBuffer.length-16;
        deciper.setAuthTag(combinerBuffer.slice(temp));
        return deciper.update(combinerBuffer.slice(12,temp), 'utf8') + deciper.final('utf8');
    }

let p = 'password', i = 'This is data';
let c = encryptData(i,p); console.log(c);
let d = decryptData(c,p); console.log(d);

最后,对密钥使用单个快速且未加盐的密码哈希值的安全性非常低,并且很可能会被破坏。但这是一个设计问题,对于 SO 来说是题外话。如果您有能力更改此设计并关心实际安全性,请参阅 security.SX,您会在其中找到许多建议,至少使用 PBKDF2(一种迭代的盐渍 HMAC)之类的东西,或者甚至更好的一种新的内存-硬密码哈希,如 scrypt 或 argon2。

另外,正如我所评论的,GCM 的 IV/nonce 仅需要是唯一的;使用安全随机生成器是获得唯一值的一种常用方法,但不是唯一的方法。 (这与 CBC 模式形成对比,其中 IV 必须是唯一的且不可预测,在实践中需要随机或 SIV。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-06-04
    • 2020-07-03
    • 2020-01-27
    • 1970-01-01
    • 1970-01-01
    • 2021-09-03
    • 2020-12-03
    • 2021-11-07
    相关资源
    最近更新 更多