web-dev-qa-db-ja.com

Java OpenSSL AESCBC暗号化に相当

私は暗号化の専門家ではありません。特に、OpenSSLには不足しているドキュメントがたくさんあるため、この問題をどのように解決できるかわかりません。

暗号化されたメッセージの受信を期待する外部システムがあります。提供されている唯一の例では、OpenSSLを次のように使用しています。

$ openssl enc -aes-256-cbc -a -in t.txt -k testpass
U2FsdGVkX1/RUdaSJKRXhHv3zUyTsQwu5/ar2ECKDlrNyH5GL4xRR4fgxkiWqkS1
cQstcoSIgWfRPSOFj/5OtdNLeNXiVR6MxSKJ+NvS9LyUD8+Rg6XIcYUvxR4gHi3w
DWT44LAMCpRAh1Q0t4Z2g7rwb0D05T6ygLaWvB5zD/xGZD3brTqSlWmiJb9Imgda
M6soZO7BhbYdqWqEUl5r6+EbkD21f6L3NX3hJFo+BJ+VFctiAlBO8NwT5l4ogo/s
GErm8gqRr57XoX/kvKAimg==

どこ t.txtファイルには、次の文字列が1行に含まれています。

AMOUNT=10&TID=#19:23&CURRENCY=EUR&LANGUAGE=DE&SUCCESS_URL=http://some.url/sucess&ERROR_URL=http://some.url/error&CONFIRMATION_URL=http://some.url/confirm&NAME=customer full name`

私は this 他の質問を見つけ、次のコードを使用して暗号化を行うことができました:

String password = "passPhrase";
String salt = "15charRandomSalt";
int iterations = 100;

/* Derive the key, given password and salt. */
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(Charset.forName("UTF8")), iterations, 256);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");

/* Encrypt the message. */
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[] cipherText = cipher.doFinal(toBeEncrypted.getBytes("UTF-8"));
encryptedData = Base64.getEncoder().encodeToString(cipherText);
encryptedData += Base64.getEncoder().encodeToString(iv);

私が理解できないのは、OpenSSLと同様の出力(encryptedData)をどのように生成する必要があるかです。私はsalt、iv、cipherTextを持っていますが、OpenSSL出力のBase64でエンコードされた結果はこれらを連結したものですか?またはそれらの1つだけ?

暗号化の前に他のシステムと共有するのはパスフレーズだけです。ソルトと反復回数がわからない場合、どのようにして結果を復号化できますか?

誰かがそれらの未知のパラメータに答えて、上記のコードがOpenSSLプロセスと同等であるかどうかを教えてもらえますか?

9
mohamnag

以下は、上記のOPENSSL暗号化を復号化するJavaプログラムです(Java 8)が必要です)。

import Java.nio.charset.StandardCharsets;
import Java.nio.file.Files;
import Java.nio.file.Paths;
import Java.security.MessageDigest;
import Java.util.Arrays;
import Java.util.Base64;
import Java.util.Base64.Decoder;

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

public class TestAesDecrypt {

    public static void main(final String[] args) throws Exception {
        final byte[] pass = "testpass".getBytes(StandardCharsets.US_ASCII);
        final byte[] magic = "Salted__".getBytes(StandardCharsets.US_ASCII);
        final String inFile = "e:/t/e.txt";

        String source = new String(Files.readAllBytes(Paths.get(inFile)),
                StandardCharsets.US_ASCII);
        source = source.replaceAll("\\s", "");
        final Decoder decoder = Base64.getDecoder();
        final byte[] inBytes = decoder.decode(source);

        final byte[] shouldBeMagic = Arrays.copyOfRange(inBytes, 0,
                magic.length);
        if (!Arrays.equals(shouldBeMagic, magic)) {
            System.out.println("Bad magic number");
            return;
        }

        final byte[] salt = Arrays.copyOfRange(inBytes, magic.length,
                magic.length + 8);

        final byte[] passAndSalt = concat(pass, salt);

        byte[] hash = new byte[0];
        byte[] keyAndIv = new byte[0];
        for (int i = 0; i < 3; i++) {
            final byte[] data = concat(hash, passAndSalt);
            final MessageDigest md = MessageDigest.getInstance("MD5");
            hash = md.digest(data);
            keyAndIv = concat(keyAndIv, hash);
        }

        final byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32);
        final byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48);
        final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        final SecretKeySpec key = new SecretKeySpec(keyValue, "AES");
        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
        final byte[] clear = cipher.doFinal(inBytes, 16, inBytes.length - 16);
        final String clearText = new String(clear, StandardCharsets.ISO_8859_1);
        System.out.println(clearText);
    }

    private static byte[] concat(final byte[] a, final byte[] b) {
        final byte[] c = new byte[a.length + b.length];
        System.arraycopy(a, 0, c, 0, a.length);
        System.arraycopy(b, 0, c, a.length, b.length);
        return c;
    }
}
4

この質問には少し古い回答が受け入れられていますが、これは何度も何度も出てくるもののようです。サードパーティと通信する2つのプロジェクトがあり、暗号は事前共有キーを使用したOpenSSLAESです。

私はまだ一般的ではないsslライブラリを使用しました。ただし、バージョン0.3.xでスタックしているようで、ほぼ2年間リリースがなく、メーリングリストのトラフィックや目に見える開発はありません。これは本質的に死んでいると結論付ける必要があります。

