IOS應用安全-加解密算法簡述

IOS應用安全-加解密算法簡述

導讀
客戶端經常遇到需要對數據進行加密的情況,那應該如何加密,選用什么樣的加密算法,是本文想要討論的問題。

如果把我們的數據比作筆記,那數據加密相當于給筆記本上了鎖,解密相當于打開鎖看到筆記。而打開鎖的鑰匙一定是在私人手里的,外人是打不開的。所以數據加密一定有三個關鍵字:

1.加密
2.解密
3.秘鑰

所以有些常見的算法不是數據加密的范圍,這個開發需要注意。比如Base64編碼,MD5算法。

Base64只是把數據編碼,通俗講只是把原來用漢語寫的筆記內容,改成用英語寫的內容,只要懂轉換規則的任何人都能得到數據。所以老板說把數據加下密,一定不是讓你Base64一下或者用其他編碼重新編碼下,編碼算法不涉及到數據安全。

MD5算法也是數據處理的一種方式,更多的被用在數據驗證身上。用上面的例子來講,MD5算法把整本書的內容變成了一句標題,通過標題是沒辦法推算出整個書講什么的。因為根本沒有解密的步驟,所以也不屬于加密算法。

字符編碼

計算機的所有數據,最終都是由多個二進制bit(0/1)來存儲和傳輸的,但是怎么從0/1轉化成我們可讀的文字,就涉及到編碼的知識了。下面是基礎的編碼概念。

ASCII (NSASCIIStringEncoding)

使用一個字節大小表示的128個字符。其中這些字符主要是英文字符,現在很少使用這個編碼,因為不夠用。ASCII字符占用一個字節ASCII碼表

主要使用到的是英文字母的大小寫轉換。大寫的A~Z編碼+32等于小寫的a~z

UNICODE (NSUnicodeStringEncoding)

ASCII只能表示128個字符,對于英文國家來說足夠了,對于我們中國來說,我們有幾萬個漢字不夠啊。于是我們創造出了GB2312等等我們自己的字符集。日本也覺得我也不夠啊,我也搞個字符集。這些字符集彼此是不兼容的,沒辦法轉換,同樣的字符ABCD,我們可能表示,日本就可能就表示。于是程序猿們覺得我要搞個標準,大家都按照標準來。

于是就有了UNICODE編碼。它是所有字符的國際標準編碼字符集。這個是為了解決ASCII字符不夠的問題。同時讓所有組織使用同一套編碼規則,解決編碼不兼容的問題。所以現在通用的編碼規則都是UNICODE編碼。UNICODE向下兼容ASCII編碼。UNICODE最大長度可以到4個字節。不過通常只使用兩個字節表示。所以通常認為UNICODE占用2字節數據

UTF-8 (NSUTF8StringEncoding)

其實UNICODE已經足夠使用了,不過因為如果是ASCII表示的字符(比如英文)只需要1字節就可以了,UNICODE表示的話其中一個字節全是0,這個字節浪費了,英語國家的程序猿覺得:我靠,我又不需要那么多復雜的字符,浪費我流量和空間啊,不行!!,于是出現了對UNICODE的轉換,也就是UTF-8格式,可以保證原ASCII字符依然用一個字節表示,非ASCII字符使用多個字符表示。

UNICODE到UTF-8的規則如下:

  1. 按照UNICODE編碼的范圍,算出需要幾個字節,比如1個字節數,2個字數節,3個字節數,4個字節數。具體范圍參考下面的圖。
  2. 單字節和ASCII碼完全相同,
  3. 對于其他字節數,字節1的前面用1填充,幾個字節數就添加幾個1,后面補一個0。其他字節都用10開頭。
  4. 剩余的位置,按照順序把原始數據補齊。
utf_8.png

例子:

“漢”字的Unicode編碼是0x6C49。0x6C49在0x0800-0xFFFF之間,使用用3字節模板了:1110xxxx 10xxxxxx 10xxxxxx。將0x6C49寫成二進制是:0110 1100 0100 1001, 用這個比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。

對于UTF-8編碼的文件,會在文件頭寫入EF BB BF,表明是UTF-8編碼。

UTF-16 (NSUTF16StringEncoding)

