# 对称加解密概述

加解密采用AEAD_AES_256_GCM 算法,细节请参考 rfc5116。密钥 key 长度为 32 个字节,随机串 nonce 长度小于 32 个字节,associated_data 长度小于 16 个字节。

# 加密示例

GO

// Encrypt 加密
func Encrypt(data []byte, key, associatedData string) (ciphertext []byte, nonce string, err error) {
	block, err := aes.NewCipher([]byte(key))
	if err != nil {
		return
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return
	}

	nonce = util.NonceStr(gcm.NonceSize())

	ciphertext = gcm.Seal(nil, []byte(nonce), data, []byte(associatedData))

	return
}

JAVA

import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AesUtil {

    static final int KEY_LENGTH_BYTE = 32;
    static final int TAG_LENGTH_BIT = 128;
    private final byte[] aesKey;

    public AesUtil(byte[] key) {
        if (key.length != KEY_LENGTH_BYTE) {
            throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
        }
        this.aesKey = key;
    }

    // 加密方法
    public String encrypt(byte[] associatedData, byte[] nonce, byte[] data)
            throws GeneralSecurityException {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);

            cipher.init(Cipher.ENCRYPT_MODE, keySpec, spec);
            cipher.updateAAD(associatedData);

            byte[] encryptedText = cipher.doFinal(data);

            return Base64.getEncoder().encodeToString(encryptedText);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        }
    }
}

PHP

class AesUtil
{
    /**
     * AES key
     *
     * @var string
     */
    private $aesKey;

    const KEY_LENGTH_BYTE = 32;
    const AUTH_TAG_LENGTH_BYTE = 16;

    /**
     * Constructor
     */
    public function __construct($aesKey)
    {
        if (strlen($aesKey) != self::KEY_LENGTH_BYTE) {
            throw new InvalidArgumentException('无效的ApiV3Key,长度应为32个字节');
        }
        $this->aesKey = $aesKey;
    }

    /**
     * Encrypt AEAD_AES_256_GCM plaintext
     *
     * @param string $associatedData AES GCM additional authentication data
     * @param string $nonceStr AES GCM nonce
     * @param string $plaintext AES GCM plain text
     *
     * @return string                Base64 encoded cipher text
     */
    public function encrypt($associatedData, $nonceStr, $plaintext)
    {
        // ext-sodium (default installed on >= PHP 7.2)
        if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') &&
            \sodium_crypto_aead_aes256gcm_is_available()) {
            $ciphertext = \sodium_crypto_aead_aes256gcm_encrypt($plaintext, $associatedData, $nonceStr, $this->aesKey);
        } // ext-libsodium (need install libsodium-php 1.x via pecl)
        elseif (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') &&
            \Sodium\crypto_aead_aes256gcm_is_available()) {
            $ciphertext = \Sodium\crypto_aead_aes256gcm_encrypt($plaintext, $associatedData, $nonceStr, $this->aesKey);
        } // openssl (PHP >= 7.1 support AEAD)
        elseif (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', openssl_get_cipher_methods())) {
            $ciphertext = null;
            $authTag = null;
            $ctext = \openssl_encrypt($plaintext, 'aes-256-gcm', $this->aesKey, \OPENSSL_RAW_DATA, $nonceStr,
                $authTag, $associatedData);
            if ($ctext !== false) {
                $ciphertext = $ctext . $authTag;
            } else {
                throw new \RuntimeException('Encryption failed');
            }
        } else {
            throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
        }

        return \base64_encode($ciphertext);
    }
}

# 解密示例

JAVA

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AesUtil {

  static final int KEY_LENGTH_BYTE = 32;
  static final int TAG_LENGTH_BIT = 128;
  private final byte[] aesKey;

  public AesUtil(byte[] key) {
    if (key.length != KEY_LENGTH_BYTE) {
      throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
    }
    this.aesKey = key;
  }

  public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
      throws GeneralSecurityException, IOException {
    try {
      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

      SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
      GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);

      cipher.init(Cipher.DECRYPT_MODE, key, spec);
      cipher.updateAAD(associatedData);

      return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
      throw new IllegalStateException(e);
    } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
      throw new IllegalArgumentException(e);
    }
  }
}