いくつかの追加のstackoverflowの質問に基づいて、 Spring SecurityEncryptor4j の両方が見つかりました。どちらも、合理的にパッケージ化されたテキストエンコーディングを提供しているようです。ただし、Spring SecurityのEncryptorsを既知のエンコードされたテキスト文字列のデコードで機能させる試みは失敗しました。OpenSSLで使用されるIVおよびキーの生成は、提供された実装ではサポートされていないだけだと思います。

上記のコードと、既知の動作するC#およびPHP実装)を調べることで、現在相互運用性のテストに合格しているユーティリティクラスを思い付くことができました。既知のライブラリを使用することを好みますが、ライブラリがある場合は見つけることができませんでした。クラス( https://Gist.github.com/rrsIPOV/4d0f6be7c58173c16e9edf9f97c7d7f2 )は次のとおりです。

import groovy.transform.CompileStatic;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import Java.security.MessageDigest;
import Java.security.SecureRandom;
import static Java.nio.charset.StandardCharsets.*

/**
* Mimics the OpenSSL AES Cipher options for encrypting and decrypting messages using a shared key (aka password) with symetric ciphers.
*/
@CompileStatic
class OpenSslAes {

/** OpenSSL's magic initial bytes. */
private static final String SALTED_STR = "Salted__";
private static final byte[] SALTED_MAGIC = SALTED_STR.getBytes(US_ASCII);


static String encryptAndURLEncode(String password, String clearText) {
    String encrypted = encrypt(password, clearText);
    return URLEncoder.encode(encrypted, UTF_8.name() );
}

/**
 *
 * @param password  The password / key to encrypt with.
 * @param data      The data to encrypt
 * @return  A base64 encoded string containing the encrypted data.
 */
static String encrypt(String password, String clearText) {
    final byte[] pass = password.getBytes(US_ASCII);
    final byte[] salt = (new SecureRandom()).generateSeed(8);
    final byte[] inBytes = clearText.getBytes(UTF_8);

    final byte[] passAndSalt = array_concat(pass, salt);
    byte[] hash = new byte[0];
    byte[] keyAndIv = new byte[0];
    for (int i = 0; i < 3 && keyAndIv.length < 48; i++) {
        final byte[] hashData = array_concat(hash, passAndSalt);
        final MessageDigest md = MessageDigest.getInstance("MD5");
        hash = md.digest(hashData);
        keyAndIv = array_concat(keyAndIv, hash);
    }

    final byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32);
    final byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48);
    final SecretKeySpec key = new SecretKeySpec(keyValue, "AES");

    final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
    byte[] data = cipher.doFinal(inBytes);
    data =  array_concat(array_concat(SALTED_MAGIC, salt), data);
    return Base64.getEncoder().encodeToString( data );
}

/**
 * @see http://stackoverflow.com/questions/32508961/Java-equivalent-of-an-openssl-aes-cbc-encryption  for what looks like a useful answer.  The not-yet-commons-ssl also has an implementation
 * @param password
 * @param source The encrypted data
 * @return
 */
static String decrypt(String password, String source) {
    final byte[] pass = password.getBytes(US_ASCII);

    final byte[] inBytes = Base64.getDecoder().decode(source);

    final byte[] shouldBeMagic = Arrays.copyOfRange(inBytes, 0, SALTED_MAGIC.length);
    if (!Arrays.equals(shouldBeMagic, SALTED_MAGIC)) {
        throw new IllegalArgumentException("Initial bytes from input do not match OpenSSL SALTED_MAGIC salt value.");
    }

    final byte[] salt = Arrays.copyOfRange(inBytes, SALTED_MAGIC.length, SALTED_MAGIC.length + 8);

    final byte[] passAndSalt = array_concat(pass, salt);

    byte[] hash = new byte[0];
    byte[] keyAndIv = new byte[0];
    for (int i = 0; i < 3 && keyAndIv.length < 48; i++) {
        final byte[] hashData = array_concat(hash, passAndSalt);
        final MessageDigest md = MessageDigest.getInstance("MD5");
        hash = md.digest(hashData);
        keyAndIv = array_concat(keyAndIv, hash);
    }

    final byte[] keyValue = Arrays.copyOfRange(keyAndIv, 0, 32);
    final SecretKeySpec key = new SecretKeySpec(keyValue, "AES");

    final byte[] iv = Arrays.copyOfRange(keyAndIv, 32, 48);

    final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
    final byte[] clear = cipher.doFinal(inBytes, 16, inBytes.length - 16);
    return new String(clear, UTF_8);
}


private static byte[] array_concat(final byte[] a, final byte[] b) {
    final byte[] c = new byte[a.length + b.length];
    System.arraycopy(a, 0, c, 0, a.length);
    System.arraycopy(b, 0, c, a.length, b.length);
    return c;
}
}
9
Robert

このディスカッション 2つのMD5ハッシュの連結としてキー生成アルゴリズムを指定しているのを見ることができます。

そこに記載されている塩に関して、 opensssl encのmanページ は次のように述べています。

ソルトが使用されている場合、暗号化されたデータの最初の8バイトはソルト用に予約されています。ファイルの暗号化時にランダムに生成され、復号化時に暗号化されたファイルから読み取られます。

1

現時点では、opensslバージョン1.1.0f-3にはダイジェスト関数SHA-256が必要です。これがないと、デコードに失敗します。

1
Ruslan Yaniuk