对比HTTPS

  • HTTPS是对称与非对称加密的一种实现方式
  • HTTPS可能存在中间人劫持(服务器和客户端进行通信,C在服务器、客户端之间劫持双方的请求,作为中间人将双方的数据分别解加密后转发给对方,此时C需要拥有受客户端信任的证书)

为同时兼顾安全性及性能,建议采用由非对称加密进行对称加密密钥的传输,定期更换对称密钥(对称加密在当前计算水平上只能保证在一段时间内是安全的),使用对称加密完成所有数据的加密传输。

非对称加密

特点

  • 数据加密安全性最高,但只能用于小数据加密,且加解密耗时较高。

用途

  • 加密传输数据

此时A、B双方各持有对方公钥(公钥公开),双方使用对方公钥对待传输的数据进行加密,如此加密后的数据只有对方所持有的私钥可以解开。

  • 防止数据被伪造

此时A、B双方各持有对方公钥(公钥公开),双方使用各自的私钥对待传输的数据进行加密,如此加密后的数据只能使用公钥解密而无法伪造。

  • 加密并防止传输数据伪造
  1. 此时A、B双方各持有对方的公钥(公钥公开),对待数据进行两次非对称加密(以各自私钥进行第一次非对称加密确保数据无法被伪造,以对方公钥进行第二次非对称加密确保数据无法被第三方解密),如此加密后的数据可以达到加密并防止传输数据伪造的目的。
  2. 此时A、B双方各持有对方的公钥(公钥不公开),双方使用各自私钥对待传输数据的数据进行加密,如此加密后的数据也可以达到加密并防止传输数据伪造的目的。

对称加密

特点

  • 数据加密安全性较高,能用于大量数据加密,且加解密耗时较少。

用途

  • 加密并防止传输数据伪造
  • 对大量数据进行加密

兼容性

  • 由于各语言所支持的加密算法和填充算法不同,经实践后确定能够同时兼容PHP、iOS、android、java的加密算法为:AES-256-CBC,填充算法为:PKCS7Padding,密钥转换AES密钥算法为:PBKDF2WithHmacSHA1。

算法实现

PHP实现

/**
 * 对称加密算法类
 */
class SymmetricEncryption{
    const CIPHER = MCRYPT_RIJNDAEL_128;
    const MODEL = MCRYPT_MODE_CBC;
    const HASH_ALGORITHM = 'sha1';
    // 对应android、iOS的{0,1,2,3,4,5,6,7,8,9,0xA,0xB,0xC,0xD,0xE,0xF}
    const HASH_SALT = 'AAECAwQFBgcICQoLDA0ODw==';
    const HASH_ITERATIONS = 10000;
    // 对应android、iOS的 KEY_LENGTH/8
    const HASH_KEY_LENGTH = 32;
    // 对应android、iOS的{0xA,1,0xB,5,4,0xF,7,9,0x17,3,1,6,8,0xC,0xD,91}
    const IV = 'CgELBQQPBwkXAwEGCAwNWw==';

    /**
     * 加密
     * @param $key string 加密密钥
     * @param $data string 待加密明文
     * @return string 密文
     */
    public static function encrypt($key, $data){
        // 对应安卓和iOS的PBKDF2WithHmacSHA1
        $key = hash_pbkdf2(
            self::HASH_ALGORITHM,
            $key,
            base64_decode(self::HASH_SALT),
            self::HASH_ITERATIONS,
            self::HASH_KEY_LENGTH,
            true
        );
        // 使用PKCS7Padding的方式进行填充
        $data = self::PKCS7Padding($data);
        # 创建和 AES 兼容的密文(Rijndael 分组大小 = 256)
        $cipherText = mcrypt_encrypt(
            self::CIPHER,
            $key,
            $data,
            self::MODEL,
            base64_decode(self::IV)
        );
        # 对密文进行 base64 编码便于显性复制
        $cipherTextBase64 = base64_encode($cipherText);
        return $cipherTextBase64;
    }

    /**
     * 解密
     * @param $key string 解密密钥
     * @param $data string 待解密密文
     * @return string 明文
     */
    public static function decrypt($key, $data){
        // 对应安卓和iOS的PBKDF2WithHmacSHA1
        $key = hash_pbkdf2(
            self::HASH_ALGORITHM,
            $key,
            base64_decode(self::HASH_SALT),
            self::HASH_ITERATIONS,
            self::HASH_KEY_LENGTH,
            true
        );
        # --- 解密 ---
        $cipherTextDec = base64_decode($data);
        $plaintextDec = mcrypt_decrypt(
            self::CIPHER,
            $key,
            $cipherTextDec,
            self::MODEL,
            base64_decode(self::IV)
        );
        // 移除PKCS7Padding填充
        $plaintextDec = self::PKCS7REmovePadding($plaintextDec);
        return $plaintextDec;
    }

