恢复加密代理密钥

recover encryption surrogate key

我正在尝试使用 AES 加密实施 this 加密方案。

基本上是这样的:

  1. 用户数据使用代理密钥 (surrogateKey) 加密
  2. 代理密钥与从密码派生的密钥进行异或运算 (passwordKey) 和存储 (storedKey)
  3. 当需要密钥(加密或解密用户数据)时,从数据库中检索存储的密钥,并再次与新生成的密码密钥进行异或运算以恢复代理密钥

除了,我一定是在实现中做错了什么,因为我似乎永远无法恢复有效的 surrogateKey 并且任何解密尝试都会给我一个 BadPaddingException。

下面的代码演示了这个问题。抱歉,如果它有点长,但您应该可以将其复制并粘贴到您的 IDE.`

import java.io.ByteArrayOutputStream;
import java.security.AlgorithmParameters;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

public class SurrogateTest {
    private static final String alphanumeric =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                    + "abcdefghijklmnopqrstuvwxyz"
                    + "1234567890"
                    + "!#$%&()+*<>?_-=^~|";

    private static String plainText = "I am the very model of a modern major general";
    private static String cipherText = "";
    private static SecureRandom rnd = new SecureRandom();

    private static byte[] salt;

    public static void main(String[] args) {
        // arguments are password and recovery string
        if (args.length > 1) {
            System.out.println("password: " + args[0] + "; recovery: " + args[1]);
        }
        String password = args[0];

        // passwordKey
        SecretKey passwordKey = getKey(password);
        System.out.println("passwordKey: " + DatatypeConverter.printBase64Binary(passwordKey.getEncoded()));

        // Generate surrogate encryption key from random string
        String rand = randomString(24);
        SecretKey surrogateKey = getKey(rand);
        byte[] surrogateByteArray = surrogateKey.getEncoded();
        System.out.println("surrogate: " + DatatypeConverter.printBase64Binary(surrogateByteArray));

        // encrypt plainText
        System.out.println("text to encrypt: " + plainText);
        cipherText = encryptWithKey(plainText, surrogateKey);

        // XOR surrogateKey with passwordKey to get storedKey
        SecretKey storedKey = xorWithKey(surrogateKey, passwordKey);
        String storedKeyString = DatatypeConverter.printBase64Binary(storedKey.getEncoded());
        System.out.println("storedKey: " + storedKeyString);

        byte[] storedKey2Array = DatatypeConverter.parseBase64Binary(storedKeyString);
        SecretKey storedKey2 = new SecretKeySpec(storedKey2Array, 0, storedKey2Array.length, "AES");
        String storedKey2String = DatatypeConverter.printBase64Binary(storedKey2.getEncoded());
        System.out.println("storedKey->String->key->string: " + storedKey2String);

        // recover surrogateKey from storedKey2
        SecretKey password2Key = getKey(password);
        System.out.println("password2Key: " + DatatypeConverter.printBase64Binary(password2Key.getEncoded()));
        SecretKey surrogate2Key = xorWithKey(storedKey2, password2Key);
        System.out.println("surrogate2 (recovered): " + DatatypeConverter.printBase64Binary(surrogate2Key.getEncoded()));

        // decrypt text
        String decryptedText = decryptWithKey(cipherText, surrogate2Key);
        System.out.println("decryptedText: " + decryptedText);
    }

    private static SecretKey xorWithKey(SecretKey a, SecretKey b) {
        byte[] out = new byte[b.getEncoded().length];
        for (int i = 0; i < b.getEncoded().length; i++) {
            out[i] = (byte) (b.getEncoded()[i] ^ a.getEncoded()[i % a.getEncoded().length]);
        }
        SecretKey outKey = new SecretKeySpec(out, 0, out.length, "AES");

        return outKey;
    }

    private static String randomString(int length) {
        StringBuilder sb = new StringBuilder(length);
        for (int i = 0; i < length; i++)
            sb.append(alphanumeric.charAt(rnd.nextInt(alphanumeric.length())));
        return sb.toString();
    }

    // return encryption key
    private static SecretKey getKey(String password) {
        try {
            SecureRandom random = new SecureRandom();
            salt = new byte[16];
            random.nextBytes(salt);

            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            // obtain secret key
            KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 256);
            SecretKey tmp = factory.generateSecret(spec);
            SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");

            return secret;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static String encryptWithKey(String str, SecretKey secret) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, secret);
            AlgorithmParameters params = cipher.getParameters();
            byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();
            byte[] encryptedText = cipher.doFinal(str.getBytes("UTF-8")); // encrypt the message str here

            // concatenate salt + iv + ciphertext
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            outputStream.write(salt);
            outputStream.write(iv);
            outputStream.write(encryptedText);

            // properly encode the complete ciphertext
            String encrypted = DatatypeConverter.printBase64Binary(outputStream.toByteArray());
            return encrypted;

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static String decryptWithKey(String str, SecretKey secret) {
        try {
            byte[] ciphertext = DatatypeConverter.parseBase64Binary(str);
            if (ciphertext.length < 48) {
                return null;
            }
            salt = Arrays.copyOfRange(ciphertext, 0, 16);
            byte[] iv = Arrays.copyOfRange(ciphertext, 16, 32);
            byte[] ct = Arrays.copyOfRange(ciphertext, 32, ciphertext.length);

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

            cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
            byte[] plaintext = cipher.doFinal(ct);

            return new String(plaintext, "UTF-8");

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

知道我做错了什么吗?

运行 发布的代码显示 surrogate2 (recovered) 不同于 surrogate。很明显,这是严重错误的。原因是在加密时,您使用随机盐导出 'password'(包装)密钥,并将该盐写入数据 blob 的开头;在解密时,您 使用 new salt 导出解包密钥,然后从 blob 中读取正确的 salt 并完全忽略它。这意味着你的解包密钥是错误的,所以你的解包数据密钥是错误的,所以你的解密是错误的。

PS:用于直接加密和解密数据的随机密钥通常称为'data'密钥(正如我刚才所做的)或DEK(Data Encryption或Encrypting Key的缩写),或更具体的术语,如 'session key' 或 'message key'(在本例中),或 'working key' 或 'transient key' 以强调其有限范围。它通常不被称为 'surrogate'。并且使用 PBKDF2 从强随机字符串中导出此数据密钥是浪费时间;直接使用 SecureRandom_instance.nextBytes(byte[]) 作为数据密钥。