使用 PHP OpenSSL 使用 JAVA 密码解密 aes-128-gcm 编码内容

Decrypt aes-128-gcm encoded content with JAVA Cipher using PHP OpenSSL

我必须使用用 JAVA 密码加密的 aes-128-gcm 解密一些发送到我网站的数据。

客户有一个与 Talend 合作的提供商,他必须通过 URL 参数向我发送一些信息,而我一直在尝试使用 PHP Openssl 解密数据。提供者并不能真正告诉我它是如何在他这边工作的,并且不太确定生成代码中使用的变量(例如 IV Length)。

加密方法是在 JAVA 上使用 Talend 组件完成的。提供商将 Talend 生成的代码作为示例发给我。

private static String encrypt(String data, String mainKey, int ivLength) throws Exception {
  final byte[] dataBytes = data.getBytes(UNICODE_FORMAT);
  byte[] initializationVector = generateInitializationVector(ivLength);
  final Cipher cipher = getAesGcmCipher(Cipher.ENCRYPT_MODE, mainKey, initializationVector);

  final byte[] encryptedData = cipher.doFinal(dataBytes);
  final byte[] encryptedBytes = new byte[encryptedData.length + ivLength];
  System.arraycopy(initializationVector, 0, encryptedBytes, 0, ivLength);
  System.arraycopy(encryptedData, 0, encryptedBytes, ivLength, encryptedData.length);

  return BASE64_ENCODER.apply(encryptedBytes);
}

private static String decrypt(String data, String mainKey, int ivLength) throws Exception {
  final byte[] encryptedBytes = BASE64_DECODER.apply(data.getBytes(UNICODE_FORMAT));

  final byte[] initializationVector = new byte[ivLength];
  System.arraycopy(encryptedBytes, 0, initializationVector, 0, ivLength);

  final Cipher cipher = getAesGcmCipher(Cipher.DECRYPT_MODE, mainKey, initializationVector);
  return new String(cipher.doFinal(encryptedBytes, ivLength, encryptedBytes.length - ivLength),
      UNICODE_FORMAT);
}

他还给我发送了一些用他正在使用的 class 初始化的常量:

static final String ALGO = "AES"; //$NON-NLS-1$
static final String GCMALGO = "AES/GCM/NoPadding"; //$NON-NLS-1$
static final String UNICODE_FORMAT = "UTF8"; //$NON-NLS-1$
static final String DES_ENCRYPTION_SCHEME = "DES"; //$NON-NLS-1$
private static final int DEFAULT_IV_LENGTH = 16;
public static final String NULL_PARAMETER_MESSAGE = "The parameter should not be null"; //$NON-NLS-1$
public static final String EMPTY_PARAMETER_MESSAGE = "String is empty"; //$NON-NLS-1$
private static final String KEY_GEN_ALGO = "PBKDF2WithHmacSHA256"; //$NON-NLS-1$
static final Random random = new SecureRandom();
static final BASE64Encoder b64Encoder = new DataMasking().new BASE64Encoder();
static final BASE64Decoder b64Dencoder = new DataMasking().new BASE64Decoder();
public static final Function<byte[], String> BASE64_ENCODER = bytes -> Base64.getEncoder().encodeToString(bytes);
public static final Function<byte[], byte[]> BASE64_DECODER = bytes -> Base64.getDecoder().decode(bytes);

据我了解,初始化向量位于字符串的开头,并且我了解到 Java 密码对象会自动将标签放在末尾。

我尝试在我的 PHP 端使用 openssl_encrypt() 自己加密和解密一些内容 和 openssl_decrypt(),它工作正常,但我无法解密 JAVA 应用程序发送给我的数据。

我想知道这是否与我必须对二进制数据进行的字符串操作有关,或者提供商没有给我所需的所有必要信息。

我也不知道 Tag_length 我必须使用什么来解密数据。 提供商还告诉我 IV_Length 是 16 个字符长,但是当我使用 openssl_cipher_iv_length('aes-128-gcm') 时,它建议 12.

这是我现在 PHP 端的代码:

/**
 * @param string $str
 *   The URL parameter string
 */