PHP

class AesUtil{
  /**
    * AES key
    *
    * @var string
    */
  private $aesKey;

  const KEY_LENGTH_BYTE = 32;
  const AUTH_TAG_LENGTH_BYTE = 16;

  /**
    * Constructor
    */
  public function __construct($aesKey)
  {
      if (strlen($aesKey) != self::KEY_LENGTH_BYTE) {
          throw new InvalidArgumentException('无效的ApiV3Key,长度应为32个字节');
      }
      $this->aesKey = $aesKey;
  }

  /**
    * Decrypt AEAD_AES_256_GCM ciphertext
    *
    * @param string    $associatedData     AES GCM additional authentication data
    * @param string    $nonceStr           AES GCM nonce
    * @param string    $ciphertext         AES GCM cipher text
    *
    * @return string|bool      Decrypted string on success or FALSE on failure
    */
  public function decryptToString($associatedData, $nonceStr, $ciphertext)
  {
      $ciphertext = \base64_decode($ciphertext);
      if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
          return false;
      }

      // ext-sodium (default installed on >= PHP 7.2)
      if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') &&
          \sodium_crypto_aead_aes256gcm_is_available()) {
          return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->aesKey);
      }

      // ext-libsodium (need install libsodium-php 1.x via pecl)
      if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') &&
          \Sodium\crypto_aead_aes256gcm_is_available()) {
          return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->aesKey);
      }

      // openssl (PHP >= 7.1 support AEAD)
      if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
          $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
          $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);

          return \openssl_decrypt($ctext, 'aes-256-gcm', $this->aesKey, \OPENSSL_RAW_DATA, $nonceStr,
              $authTag, $associatedData);
      }

      throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
  }
}

.NET

public class AesGcm
{
    private static string ALGORITHM = "AES/GCM/NoPadding";
    private static int TAG_LENGTH_BIT = 128;
    private static int NONCE_LENGTH_BYTE = 12;
    private static string AES_KEY = "yourkeyhere";

    public static string AesGcmDecrypt(string associatedData, string nonce, string ciphertext)
    {
        GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine());
        AeadParameters aeadParameters = new AeadParameters(
            new KeyParameter(Encoding.UTF8.GetBytes(AES_KEY)),
            128,
            Encoding.UTF8.GetBytes(nonce),
            Encoding.UTF8.GetBytes(associatedData));
        gcmBlockCipher.Init(false, aeadParameters);

        byte[] data = Convert.FromBase64String(ciphertext);
        byte[] plaintext = new byte[gcmBlockCipher.GetOutputSize(data.Length)];
        int length = gcmBlockCipher.ProcessBytes(data, 0, data.Length, plaintext, 0);
        gcmBlockCipher.DoFinal(plaintext, length);
        return Encoding.UTF8.GetString(plaintext);
    }
}

PYTHON

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64

def decrypt(nonce, ciphertext, associated_data):
    key = "Your32Apiv3Key"

    key_bytes = str.encode(key)
    nonce_bytes = str.encode(nonce)
    ad_bytes = str.encode(associated_data)
    data = base64.b64decode(ciphertext)

    aesgcm = AESGCM(key_bytes)
    return aesgcm.decrypt(nonce_bytes, data, ad_bytes)

GO

// Decrypt 解密
func Decrypt(ciphertext []byte, key, nonce, associatedData string) (text []byte, err error) {
	block, err := aes.NewCipher([]byte(key))
	if err != nil {
		return
	}

	aesgcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}

	text, err = aesgcm.Open(nil, []byte(nonce), ciphertext, []byte(associatedData))
	if err != nil {
		return
	}

	return
}