WebCrypto AES-CBC输出256位而不是128位

时间:2018-12-13 15:32:48

标签: aes webcryptoapi

我正在使用WebCrypto,却得到了令人困惑的输出。

以下测试用例使用新生成的128位密钥和128位随机IV加密了一个随机的16字节(128位)纯文本,但是输出了32字节(256位)输出。

如果我还记得AES-CBC的细节,它应该输出128位块。

function test() { 

    var data = new Uint8Array(16);
    window.crypto.getRandomValues(data);
    console.log(data)


    window.crypto.subtle.generateKey(
        {
            name: "AES-CBC",
            length: 128, 
        },
        false, 
        ["encrypt", "decrypt"] 
    )
    .then(function(key){
        //returns a key object
        console.log(key);

        window.crypto.subtle.encrypt(
            {
                name: "AES-CBC",
                iv: window.crypto.getRandomValues(new Uint8Array(16)),
            },
            key, 
            data 
        )
        .then(function(encrypted){
            console.log(new Uint8Array(encrypted));
        })
        .catch(function(err){
            console.error(err);
        });
    })
    .catch(function(err){
        console.error(err);
    });

}

示例输出:

Uint8Array(16) [146, 207, 22, 56, 56, 151, 125, 174, 137, 69, 133, 36, 218, 114, 143, 174]
CryptoKey {
   algorithm: {name: "AES-CBC", length: 128}
   extractable: false
   type: "secret"
   usages: (2) ["encrypt", "decrypt"]
   __proto__: CryptoKey
Uint8Array(32) [81, 218, 52, 158, 115, 105, 57, 230, 45, 253, 153, 54, 183, 19, 137, 240, 183, 229, 241, 75, 182, 19, 237, 8, 238, 5, 108, 107, 123, 84, 230, 209]

任何想法我错了。

(如果更合适,可以打开进入crypto.stackexchange.com)

我目前正在MacOS的Chrome 71上进行测试。

1 个答案:

答案 0 :(得分:4)

是的。额外的16个字节是填充。即使消息文本是块大小的倍数,也要添加填充,否则解密逻辑将不知道何时查找填充。

Web Cryptography API Specification说:

  

在CBC模式下运行时,消息不是以下内容的精确倍数:   AES块大小(16字节)可以在各种填充下填充   计划。在Web Crypto API中,唯一的填充模式是   如第10.3节第2步中所述,支持的是PKCS#7   [RFC2315]。

这意味着与其他语言实现(例如Java)不同,您可以在知道输入消息文本始终为块大小的倍数(AES为128位)的情况下指定NoPadding,Web密码学API强制您进行PKCS#7填充。

如果我们调查RFC2315

  

某些内容加密算法假定输入长度为   k个八位组的倍数,其中k> 1,并让应用程序定义一个   处理长度不是k的倍数的输入的方法   八位位组。对于此类算法,方法应为将输入填充到   具有k-(l mod k)个八位位组的尾端均具有值k-(l mod   k),其中l是输入的长度。换句话说,输入是   在末尾填充以下字符串之一:

     01 -- if l mod k = k-1
    02 02 -- if l mod k = k-2
                .
                .
                .
  k k ... k k -- if l mod k = 0
     

由于所有输入均已填充且   没有填充字符串是另一个后缀。这种填充方法是   当且仅当k <256时定义良好;大k的方法是开放的   问题有待进一步研究。

注意:k k ... k k -- if l mod k = 0

如果引用subtle.encrypt签名,则无法指定填充模式。这意味着解密逻辑总是希望填充。

但是,在您的情况下,如果您仅将Web密码学API用于加密,而将Python应用程序(带有NoPadding)仅用于解密,我认为您可以简单地从密文中删除最后16个字节在将其提供给Python应用之前。这是仅用于演示目的的代码示例:

function test() { 

    let plaintext = 'GoodWorkGoodWork';
    let encoder = new TextEncoder('utf8');
    let dataBytes = encoder.encode(plaintext);

    window.crypto.subtle.generateKey(
        {
            name: "AES-CBC",
            length: 128, 
        },
        true, 
        ["encrypt", "decrypt"] 
    )
    .then(function(key){
        crypto.subtle.exportKey('raw', key)
        .then(function(expKey) {
            console.log('Key = ' + btoa(String.
                fromCharCode(...new Uint8Array(expKey))));
        });

        let iv = new Uint8Array(16);
        window.crypto.getRandomValues(iv);
        let ivb64 = btoa(String.fromCharCode(...new Uint8Array(iv)));
        console.log('IV = ' + ivb64);

        window.crypto.subtle.encrypt(
            {
                name: "AES-CBC",
                iv: iv,
            },
            key, 
            dataBytes 
        )
        .then(function(encrypted){
            console.log('Cipher text = ' + 
                btoa(String.fromCharCode(...new Uint8Array(encrypted))));
        })
        .catch(function(err){
            console.error(err);
        });
    })
    .catch(function(err){
        console.error(err);
    });

}

上面的输出是:

IV = qW2lanfRo2H/3aSLzxIecA==
Key = 0LDBq5iz243HBTUE/lrM+A==
Cipher text = Wa4nIF0tt4PEBUChiH1KCkSOg6L2daoYdboEEf+Oh6U=

现在,我使用这些作为输入,剥离密码文本的最后16个字节,并使用以下Java代码在解密后仍得到相同的消息文本:

package com.sapbasu.javastudy;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class EncryptCBC {
  public static void main(String[] arg) throws Exception {

    SecretKey key = new SecretKeySpec(Base64.getDecoder().decode(
        "0LDBq5iz243HBTUE/lrM+A=="),
        "AES");

    IvParameterSpec ivSpec = new IvParameterSpec(Base64.getDecoder().decode(
        "qW2lanfRo2H/3aSLzxIecA=="));

    Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
    cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);

    byte[] cipherTextWoPadding = new byte[16];
    System.arraycopy(Base64.getDecoder().decode(
        "Wa4nIF0tt4PEBUChiH1KCkSOg6L2daoYdboEEf+Oh6U="),
        0, cipherTextWoPadding, 0, 16);

    byte[] decryptedMessage = cipher.doFinal(cipherTextWoPadding);
    System.out.println(new String(decryptedMessage, StandardCharsets.UTF_8));
  }
}