function test_decrypt($str) {
  $key = 'MySuperPassword7';
  $cipher = 'aes-128-gcm';

  $iv_len = 16;
  $tag_length = 16;

  echo $str . '<br>';
  /**
   * Encryption test
   */
//  $tag = ""; 
//  $iv = openssl_random_pseudo_bytes($iv_len);
//  $enc_str = openssl_encrypt('Test of data to send', $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag, "", $tag_length);
//  $encrypt = base64_encode($iv.$enc_str.$tag);

//  echo $key . '::' . $iv . '::' . $tag . '<br>';
//  echo '$enc_str<pre>';
//  var_dump($enc_str);
//  echo '</pre>';
//  echo '$encrypt<pre>';
//  var_dump($encrypt);
//  echo '</pre>';

  /**
   * Decryption part
   */
  $encrypt = base64_decode($str);
  $iv = substr($encrypt, 0, $iv_len);
  $tag = substr($encrypt, - $tag_length);
  $ciphertext = substr($encrypt, $iv_len, -$tag_length);

  $uncrypt = openssl_decrypt($ciphertext, $cipher, $key, 0, $iv, $tag);//OPENSSL_RAW_DATA + OPENSSL_NO_PADDING
  echo $iv_len . '::' . $tag_length . '<br>';
  echo $key . '::' . $iv . '::' . $tag . '<br>';
  echo '$encrypt<pre>';
  var_dump($encrypt);
  echo '</pre>';
  echo '$ciphertext<pre>';
  var_dump($ciphertext);
  echo '</pre>';
  echo '$uncrypt<pre>';
  var_dump($uncrypt);
  echo '</pre>';
  exit;
}

我也尝试使用在线工具来解密 Java 应用程序发送给我的数据,但到目前为止我找不到任何有用的东西。

编辑

正如@JohnConde 在评论中所要求的,我可以分享我目前正在尝试解密的加密字符串和密钥。 我们决定使用与 Michael Fehr 的答案相同的数据进行测试:

长度与加密后的答案相同(P8kU/Gf27LU9qgmLpA8gpfHmdVuW5cC7XeKVIgmb3rgGzyjG0YHX5P/cFfRRoZpUxxTEKzj8)。

和以前一样,我无法解密数据...

这里是 Talend 组件的完整代码:

// ============================================================================
//
// Copyright (C) 2006-2019 Talend Inc. - www.talend.com
//
// This source code is available under agreement available at
// %InstallDIR%\features\org.talend.rcp.branding.%PRODUCTNAME%\%PRODUCTNAME%license.txt
//
// You should have received a copy of the agreement
// along with this program; if not, write to Talend SA
// 9 rue Pages 92150 Suresnes, France
//
// ============================================================================

package routines;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PushbackInputStream;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.Stream;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;


/**
 * created by talend on 2016-04-08 Detailled comment.
 *
 */
public class DataMasking {

    static final String ALGO = "AES"; //$NON-NLS-1$

    static final String GCMALGO = "AES/GCM/NoPadding"; //$NON-NLS-1$

    static final String UNICODE_FORMAT = "UTF8"; //$NON-NLS-1$

    static final String DES_ENCRYPTION_SCHEME = "DES"; //$NON-NLS-1$

    private static final int DEFAULT_IV_LENGTH = 16;

    public static final String NULL_PARAMETER_MESSAGE = "The parameter should not be null"; //$NON-NLS-1$

    public static final String EMPTY_PARAMETER_MESSAGE = "String is empty"; //$NON-NLS-1$

    private static final String KEY_GEN_ALGO = "PBKDF2WithHmacSHA256"; //$NON-NLS-1$

    static final Random random = new SecureRandom();

    static final BASE64Encoder b64Encoder = new DataMasking().new BASE64Encoder();

    static final BASE64Decoder b64Dencoder = new DataMasking().new BASE64Decoder();

    public static final Function<byte[], String> BASE64_ENCODER = bytes -> Base64.getEncoder().encodeToString(bytes);

    public static final Function<byte[], byte[]> BASE64_DECODER = bytes -> Base64.getDecoder().decode(bytes);

    public static class DataMaskingRoutineException extends RuntimeException {

        private static final long serialVersionUID = -8622896150657449668L;

        public DataMaskingRoutineException() {
            super();
        }

        public DataMaskingRoutineException(String s) {
            super(s);
        }

        public DataMaskingRoutineException(String s, Object o) {
            super(s);
            System.out.println(o);
        }

    }


