【问题标题】:How to decrypt file in Java encrypted with openssl command using AES?如何使用 AES 解密用 openssl 命令加密的 Java 文件?
【发布时间】:2012-07-31 18:07:04
【问题描述】:

我需要在 JAVA 中解密一个在 UNIX 中使用以下命令加密的文件:

openssl aes-256-cbc -a -salt -in password.txt -out password.txt.enc
mypass
mypass

我必须像在 UNIX 中一样在 java 中解密

openssl aes-256-cbc -d -a -in password.txt.enc -out password.txt.new
mypass

谁能给我一个java代码来做这个?

【问题讨论】:

    标签: java openssl aes


    【解决方案1】:

    OpenSSL一般使用自己的基于密码的密钥派生方法,在EVP_BytesToKey中指定,请看下面的代码。此外,它将密文隐式编码为多行的 base 64,这将需要在邮件正文中发送。

    所以结果是,在伪代码中:

    salt = random(8)
    keyAndIV = BytesToKey(password, salt, 48)
    key = keyAndIV[0..31]
    iv = keyAndIV[32..47]
    ct = AES-256-CBC-encrypt(key, iv, plaintext)
    res = base64MimeEncode("Salted__" | salt | ct))
    

    因此解密为:

    (salt, ct) = base64MimeDecode(res)
    key = keyAndIV[0..31]
    iv = keyAndIV[32..47]
    pt = AES-256-CBC-decrypt(key, iv, plaintext)
    

    在Java中可以这样实现:

    import java.io.File;
    import java.io.IOException;
    import java.nio.charset.Charset;
    import java.nio.file.Files;
    import java.security.GeneralSecurityException;
    import java.security.MessageDigest;
    import java.util.Arrays;
    import java.util.List;
    
    import javax.crypto.BadPaddingException;
    import javax.crypto.Cipher;
    import javax.crypto.IllegalBlockSizeException;
    import javax.crypto.spec.IvParameterSpec;
    import javax.crypto.spec.SecretKeySpec;
    
    import org.bouncycastle.util.encoders.Base64;
    
    public class OpenSSLDecryptor {
        private static final Charset ASCII = Charset.forName("ASCII");
        private static final int INDEX_KEY = 0;
        private static final int INDEX_IV = 1;
        private static final int ITERATIONS = 1;
    
        private static final int ARG_INDEX_FILENAME = 0;
        private static final int ARG_INDEX_PASSWORD = 1;
    
        private static final int SALT_OFFSET = 8;
        private static final int SALT_SIZE = 8;
        private static final int CIPHERTEXT_OFFSET = SALT_OFFSET + SALT_SIZE;
    
        private static final int KEY_SIZE_BITS = 256;
    
        /**
         * Thanks go to Ola Bini for releasing this source on his blog.
         * The source was obtained from <a href="http://olabini.com/blog/tag/evp_bytestokey/">here</a> .
         */
        public static byte[][] EVP_BytesToKey(int key_len, int iv_len, MessageDigest md,
                byte[] salt, byte[] data, int count) {
            byte[][] both = new byte[2][];
            byte[] key = new byte[key_len];
            int key_ix = 0;
            byte[] iv = new byte[iv_len];
            int iv_ix = 0;
            both[0] = key;
            both[1] = iv;
            byte[] md_buf = null;
            int nkey = key_len;
            int niv = iv_len;
            int i = 0;
            if (data == null) {
                return both;
            }
            int addmd = 0;
            for (;;) {
                md.reset();
                if (addmd++ > 0) {
                    md.update(md_buf);
                }
                md.update(data);
                if (null != salt) {
                    md.update(salt, 0, 8);
                }
                md_buf = md.digest();
                for (i = 1; i < count; i++) {
                    md.reset();
                    md.update(md_buf);
                    md_buf = md.digest();
                }
                i = 0;
                if (nkey > 0) {
                    for (;;) {
                        if (nkey == 0)
                            break;
                        if (i == md_buf.length)
                            break;
                        key[key_ix++] = md_buf[i];
                        nkey--;
                        i++;
                    }
                }
                if (niv > 0 && i != md_buf.length) {
                    for (;;) {
                        if (niv == 0)
                            break;
                        if (i == md_buf.length)
                            break;
                        iv[iv_ix++] = md_buf[i];
                        niv--;
                        i++;
                    }
                }
                if (nkey == 0 && niv == 0) {
                    break;
                }
            }
            for (i = 0; i < md_buf.length; i++) {
                md_buf[i] = 0;
            }
            return both;
        }
    
    
        public static void main(String[] args) {
            try {
                // --- read base 64 encoded file ---
    
                File f = new File(args[ARG_INDEX_FILENAME]);
                List<String> lines = Files.readAllLines(f.toPath(), ASCII);
                StringBuilder sb = new StringBuilder();
                for (String line : lines) {
                    sb.append(line.trim());
                }
                String dataBase64 = sb.toString();
                byte[] headerSaltAndCipherText = Base64.decode(dataBase64);
    
                // --- extract salt & encrypted ---
    
                // header is "Salted__", ASCII encoded, if salt is being used (the default)
                byte[] salt = Arrays.copyOfRange(
                        headerSaltAndCipherText, SALT_OFFSET, SALT_OFFSET + SALT_SIZE);
                byte[] encrypted = Arrays.copyOfRange(
                        headerSaltAndCipherText, CIPHERTEXT_OFFSET, headerSaltAndCipherText.length);
    
                // --- specify cipher and digest for EVP_BytesToKey method ---
    
                Cipher aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding");
                MessageDigest md5 = MessageDigest.getInstance("MD5");
    
                // --- create key and IV  ---
    
                // the IV is useless, OpenSSL might as well have use zero's
                final byte[][] keyAndIV = EVP_BytesToKey(
                        KEY_SIZE_BITS / Byte.SIZE,
                        aesCBC.getBlockSize(),
                        md5,
                        salt,
                        args[ARG_INDEX_PASSWORD].getBytes(ASCII),
                        ITERATIONS);
                SecretKeySpec key = new SecretKeySpec(keyAndIV[INDEX_KEY], "AES");
                IvParameterSpec iv = new IvParameterSpec(keyAndIV[INDEX_IV]);
    
                // --- initialize cipher instance and decrypt ---
    
                aesCBC.init(Cipher.DECRYPT_MODE, key, iv);
                byte[] decrypted = aesCBC.doFinal(encrypted);
    
                String answer = new String(decrypted, ASCII);
                System.out.println(answer);
            } catch (BadPaddingException e) {
                // AKA "something went wrong"
                throw new IllegalStateException(
                        "Bad password, algorithm, mode or padding;" +
                        " no salt, wrong number of iterations or corrupted ciphertext.");
            } catch (IllegalBlockSizeException e) {
                throw new IllegalStateException(
                        "Bad algorithm, mode or corrupted (resized) ciphertext.");
            } catch (GeneralSecurityException e) {
                throw new IllegalStateException(e);
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
        }        
    }
    

    请注意,代码将 ASCII 指定为字符集。使用的字符集可能因您的应用程序/终端/操作系统而异。


    一般而言,您应该强制 OpenSSL 使用 NIST 批准的 PBKDF2 算法,因为使用 OpenSSL 密钥派生方法 - 迭代计数为 1 - 是不安全的。这可能会迫使您使用与 OpenSSL 不同的解决方案。请注意,基于密码的加密本质上是相当不安全的 - 密码的安全性远低于随机生成的对称密钥。


    OpenSSL 1.1.0c changed the digest algorithm 用于一些内部组件。以前使用MD5,1.1.0改用SHA256。请注意,EVP_BytesToKeyopenssl enc 等命令中的更改不会影响您。

    最好在命令行界面中明确指定摘要(例如,-md md5 用于向后兼容或sha-256 用于向前兼容)并确保 Java 代码使用相同的摘要算法("MD5""SHA-256",包括破折号)。另见信息in this answer

    【讨论】:

    • 它们不应该有所不同——这就是像 PKCS5 这样的标准的全部意义所在。不过,您的建议总体上肯定是好的
    • "别忘了匹配character-encoding..." 加密确实应该匹配,但编码可能不同。
    • 我用过这个函数,它非常适合加盐加密和解密。当没有盐时,它也能正确解密。但是,在我编写的程序中,它似乎没有正确加密非加盐字符串。加密非加盐字符串时是否有我遗漏的警告?
    • “一般情况下,您应该强制 OpenSSL 使用 NIST 批准的 PBKDF2 算法” - 如果您使用 OpenSSL 的 enc 命令,这也是不可能的(除非您指定直接使用密钥和 IV;但无法控制使用哪个 KDF OpenSSL)
    • @NullUserException+ 更新:OpenSSL 1.1.1 (2018-09) enc 确实支持 -pbkdf2 和用户可选择的 -iter N
    【解决方案2】:

    以下是 OpenSSLPBEInputStreamOpenSSLPBEOutputStream,它们可用于以与 OpenSSL 兼容的方式加密/解密任意字节流。

    示例用法:

        // The original clear text bytes
        byte[] originalBytes = ...
    
        // Encrypt these bytes
        char[] pwd = "thePassword".toCharArray();
        ByteArrayOutputStream byteOS = new ByteArrayOutputStream();
        OpenSSLPBEOutputStream encOS = new OpenSSLPBEOutputStream(byteOS, ALGORITHM, 1, pwd);
        encOS.write(originalBytes);
        encOS.flush();
        byte[] encryptedBytes = byteOS.toByteArray();
    
        // Decrypt the bytes
        ByteArrayInputStream byteIS = new ByteArrayInputStream(encryptedBytes);
        OpenSSLPBEInputStream encIS = new OpenSSLPBEInputStream(byteIS, ALGORITHM, 1, pwd);
    

    算法(仅使用 JDK 类)可以是:“PBEWithMD5AndDES”、“PBEWithMD5AndTripleDES”、“PBEWithSHA1AndDESede”、“PBEWithSHA1AndRC2_40”。

    要处理原始海报的“openssl aes-256-cbc -a -salt -in password.txt -out password.txt.enc”,将bouncey castle添加到类路径,并使用algorthm=“PBEWITHMD5AND256BITAES-CBC- OPENSSL”。

    /* Add BC provider, and fail fast if BC provider is not in classpath for some reason */
    Security.addProvider(new BouncyCastleProvider());
    

    依赖:

        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk16</artifactId>
            <version>1.44</version>
        </dependency>
    

    输入流:

    import javax.crypto.BadPaddingException;
    import javax.crypto.Cipher;
    import javax.crypto.IllegalBlockSizeException;
    import javax.crypto.NoSuchPaddingException;
    import java.io.IOException;
    import java.io.InputStream;
    import java.security.InvalidAlgorithmParameterException;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    import java.security.spec.InvalidKeySpecException;
    
    public class OpenSSLPBEInputStream extends InputStream {
    
        private final static int READ_BLOCK_SIZE = 64 * 1024;
    
        private final Cipher cipher;
        private final InputStream inStream;
        private final byte[] bufferCipher = new byte[READ_BLOCK_SIZE];
    
        private byte[] bufferClear = null;
    
        private int index = Integer.MAX_VALUE;
        private int maxIndex = 0;
    
        public OpenSSLPBEInputStream(final InputStream streamIn, String algIn, int iterationCount, char[] password)
                throws IOException {
            this.inStream = streamIn;
            try {
                byte[] salt = readSalt();
                cipher = OpenSSLPBECommon.initializeCipher(password, salt, Cipher.DECRYPT_MODE, algIn, iterationCount);
            } catch (InvalidKeySpecException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException e) {
                throw new IOException(e);
            }
        }
    
        @Override
        public int available() throws IOException {
            return inStream.available();
        }
    
        @Override
        public int read() throws IOException {
    
            if (index > maxIndex) {
                index = 0;
                int read = inStream.read(bufferCipher);
                if (read != -1) {
                    bufferClear = cipher.update(bufferCipher, 0, read);
                }
                if (read == -1 || bufferClear == null || bufferClear.length == 0) {
                    try {
                        bufferClear = cipher.doFinal();
                    } catch (IllegalBlockSizeException | BadPaddingException e) {
                        bufferClear = null;
                    }
                }
                if (bufferClear == null || bufferClear.length == 0) {
                    return -1;
                }
                maxIndex = bufferClear.length - 1;
            }
            return bufferClear[index++] & 0xff;
    
        }
    
        private byte[] readSalt() throws IOException {
    
            byte[] headerBytes = new byte[OpenSSLPBECommon.OPENSSL_HEADER_STRING.length()];
            inStream.read(headerBytes);
            String headerString = new String(headerBytes, OpenSSLPBECommon.OPENSSL_HEADER_ENCODE);
    
            if (!OpenSSLPBECommon.OPENSSL_HEADER_STRING.equals(headerString)) {
                throw new IOException("unexpected file header " + headerString);
            }
    
            byte[] salt = new byte[OpenSSLPBECommon.SALT_SIZE_BYTES];
            inStream.read(salt);
    
            return salt;
        }
    
    }
    

    输出流:

    import javax.crypto.BadPaddingException;
    import javax.crypto.Cipher;
    import javax.crypto.IllegalBlockSizeException;
    import javax.crypto.NoSuchPaddingException;
    import java.io.IOException;
    import java.io.OutputStream;
    import java.security.InvalidAlgorithmParameterException;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    import java.security.SecureRandom;
    import java.security.spec.InvalidKeySpecException;
    
    public class OpenSSLPBEOutputStream extends OutputStream {
    
    private static final int BUFFER_SIZE = 5 * 1024 * 1024;
    
    private final Cipher cipher;
    private final OutputStream outStream;
    private final byte[] buffer = new byte[BUFFER_SIZE];
    private int bufferIndex = 0;
    
    public OpenSSLPBEOutputStream(final OutputStream outputStream, String algIn, int iterationCount,
                                  char[] password) throws IOException {
        outStream = outputStream;
        try {
            /* Create and use a random SALT for each instance of this output stream. */
            byte[] salt = new byte[PBECommon.SALT_SIZE_BYTES];
            new SecureRandom().nextBytes(salt);
            cipher = OpenSSLPBECommon.initializeCipher(password, salt, Cipher.ENCRYPT_MODE, algIn, iterationCount);
            /* Write header */
            writeHeader(salt);
        } catch (InvalidKeySpecException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IOException(e);
        }
    }
    
    @Override
    public void write(int b) throws IOException {
        buffer[bufferIndex] = (byte) b;
        bufferIndex++;
        if (bufferIndex == BUFFER_SIZE) {
            byte[] result = cipher.update(buffer, 0, bufferIndex);
            outStream.write(result);
            bufferIndex = 0;
        }
    }
    
    @Override
    public void flush() throws IOException {
        if (bufferIndex > 0) {
            byte[] result;
            try {
                result = cipher.doFinal(buffer, 0, bufferIndex);
                outStream.write(result);
            } catch (IllegalBlockSizeException | BadPaddingException e) {
                throw new IOException(e);
            }
            bufferIndex = 0;
        }
    }
    
    @Override
    public void close() throws IOException {
        flush();
        outStream.close();
    }
    
    private void writeHeader(byte[] salt) throws IOException {
        outStream.write(OpenSSLPBECommon.OPENSSL_HEADER_STRING.getBytes(OpenSSLPBECommon.OPENSSL_HEADER_ENCODE));
        outStream.write(salt);
    }
    
    }
    

    小普通类:

    import javax.crypto.Cipher;
    import javax.crypto.NoSuchPaddingException;
    import javax.crypto.SecretKey;
    import javax.crypto.SecretKeyFactory;
    import javax.crypto.spec.PBEKeySpec;
    import javax.crypto.spec.PBEParameterSpec;
    import java.security.InvalidAlgorithmParameterException;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    import java.security.spec.InvalidKeySpecException;
    
    class OpenSSLPBECommon {
    
    protected static final int SALT_SIZE_BYTES = 8;
    protected static final String OPENSSL_HEADER_STRING = "Salted__";
    protected static final String OPENSSL_HEADER_ENCODE = "ASCII";
    
    protected static Cipher initializeCipher(char[] password, byte[] salt, int cipherMode,
                                             final String algorithm, int iterationCount) throws NoSuchAlgorithmException, InvalidKeySpecException,
            InvalidKeyException, NoSuchPaddingException, InvalidAlgorithmParameterException {
    
        PBEKeySpec keySpec = new PBEKeySpec(password);
        SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm);
        SecretKey key = factory.generateSecret(keySpec);
    
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(cipherMode, key, new PBEParameterSpec(salt, iterationCount));
    
        return cipher;
    }
    
    }
    

    【讨论】:

      【解决方案3】:

      在 Kotlin 中:

      package io.matthewnelson.java_crypto
      
      import java.util.*
      import javax.crypto.Cipher
      import javax.crypto.SecretKeyFactory
      import javax.crypto.spec.IvParameterSpec
      import javax.crypto.spec.PBEKeySpec
      import javax.crypto.spec.SecretKeySpec
      
      class OpenSSL {
      
          /**
           * Will decrypt a string value encrypted by OpenSSL v 1.1.1+ using the following cmds from terminal:
           *
           *   echo "Hello World!" | openssl aes-256-cbc -e -a -p -salt -pbkdf2 -iter 15739 -k qk4aX-EfMUa-g4HdF-fjfkU-bbLNx-15739
           *
           * Terminal output:
           *   salt=CC73B7D29FE59CE1
           *   key=31706F84185EA4B5E8E040F2C813F79722F22996B48B82FF98174F887A9B9993
           *   iv =1420310D41FD7F48E5D8722B9AC1C8DD
           *   U2FsdGVkX1/Mc7fSn+Wc4XLwDsmLdR8O7K3bFPpCglA=
           * */
          fun decrypt_AES256CBC_PBKDF2_HMAC_SHA256(
              password: String,
              hashIterations: Int,
              encryptedString: String
          ): String {
              val encryptedBytes = Base64.getDecoder().decode(encryptedString)
      
              // Salt is bytes 8 - 15
              val salt = encryptedBytes.copyOfRange(8, 16)
      //        println("Salt: ${salt.joinToString("") { "%02X".format(it) }}")
      
              // Derive 48 byte key
              val keySpec = PBEKeySpec(password.toCharArray(), salt, hashIterations, 48 * 8)
              val keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
              val secretKey = keyFactory.generateSecret(keySpec)
      
              // Decryption Key is bytes 0 - 31 of the derived key
              val key = secretKey.encoded.copyOfRange(0, 32)
      //        println("Key: ${key.joinToString("") { "%02X".format(it) }}")
      
              // Input Vector is bytes 32 - 47 of the derived key
              val iv = secretKey.encoded.copyOfRange(32, 48)
      //        println("IV: ${iv.joinToString("") { "%02X".format(it) }}")
      
              // Cipher Text is bytes 16 - end of the encrypted bytes
              val cipherText = encryptedBytes.copyOfRange(16, encryptedBytes.lastIndex + 1)
      
              // Decrypt the Cipher Text and manually remove padding after
              val cipher = Cipher.getInstance("AES/CBC/NoPadding")
              cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
              val decrypted = cipher.doFinal(cipherText)
      //        println("Decrypted: ${decrypted.joinToString("") { "%02X".format(it) }}")
      
              // Last byte of the decrypted text is the number of padding bytes needed to remove
              val plaintext = decrypted.copyOfRange(0, decrypted.lastIndex + 1 - decrypted.last().toInt())
      
              return plaintext.toString(Charsets.UTF_8)
          }
      }
      

      【讨论】:

        【解决方案4】:

        不要使用 ase-128-cbc,使用 ase-128-ecb。

        只取前 16 个字节作为 key,因为 key 是 128 位

        hash输出以十六进制打印,每2个字符代表一个字节值

        hashpwd=echo -n $password| openssl sha1 | sed 's#.*=\\s*##g' | cut -c 1-32

        openssl enc -aes-128-ecb -salt -in -out -K $hashpwd

        Java 代码在这里:

        import sun.misc.BASE64Decoder;
        import sun.misc.BASE64Encoder;
        
        import javax.crypto.Cipher;
        import javax.crypto.spec.SecretKeySpec;
        import java.io.*;
        import java.security.MessageDigest;
        import java.security.NoSuchAlgorithmException;
        import java.util.ArrayList;
        import java.util.Arrays;
        
        
            //openssl enc -nosalt -aes-128-ecb
            // -in <input file>
            // -out <output file>
            // -K <16 bytes in hex, for example : "abc" can be hashed in SHA-1, the first 16 bytes in hex is a9993e364706816aba3e25717850c26c>
            private final static String TRANSFORMATION = "AES"; // use aes-128-ecb in openssl
        
        public static byte[] encrypt(String passcode, byte[] data) throws CryptographicException {
                try {
                    Cipher cipher = Cipher.getInstance(TRANSFORMATION);
                    cipher.init(Cipher.ENCRYPT_MODE, genKeySpec(passcode));
                    return cipher.doFinal(data);
                } catch (Exception ex) {
                    throw new CryptographicException("Error encrypting", ex);
                }
            }
        
        
            public static String encryptWithBase64(String passcode, byte[] data) throws CryptographicException {
                return new BASE64Encoder().encode(encrypt(passcode, data));
            }
        
            public static byte[] decrypt(String passcode, byte[] data) throws CryptographicException {
                try {
                    Cipher dcipher = Cipher.getInstance(TRANSFORMATION);
                    dcipher.init(Cipher.DECRYPT_MODE, genKeySpec(passcode));
                    return dcipher.doFinal(data);
                } catch (Exception e) {
                    throw new CryptographicException("Error decrypting", e);
                }
            }
        
        
            public static byte[] decryptWithBase64(String passcode, String encrptedStr) throws CryptographicException {
                try {
                    return decrypt(passcode, new BASE64Decoder().decodeBuffer(encrptedStr));
                } catch (Exception e) {
                    throw new CryptographicException("Error decrypting", e);
                }
            }
        
            public static SecretKeySpec genKeySpec(String passcode) throws UnsupportedEncodingException, NoSuchAlgorithmException {
                byte[] key = passcode.getBytes("UTF-8");
                MessageDigest sha = MessageDigest.getInstance("SHA-1");
                key = sha.digest(key);
                key = Arrays.copyOf(key, 16); // use only first 128 bit
                return new SecretKeySpec(key, TRANSFORMATION);
            }
        

        在jdk6和jdk8中测试并通过。

        【讨论】:

        • 不要使用 ECB 模式,它不安全,请参阅ECB mode,向下滚动到 Penguin。而是使用带有随机IV的CBC模式,只需在加密数据前加上IV用于解密即可,它不需要不保密。
        • 我很确定声明:不要使用实现类(例如 sun.misc 包中的 Java 教程自 1.0 版以来一直存在,已在很久以前发布。您需要密码哈希从密码中安全地导出密钥的功能。使用 ECB 代替 CBC 和 SHA1 代替 EVP_BytesToKey 使其甚至比 OpenSSL 代码更不安全,OpenSSL 代码本身就是一门艺术;它已经远远低于标准.
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-01-07
        • 2015-02-18
        • 2020-01-20
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多