UTF-16的編碼方法是:

  • 如果二進制(b字符編碼小于0x10000,也就是十進制的0到65535之內,則直接使用兩字節表示。
  • 如果二進制b字符編碼大于等于0x10000,將b-0x10000的結果中的前 10 位作為高位和0xD800進行邏輯或操作,將后10 bit作為低位和0xDC00做邏輯或操作,這樣組成的4個字節就構成了對應的編碼。

舉個例子。假設要算(U+2A6A5,對應繁體字龍)在UTF-16下的值,因為它超過 U+FFFF,所以 2A6A5-10000=0x1A6A5。

前10位0001 1010 01 | 0xD800 = 0xD896。

后10位10 1010 0101 | 0xDC00 = 0xDEA5。

所以U+ 2A6A5 在UTF-16中的編碼是D8 96 DE A5。

注:上文參考:精確解釋Unicode

在IOS程序里面NSUTF16StringEncoding和NSUnicodeStringEncoding是等價的。

UTF-16大端/小端(NSUTF16BigEndianStringEncoding/NSUTF16LittleEndianStringEncoding)

大小端主要表明了,系統存儲數據的順序。因為UTF-16至少兩個字節,這兩個字節傳輸過來后,接收的人需要知道哪個字節是在前,哪個字節在后。然后系統才知道改如何存取。

Unicode規范中用字節序標記字符(BOM)來標識字節序,它的編碼是FEFF。這樣如果接收者收到FEFF,就表明這個字節流是高位在前的;如果收到FFFE,就表明這個字節流是低位在前的。

比如“漢”字的Unicode編碼是0x6C49。

對于大端的文件數據為:FE FF 6c 49
對于小端的文件數據為:FF FE 49 6c

對于大小端的概念,本人經常搞混,什么高地址存低字節的,繞一繞就暈了。下面是我的理解:

  1. 對于一個16進制數0x1234,我們知道這個數對應的是兩個字節,占用16個比特。
  2. 系統中是按照字節為單位去保存數據的。一個地址空間對應1個字節。比如0x1234如果要存儲在計算機里,需要占用兩個地址空間。我們假設這個地址空間起始是0x00,因為需要兩個字節,所以還需要一個地址空間來保存,即0x01。其中明顯0x01是高地址空間。
  3. 所以問題就在于,對于0x1234這個數據保存,是0x01地址保存0x12還是保存0x34。
  4. 如果把0x1234看成字符串形式,按照正常順序存儲,先存0x12,后存0x34,對應的就是大端模式。
  5. 如果按照字節順序,0x12是高位,0x34是低位,應該0x12存儲在高位地址0x02,低位字節0x34存儲在低位地址0x01。這種方式就是小端模式。
  6. 為了怕記混,可以這么記:我最大,按字符串順序存儲,我看的最舒服所以是大端。反面的就是小端的。
地址偏移 大端模式 小端模式
0x00 12 34
0x01 34 12

附:代碼判斷大小端的代碼。

原理是生成一個兩字節的數據,然后轉為1字節的char數據。大端取到的是第一個高字節,小端取到的是第二個低字節。

#include<stdio.h>

int main()
{
    short x = 1; //0x0001
    char *p = (char *)&x;

    if(*p)
    {
        printf("little\n");
    }
    else
    {
        printf("large\n");
    }
    return 0;
}

UTF-32

詳細的本人沒看懂,實際中沒有用到這個編碼,這個編碼使用4字節存儲。也有大小端之分

總結

  1. 字符編碼就是把可讀的字符轉化為二進制數據方法,字符解碼就是把二進制數據轉化為可讀的方法。
  2. ASCII占用1個字節,只有128個字符,主要是英文字符。
  3. UNICODE是國際標準編碼字符集,包含了所有已知符號。
  4. UTF-8是UNICODE編碼的一種實現方式,兼容ASCII碼,也就是英文字符占1個字節,漢字可能占兩個字節或三個字節。
  5. UTF-16也是UNICODE編碼的一種實現方式,通常和UNICODE編碼一致,占用兩個字節,分大小端。

Base64編碼

Base64編碼的作用是把非ASCII的字符轉換為ASCII的字符。很多加密算法,很喜歡做一次Base64轉換。原因是使用Base64編碼后,所有的數據都是ASCII字符,方便在網絡上傳輸。

設計思路是:Base64把每三個8Bit的字節轉換為四個6Bit的字節(3*8 = 4*6 = 24),然后把6Bit再添兩位高位0,組成四個8Bit的字節。所以Base64算法生成的數據會比原數據大1/3左右。

比如:

  1. 圖片這種二進制數據就可以轉換為Base64作為文本傳輸。
  2. 比如有中文的數據,可以通過Base64轉為可以顯示的ASCII數據

簡單說明:

  1. 將字符按照文字編碼轉化為二進制字節。
  2. 每3字節化為一組(24bit),如果字節不夠,最后輸出結果補=。然后再把每一組拆分成4個組,每個組6bit,如果不足6bit后面補0。
  3. 將每個6bit前面補足兩個0,湊夠8位。
  4. 然后按照新分出來的每8位轉成10進制數,按照表里面的查找,轉為對應的ASCII字符。
base_64.png

舉例:

字符bl如何轉化為Base64編碼:

  1. bl對應的ASCII碼為: 0110001001101100,因為只有兩個,所以有一個輸出結果是=
  2. 按照每三個字節分組:0110001001101100
  3. 按照每個組6bit分4個組,不足6位的補0:011000,100110,110000
  4. 在前面補0,湊夠8位:00011000,00100110,00110000
  5. 轉為10進制:24,38,48
  6. 查表得到:Y,m,w
  7. 最后補=,所以結果為Ymw=

標準的程序實現可以參考:GTMBase64.m

說明:

Base64是一種編碼算法,不是加密算法,他的作用不是加密,而是用最簡的ASCII碼來傳輸文本數據,屏蔽掉設備網絡差異,是為了方便傳輸的一種算法。很多加密算法,最后生成的是二進制數據,不是可見字符,而傳輸的一般是通過字符傳輸,所以常見的二進制轉化方式就是Base64算法。

關于Base64編碼后拼接到url上要注意的地方

Base64編碼后會有+\,這些如果拼接在url作為參數傳輸的時候會被瀏覽器解析為空格和路徑分隔符,導致參數解析異常,所以有些庫會把+轉義為.或下劃線_
同時建議做完Base64,同時自動做一次URLEncode 。 這樣+會被轉義為%2B。服務端收到后先做URLDecode然后做Base64Decode。

哈希散列算法

一個蘿卜一個坑這個俗語形容這個算法很貼切。官方的定義為:

散列(Hash)函數提供了這一服務,它對不同長度的輸入消息,產生固定長度的輸出。

安全的哈希算法要滿足下面條件:

  1. 固定長度。不同長度的數據,生成的固定長度的數據
  2. 唯一性。不同的數據,生成的結果一定不同。相同的數據,每次輸出的結果一定一樣。
  3. 不可逆。對于生成后的數據,反推回原數據,通過算法是不可能的。
  4. 防篡改。兩個輸出的散列值相同,則原數據一定相同。如果兩個輸出的散列值不同,則原數據一定不同。

從上面的特點可以知道散列值主要使用的場景:

  1. 生成唯一的值做索引,比如哈希表
  2. 用作數據簽名,校驗數據完整性和有效性。
  3. 密碼脫敏處理。

MD5算法

MD5算法是最常用的散列算法。

對MD5算法簡要的敘述可以為:MD5以512位分組來處理輸入的信息,且每一分組又被劃分為十六個32位子分組,經過了一系列的處理后,算法的輸出由4個32位分組組成,將這4個32位分組級聯后將生成1個128位散列值。

算法有點復雜,沒有看懂,放下不表。

下面是本人的簡單理解:

  1. MD5算法效率是比較快的。
  2. MD5防碰撞能力比較強,只有少數的幾個例子有出現碰撞的情況。但也不影響安全性。
  3. MD5生成的是固定128位,16個字節。

MD5算法安全性

目前主流看法是MD5逐漸有被攻克的風險。但是目前還沒有有效算法破解。

主要的破解方法是使用數據庫保存常見的字符串的MD5值,然后通過反查得到原始數據。也就是如果用戶的密碼很常見就很容易破解。如果用戶密碼是隨機的,那就沒什么平臺可以破解了。

下面對于是用MD5的觀點:

  1. MD5不是加密算法,重要的用戶密碼應該加密存儲。做MD5只是為了脫敏,也就是不讓相關人員知道原文是什么(包括內鬼)。
  2. 極重要數據是用更安全的算法:比如用戶密碼數據使用更安全的算法,比如SHA1算法。傳輸過程中也進一步加密。
  3. 如果使用MD5算法,在原始值里面加入鹽值。鹽值要盡量隨機。因為如果加入隨機值后原始值也變得隨機,使用暴力破解就基本不可能了。即result = MD5(password + salt)

關于加鹽

這里有個破解的網站,大家可以看下常用的策略其實都可以破解。安全性主要是鹽如何選擇。

  1. 鹽值要是隨機字符,數據盡量長一些,只有這樣才能保證最后數據的隨機。
  2. 鹽值盡量保證每個用戶不一樣,增加破解的難度。
  3. 鹽值的保存可以是前后端約定,固化在APP里,但是也應該和用戶相關,比如salt=(固化的值+用戶信息)。可以是通過一些隨機值變化得來:比如用戶注冊時間等信息做鹽值。可以是每次隨機生成,當做參數帶給后端,后端保存密碼+鹽值。安全性從低到高。還有做多次MD5的,個人覺得意義不大。
  4. 個人推薦的一個方案。result = MD5 (password + salt)。salt的計算方法是:MD5(Random(128)+ uid)。其中Random(128)表示一個隨機128位字符串,兩端可以一致,固化在代碼里。uid是用戶唯一標示,比如登陸用的用戶名。這樣對于破解者來說就需要先拿到這個salt值,然后對每個用戶都要生成一個唯一的128位的鹽值,去生成對應的庫,破解成本就非常高了。

其實目前暴漏出來的是攻擊者把整個數據庫的內容拿到后,暴力解密出原文。但是MD5加鹽也好變換也好都是可以通過前端代碼查到算法的,通過算法就可以生成常用數據對應的MD5庫。所以密碼做MD5更重要的是脫敏處理,不能做為安全的加密使用,重要的用戶密碼持久化或傳輸過程中一定是要通過加密算法處理的。這樣只要安全保存私鑰就可以了。在很多金融公司,大量使用硬件加密機做加密處理,然后保存,更加大了破解難度。所以如果你的密碼是使用加密再保存的,使用固定鹽值的已經可以滿足要求了。如果擔心可以加上用戶的注冊時間或服務器時間戳做鹽值。

SHA1

SHA1也是一種HASH算法。是MD5的替代方案。生成的數據是160位,20個字節。

目前SHA1也被認為不安全,google找到了算法進行了碰撞,所以普遍推薦使用新的SHA2代替。Google已經開始廢棄這個算法了。

SHA2

  • SHA-224、SHA-256、SHA-384,和SHA-512并稱為SHA-2。
  • 新的散列函數并沒有接受像SHA-1一樣的公眾密碼社區做詳細的檢驗,所以它們的密碼安全性還不被大家廣泛的信任。
  • 雖然至今尚未出現對SHA-2有效的攻擊,它的算法跟SHA-1基本上仍然相似;因此有些人開始發展其他替代的散列算法。

所以目前推薦使用SHA2相關的算法做散列算法。

其中SHA-256輸出為256位,32字節。
SHA-512輸出為512位,64字節。

HMac

HMac是秘鑰相關的哈希算法。和之前的算法不同的在于需要一個秘鑰,才能生成輸出。主要是基于簽名散列算法。可以認為是散列算法加入了加密邏輯,所以相比SHA算法更難破解,包含下面的算法。

/*!
    @enum       CCHmacAlgorithm
    @abstract   Algorithms implemented in this module.

    @constant   kCCHmacAlgSHA1      HMAC with SHA1 digest
    @constant   kCCHmacAlgMD5       HMAC with MD5 digest
    @constant   kCCHmacAlgSHA256    HMAC with SHA256 digest
    @constant   kCCHmacAlgSHA384    HMAC with SHA384 digest
    @constant   kCCHmacAlgSHA512    HMAC with SHA512 digest
    @constant   kCCHmacAlgSHA224    HMAC with SHA224 digest
 */
enum {
    kCCHmacAlgSHA1,
    kCCHmacAlgMD5,
    kCCHmacAlgSHA256,
    kCCHmacAlgSHA384,
    kCCHmacAlgSHA512,
    kCCHmacAlgSHA224
};
typedef uint32_t CCHmacAlgorithm;

HMAC主要應用場景:

  1. 密碼的散列存儲,因為需要散列的時候需要密碼,實際上相當于算法里加了鹽值。使用的密碼要隨機和用戶相關,請參考鹽值的生產規則。
  2. 用于數據簽名。雙方使用共同的秘鑰,然后做簽名驗證。秘鑰可以固化,也可以會話開始前協商,增加簽名篡改和被破解的難度。

PS:目前項目中的密碼散列算法,采用的就是HMac算法。

總結

  1. 密碼保存和傳輸需要做散列處理。但是散列算法主要是脫敏,不能替代加密算法。
  2. 如今常用的Md5算法和SHA1算法都不再安全。所以推薦使用SHA-2相關算法。
  3. 散列算法應該加入鹽值即:result=HASH(password+salt)。其中鹽值應該是隨機字符串且每個用戶不一樣。
  4. HMac引入了秘鑰的概念,如果不知道秘鑰,秘鑰不同,散列值也不同,相當于散列算法加入了鹽值。可以把它當做更安全的散列算法使用。

算法實現

算法都是使用蘋果自己的Security.framework框架實現的,只需要調用相關算法就可以了。推薦一個github

//
//  NSData+KKHASH.m
//  SecurityiOS
//
//  Created by cocoa on 16/12/15.
//  Copyright ? 2016年 dev.keke@gmail.com. All rights reserved.
//

#import "NSData+KKHASH.h"
#include <CommonCrypto/CommonDigest.h>
#import <CommonCrypto/CommonHMAC.h>

@implementation NSData (KKHASH)
- (NSData *)hashDataWith:(CCDIGESTAlgorithm )ccAlgorithm
{
    NSData *retData = nil;
    if (self.length <1) {
        return nil;
    }
    
    unsigned char *md;
    
    switch (ccAlgorithm) {
        case CCDIGEST_MD2:
        {
            md = malloc(CC_MD2_DIGEST_LENGTH);
            bzero(md, CC_MD2_DIGEST_LENGTH);
            CC_MD2(self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_MD2_DIGEST_LENGTH];
        }
            break;
        case CCDIGEST_MD4:
        {
            md = malloc(CC_MD4_DIGEST_LENGTH);
            bzero(md, CC_MD4_DIGEST_LENGTH);
            CC_MD4(self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_MD4_DIGEST_LENGTH];

        }
            break;
        case CCDIGEST_MD5:
        {
            md = malloc(CC_MD5_DIGEST_LENGTH);
            bzero(md, CC_MD5_DIGEST_LENGTH);
            CC_MD5(self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_MD5_DIGEST_LENGTH];

        }
            break;
        case CCDIGEST_SHA1:
        {
            md = malloc(CC_SHA1_DIGEST_LENGTH);
            bzero(md, CC_SHA1_DIGEST_LENGTH);
            CC_SHA1(self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH];
            
        }
            break;
        case CCDIGEST_SHA224:
        {
            md = malloc(CC_SHA224_DIGEST_LENGTH);
            bzero(md, CC_SHA224_DIGEST_LENGTH);
            CC_SHA224(self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_SHA224_DIGEST_LENGTH];
            
        }
            break;
        case CCDIGEST_SHA256:
        {
            md = malloc(CC_SHA256_DIGEST_LENGTH);
            bzero(md, CC_SHA256_DIGEST_LENGTH);
            CC_SHA256(self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_SHA256_DIGEST_LENGTH];
            
        }
            break;
        case CCDIGEST_SHA384:
        {
            md = malloc(CC_SHA384_DIGEST_LENGTH);
            bzero(md, CC_SHA384_DIGEST_LENGTH);
            CC_SHA384(self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_SHA384_DIGEST_LENGTH];
            
        }
            break;
        case CCDIGEST_SHA512:
        {
            md = malloc(CC_SHA512_DIGEST_LENGTH);
            bzero(md, CC_SHA512_DIGEST_LENGTH);
            CC_SHA512(self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_SHA512_DIGEST_LENGTH];
            
        }
            break;
            
        default:
            md = malloc(1);
            break;
    }
    
    free(md);
    md = NULL;
    
    return retData;
    
}

- (NSData *)hmacHashDataWith:(CCHmacAlgorithm )ccAlgorithm key:(NSString *)key {
    NSData *retData = nil;
    if (self.length <1) {
        return nil;
    }
    
    unsigned char *md;
    const char *cKey    = [key cStringUsingEncoding:NSUTF8StringEncoding];
    
    switch (ccAlgorithm) {
        case kCCHmacAlgSHA1:
        {
            md = malloc(CC_SHA1_DIGEST_LENGTH);
            bzero(md, CC_SHA1_DIGEST_LENGTH);
            CC_SHA1(self.bytes, (CC_LONG)self.length, md);
            CCHmac(kCCHmacAlgSHA1, cKey, strlen(cKey), self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH];
        }
            break;
        case kCCHmacAlgSHA224:
        {
            md = malloc(CC_SHA224_DIGEST_LENGTH);
            bzero(md, CC_SHA224_DIGEST_LENGTH);
            CCHmac(kCCHmacAlgSHA224, cKey, strlen(cKey), self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_SHA224_DIGEST_LENGTH];
            
        }
            break;
        case kCCHmacAlgSHA256:
        {
            md = malloc(CC_SHA256_DIGEST_LENGTH);
            bzero(md, CC_SHA256_DIGEST_LENGTH);
            CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_SHA256_DIGEST_LENGTH];
            
        }
            break;
        case kCCHmacAlgSHA384:
        {
            md = malloc(CC_SHA384_DIGEST_LENGTH);
            bzero(md, CC_SHA384_DIGEST_LENGTH);
            CCHmac(kCCHmacAlgSHA384, cKey, strlen(cKey), self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_SHA384_DIGEST_LENGTH];
            
        }
            break;
        case kCCHmacAlgSHA512:
        {
            md = malloc(CC_SHA512_DIGEST_LENGTH);
            bzero(md, CC_SHA512_DIGEST_LENGTH);
            CCHmac(kCCHmacAlgSHA512, cKey, strlen(cKey), self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_SHA512_DIGEST_LENGTH];
            
        }
            break;
            
        case CCDIGEST_MD5:
        {
            md = malloc(CC_MD5_DIGEST_LENGTH);
            bzero(md, CC_MD5_DIGEST_LENGTH);
            CCHmac(kCCHmacAlgMD5, cKey, strlen(cKey), self.bytes, (CC_LONG)self.length, md);
            retData = [NSData dataWithBytes:md length:CC_MD5_DIGEST_LENGTH];
            
        }
            break;
        default:
            md = malloc(1);
            break;
    }
    
    free(md);
    md = NULL;
    
    return retData;
}