    /**
     * PKCS7Padding模式填充
     * @param $data
     * @return string
     */
    public static function PKCS7Padding($data){
        $blockSize = mcrypt_get_block_size(self::CIPHER,self::MODEL);
        $pad = $blockSize - (strlen($data) % $blockSize);
        return $data . str_repeat(chr($pad), $pad);
    }

    /**
     * 移除PKCS7Padding模式填充
     * @param $data
     * @return string
     */
    public static function PKCS7RemovePadding($data){
        $pad = ord($data[strlen($data) - 1]);
        return substr($data, 0, -$pad);
    }

    /**
     * 生成16位16进制对称加密密钥
     */
    public static function generateKey(){
        $num = '';
        for($i=0;$i<16;$i++){
            $num .= dechex(rand(0,15));
        }
        return $num;
    }

    /**
     * PKCS5Padding模式填充
     * @param $text
     * @param $blockSize
     * @return string
     */
    public static function PKCS5Padding ($text, $blockSize){
        $pad = $blockSize - (strlen($text) % $blockSize);
        return $text . str_repeat(chr($pad), $pad);
    }

    /**
     * 移除PKCS5Padding模式填充
     * @param $text
     * @return string
     */
    public static function PKCS5RemovePadding($text){
        $pad = ord($text{strlen($text)-1});
        if ($pad > strlen($text))
        {
            return false;
        }
        if (strspn($text, chr($pad), strlen($text) - $pad) != $pad)
        {
            return false;
        }
        return substr($text, 0, -1 * $pad);
    }
}

iOS实现

static const char encodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
 
const NSUInteger kAlgorithmKeySize = kCCKeySizeAES256;
 
const NSUInteger kPBKDFRounds = 10000;  // ~80ms on an iPhone 4
 
 
static Byte saltBuff[] = {0,1,2,3,4,5,6,7,8,9,0xA,0xB,0xC,0xD,0xE,0xF};
 
 
static Byte ivBuff[]   = {0xA,1,0xB,5,4,0xF,7,9,0x17,3,1,6,8,0xC,0xD,91};
 
 
@implementation NSData (AES256)
 
+ (NSData *)AESKeyForPassword:(NSString *)password{ //Derive a key from a text password/passphrase
 
        NSMutableData *derivedKey = [NSMutableData dataWithLength:kAlgorithmKeySize];
    NSData *salt = [NSData dataWithBytes:saltBuff length:kCCKeySizeAES128];
    int result = CCKeyDerivationPBKDF(kCCPBKDF2,        // algorithm算法
                                  password.UTF8String,  // password密码
                                  password.length,      // passwordLength密码的长度
                                  salt.bytes,           // salt内容
                                  salt.length,          // saltLen长度
                                  kCCPRFHmacAlgSHA1,    // PRF
                                  kPBKDFRounds,         // rounds循环次数
                                  derivedKey.mutableBytes, // derivedKey
                                  derivedKey.length);   // derivedKeyLen derive:出自
    NSAssert(result == kCCSuccess,
             @"Unable to create AES key for spassword: %d", result);
 
    return derivedKey;
}
 
/*加密方法*/
 
+ (NSString *)AES256EncryptWithPlainText:(NSString *)plain key:(NSString *)key {
    NSData *plainText = [plain dataUsingEncoding:NSUTF8StringEncoding];
    // 'key' should be 32 bytes for AES256, will be null-padded otherwise
    char keyPtr[kCCKeySizeAES256+1]; // room for terminator (unused)
    bzero(keyPtr, sizeof(keyPtr)); // fill with zeroes (for padding)
    NSUInteger dataLength = [plainText length];
    size_t bufferSize = dataLength + kCCBlockSizeAES128;
    void *buffer = malloc(bufferSize);
    bzero(buffer, sizeof(buffer));
    size_t numBytesEncrypted = 0;
    CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt, kCCAlgorithmAES128,kCCOptionPKCS7Padding,
                                          [[NSData AESKeyForPassword:key] bytes], kCCKeySizeAES256,
                                          ivBuff /* initialization vector (optional) */,
                                          [plainText bytes], dataLength, /* input */
                                          buffer, bufferSize, /* output */
                                          &numBytesEncrypted);
 
    if (cryptStatus == kCCSuccess) {
        NSData *encryptData = [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
        return [encryptData base64Encoding];
    }
 
    free(buffer); //free the buffer;
 
    return nil;
}
 
/*解密方法*/
 