    /**
     * Encrypt String: Encrypts a string using AES 128 .
     * warning: this is not considered a secure function.
     *
     *
     * {talendTypes} String
     *
     * {Category} Data Masking
     *
     * {param} String("foo") encryptString: The string to be encrypted.
     * 
     * {param} byte[](new byte[] { 'T', 'a', 'l', 'e', 'n', 'd', 's', 'S', 'e', 'c', 'r', 'e', 't', 'K', 'e', 'y' })
     * keyValue: the
     * key material of the secret key. The contents of the array are copied to protect against subsequent modification.
     * 
     * {example} encryptAES("foo", new byte[] { 'T', 'a', 'l', 'e', 'n', 'd', 's', 'S', 'e', 'c', 'r', 'e', 't', 'K', 'e', 'y' })
     * result is UQ0VJZq5ymFkMYQeDrPi0A==
     *
     * @deprecated use {@link #encryptAESGCM(String, String, int)} instead of it
     * 
     */

    @Deprecated
    public static String encryptAES(String encryptString, byte[] keyValue) {

        if (encryptString == null || keyValue == null) {
            return NULL_PARAMETER_MESSAGE;
        }
        if (encryptString.length() == 0) {
            return EMPTY_PARAMETER_MESSAGE;
        }
        try {
            Key key = new SecretKeySpec(keyValue, ALGO);
            Cipher cipher = Cipher.getInstance(ALGO);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] encVal = cipher.doFinal(encryptString.getBytes());
            String encryptedValue = b64Encoder.encode(encVal);
            return encryptedValue;

        } catch (Exception e) {
            throw new DataMaskingRoutineException(e.getMessage(), e);
        }
    }


    /**
     * Encrypt String: Encrypts a string using AES GCM 128 .
     * 
     * {talendTypes} String
     * 
     * {Category} Data Masking
     * 
     * {param} String("foo") encryptString: The string to be encrypted.
     * 
     * {param} String("TalendMainKey123") the main key used to encrypt the data.
     * 
     * {example} encryptAESGCM("foo","TalendMainKey123") result could be
     * +ngs4RohAx8OvInmioRwRYS0pymcNkQJCKqsdUPZqUZppN4= (but it should change from an execution to another).
     * 
     */

    public static String encryptAESGCM(String encryptString, String mainKey) {

        return encryptAESGCM(encryptString, mainKey, DEFAULT_IV_LENGTH);
    }

    /**
     * Encrypt String: Encrypts a string using AES GCM 128 .
     * 
     * {talendTypes} String
     * 
     * {Category} Data Masking
     * 
     * {param} String("foo") encryptString: The string to be encrypted.
     * 
     * {param} String("TalendMainKey123") the main key used to encrypt the data.
     * 
     * {param} int the length of initializationVector. must be one of 12/13/14/15/16.
     * 
     * {example} encryptAESGCM("foo","TalendMainKey123",16) result could be
     * +ngs4RohAx8OvInmioRwRYS0pymcNkQJCKqsdUPZqUZppN4= (but it should change from an execution to another).
     * 
     */

    public static String encryptAESGCM(String encryptString, String mainKey, int ivLength) {

        if (encryptString == null || mainKey == null) {
            return NULL_PARAMETER_MESSAGE;
        }

        if (encryptString.length() == 0) {
            return EMPTY_PARAMETER_MESSAGE;
        }

        try {
            return encrypt(encryptString, mainKey, ivLength);
        } catch (Exception e) {
            throw new DataMaskingRoutineException(e.getMessage(), e);
        }
    }



    /**
     * decrypt String: Decrypts a string using AES 128.
     * warning: this is not considered a secure function.
     *
     *
     * {talendTypes} String
     *
     * {Category} Data Masking
     *
     * {param} String("UQ0VJZq5ymFkMYQeDrPi0A==") encryptedString: The string to be decrypted.
     *
     * {param} byte[](new byte[] { 'T', 'a', 'l', 'e', 'n', 'd', 's', 'S', 'e', 'c', 'r', 'e', 't', 'K', 'e', 'y' })
     * keyValue: the key material of the secret key. The contents of the array are copied to protect against subsequent
     * modification.
     * 
     * {example} decryptAES("UQ0VJZq5ymFkMYQeDrPi0A==",new byte[] { 'T', 'a', 'l', 'e', 'n', 'd', 's', 'S', 'e', 'c', 'r', 'e', 't', 'K', 'e', 'y' })
     * result is "foo"
     * 
     * @deprecated use {@link #decryptAESGCM(String, String, int)} instead of it
     * 
     */

    @Deprecated
    public static String decryptAES(String encryptedString, byte[] keyValue) {

        if (encryptedString == null || keyValue == null) {
            return NULL_PARAMETER_MESSAGE;
        }

        if (encryptedString.length() == 0) {
            return EMPTY_PARAMETER_MESSAGE;
        }
        try {
            Key key = new SecretKeySpec(keyValue, ALGO);
            Cipher cipher = Cipher.getInstance(ALGO);
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] decordedValue = b64Dencoder.decodeBuffer(encryptedString);
            byte[] decValue = cipher.doFinal(decordedValue);
            String decryptedValue = new String(decValue);
            return decryptedValue;

        } catch (Exception e) {
            throw new DataMaskingRoutineException(e.getMessage(), e);
        }
    }

    /**
     * decrypt String: Decrypts a string using AES GCM 128.
     * 
     * {talendTypes} String
     * 
     * {Category} Data Masking
     * 
     * {param} String("+ngs4RohAx8OvInmioRwRYS0pymcNkQJCKqsdUPZqUZppN4=") encryptedString: The string to be decrypted.
     * 
     * {param} String("TalendMainKey123") the main key used to decrypt the data.
     * 
     * {param} int the length of initializationVector. must be one of 12/13/14/15/16.
     * 
     * {example} decryptAESGCM("+ngs4RohAx8OvInmioRwRYS0pymcNkQJCKqsdUPZqUZppN4=","TalendMainKey123",16) result
     * is "foo"
     * 
     */

    public static String decryptAESGCM(String encryptedString, String mainKey, int ivLength) {

        if (encryptedString == null || mainKey == null) {
            return NULL_PARAMETER_MESSAGE;
        }

        if (encryptedString.length() == 0) {
            return EMPTY_PARAMETER_MESSAGE;
        }

        try {
            return decrypt(encryptedString, mainKey, ivLength);

        } catch (Exception e) {
            throw new DataMaskingRoutineException(e.getMessage(), e);
        }

    }

    /**
     * decrypt String: Decrypts a string using AES GCM 128.
     * 
     * {talendTypes} String
     * 
     * {Category} Data Masking
     * 
     * {param} String("+ngs4RohAx8OvInmioRwRYS0pymcNkQJCKqsdUPZqUZppN4=") encryptedString: The string to be decrypted.
     * 
     * {param} String("TalendMainKey123") the main key used to decrypt the data.
     * 
     * {example} decryptAESGCM("+ngs4RohAx8OvInmioRwRYS0pymcNkQJCKqsdUPZqUZppN4=","TalendMainKey123") result
     * is "foo"
     * 
     */

    public static String decryptAESGCM(String encryptedString, String mainKey) {

        return decryptAESGCM(encryptedString, mainKey, DEFAULT_IV_LENGTH);
    }

    /**
     * Encrypt String: Encrypts a string using DES .
     * warning: this is not considered a secure function.
     *
     *
     * {talendTypes} String
     *
     * {Category} Data Masking
     *
     * {param} String("foo") unencryptedString: The string to be encrypted.
     *
     * {param} String("ThisIsSecretEncryptionKey") myEncryptionKey: the string with the DES key material.
     *
     * {example} encryptDES("foo") result is DmNj+x2LUXA=
     *
     * @throws Exception
     */

    public static String encryptDES(String unencryptedString, String myEncryptionKey) {

        if (unencryptedString == null || myEncryptionKey == null) {
            return NULL_PARAMETER_MESSAGE;
        }
        if (unencryptedString.length() == 0) {
            return EMPTY_PARAMETER_MESSAGE;
        }
        try {
            String encryptedString = null;

            String myEncryptionScheme = DES_ENCRYPTION_SCHEME;
            byte[] keyAsBytes = myEncryptionKey.getBytes(UNICODE_FORMAT);
            KeySpec myKeySpec = new DESKeySpec(keyAsBytes);
            SecretKeyFactory mySecretKeyFactory = SecretKeyFactory.getInstance(myEncryptionScheme);
            Cipher encipher = Cipher.getInstance(myEncryptionScheme);
            SecretKey key = mySecretKeyFactory.generateSecret(myKeySpec);

            encipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] plainText = unencryptedString.getBytes(UNICODE_FORMAT);
            byte[] encryptedText = encipher.doFinal(plainText);
            encryptedString = b64Encoder.encode(encryptedText);

            return encryptedString;

        } catch (Exception e) {
            throw new DataMaskingRoutineException(e.getMessage());
        }

    }

    /**
     * Decrypt String: Decrypts a string using DES .
     * warning: this is not considered a secure function.
     *
     *
     * {talendTypes} String
     *
     * {Category} Data Masking
     *
     * {param} String("DmNj+x2LUXA=") encryptedString: the string with the DES key material.
     *
     * {param} String("ThisIsSecretEncryptionKey") myDecryptionKey: The string to be encrypted.
     *
     * {example} decryptDES("DmNj+x2LUXA=") result is "foo"
     *
     */

    public static String decryptDES(String encryptedString, String myDecryptionKey) {

        if (encryptedString == null || myDecryptionKey == null) {
            return NULL_PARAMETER_MESSAGE;
        }

        if (encryptedString.length() == 0) {
            return EMPTY_PARAMETER_MESSAGE;
        }
        try {
            String decryptedText = null;

            String myDecryptionScheme = DES_ENCRYPTION_SCHEME;

            byte[] keyAsBytes = myDecryptionKey.getBytes(UNICODE_FORMAT);

            KeySpec myKeySpec = new DESKeySpec(keyAsBytes);
            Cipher decipher = Cipher.getInstance(myDecryptionScheme);
            SecretKeyFactory mySecretKeyFactory = SecretKeyFactory.getInstance(myDecryptionScheme);
            SecretKey key = mySecretKeyFactory.generateSecret(myKeySpec);

            decipher.init(Cipher.DECRYPT_MODE, key);
            byte[] encryptedText = b64Dencoder.decodeBuffer(encryptedString);
            byte[] plainText = decipher.doFinal(encryptedText);

            StringBuilder stringBuilder = new StringBuilder();
            for (byte element : plainText) {
                stringBuilder.append((char) element);
            }
            decryptedText = stringBuilder.toString();
            return decryptedText;

        } catch (Exception e) {
            throw new DataMaskingRoutineException(e.getMessage(), e);
        }

    }


    /**
     * This method generates a secret Key using the key-stretching algorithm PBKDF2 of
     * <a href="https://docs.oracle.com/javase/7/docs/api/javax/crypto/package-summary.html">javax.crypto</a>.
     * It is basically a hashing algorithm slow by design, in order to increase the time
     * required for an attacker to try a lot of passwords in a bruteforce attack.
     * <br>
     * About the salt :
     * <ul>
     * <li>The salt is not secret, the use of Random is not critical and ensure determinism.</li>
     * <li>The salt is important to avoid rainbow table attacks.</li>
     * <li>The salt should be generated with SecureRandom() in case the passwords are stored.</li>
     * <li>In that case the salt should be stored in plaintext next to the password and a unique user identifier.</li>
     * </ul>
     *
     * @param password a password given as a {@code String}.
     * @param keyLength key length to generate
     * @return a {@code SecretKey} securely generated.
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    private static byte[] generateSecretKeyFromPassword(String password, int keyLength)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] salt = new byte[keyLength];
        new Random(password.hashCode()).nextBytes(salt);
        SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_GEN_ALGO);
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, keyLength << 3);
        return factory.generateSecret(spec).getEncoded();
    }

    private static String encrypt(String data, String mainKey, int ivLength) throws Exception {
        final byte[] dataBytes = data.getBytes(UNICODE_FORMAT);
        byte[] initializationVector = generateInitializationVector(ivLength);
        final Cipher cipher = getAesGcmCipher(Cipher.ENCRYPT_MODE, mainKey, initializationVector);

        final byte[] encryptedData = cipher.doFinal(dataBytes);
        final byte[] encryptedBytes = new byte[encryptedData.length + ivLength];
        System.arraycopy(initializationVector, 0, encryptedBytes, 0, ivLength);
        System.arraycopy(encryptedData, 0, encryptedBytes, ivLength, encryptedData.length);

        return BASE64_ENCODER.apply(encryptedBytes);
    }


    private static String decrypt(String data, String mainKey, int ivLength) throws Exception {
        final byte[] encryptedBytes = BASE64_DECODER.apply(data.getBytes(UNICODE_FORMAT));

        final byte[] initializationVector = new byte[ivLength];
        System.arraycopy(encryptedBytes, 0, initializationVector, 0, ivLength);

        final Cipher cipher = getAesGcmCipher(Cipher.DECRYPT_MODE, mainKey, initializationVector);
        return new String(cipher.doFinal(encryptedBytes, ivLength, encryptedBytes.length - ivLength),
                UNICODE_FORMAT);
    }

}

** 编辑 2 **

在与提供者调试了一段时间后,我们最终决定使用 Michael Fehr 提供的代码。

我从组件的代码中了解到,区别在于主键的使用方式。但我不是 Java 开发人员,也许我误解了什么。

以防万一它可以帮助某人,这里是额外的 Java IV 生成和密钥生成缺少的代码。

private static Cipher getAesGcmCipher(int encryptMode, String mainKey, byte[] initializationVector)
            throws Exception {
    int ivLength = initializationVector.length;
    if (Stream.of(12, 13, 14, 15, 16).noneMatch(i -> i == ivLength)) {
        throw new IllegalArgumentException("Invalid IV length"); //$NON-NLS-1$
    }
    final Cipher cipher = Cipher.getInstance(GCMALGO);
    SecretKey key = new SecretKeySpec(generateSecretKeyFromPassword(mainKey, mainKey.length()), ALGO);
    final GCMParameterSpec spec = new GCMParameterSpec(ivLength * 8, initializationVector);
    cipher.init(encryptMode, key, spec);
    return cipher;
}



private static byte[] generateSecretKeyFromPassword(String password, int keyLength)
          throws NoSuchAlgorithmException, InvalidKeySpecException {
      byte[] salt = new byte[keyLength];
      new Random(password.hashCode()).nextBytes(salt);
      SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_GEN_ALGO);
      KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, keyLength << 3);
      return factory.generateSecret(spec).getEncoded();
}

我在 Java 中设置了您的代码,并使用(固定)密钥和随机初始化向量进行了加密:

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Random;

public class SO_Main_Final {

    static final String ALGO = "AES"; //$NON-NLS-1$
    static final String GCMALGO = "AES/GCM/NoPadding"; //$NON-NLS-1$
    static final String UNICODE_FORMAT = "UTF8"; //$NON-NLS-1$
    static final Random random = new SecureRandom();

    public static void main(String[] args) throws Exception {
        System.out.println("
        String myData = "Secret data for TytooF";
        String myKey = "1234567890123456";
        String encryptString = encrypt(myData, myKey, 16);
        String decryptString = decrypt(encryptString, myKey, 16);
        System.out.println("encryptString:         " + encryptString);
        System.out.println("decryptString:         " + decryptString);

    }

    private static String encrypt(String data, String mainKey, int ivLength) throws Exception {
        final byte[] dataBytes = data.getBytes(UNICODE_FORMAT);
        // byte[] initializationVector = generateInitializationVector(ivLength);
        byte[] initializationVector = new byte[ivLength];
        random.nextBytes(initializationVector);
        SecretKeySpec secretKeySpec = new SecretKeySpec(mainKey.getBytes(UNICODE_FORMAT), ALGO);
        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
        Cipher cipher = Cipher.getInstance(GCMALGO);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec);
        final byte[] encryptedData = cipher.doFinal(dataBytes);
        final byte[] encryptedBytes = new byte[encryptedData.length + ivLength];
        System.arraycopy(initializationVector, 0, encryptedBytes, 0, ivLength);
        System.arraycopy(encryptedData, 0, encryptedBytes, ivLength, encryptedData.length);
        System.out.println("data [String]        : " + data);
        System.out.println("data           length: " + dataBytes.length
                + " data: " + bytesToHex(dataBytes));
        System.out.println("mainKey        length: " + mainKey.getBytes(UNICODE_FORMAT).length
                + " data: " + bytesToHex(mainKey.getBytes(UNICODE_FORMAT)));
        System.out.println("initvector     length: " + initializationVector.length
                + " data: " + bytesToHex(initializationVector));
        System.out.println("encryptedBytes length: " + encryptedBytes.length
                + " data: " + bytesToHex(encryptedBytes));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    private static String decrypt(String data, String mainKey, int ivLength) throws Exception {
        final byte[] encryptedBytes = Base64.getDecoder().decode(data.getBytes(UNICODE_FORMAT));
        final byte[] initializationVector = new byte[ivLength];
        System.arraycopy(encryptedBytes, 0, initializationVector, 0, ivLength);
        SecretKeySpec secretKeySpec = new SecretKeySpec(mainKey.getBytes(UNICODE_FORMAT), ALGO);
        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
        Cipher cipher = Cipher.getInstance(GCMALGO);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
        return new String(cipher.doFinal(encryptedBytes, ivLength, encryptedBytes.length - ivLength),
                UNICODE_FORMAT);
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuffer result = new StringBuffer();
        for (byte b : bytes) result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
        return result.toString();
    }
}

最后我得到了这个结果——“encryptString”需要转移到您的网站:


data [String]        : Secret data for TytooF
data           length: 22 data: 536563726574206461746120666f72205479746f6f46
mainKey        length: 16 data: 31323334353637383930313233343536
initvector     length: 16 data: 3fc914fc67f6ecb53daa098ba40f20a5
encryptedBytes length: 54 data: 3fc914fc67f6ecb53daa098ba40f20a5f1e6755b96e5c0bb5de29522099bdeb806cf28c6d181d7e4ffdc15f451a19a54c714c42b38fc
encryptString:         P8kU/Gf27LU9qgmLpA8gpfHmdVuW5cC7XeKVIgmb3rgGzyjG0YHX5P/cFfRRoZpUxxTEKzj8
decryptString:         Secret data for TytooF

在 webserver/PHP-side 我稍微修改了你的代码(“主要变化”是行 openssl_decrypt($ciphertext, $cipher, $key, true, $iv, $tag) 因为我们没有将 Base64 编码的数据提供给解密 方法,因为这是在之前完成的一些代码行 ($encrypt = base64_decode($str).

这里是 PHP-代码:

<?php

/**
 * @param string $str
 *   The URL parameter string
 */