- (NSString *)hexString
{
    NSMutableString *result = nil;
    if (self.length <1) {
        return nil;
    }
    result = [[NSMutableString alloc] initWithCapacity:self.length * 2];
    for (size_t i = 0; i < self.length; i++) {
        [result appendFormat:@"%02x", ((const uint8_t *) self.bytes)[i]];
    }
    return result;
}


+ (NSData *)dataWithHexString:(NSString *)hexString {
    NSMutableData *     result;
    NSUInteger          cursor;
    NSUInteger          limit;
    
    NSParameterAssert(hexString != nil);
    
    result = nil;
    cursor = 0;
    limit = hexString.length;
    if ((limit % 2) == 0) {
        result = [[NSMutableData alloc] init];
        
        while (cursor != limit) {
            unsigned int    thisUInt;
            uint8_t         thisByte;
            
            if ( sscanf([hexString substringWithRange:NSMakeRange(cursor, 2)].UTF8String, "%x", &thisUInt) != 1 ) {
                result = nil;
                break;
            }
            thisByte = (uint8_t) thisUInt;
            [result appendBytes:&thisByte length:sizeof(thisByte)];
            cursor += 2;
        }
    }
    
    return result;
}
@end

對稱加密算法

對稱加密,指雙方使用的秘鑰是相同的。加密和解密都使用這個秘鑰。