+ (NSString *)AES256DecryptWithCiphertext:(NSString *)ciphertexts key:(NSString *)key{
    NSData *cipherData = [NSData dataWithBase64EncodedString:ciphertexts];
    // 'key' should be 32 bytes for AES256, will be null-padded otherwise
    char keyPtr[kCCKeySizeAES256+1]; // room for terminator (unused)
    bzero(keyPtr, sizeof(keyPtr)); // fill with zeroes (for padding)
    NSUInteger dataLength = [cipherData length];
    size_t bufferSize = dataLength + kCCBlockSizeAES128;
    void *buffer = malloc(bufferSize);
    size_t numBytesDecrypted = 0;
    CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt, kCCAlgorithmAES128, kCCOptionPKCS7Padding,
                                          [[NSData AESKeyForPassword:key] bytes], kCCKeySizeAES256,
                                          ivBuff ,/* initialization vector (optional) */
                                          [cipherData bytes], dataLength, /* input */
                                          buffer, bufferSize, /* output */
                                          &numBytesDecrypted);
    if (cryptStatus == kCCSuccess) {
        NSData *encryptData = [NSData dataWithBytesNoCopy:buffer length:numBytesDecrypted];
        return [[NSString alloc] initWithData:encryptData encoding:NSUTF8StringEncoding];
    }
 
    free(buffer); //free the buffer;
 
    return nil;
}
 
+ (id)dataWithBase64EncodedString:(NSString *)string;
{
    if (string == nil)
        [NSException raise:NSInvalidArgumentException format:nil];
    if ([string length] == 0)
        return [NSData data];
 
    static char *decodingTable = NULL;
    if (decodingTable == NULL)
    {
        decodingTable = malloc(256);
        if (decodingTable == NULL)
            return nil;
 
        memset(decodingTable, CHAR_MAX, 256);
 
        NSUInteger i;
 
        for (i = 0; i < 64; i++)
 
            decodingTable[(short)encodingTable[i]] = i;
 
    }
 
    const char *characters = [string cStringUsingEncoding:NSASCIIStringEncoding];
 
    if (characters == NULL)     //  Not an ASCII string!
 
        return nil;
 
    char *bytes = malloc((([string length] + 3) / 4) * 3);
 
    if (bytes == NULL)
 
        return nil;
 
    NSUInteger length = 0;
 
    NSUInteger i = 0;
 
    while (YES)
    {
        char buffer[4];
 
        short bufferLength;
 
        for (bufferLength = 0; bufferLength < 4; i++)
        {
            if (characters[i] == '\0')
                break;
 
            if (isspace(characters[i]) || characters[i] == '=')
                continue;
 
            buffer[bufferLength] = decodingTable[(short)characters[i]];
 
            if (buffer[bufferLength++] == CHAR_MAX)      //  Illegal character!
            {
                free(bytes);
                return nil;
            }
 
        }
 
        if (bufferLength == 0)
            break;
 
        if (bufferLength == 1)      //  At least two characters are needed to produce one byte!
        {
            free(bytes);
 
            return nil;
 
        }
 
        //  Decode the characters in the buffer to bytes.
        bytes[length++] = (buffer[0] << 2) | (buffer[1] >> 4);
        if (bufferLength > 2)
            bytes[length++] = (buffer[1] << 4) | (buffer[2] >> 2);
        if (bufferLength > 3)
            bytes[length++] = (buffer[2] << 6) | buffer[3];
 
    }
 
    bytes = realloc(bytes, length);
 
    return [NSData dataWithBytesNoCopy:bytes length:length];
 
}
 
- (NSString *)base64Encoding;
{
    if ([self length] == 0)
 
        return @"";
 
    char *characters = malloc((([self length] + 2) / 3) * 4);
    if (characters == NULL)
        return nil;
 
    NSUInteger length = 0;
 
    NSUInteger i = 0;
 
    while (i < [self length])
    {
        char buffer[3] = {0,0,0};
 
        short bufferLength = 0;
 
        while (bufferLength < 3 && i < [self length])
 
            buffer[bufferLength++] = ((char *)[self bytes])[i++];
 
        //  Encode the bytes in the buffer to four characters, including padding "=" characters if necessary.
 
        characters[length++] = encodingTable[(buffer[0] & 0xFC) >> 2];
 
        characters[length++] = encodingTable[((buffer[0] & 0x03) << 4) | ((buffer[1] & 0xF0) >> 4)];
 
        if (bufferLength > 1)
            characters[length++] = encodingTable[((buffer[1] & 0x0F) << 2) | ((buffer[2] & 0xC0) >> 6)];
 
        else characters[length++] = '=';
 
        if (bufferLength > 2)
            characters[length++] = encodingTable[buffer[2] & 0x3F];
 
        else characters[length++] = '=';    
 
    }
 
    return [[NSString alloc] initWithBytesNoCopy:characters length:length encoding:NSASCIIStringEncoding freeWhenDone:YES];
 
}
 
@end

android实现

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;
 
public class AES {
   // private final String KEY_GENERATION_ALG = "PBEWITHSHAANDTWOFISH-CBC";
 