function test_decrypt($str)
{
    $key = '1234567890123456';
    $cipher = 'aes-128-gcm';
    $iv_len = 16;
    $tag_length = 16;
    echo $str . '<br>';
    /**
     * Decryption part
     */
    $encrypt = base64_decode($str);
    $iv = substr($encrypt, 0, $iv_len);
    $tag = substr($encrypt, -$tag_length);
    $ciphertext = substr($encrypt, $iv_len, -$tag_length);
    echo "" . "\n";
    $value = unpack('H*', $iv);
    echo '<br>iv:' . $value[1];
    echo "" . "\n";
    $value = unpack('H*', $ciphertext);
    echo '<br>ciphertext:' . $value[1];
    echo "" . "\n";
    $value = unpack('H*', $tag);
    echo '<br>tag:' . $value[1];
    echo "<br>" . "\n";
    $uncrypt = openssl_decrypt($ciphertext, $cipher, $key, true, $iv, $tag);//OPENSSL_RAW_DATA + OPENSSL_NO_PADDING
    echo '<br>DecryptedString: ' . $uncrypt . "\n";
    $value = unpack('H*', $uncrypt);
    echo '<br>DecryptedString [byte[]]:' . $value[1];
    exit;
}

echo '<b>Output for </b><br>' . "\n";
echo '' . "\n";
echo 'Start decryption' . "\n";
$receivedData = "P8kU/Gf27LU9qgmLpA8gpfHmdVuW5cC7XeKVIgmb3rgGzyjG0YHX5P/cFfRRoZpUxxTEKzj8";
test_decrypt($receivedData);
?>

这是网络服务器上的解密输出:

Output for 
Start decryption P8kU/Gf27LU9qgmLpA8gpfHmdVuW5cC7XeKVIgmb3rgGzyjG0YHX5P/cFfRRoZpUxxTEKzj8

iv:3fc914fc67f6ecb53daa098ba40f20a5
ciphertext:f1e6755b96e5c0bb5de29522099bdeb806cf28c6d181
tag:d7e4ffdc15f451a19a54c714c42b38fc

DecryptedString: Secret data for TytooF
DecryptedString [byte[]]:536563726574206461746120666f72205479746f6f46

2021 年 10 月 10 日编辑: 上面的代码使用的 IV/nonce 长度为 16 个字节,但 推荐 IV/nonce长度为12.