對稱加密的優點為:

  1. 加密效率高
  2. 加密速度快
  3. 可以對大數據進行加密

缺點為:

  1. 秘鑰安全性無法保證,以現在的技術手段來說,默認對稱秘鑰的秘鑰是非安全的,可以被拿到的。

加密方法

  • DES :數據加密標準。
    是一種分組數據加密技術,先將數據分成固定長度64位的小數據塊,之后進行加密。
    速度較快,適用于大量數據加密。DES密鑰為64位,實際使用56位。將64位數據加密成64位數據。
  • 3DES:使用三組密鑰做三次加密。
    是一種基于 DES 的加密算法,使用3個不同密鑰對同一個分組數據塊進行3次加密,如此以使得密文強度更高。3DES秘鑰為DES兩倍或三倍,即112位或168位。其實就是DES的秘鑰加強版。
  • AES :高級加密標準。
    是美國聯邦政府采用的一種區塊加密標準。
    相較于 DES 和 3DES 算法而言,AES 算法有著更高的速度和資源使用效率,安全級別也較之更高了,被稱為下一代加密標準。AES秘鑰長度為128、192、256位

使用到的基礎數學方法:

  • 移位和循環移位
      移位就是將一段數碼按照規定的位數整體性地左移或右移。循環右移就是當右移時,把數碼的最后的位移到數碼的最前頭,循環左移正相反。例如,對十進制數碼12345678循環右移1位(十進制位)的結果為81234567,而循環左移1位的結果則為23456781。
  • 置換
      就是將數碼中的某一位的值根據置換表的規定,用另一位代替。它不像移位操作那樣整齊有序,看上去雜亂無章。這正是加密所需,被經常應用。
  • 擴展
      就是將一段數碼擴展成比原來位數更長的數碼。擴展方法有多種,例如,可以用置換的方法,以擴展置換表來規定擴展后的數碼每一位的替代值。
  • 壓縮
      就是將一段數碼壓縮成比原來位數更短的數碼。壓縮方法有多種,例如,也可以用置換的方法,以表來規定壓縮后的數碼每一位的替代值。
  • 異或
      這是一種二進制布爾代數運算。異或的數學符號為⊕ ,它的運算法則如下:
    1⊕1 = 0
    0⊕0 = 0
    1⊕0 = 1
    0⊕1 = 1
      也可以簡單地理解為,參與異或運算的兩數位如相等,則結果為0,不等則為1。
  • 迭代
      迭代就是多次重復相同的運算,這在密碼算法中經常使用,以使得形成的密文更加難以破解。