   private final String KEY_GENERATION_ALG = "PBKDF2WithHmacSHA1";
 
   private final int HASH_ITERATIONS = 10000;
   private final int KEY_LENGTH = 256;
 
    private byte[] salt = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD,
            0xE, 0xF }; // must save this for next time we want the key
    private byte[] iv = { 0xA, 1, 0xB, 5, 4, 0xF, 7, 9, 0x17, 3, 1, 6, 8, 0xC,
            0xD, 91 };
 
    private final String CIPHERMODEPADDING = "AES/CBC/PKCS7Padding";
   private PBEKeySpec myKeyspec = null;
   private SecretKeyFactory keyfactory = null;
   private SecretKey sk = null;
   private SecretKeySpec skforAES = null;
 
   private IvParameterSpec IV;
 
   public AES(String password) {
      try {
            myKeyspec = new PBEKeySpec(password.toCharArray(), salt,
                    HASH_ITERATIONS, KEY_LENGTH);
         keyfactory = SecretKeyFactory.getInstance(KEY_GENERATION_ALG);
         sk = keyfactory.generateSecret(myKeyspec);
      } catch (Exception ex) {
            System.err.println("Encryption Exception: " + ex.toString());
        }
 
      // This is our secret key. We could just save this to a file instead of
      // regenerating it
      // each time it is needed. But +that file cannot be on the device (too
      // insecure). It could
      // be secure if we kept it on a server accessible through https.
 
      byte[] skAsByteArray = sk.getEncoded();
 
      skforAES = new SecretKeySpec(skAsByteArray, "AES");
 
      IV = new IvParameterSpec(iv);
 
   }
 
   public String encrypt(byte[] plaintext) {
 
      byte[] ciphertext = encrypt(CIPHERMODEPADDING, skforAES, IV, plaintext);
      String base64_ciphertext = Base64Encoder.encode(ciphertext);
      return base64_ciphertext;
   }
 
   public String decrypt(String ciphertext_base64) {
      byte[] s = Base64Decoder.decodeToBytes(ciphertext_base64);
      String decrypted = new String(decrypt(CIPHERMODEPADDING, skforAES, IV,
            s));
      return decrypted;
   }
 
   // Use this method if you want to add the padding manually
   // AES deals with messages in blocks of 16 bytes.
   // This method looks at the length of the message, and adds bytes at the end
   // so that the entire message is a multiple of 16 bytes.
   // the padding is a series of bytes, each set to the total bytes added (a
   // number in range 1..16).
   private byte[] addPadding(byte[] plain) {
      byte plainpad[] = null;
      int shortage = 16 - (plain.length % 16);
      // if already an exact multiple of 16, need to add another block of 16
      // bytes
      if (shortage == 0)
         shortage = 16;
 
      // reallocate array bigger to be exact multiple, adding shortage bits.
      plainpad = new byte[plain.length + shortage];
      for (int i = 0; i < plain.length; i++) {
         plainpad[i] = plain[i];
      }
      for (int i = plain.length; i < plain.length + shortage; i++) {
         plainpad[i] = (byte) shortage;
      }
      return plainpad;
   }
 
   // Use this method if you want to remove the padding manually
   // This method removes the padding bytes
   private byte[] dropPadding(byte[] plainpad) {
      byte plain[] = null;
      int drop = plainpad[plainpad.length - 1]; // last byte gives number of
                                       // bytes to drop
 
      // reallocate array smaller, dropping the pad bytes.
      plain = new byte[plainpad.length - drop];
      for (int i = 0; i < plain.length; i++) {
         plain[i] = plainpad[i];
         plainpad[i] = 0; // don't keep a copy of the decrypt
      }
      return plain;
   }
 
   private byte[] encrypt(String cmp, SecretKey sk, IvParameterSpec IV,
         byte[] msg) {
      try {
         Cipher c = Cipher.getInstance(cmp);
         c.init(Cipher.ENCRYPT_MODE, sk, IV);
         return c.doFinal(msg);
      } catch (Exception ex) {
            System.err.println("Encryption Exception: " + ex.toString());
        }
      return null;
   }
 
   private byte[] decrypt(String cmp, SecretKey sk, IvParameterSpec IV,
         byte[] ciphertext) {
      try {
         Cipher c = Cipher.getInstance(cmp);
         c.init(Cipher.DECRYPT_MODE, sk, IV);
         return c.doFinal(ciphertext);
      }catch (Exception ex) {
            System.err.println("Decryption Exception: " + ex.toString());
        }
 
      return null;
   }
 
}

说明:PHP官方文档中使用对称加密的方式是生成随机的向量机并把它合并到加密数据之前一起传输(优点是对于相同的数据每次加密后的数据都不同),但是android和iOS的做法则是使用salt将对称密钥转换AES密钥后作为实际加解密的密钥。