對于對稱加密來說,有幾個共同要點:

  1. 密鑰長度;(關系到密鑰的強度)
  2. 加密模式;(ecb、cbc等等)
  3. 塊加密算法里的塊大小和填充方式區分;

加密模式

ECB 模式

ECB :電子密本方式,最古老,最簡單的模式,將加密的數據分成若干組,每組的大小跟加密密鑰長度相同;
然后每組都用相同的密鑰加密。OC對應的為kCCOptionECBMode

ECB的特點為:

  • 每次Key、明文、密文的長度都必須是64位;
  • 數據塊重復排序不需要檢測;
  • 相同的明文塊(使用相同的密鑰)產生相同的密文塊,容易遭受字典攻擊;
  • 一個錯誤僅僅會對一個密文塊產生影響,所以支持并行計算;

CBC模式

  • CBC :密文分組鏈接方式。與ECB相比,加入了初始向量IV。將加密的數據分成若干組,加密時第一個數據需要先和向量異或之后才加密。后面的數據需要先和前面的數據異或,然后再加密。是OC默認的加密模式。

CBC的特點為:

  • 每次加密的密文長度為64位(8個字節);
  • 當相同的明文使用相同的密鑰和初始向量的時候CBC模式總是產生相同的密文;
  • 密文塊要依賴以前的操作結果,所以,密文塊不能進行重新排列;
  • 可以使用不同的初始化向量來避免相同的明文產生相同的密文,一定程度上抵抗字典攻擊;
  • 一個錯誤發生以后,當前和以后的密文都會被影響;

塊大小和填充方式

對稱算法的第一步就是對數據進行分組,每一個組的大小稱為快大小,比如DES需要將數據分組為64位(8個字節),如果數據不夠64位就需要進行補位。

PKCS7Padding填充

OC中指定的填充方法只有kCCOptionPKCS7Padding,對應JAVA的PKCS5Padding填充方式。算法為計算缺幾位數,然后就補幾位數,數值為下面的公式:

value=k - (l mod k) ,K=塊大小,l=數據長度,如果l=8, 則需要填充額外的8個byte的8

比如塊大小為8字節,數據為DD DD DD DD4個字節,帶入公式,l=4,k=8,計算 8 - (4 mod 8)= 4 ,所以補充4個4,補位后得到DD DD DD DD 04 04 04 04

唯一特別的是如果最后位數是夠的,也需要額外補充,比如數據是DD DD DD DD DD DD DD DD8個字節,帶入公式,l=8,k=8,計算 8 - (8 mod 8)= 8,所以補位后得到DD DD DD DD DD DD DD DD 08 08 08 08 08 08 08 08。 所以如果考慮補位,實際輸出buffer大小要加上快大小,防止buffer不夠。

Zero Padding(No Padding)

補位的算法和PKCS7Padding一致,只不過補的位為0x00,比如數據為DD DD DD DD4個字節,帶入公式,l=4,k=8,計算 8 - (4 mod 8)= 4 ,所以補充4個00,補位后得到DD DD DD DD 00 00 00 00

非常不建議用這種模式,因為解密后的數據會多出補的00。如果原始數據以00結尾(ASCII碼代表空字符),就沒辦法區分出來了。

幾種算法比較

算法 秘鑰長度(字節) 分組長度(字節) 加密效率 破解難度
DES 8 8 較快(22.5MB/S) 簡單
3DES 24 8 慢(12MB/S)
AES 16/24/32 16 快(51.2MB/s)

IOS 代碼實現解析

下面以AES代碼實現為例,說明下IOS加解密算法的實現。

+ (NSString *)AES128Encrypt:(NSString *)plainText key:(NSString *)gkey iv:(NSString *)gIv padding:(BOOL)padding
{
    //先處理秘鑰,如果秘鑰不夠算法長度,就用0填充,如果長于算法長度就截斷。
    char keyPtr[kCCKeySizeAES128+1]; //申請秘鑰buffer,這里根據不同算法導入需要的key長度。AES128是16個字節,對應的值kCCKeySizeAES128。
    memset(keyPtr, 0, sizeof(keyPtr)); //使用0填充,保證秘鑰長度達到要求。
    [gkey getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding]; //將傳入的秘鑰copy進秘鑰buffer里
    
    //注意這個只在模式為CBC下有效,
    //處理向量值,默認模式為CBC。如果指定了kCCOptionECBMode模式,就不需要這個向量。
    char ivPtr[kCCBlockSizeAES128+1]; //申請向量的buffer,長度為塊長度。AES128塊長度為kCCBlockSizeAES128。
    memset(ivPtr, 0, sizeof(ivPtr));
    [gIv getCString:ivPtr maxLength:sizeof(ivPtr) encoding:NSUTF8StringEncoding]; //將傳入的值copy進向量buffer
    
    
    NSData* data = [plainText dataUsingEncoding:NSUTF8StringEncoding];
    NSUInteger dataLength = [data length];
    
    //注意這個只在不指定padding的情況下有效,需要填充0,算法為num_to_fill= k - (length mod k),如果指定了kCCOptionPKCS7Padding,就不需要人為填充。
    
    long long newSize = dataLength;
    int diff = padding ? 0 : kCCKeySizeAES128 - (dataLength % kCCKeySizeAES128);
    if(diff > 0) {
        newSize = dataLength + diff;
    }
    char dataPtr[newSize];
    memcpy(dataPtr, [data bytes], [data length]);
    for(int i = 0; i < diff; i++) {
        dataPtr[i + dataLength] = 0x00;
    }
    
    
    //輸出的buffer
    size_t bufferSize = newSize + kCCBlockSizeAES128;
    void *buffer = malloc(bufferSize);
    memset(buffer, 0, bufferSize);
    
    size_t numBytesCrypted = 0;
    
    CCOptions option = padding ? kCCOptionPKCS7Padding : 0x0000;
    option = gIv.length > 0 ? option : option | kCCOptionECBMode;
    
    
    CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt,
                                          kCCAlgorithmAES128,
                                          option,
//                                          0x0000,               //No padding | CBC模式  需要補零且需要iv向量
//                                          kCCOptionPKCS7Padding,  //  kCCOptionPKCS7Padding | CBC模式   需要iv向量
                                          //kCCOptionPKCS7Padding | kCCOptionECBMode, // kCCOptionPKCS7Padding | kCCOptionECBMode 不需要iv向量,也不需要補零
//                                          kCCOptionECBMode, // No padding | kCCOptionECBMode 不需要補零,不需要iv向量
                                          keyPtr,
                                          kCCKeySizeAES128,
                                          ivPtr,
                                          dataPtr,
                                          sizeof(dataPtr),
                                          buffer,
                                          bufferSize,
                                          &numBytesCrypted);
    
    if (cryptStatus == kCCSuccess) {
        NSData *resultData = [NSData dataWithBytesNoCopy:buffer length:numBytesCrypted];
        resultData = [resultData base64EncodedDataWithOptions:(NSDataBase64EncodingOptions)0];
        NSString *encryptedString = [[NSString alloc] initWithData:resultData encoding:NSUTF8StringEncoding];
        return encryptedString;
    }
    free(buffer);
    return nil;
}

+ (NSString *)AES128Decrypt:(NSString *)encryptText key:(NSString *)gkey iv:(NSString *)gIv padding:(BOOL)padding
{
    //復制秘鑰buffer
    char keyPtr[kCCKeySizeAES128 + 1];
    memset(keyPtr, 0, sizeof(keyPtr));
    [gkey getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];
    
    //復制向量buffer
    char ivPtr[kCCBlockSizeAES128 + 1];
    memset(ivPtr, 0, sizeof(ivPtr));
    [gIv getCString:ivPtr maxLength:sizeof(ivPtr) encoding:NSUTF8StringEncoding];
    NSData *data = [[NSData alloc] initWithBase64EncodedString:encryptText options:0];
    NSUInteger dataLength = [data length];
    size_t bufferSize = dataLength + kCCBlockSizeAES128;
    void *buffer = malloc(bufferSize);
    
    //計算采用哪種模式和填充方式
    CCOptions option = padding ? kCCOptionPKCS7Padding : 0x0000;
    option = gIv.length > 0 ? option : option | kCCOptionECBMode;
    
    size_t numBytesCrypted = 0;
    //解密
    CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
                                          kCCAlgorithmAES128,
                                          option,
//                                          0x0000,               //No padding | CBC模式  需要補零且需要iv向量
//                                          kCCOptionPKCS7Padding,  //  kCCOptionPKCS7Padding | CBC模式   需要iv向量
                                          //kCCOptionPKCS7Padding | kCCOptionECBMode, // kCCOptionPKCS7Padding | kCCOptionECBMode 不需要iv向量,也不需要補零
//                                          kCCOptionECBMode, // No padding | kCCOptionECBMode 不需要補零,不需要iv向量
                                          keyPtr,
                                          kCCBlockSizeAES128,
                                          ivPtr,
                                          [data bytes],
                                          dataLength,
                                          buffer,
                                          bufferSize,
                                          &numBytesCrypted);
    if (cryptStatus == kCCSuccess) {
        NSData *resultData = [NSData dataWithBytesNoCopy:buffer length:numBytesCrypted];
        NSString *result = [[NSString alloc] initWithData:resultData encoding:NSUTF8StringEncoding];
        if ([result length] > 0 && !padding) {
            //如果是非填充模式,解析后的數據會多出填充的'\0',所以需要去掉。
            long byteWithoutZero = numBytesCrypted;
            const char *utf8Str =  [result UTF8String];
            //從后開始掃描,查到需要截斷的長度
            for (long i = byteWithoutZero - 1; i > 0; i --) {
                if (utf8Str[i] != '\0') {
                    break;
                }
                byteWithoutZero --;
            }

            NSString *finalReslut = [[NSString alloc] initWithBytes:utf8Str length:byteWithoutZero encoding:NSUTF8StringEncoding];
            
            return finalReslut;
        }
        
        return result;
    }
    free(buffer);
    return nil;
}

建議和說明

  1. 建議使用CBC模式(kCCOptionECBMode),填充采用kCCOptionPKCS7Padding。這種使用最廣泛,和PHP、JAVA(AES/CBC/PKCS5Padding)都適配。聯調的時候需要注意兩端是否一致,不一致是調不通的。
  2. 通常數據加密后,會做一次Base64編碼進行傳輸,有些應用也會將數據轉為二進制字符串傳輸。
  3. 如果不指定模式,則默認是CBC模式,需要用到向量IV。
  4. 如果不指定填充格式,則需要自行補0x00處理,在解碼后也需要把補的0x00去除掉,網上很多資料解碼后沒有去除,會多出\0

說明和總結

  1. 建議對稱加密使用AES加密。DES無論安全性和效率都不如AES算法。
  2. 加密建議用kCCOptionPKCS7Padding填充方式,對應的JAVA模式為PKCS5Padding
  3. 如果用CBC模式,需要使用初始向量,初始向量兩端應該一致。如果不使用應該指定kCCOptionECBMode。也建議用這個模式,兼容性最好。
  4. 秘鑰應該用隨機數生成對應的位數。AES128為16個字節,也就是16個字符。不要用短密碼,比如:111111,這樣真的很蠢。
  5. 對稱加密的安全隱患主要在于秘鑰的保存。重要會話的秘鑰應該隨機生成,使用非對稱加密來溝通交換秘鑰,策略可以參考我的另一篇文章IOS應用安全-HTTP/HTTPS網絡安全(一)
  6. 如果秘鑰需要硬編碼到程序里,應該做脫敏運算,比如做位運算進行變形等。后面會專門寫怎么解決秘鑰硬編碼問題。

非對稱加密算法

非對稱秘鑰加密算法的特點是:加密和解密使用不同的秘鑰

非對稱加密需要兩個秘鑰:公開秘鑰和私有秘鑰。兩個秘鑰是不同的,而且通過公鑰是無法推算出私鑰的,使用公鑰加密的數據只有用私鑰解密。

非對稱算法的特點:

  1. 解決了秘鑰保存的問題。公鑰可以發布出去,任何人都可以使用,也不用擔心被人獲取到,只要保證私鑰的安全就可以了。而對稱加密,因為秘鑰相同,客戶端泄露了就不安全了。
  2. 加密和解密的效率不高,只適合加解密少量的數據。而對稱加密效率要高。這里有一篇文章對比AES和RSA算法的性能對比

RSA算法

RSA是目前最常用的非對稱加密算法。

算法原理可以看下這篇文章:RSA算法原理

RSA算法基于一個十分簡單的數論事實:將兩個大質數相乘十分容易,但是想要對其乘積進行因式分解卻極其困難,因此可以將乘積公開作為加密密鑰。RSA的秘鑰長度在2048位,現有的技術手段是無法破解的(實際的可以暴力破解的位數為768位,也就是768位的大數才有可能暴力進行因數分解)。

RSA算法優點:

  1. 算法原理簡單,我都快看懂了。
  2. 安全性也足夠高,目前沒有證據和方案可以破解1048位以上秘鑰的RSA算法。

缺點:

  1. 安全性取決于秘鑰長度,推薦的要至少1048位,但是這么高位數的秘鑰生成速度很慢,所以沒法做一次會話一次秘鑰。
  2. 加解密的效率很低,相對于對稱加密,差好幾個量級,而且也不支持加密長數據。

國密算法SM2

中國特有的算法,國家強制要求金融機構使用國密算法。包括SM1/SM2/SM3/SM4。其中SM4為對稱加密算法。SM3是哈希算法。SM2為非對稱加密算法。但是國家只給算法原理,沒有給出常用的算法實現,所以是件蛋疼的事情。

算法我也沒看懂。因為項目中使用到了,所以做了一些研究。相關代碼可以參考我的github,IOS SM2開源實現非常少,而且都有些問題,要么基于openSSL,代碼特別大。要么基于libtommath庫,但是有一些問題,SM2無法調通。所以兩個結合重新整理的下代碼。這個代碼只保證SM2算法有效性,因為經過實際使用過,其他的項目未用到。

SM2的加密流程

safe_encode_sm2.png

除掉數學方法,下面是本人的一些理解:

  1. SM2需要依賴于一個曲線,一般使用國家推薦曲線。如果曲線不對,肯定是無法加解密的。曲線參數

    #define SM2_P     "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF"
    #define SM2_A     "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC"
    #define SM2_B     "28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93"
    #define SM2_N     "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123"
    #define SM2_G_X   "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7"
    #define SM2_G_Y   "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0"
    
  2. SM2公鑰分為兩部分:Pub_x和Pub_y。每個都是32字節,總共是64字節。私鑰長度現在還不清楚是多少,有資料說是要32位,但是文檔里面未提到。字節數如果不對說明生成秘鑰算法有問題。

  3. 輸出數據分為3段:C1C2C3,其中C1是64個字節,C2和原始數據大小相同,即原文是6個字節,C2就是6個字節,C3是32個字節。所以總長度是64+32+原文長度(字節)。如果長度不對,要看下是否是人為添加了其他字段。

  4. 算法涉及到哈希算法,標準是使用SM3的hash算法,SM3的Hash算法生成的字節為32字節,這個聯調的時候一定要保證一致。

加密步驟說明:

  1. 第一步計算隨機數,如果這個不是隨機的,是固定的,那后面的結果每次輸出就是唯一的。
  2. 通過隨機數rank和曲線的G_x、G_y、P、A五個參數,通過ECC算法C1=[k]G = (x1,y1)生成一個點(x1,y1)。拼接起來就是C1數據。C1數據應該是64個字節。有些算法里面會在前面填充0x04,變成65個字節
  3. 通過公鑰的P_x和P_y,隨機數rank,A,P,通過ECC算法[k]PukeyB = [k](XB,YB) = (x2,y2)計算出(x2,y2),x2和y2的大小為分別為32字節
  4. 將上面的(x2,y2)拼接,然后做KDF(密碼派生算法)計算,輸出原文長度(klen)的t值。t= KDF(x2||y2, klen),KDF一般使用的是SM3的算法。結果t的大小和原文的大小一致。
  5. 然后將t和原文做異或運算,得到C2,C2的大小和原文一致。
  6. 然后將(x2,原文,x3)拼接,計算一次SM3的Hash算法,生成的數據放入C3中,C3的大小為32字節。
  7. 最后把C1C2C3拼接到一起,長度為64+原文長度+32字節。注意,老的標準為C1C3C2,有些實現的是這種模式。

注:這其中ECC算法是標準算法,大部分第三方實現的都沒有問題。主要是KDF算法和Hash算法會有不同。這個聯調的時候需要搞清楚。

SM2解密流程

流程圖如下:

safe_decode_sm2.png

解密步驟說明

  1. 先判斷C1是否在曲線上。C1長度為64字節,取數據的前64字節就可以了。所以兩端一定要用同樣的曲線。
  2. 使用C1的數據,曲線參數(A,P),私鑰dA,使用ECC算法生成(x2,y2),dA*C1 = dA*(x2,y2) = dA*[k]*(Xg,Yg)
  3. 使用(x2,y2)和C2的長度(總長度-64-32),使用KDF計算t。
  4. 使用c2異或t,達到M’
  5. 計算(x2,M’,y2)的hash值U。
  6. 比較U和C3數據是否是一致的,如果一致就輸出M’

KDF算法說明:

文檔里的描述

密鑰派生函數的作用是從一個共享的秘密比特串中派生出密鑰數據。在密鑰協商過程中,密鑰派
生函數作用在密鑰交換所獲共享的秘密比特串上,從中產生所需的會話密鑰或進一步加密所需的密鑰
數據。
密鑰派生函數需要調用密碼雜湊函數。
設密碼雜湊函數為Hv( ),其輸出是長度恰為v比特的雜湊值。
密鑰派生函數KDF(Z, klen):
輸入:比特串Z,整數klen(表示要獲得的密鑰數據的比特長度,要求該值小于(232-1)v)。
輸出:長度為klen的密鑰數據比特串K。
a)初始化一個32比特構成的計數器ct=0x00000001;
b)對i從1到?klen/v?執行:
b.1)計算Hai=Hv(Z ∥ ct);
b.2) ct++;
c)若klen/v是整數,令Ha!?klen/v? = Ha?klen/v?,否則令Ha!?klen/v?為Ha?klen/v?最左邊的(klen ?
(v × ?klen/v?))比特;
d)令K = Ha1||Ha2|| · · · ||Ha?klen/v??1||Ha!?klen/v?。

簡化下說明:

  1. 先分組,分組的大小為klen/v,向上取整,其中klen是數據長度,v是HASH算法輸出長度。SM3的輸出長度為32字節。
  2. 然后每一組循環,把原始數據Z和計數器ct拼接,做SM3_Hash運算得到Hai。然后計數器ct+1。
  3. 最終生成的數據Ha1,Ha2…拼接起來,然后截斷到klen長度也就是數據長度。

HASH算法說明

官方使用的是SM3密碼雜湊算法,輸入為小于2的64次方bit,輸出為256bit(32字節)

總結:

  1. 國密算法的基礎是使用曲線計算。曲線應該使用官方推薦的曲線,曲線不同加解密肯定失敗。
  2. 國密算法生成的數據為C1C2C3,其中C1為固定的64字節,c2和原始數據一樣長,C3為固定的32字節。有些要求數據前面加上’0x04’,舊的版本輸出是C3C1C2,這兩點要注意。
  3. 公鑰分為P_x和P_y,都是32字節長度。私鑰長度從資料上看沒有限制,是一個隨機數[1,N-2]。N為曲線參數。
  4. 加密過程中使用了SM3的散列算法(官方叫雜湊算法),這個算法輸出為32字節的數據。如果對端沒有用這個算法,兩端也無法加解密成功。

總結

  1. 字符編碼是為了把可見字符和二進制之間做一層轉化。其中UNICODE編碼是國際編碼標準。UTF-8是這種編碼格式的實現方式。特點是ASCII碼的字符占用一個字節,其他的比如中文字符占用兩到三個字符。
  2. Base64也是一種編碼方式,主要用于把二進制數據轉化為ASCII字符,方便傳輸。現在很多加密算法習慣在加密后把二進制數做一次Base64進行傳輸。相對于原文,長度會多出1/3。也有把二進制轉為字符串的形式,不過長度是原文的2倍。
  3. 哈希散列算法,主要用于脫敏處理和信息簽名防篡改,做哈希運算應該加鹽處理。鹽值應該是隨機值,而且和用戶相關,建議使用(隨機數 + 用戶名)。
  4. 對稱加密兩端秘鑰相同,加密速度快,可以加密大數據,但是秘鑰保存一直是個難題。
  5. 非對稱加密分為公鑰和私鑰,公鑰可以公開。加密速度慢,只能加密小數據,但是只需要妥善保存私鑰就可以了。

通常一個信息加密傳輸流程為:

  1. 雙方約定好使用的編碼格式。通常常用的是UTF-8編碼。
  2. 客戶端隨機生成對稱秘鑰作為會話秘鑰。使用非對稱加密傳輸給后端,后端保存這個對稱秘鑰用于之后的加解密過程。
  3. 用戶使用對稱加密(通常為AES)加密整個數據,結果通常使用Base64做編碼(通常還要做一次URLEncode操作),整個相關數據按照規則使用Hash算法(通常為SHA256算法)做數據簽名。最后做傳輸
  4. 如果是用戶密碼的話建議用HMac做Hash脫敏處理,然后單獨使用非對稱加密進一步加強安全性。

參考:

  1. 字符編碼筆記:ASCII,Unicode和UTF-8
  2. 百度百科-ASCII
  3. 深入淺出大小端
  4. Base64 編碼
  5. MD5+Salt安全淺析
  6. 哈希加密算法 MD5,SHA-1,SHA-2,SHA-256,SHA-512,SHA-3,RIPEMD-160 - aTool
  7. DES加密模式詳解
  8. DES加密算法原理
  9. 關于PKCS5Padding與PKCS7Padding的區別
  10. 各種加密算法比較
  11. AES在線加解密
  12. iOS - Safe iOS 加密安全
  13. RSA算法原理
  14. SM2國密算法官方說明
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容