密碼散列函數是一種純確定性函數,它將輸入從大空間映射到固定集中輸出。這些輸出通常稱為輸入摘要。例如,輸入可以是本書序言的全部文本,它的摘要可以是從 128 位值空間中的十六進制
在不使用形式化的情況下,應該使用安全的散列函數。這意味著幾乎不可能找到產生相同摘要的兩種不同的輸入。哈希函數也應該是不可逆轉的。如果只給文摘,就不可能找到產生文摘的輸入。同樣,對輸入的微小變化應該會對輸出摘要產生很大的變化 —— 兩個類似的輸入應該有非常不同的摘要。哈希函數也應該相對較快地從它們的輸入中進行計算,因此驗證輸入匹配及其摘要是一項簡單的任務。
哈希數值是保持區塊鏈完整性的核心,也是工作證明共識機制的基礎。我們將在幾個頁面中看到這兩種用途。
公鑰密碼學
公鑰密碼可以通過許多不同的算法實現,其中最流行的算法之一是 RiVest-Shamir-Adleman(更著名的名字是 RSA),然而,它依賴於橢圓曲線數字簽名算法(ECDSA)。這算法還允許恢復公鑰,給定消息及其簽名。
有了這兩個密碼學概念,我們現在終於可以開始學習如何構建區塊鏈了。
區塊鏈是一種分散的、不可磨滅的公共數字分類帳。我們可以把區塊鏈看作一個分佈式數據庫,在這個數據庫中,一旦一個記錄被確認,它就永遠不能被刪除或修改,並且沒有一個權威機構可以控制這個數據庫,這個數據庫在對等網絡中的所有節點上都被複製。實際上存儲在數據庫中的內容可能會有所不同;它可以是貨幣、資產註冊,甚至是可執行的代碼。
在區塊鏈中,每個狀態更改都是事務的一部分。將事務看作是來自全局數據庫中的用戶的解剖寫入操作,該操作可能會更改一個或多個數組。網絡中的任何用戶都可以提交要執行的事務。
如何處理傳輸是區塊鏈狀態傳遞關係的一部分,通過處理它接收的每個事務,區塊鏈從一種狀態過渡到另一種狀態。例如,管理貨幣的區塊鏈可能處理兩個帳戶之間的貨幣轉移:它減少發送方的餘額,並將收件人的餘額增加相同的金額。其他區塊鏈甚至允許事務在鏈上創建和執行完整的程序
當區塊被添加到區塊鏈時,它通過對等網絡傳播到所有節點。每個節點將重新執行區塊中的所有事務,以檢查它們是否有效,如果它們注意到任何非法更改則拒絕該區塊。這意味著每個事務實際上在整個網絡中每一個單字節執行一次。這允許區塊鏈完全分散,因為每個節點都檢查所有正在運行的事務,但這是有代價的:混合開銷限制了可以運行的事務的數量。
注意:公共區塊鏈不要求用戶註冊。他們只需創建一個新的密鑰對,就可以開始對事務進行簽名以參與網絡,但是,他們可能要求用戶擁有與區塊鏈相關聯的貨幣來處理他們的事務。
事務以區塊的形式分批處理,然後將它們鏈接在一起形成實際的區塊鏈。這些區塊構成了區塊鏈的歷史,每個區塊打包了一組改變其狀態的事務。在每個區塊中選擇和排序事務的方式取決於區塊鏈協商一致規則,我們將在後面的頁面中看到這些規則。
當區塊被添加到區塊鏈時,它通過對等網絡傳播到所有節點。每個節點將重新執行區塊中的所有事務,以檢查它們是否有效,如果它們注意到任何非法更改則拒絕該區塊。這意味著每個事務實際上在整個網絡中每一個單字節執行一次。這允許區塊鏈完全分散,因為每個節點都檢查所有正在運行的事務,但這是有代價的:混合開銷限制了可以運行的事務的數量。
當區塊被添加到區塊鏈時,它通過對等網絡傳播到所有節點。每個節點將重新執行區塊中的所有事務,以檢查它們是否有效,如果它們注意到任何非法更改則拒絕該區塊。這意味著每個事務實際上在整個網絡中每一個單字節執行一次。這允許區塊鏈完全分散,因為每個節點都檢查所有正在運行的事務,但這是有代價的:混合開銷限制了可以運行的事務的數量。
網絡每秒處理。換句話說,區塊是以交易數量下放為交換的。
考慮到這一高成本處理一個區塊鏈的變更,所有的交易費用都要支付。這種費用通常是以一種原生於區塊鏈的貨幣支付的(比特幣網絡中的比特幣,或者以太坊中的以太)。不管你是這項費用的受益者,我們將在幾頁中看到,該費用的目標是防止攻擊者將需要由每個節點處理的事務淹沒網絡,並為向鏈添加新區塊的節點提供獎勵。
區塊鏈通過在每個區塊上保存他們整個歷史的摘要來抵抗變化。該鏈中的每個區塊都是通過在其自身事務上計算的散列以及前一個區塊的哈希來標識的
使用此方案,對鏈中任何區塊上的任何事務的任何更改都將導致所有後續哈希的更改,從而使任何修改變得微不足道。例如,如果攻擊者試圖更改十個區塊前發生的事務,那麼不僅該區塊的摘要會發生變化,而且下一個區塊的摘要也會發生變化(因為它是基於前一個區塊哈希計算的),並且一直到鏈的頭但是,為了防止攻擊者修改區塊鏈並在網絡中分發虛假副本,這種機制必須對攻擊者重新生成所有區塊進行區分。這就是工作證明的來源。
交易是有序的,並包括在區塊鏈的區塊將取決於衰變的 Igorifhn 的網絡。既然我們所處理的是一個分散化的數據庫,我們需要一種讓所有參與者都同意如何給粉筆增加變化的方法。例如,如果一個賣家在區塊鏈上提供一項資產,而兩個買家爭相搶購,一個分散的網絡怎麼能決定誰是第一個呢?更糟糕的是,如何阻止賣方告訴這兩位買家他們兩次進行了購買和現金交易?“我們需要一種方法來確定交易是如何按順序選擇和訂購的,以保持區塊鏈的單一狀態。換句話說,我們需要一種方法,在哪些區塊上添加區塊。”
許多公共區塊,如比特幣或以太坊,都依賴一種被稱為 “工作證明” 的共識算法。工作證明是用於執行計算的大量 CPU 循環的密碼證明;在這種情況下,基於區塊計算一個困難的數字。為了將一個區塊添加到區塊鏈中,它必須伴隨著它的證明獲得添加區塊的節點將得到回報,以回報他們的努力。完成這個角色的節點被稱為 “礦工”,每當添加一個新的區塊時,他們都會競相添加下一個來獲取相應的獎勵。注意,這些證明的機制實際上很簡單。鏈中的標識符前端區塊是一個散列,它包括前一個區塊的標識符、區塊中的所有事務和一個隨機數。通過更改當前值,計算區塊的摘要將完全不同。為了給鏈添加一個新的區塊,攻擊者必須有一個特定的結構(從 N 個零開始)。由於預測摘要會是什麼樣子並不是被動的,礦工只能反復計算分塊,同時更改時值,直到他找到一個符合要求的摘要為止。這需要很多嘗試,因此被認為是工作的證明。
請記住,整個基礎設施運行在對等分散網絡之上。這允許節點按自己的意願運行和離開網絡,而不需要集中式服務器。在這裡,工作證明算法為新節點提供了一種方法,可以知道哪一個是實際的鏈:他們只需要尋找具有最大累積計算能力的鏈。
這也防止了惡意參與者僅僅更改鏈上的記錄並重新計算所有後續的區塊哈希,就像我們在上一節中討論的那樣。要做到這一點,攻擊者將需要解決所有來自被攻擊的區塊的所有工作的證據,這需要比網絡中的其他礦工更強的計算能力。
除了工作證明外,還有其他協商一致的機制。在…… 裡面以證明權威和利害關係證明作為建立更快和更小的鏈的備選方案。
衰老算法與網絡的最終性密切相關。當我們知道它已經包含在區塊鏈中並且不會改變的時候,我們說反交換是最終的。在最近的區塊中添加的事務遠未被認為是最終的:如果一個礦工設法在一行中挖掘兩個區塊,從 EXT 開始到最後,它們可能會生成一個新的鏈,以替換最新的區塊,而不包括該事務。
這被稱為重組,這在工作證明鏈中並不少見。要知道一個交易是最終的,我們需要在包含它的交易之上等待幾個區塊被開採,以確保它不會改變。區塊的數量將取決於特定的鏈和我們需要多少信心。論吞吐量通過設計來解決一個工作的證明在計算上是昂貴的。這本身就強制執行對區塊鏈吞吐量的依賴,在每次添加大量事務時,強制解決一個困難的難題。然而,還有另一個原因限制了每秒添加到鏈中的事務:可驗證性。為了保持區塊鏈的分散性,網絡中的每個節點都需要能夠驗證每個事務是合法的,並按照既定的規則執行。如果網絡每秒接受大量事務,那麼只有強大的設備。
將能夠驗證該鏈,將網絡遺漏給任何無法訪問必要硬件的用戶。因此,低吞吐量與保證對區塊鏈的公共訪問有關。
特別是,以太坊被設計為每秒鐘處理大約 15 個事務。注意,在以太坊中事務可能比較複雜,因為它們可以執行任意的計算,因此這個上限實際上與需要多少努力有關,需要運行並驗證每個區塊的事務。
請注意,這幾個事務每秒在網絡中的所有用戶和應用程序之間共享。即使對於一個傳統的 Web 應用程序來說,這也是一個非常低的限制。我們將在第 8 章中看到圍繞這一限制的一些方法。
從比特幣到以太坊到目前為止,WWE 已經將一個區塊數據作為一個公共數據庫,但我們還沒有深入了解該數據庫可能包含的內容。第一個著名的區塊鏈是用來跟踪擁有的一個數字貨幣,比特幣。
我們今天所理解的區塊鏈,大部分是在 2008 年由 Satoshi Nakamoto 在他的 “比特幣:對等電子現金系統” 論文中介紹的?這張紙既短又易讀,而且它包裝了今天使用的大部分區塊鏈概念。它引入了一個 “純粹的點對點版本的電子現金,沒有任何集中所有者或發行人。
總而言之,比特幣區塊鏈是一個公共分散數據庫,它跟踪用戶比特幣的餘額,並支持資金從一個審計機構轉移到另一個審計中心的交易,這是一個深度集中的電子支付平台的實現。值得一提的是,除了普通的傳輸,比特幣還支持有限的腳本語言。這種語言允許構造,例如 Timeelocks,它限制執行 fom 到將來某一段時間,或者多重簽名。交易,需要多個帳戶同意才能移動資產。用這種語言可以建立的東西仍然是有限的。正是為了支持網絡中的仲裁計算而提出的。
密碼學的歷史悠久,古時候主要應用於軍事機密的傳送,如 “口令”,“暗號” 等。在 1970 年之前,密碼學的應用範疇大部分還是在政府層面,直到標準加密系統 - 數據加密標準和非對稱加密算法的發明,密碼學才逐步被深入應用在各個領域。
密碼學的發展歷程#
密碼學的發展大致可以分為三個階段:古典密碼學 -> 現代密碼學 -> 公鑰密碼學
1. 古典密碼學:這階段的核心密碼學思想主要為代替和置換。代替就是將明文每個字符替換成另外一種字符產生密文,接收者根據對應的字符替換密文就得到明文了。置換就是將明文的字符順序按照某種規則打亂。
2. 現代密碼學:這階段的發展主要是對稱加密算法。對稱加密是發送方使用某種公開的算法使用密鑰對明文進行加密,接收方使用之前發送方給予的密鑰對密文進行解密得到明文。
3. 公匙密碼學:這個階段的發展主要是非對稱加密算法。非對稱加密的原理是公鑰加密,私鑰解密。它的實現過程是 A 通過某種算法產生一對密鑰,分別是公鑰和私鑰,然後將公鑰公開。B 想發送信息給 A,就使用 A 的公鑰對明文進行加密產生密文並發送給 A。A 接收到密文後,用自己的私鑰對密文進行解密,得到明文。
非對稱加密解密的示意圖如下
非對稱加密涉及到了:公鑰和私鑰
特點是:
-
特性 1:使用公鑰加密的數據只有私鑰才能解密,公鑰自己是解密不了的。
-
特性 2:使用私鑰加密的數據只有公鑰才能解密,私鑰自己是解密不了的。
-
服務端同時持有公鑰和私鑰(不會給任何人)。
-
服務端要跟誰通信就把自己的公鑰給它。
使用非對稱加密的交互的過程如下:客戶端先拿到服務端的公鑰,然後使用這個公鑰加密數據,再把加密後的數據發送給服務端,由於上面說的特性 1、2,這時只有服務端才能正確的解密出數據。
密碼學在區塊鏈的應用非常廣泛,可分為 3 類:對稱加密算法、非對稱加密算法和哈希散列算法。常見的方法有:Merkle tree 哈希樹算法,椭圆曲线算法,SHA-256 算法,Base58 編碼。作用有:通過 hash 算法快速查找;對明文進行加解密;對信息進行簽名以及驗證;產生數字證書;生成賬戶地址等。
對稱加密算法是應用較早的加密算法,技術成熟。
1 對稱加密的使用過程
在對稱加密算法中,數據發信方將明文(原始數據)和加密密鑰一起經過特殊加密算法處理後,使其變成複雜的加密密文發送出去。收信方收到密文後,若想解讀原文,則需要使用加密用過的密鑰及相同算法的逆算法對密文進行解密,才能使其恢復成可讀明文。在對稱加密算法中,使用的密鑰只有一個,發收信雙方都使用這個密鑰對數據進行加密和解密,這就要求解密方事先必須知道加密密鑰。
2 對稱加密的特點
對稱加密算法的特點是算法公開、計算量小、加密速度快、加密效率高。不足之處是,交易雙方都使用同樣鑰匙,安全性得不到保證。此外,每對用戶每次使用對稱加密算法時,都需要使用其他人不知道的惟一鑰匙,這會使得發收信雙方所擁有的鑰匙數量呈幾何級數增長,密鑰管理成為用戶的負擔。假設兩個用戶需要使用對稱加密方法加密然後交換數據,則用戶最少需要 2 個密鑰並交換使用,如果企業內用戶有 n 個,則整個企業共需要 n×(n-1) 個密鑰,密鑰的生成和分發將成為企業信息部門的惡夢。對稱加密算法的安全性取決於加密密鑰的保存情況,但要求企業中每一個持有密鑰的人都保守秘密是不可能的,他們通常會有意無意的把密鑰洩漏出去 —— 如果一個用戶使用的密鑰被入侵者所獲得,入侵者便可以讀取該用戶密鑰加密的所有文檔,如果整個企業共用一個加密密鑰,那整個企業文檔的保密性便無從談起。
對稱加密算法在分佈式網絡系統上使用較為困難,主要是因為密鑰管理困難,使用成本較高。而與公開密鑰加密算法也就是非對稱加密算法比起來,對稱加密算法能夠提供加密和認證,卻缺乏了簽名功能,使得使用範圍有所縮小。
3 對稱加密的劃分
對稱加密分為序列密碼和分組加密。
序列密碼,也叫流加密(stream cyphers),依次加密明文中的每一個字節。加密是指利用用戶的密鑰通過某種複雜的運算(密碼算法)產生大量的偽隨機流,對明文流的加密。解密是指用同樣的密鑰和密碼算法及與加密相同的偽隨機流,用以還原明文流。
分組密碼,也叫塊加密(block cyphers),一次加密明文中的一個塊。是將明文按一定的位長分組,明文組經過加密運算得到密文組,密文組經過解密運算(加密運算的逆運算),還原成明文組。
序列密碼,也叫流密碼,是利用種子密鑰通過密鑰流生成器產生與明文長度一致的偽隨機序列,該隨機序列與明文進行某種算法相結合產生的密文的一種密碼算法。
使用序列密碼對某一消息 m 執行加密操作時,都是按字進行加密的,一般是先將 m 分成連續的字符,m=m1m2m3…; 然後使用密鑰流 k=k1k2k3... 中的第 i 個字符 ki 對明文消息的第 i 字符 mi 執行加密變換,i=1,2,3...;所有的加密輸出連接在一起就構成了對 m 執行加密後的密文。解密需要和加密同步進行,所以使用序列密碼,發送方和接收方需要對明文或密文在信息的同一位置進行操作。
加解密過程示意圖如下:
序列密碼具有實現簡單、便於硬件實施、加解密處理速度快、沒有或只有有限的錯誤傳播等特點,但是因為這類密碼主要運用在軍事,政治機密機構上,因此它的研究成果較少有公開。目前可以公開在其他領域應用的算法有 RC4,SEAL,A5 等。
一次一密加密#
一次一密加密是 1917 年 Mauborgne 和 Vernam 聯合提出的一種理想加密方案,它要求對明文消息逐字符加密,每個字符加密都是獨立的,加密的密鑰是與明文長度一致的毫無規則隨機的密鑰序列,這個密鑰序列就是一次一密亂碼本。在使用時,密文發送方和接收方手裡頭都擁有一模一樣的亂碼本,該亂碼本是由雙方協商確定的一個足夠長的隨機密鑰序列。發送方對明文進行加密時,用到的密鑰序列是來自亂碼本發送消息長度的最前面的一段,加密完後,立馬對用過的密鑰序列進行銷毀。接收方收到密文後,用亂碼本的密鑰序列依次對密文進行解密,得到明,同時也要把用過的密鑰序列進行銷毀,不再使用。
一次一密的亂碼本被認為是無條件安全的密碼體制,即是一種不可攻破的密碼體制。竊聽者得到密文信息,根本沒有可能對其進行解密,因為加密的密鑰是完全隨機的,毫無規律,攻擊者沒有任何信息對它進行密文分析。但前提是一次一密亂碼本不能洩漏。
一次一密概念的提出對序列密碼的產生提供了方向。正是基於這個概念,越來越多的序列密碼算法不斷產生。
序列密碼的結構#
序列密碼的結構可細分為同步流密碼和自同步流密碼。同步流密碼指它的密鑰流的產生與明文無關,而是通過某種獨立的隨機方法產生的偽隨機數字流。自同步流密碼也叫異步流密碼,它與同步流密碼相反,密鑰流的產生與明文有關,具體是後一個密鑰字的產生與前一個明文加密後的字有關。自同步密碼這個特性,使得它非常難以研究,所以大部分序列密碼的研究都集中在同步密碼上。
同步流密碼#
同步流密碼產生密碼流的過程分為兩部分,一個是密鑰流產生器,另一個是加密變換器。
加密過程表達式是:ci=E (ki,mi),參數都是字節數組的單個元素。解密過程和加密過程必須同步,表達式是一個。因為密鑰流的產生每次都是不一樣的。所以加密時,每次產生的密鑰流元素先緩存到寄存器中,等解密用完這個元素以後再繼續進行加密。整個過程有點類似 tcp 協議。目前最為常用的流密碼體制是有限域 GF (2) 上的二元加法流密碼,其加密變換可表示為 ci=ki⊕mi。
特點:
在同步流密碼中,發送方和接收方必須是同步的,即雙方使用同樣的密鑰,對同一位置進行操作。一旦密文字符在傳輸中出現丟失,損壞或者刪除,那麼解密將失敗
2)無錯誤傳播
密文字符在傳輸過程中被修改,只是對該字符產生影響,並不影響其他密文字符的解密。
3)主動攻擊性破壞同步性
作為同步要求的結果,主動攻擊者對傳輸中的密文字符進行重放,插入,刪除等破壞操作,直接會造成加解密過程的同步性。所以在使用時,需要借助其他密碼學技術對傳輸的密文進行認證和完整性的驗證操作。
2.2 自同步流密碼
自同步密碼的密鑰流的產生不獨立於明文流和密文流,通常第 i 個密鑰字的產生不僅與主密鑰有關,而且與前面已經產生的若干個密文字有關。
特點:
1)自同步
發送方在傳輸密文流過程中,某些密文字符被攻擊,接收方的解密只是在這些被攻擊過的密文與發送方不同步,而其他密文流解密同步不會有問題。
2)有限的錯誤傳播
接收方的解密只是對攻擊過的 i 個密文字符有影響,而對其他密文流不會有問題。所以產生的明文至多有 i 個錯誤。
3)主動攻擊破壞當前的同步性
4)明文統計擴算
每個明文字符都會影響其後的整個密文,即明文的統計學特性擴散到了密文中。因此,自同步流密碼在抵抗利用明文冗餘而發起的攻擊方面要強於同步流密碼。
2.3 密鑰流生成器
流密鑰的重要部分是密鑰流生成器。理想的密鑰流生成器是生成完全隨機的密鑰流,但實際中因為它是根據用戶的私鑰通過一定的算法產生的,不可能做到真正的隨機,所以產生的密鑰流是偽隨機序列。
一個密鑰流生成器通常由一個線形反饋移位寄存器(LFSR)和一個非線形組合部分構成。線形反饋移位寄存器可以稱為驅動部分。其工作原理是將驅動部分在 j 時刻的狀態變量 x 作為輸入,輸入到非線形組合部分 f,將 f(x)作為當前時刻的 kj。驅動部分負責提供非線形組合部分使用的序列,而非線形部分以各時刻移位寄存器的狀態組合出密鑰序列 j 時刻的值 kj。通俗講就是驅動部分內部的變量是不斷變化的,在每個不同时刻的值都是不一樣的,它不斷向非線形組合部分輸入變量 x 的不同时刻的值,非線形組合部分接收到此時刻的 x,通過函數 f 產生當前的密鑰流字節。
2.4 反饋移位寄存器
反饋移位寄存器是流密碼產生密鑰流的一個重要組成部分,GF (2) 上一個 n 級反饋移位寄存器由 n 個二元存儲器和一個反饋函數 f (a1,a2,a3,a4,...,an) 組成,n 級反饋移位寄存器如下圖所示。
每一存儲器稱為移位寄存器的一级,在任一時刻,這些級的內容構成反饋位移寄存器的狀態,在每一狀態對應 GF (2) 上一個維向量,總共有 2^n 種可能的狀態。每一時刻的狀態可用長為 n 的序列 a1,a2,a3,...,an 或者 n 維向量(a1,a2,a3,...,an)表示,其中 ai 是當前時刻第 i 級存儲器的內容。初始狀態由用戶確定,當第 i 個移位時鐘脈沖到來時,每一级存儲器 ai 都將其內容向下一级 ai-1 傳遞,反饋函數 f (a1,a2,a3,a4,...,an) 根據寄存器當前的狀態計算出下一時刻的 an。反饋函數是一個 n 元布爾函數,即 n 個變量 a1,a2,a3,...,an 可以分別獨立地取 0 和 1 兩個可能的值,函數中的運算有邏輯與、邏輯或、邏輯補等運算,最後的函數值為 0 或 1.
RC4 算法#
RC4 加密算法是 RSA 三人組中的頭號人物 Ron Rivest 在 1987 年設計的密鑰長度可變的流加密算法簇。該算法的速度可以達到 DES 加密的 10 倍左右,且具有很高級別的非線性。1994 年 9 月,它的算法被發布在互聯網上。由於 RC4 算法加密是采用的 xor,所以,一旦子密鑰序列出現了重複,密文就有可能被破解。RC4 作為一種老舊的驗證和加密算法易於受到黑客攻擊,現在逐漸不推薦使用了。
RC4 算法的實現非常簡單,使用從 1 到 256 個字節(8 到 2048 位)可變長度密鑰初始化一個 256 個字節的狀態向量 S,S 的元素記為 S [0],S [1],S [2],...,S [255],S 先初始化為 S [i]=i。以後自始至終都包含從 0 到 255 的所有 8 比特數,只是對它進行置換操作。每次生成的密鑰字節 ki 由 S 中 256 個元素按一定方法選出一個元素而生成。每生成一個密鑰字節,S 向量中元素會進行一次置換操作。則 RC4 算法分為兩部分:初始化 S 和密鑰流的生成,其中密鑰流的生成過程中每次產生的密鑰字與對應明文的元素進行異或運算得到密文字
1.1 初始化 S
生成 S 的步驟如下:
1)聲明一個長度為 256 的字節數組,並給 S 中的元素從 0 到 255 以升序的方式填充,即 S [0]=0,S [1]=1,S [2]=2,...,S [255]=255。
2)j:=0
3)對於 0<=i<=255,循環下邊兩個方法:
j = (j + S[i] + int(K[i%keylen])) % 256
S[i], S[j]=S[j], S[i]
1.2 密鑰流的生成
步驟如下:步驟如下:
1)i=0;j=0
2)i = (i + 1) % 256
3)j = (j + S[i]) % 256
4)S[i], S[j]=S[j], S[i]
5)輸出密鑰字 key = S [(S [i]+S [j])%256]
1.3 RC4 的安全性
由於 RC4 算法加密采用的是異或方式,所以,一旦子密鑰序列出現了重複,密文就有可能被破解,但是目前還沒有發現密鑰長度達到 128 位的 RC4 有重複的可能性,所以,RC4 也是目前最安全的加密算法之一。
1.4 RC4 加密過程
簡單介紹下 RC4 的加密過程:
1)利用自己的密鑰,產生密鑰流發生器
2)密鑰流發生器根據明文的長度產生偽隨機序列
3)偽隨機序列每个位元素與明文對應的位元素進行異或運算,生成密文
示意圖:
golang 實現 RC4 加密:#
package main
import "fmt"
func main() {
data := []byte("helloworld");
output:=make([]byte,len(data))
fmt.Printf("明文:%s\n", data);
K := []byte("qwuoaknfabbalafbj");
keylen := len(K);
SetKey(K, keylen);
output=Transform(output, data, len(data));
fmt.Printf("密文: %x\n", output);
SetKey(K, keylen);
output1:=make([]byte,len(data))
output1=Transform(output1, output, len(data));
fmt.Printf("解密後明文:%s", output1);
}
var S = [256]int{}
//初始化S盒
func SetKey(K []byte, keylen int) {
for i := 0; i < 256; i++ {
S[i] = i;
}
j := 0;
for i := 0; i < 256; i++ {
j = (j + S[i] + int(K[i%keylen])) % 256;
S[i], S[j]=S[j], S[i];
}
}
//生成密鑰流
func Transform(output []byte, data []byte, lenth int)[]byte {
i := 0;
j := 0
output=make([]byte,lenth)
for k := 0; k < lenth; k++ {
i = (i + 1) % 256;
j = (j + S[i]) % 256;
S[i], S[j]=S[j], S[i];
key := S[(S[i]+S[j])%256];
//按位異或操作
output[k] = uint8(key)^data[k];
}
return output
}
利用 goland 封裝好的方法實現 RC4 加密,但是沒有解密
package main
import (
"fmt"
"crypto/rc4"
)
func main() {
var key []byte = []byte("fd6cde7c2f4913f22297c948dd530c84") //初始化用于加密的KEY
rc4obj, _ := rc4.NewCipher(key) //返回 Cipher
str := []byte("helloworld") //需要加密的字符串
plaintext := make([]byte, len(str)) //
rc4obj.XORKeyStream(plaintext, str)
//XORKeyStream方法将src的数据与秘钥生成的伪随机位流取XOR并写入dst。
//plaintext就是你加密的返回过来的结果了,注意:plaintext为base-16 编码的字符串,每个字节使用2个字符表示 必须格式化成字符串
stringinf := fmt.Sprintf("%x\n", plaintext) //转换字符串
fmt.Println(stringinf)
}
分組密碼,也叫塊加密,英文 Block Cyper,一般先對明文 m 進行填充得到一個長度是固定分組長度 s 的整數倍明文串 M;然後將 M 劃分成一個個長度為 s 的分組;最後對每個分組使用相同的密鑰執行加密變換。比較常見的算法有 AES;DES;3DES。
分組密碼中,無論是明文塊還是密文塊,塊與塊之間都有一些邏輯運算關係,這些關係即為運算的模式。
比較常見的分組密碼運算的五種模式:
-
Electronic Code Book (ECB) 電子密碼本模式
-
Cipher Block Chaining (CBC) 密文分組鏈接模式
-
Cipher Feedback Mode (CFB) 加密反饋模式
-
Output Feedback Mode (OFB) 輸出反饋模式
-
Counter mode(CTR)計數器模式
目前推薦使用的是 CBC 模式和 CTR 模式,其它模式較少使用或不推薦使用。
1. ECB 模式
ECB 又稱電子密碼本模式,英文全稱是 Electronic codebook,是最基本的塊密碼加密模式,加密前根據加密塊大小(如 AES 為 128 位)分成若干塊,如果最後一塊不足 128 位,使用填充(具體看算法,默認是 0x00),之後將每個塊使用相同的密鑰單獨加密得到密文塊,然後將密文塊連在一起就得到密文了。解密同理。
下圖展示 ECB 模式加解密的過程:
加密過程:
由此得知相同的明文內容將永遠加密成相同的密文,而且密文的格式和明文也相同。這是很不安全的,尤其是傳輸圖片或明文內容重複很多的情況下。由於所有分組的加密方式一致,明文中的重複內容會在密文中有所體現,因此難以抵抗統計分析攻擊。還有因為明文和密文的內容順序一致,攻擊者很容易破壞密文。攻擊者在密文傳輸過程中截獲,並對密文內容次序打亂,接收密文信息者得到的密文就不可能解密成原本的明文信息了。這也是 ECB 模式很少使用的原因。
特點:
1. 操作簡單,易於實現,有利於並行計算,誤差不會被傳送;
2. 不能隱藏明文的模式;
3. 可能對明文進行主動攻擊;
CBC 模式#
CBC 又稱密文分組鏈接模式,英文全稱是 Cipher Block Chaining,之所以叫這個名字,是因為密文分組像鏈條一樣相互連接在一起。
在 CBC 模式中,每個明文塊先與前一個密文塊進行異或後,再進行加密。在這種方法中,每個密文塊都依賴於它前面的所有明文塊。同時,為了保證每條消息的唯一性,在第一個塊中需要使用初始化向量。
若第一個塊的下標為 1,則 CBC 模式的加密過程為:
Ci = Ek (P ⊕ Ci-1), C0 = IV.
而其解密過程則為:
Pi = Dk (Ci) ⊕Ci-1, C0 = IV.
CBC 模式運算過程示意圖
CBC 算法優點:
-
明文的重複排列不會反映在密文中
-
支持並行計算(僅解密)
-
能夠解密任意密文分組
CBC 算法缺點: -
對包含某些錯誤比特的密文進行解密時,第一個分組的全部比特以及後一個分組的相應比特會出錯
-
加密不支持並行計算
CFB 又稱密文反饋,英文全稱為 Cipher feedback。模式類似於 CBC,可以將塊密碼變為自同步的流密碼;工作過程亦非常相似。需要使用一個與塊的大小相同的移位寄存器,並用 IV 將寄存器初始化。然後,將寄存器內容使用塊密碼加密,然後將結果的最高 x 位與平文的 x 進行異或,以產生密文的 x 位。下一步將生成的 x 位密文移入寄存器中,並對下面的 x 位平文重複這一過程。解密過程與加密過程相似,以 IV 開始,對寄存器加密,將結果的高 x 與密文異或,產生 x 位平文,再將密文的下面 x 位移入寄存器。
與 CBC 相似,明文的改變會影響接下來所有的密文,因此加密過程不能並行化;而同樣的,與 CBC 類似,解密過程是可以並行化的。
CFB 模式運算過程示意圖
CFB 模式的優點:
-
不需要填充(padding)
-
支持並行計算(僅解密)
-
能夠解密任意密文分組
CFB 模式的缺點:
-
加密不支持並行計算
-
對包含某些錯誤比特的密文進行解密時,第一個分組的全部比特以及後一個分組的相應比特會出錯
-
不能抵禦重放攻擊
OFB:將分組密碼作為同步序列密碼運行,和 CFB 相似,不過 OFB 用的是前一個 n 位密文輸出分組反饋回移位寄存器,OFB 沒有錯誤擴散問題。
輸出反饋模式(Output feedback, OFB)可以將塊密碼變成同步的流密碼。它產生密鑰流的塊,然後將其與平文塊進行異或,得到密文。與其它流密碼一樣,密文中一個位的翻轉會使平文中同樣位置的位也產生翻轉。這種特性使得許多錯誤校正碼,例如奇偶校驗位,即使在加密前計算而在加密後進行校驗也可以得出正確結果。
每個使用 OFB 的輸出塊與其前面所有的輸出塊相關,因此不能並行化處理。然而,由於平文和密文只在最終的異或過程中使用,因此可以事先對 IV 進行加密,最後並行的將平文或密文進行並行的異或處理。
可以利用輸入全 0 的 CBC 模式產生 OFB 模式的密鑰流。這種方法十分實用,因為可以利用快速的 CBC 硬件實現來加速 OFB 模式的加密過程。
加密過程:
OFB 模式的優點:
-
不需要填充(padding)
-
可事先進行加密、解密的準備
-
加密、解密使用相同結構
-
對包含某些錯誤比特的密文進行解密時,只有明文中相應的比特會出錯
OFB 模式的缺點:
-
不支持並行運算
-
主動攻擊這反轉密文分組中的某些比特時,明文分組中相對應的比特也會被反轉
CTR 模式#
計數模式(CTR 模式)加密是對一系列輸入數據塊(稱為計數)進行加密,產生一系列的流密碼,流密碼與明文異或得到密文,同樣解密就是流密碼與密文異或得到明文。
數據塊是加密之前通過將逐次累加的計數器產生不同的比特序列,它是由 nonce 和 counter(分組序號)構成的。CTR 計數器,長度是 128 比特(16 字節)。前 8 個字節是叫做 nonce 的初始值,這個值每次加密都不相同。後 8 個字節則是分組序號,也就是不斷 + 1 得到的值。nonce 的作用是讓數據塊內容複雜化。如果沒有 nonce,只有 counter,數據塊過於單一。Golang 裡封裝的計數器實現與這裡講的有些許不同,首先初始化一個長度為 BLOCK.SIZE () 的初始向量 iv,然後 iv 最後一個字節通過計數器逐組遞增,同樣也會產生分組加密之前不同的數據塊。
加密的過程就是生成一個初始的計數器。假設有 8 個分組,就通過初始計數器不斷 + 1 得到 8 個計數器值,每個計數器值再加密得到密鑰流,每個密鑰流和對應分組明文異或得到密文。所以它的加密過程相當於一次一密。
CTR 模式中可以以任意順序對分組進行加密和解密,因為在加密和解密時需要用到的 “計數器” 的值可以由 nonce 和分組序號直接計算出來。這就意味著能夠實現並行計算。在支持並行計算的系統中,CTR 模式的速度是非常快的。
下圖展示 CTR 模式的加解密的過程:
加密過程:
unc AesCTR_Encrypt(plainText,key[]byte)[]byte{
//判斷用戶傳過來的key是否符合16字節,如果不符合16字節加以處理
keylen:=len(key)
if keylen==0{ //如果用戶傳入的密鑰為空那麼就用默認密鑰
key=[]byte("wumansgygoaescbc") //默認密鑰
}else if keylen>0&&keylen<16{ //如果密鑰長度在0到16之間,那麼用0補齊剩余的
key=append(key,bytes.Repeat([]byte{0},(16-keylen))...)
}else if keylen>16{
key=key[:16]
}
//1.指定使用的加密aes算法
block, err := aes.NewCipher(key)
if err!=nil{
panic(err)
}
//2.不需要填充,直接獲取ctr分組模式的stream
// 返回一個計數器模式的、底層采用block生成key流的Stream接口,初始向量iv的長度必須等於block的塊尺寸。
iv := []byte("wumansgygoaesctr")
stream := cipher.NewCTR(block, iv)
//3.加密操作
cipherText := make([]byte,len(plainText))
stream.XORKeyStream(cipherText,plainText)
return cipherText
}
func AesCTR_Decrypt(cipherText,key []byte)[]byte{
//判斷用戶傳過來的key是否符合16字節,如果不符合16字節加以處理
keylen:=len(key)
if keylen==0{ //如果用戶傳入的密鑰為空那麼就用默認密鑰
key=[]byte("wumansgygoaescbc") //默認密鑰
}else if keylen>0&&keylen<16{ //如果密鑰長度在0到16之間,那麼用0補齊剩余的
key=append(key,bytes.Repeat([]byte{0},(16-keylen))...)
}else if keylen>16{
key=key[:16]
}
//1.指定算法:aes
block, err:= aes.NewCipher(key)
if err!=nil{
panic(err)
}
//2.返回一個計數器模式的、底層采用block生成key流的Stream接口,初始向量iv的長度必須等於block的塊尺寸。
iv := []byte("wumansgygoaesctr")
stream := cipher.NewCTR(block, iv)
//3.解密操作
plainText := make([]byte,len(cipherText))
stream.XORKeyStream(plainText,cipherText)
return plainText
}
//輸出
40dbbf4ab999bb36d0d5
helloworld
非對稱加密也叫公鑰密碼。
1976 年 Diffie 和 Hellman 首次提出了一種全新的加密思想,公鑰密碼體制思想。在當時幾乎所有的密碼體制都是對稱密碼體制,原理都是基於替換和置換這些較簡單方法。公鑰密碼體制完全與之不同,它是非對稱的,有兩個不同的密鑰,分別是公鑰和私鑰,加密的原理也不是之前的簡單替換或置換,而是一些複雜的數學函數。這些數學函數都是基於數學難題。其所依據的難題一般分為三類:大整數分解問題類、離散對數問題類、橢圓曲線類。有時也把橢圓曲線類歸為離散對數類。公鑰密碼體制是一次革命性的變革,突破了原有的密碼體制模式,它解決了傳統密碼體制的兩個大難題:密鑰分配和數字簽名。
1 非對稱加密的概述
傳統密碼體制用的都是一個密鑰,發送方傳輸密鑰給接收方成本很高,而且風險很大。接收方收到的密文如果在傳輸過程中被修改,接收方無法判斷密文的真偽性。公鑰體制完美地解決了上述問題。它有一對密鑰,一個是公鑰,完全公開,任何人都可以收到該密鑰;另一個是私鑰,自己保存,不需要告訴任何人。通過公開的公鑰是無法計算出私鑰的,所以私鑰是安全的。發送方 A 用公鑰對明文進行加密,接收方 B 用對應的私鑰進行解密。為保證傳輸密文的完整性和消息來源的準確性,需要對密文進行數字簽名。A 對密文用自己的私鑰進行再次加密,此過程叫數字簽名;B 接收到密文用該私鑰對應的公鑰進行解密,此過程叫驗簽。
所以公鑰密碼體制可以分為兩個模型:加密解密模型和簽名驗簽模型。兩個模型可以獨立使用,也可以一起混用。具體按照自己的應用場景使用,一般情況下發送的密文都是需要進行數字簽名的,發送的內容包括密文和簽名兩部分。接受者先進行驗簽,驗簽通過後,再進行解密。
非對稱加密的方式有很多,以下講解 RSA,DSA,ECDSA 這三種加密方式。
公鑰密碼體制的要求#
公鑰密碼體制要想實現必須滿足以下要求:
1. 產生一對密鑰對,即公私鑰對,在計算上是容易的;
2. 通過公鑰對明文進行加密,在計算上是容易的;
3. 通過私鑰對密文進行解密,在計算上是容易的;
4. 已知公鑰,無法計算出私鑰;
5. 已知公鑰和密文,無法計算出明文;
6. 加密和解密的順序可以交換。
目前滿足以上要求,建立公鑰密碼體制基於的困難問題有較多,我只分析以下兩種常用的:
1. 大整數分解問題
若已知兩個大素數 p 和 q,求 n=pq 是很容易的,但是已知 n,求 p 和 q 是幾乎不可能的,這就是大整數分解問題。
2. 離散對數問題
先了解兩個概念,階和原根。
設 m > 1 且 (a, m) = 1, 則使得 a^t ≡ 1 mod m 成立的最小的正整數 t 稱為 a 對模 m 的階,記為 δm (a)。
原根,是一個數學符號。設 m 是正整數,a 是整數,若 a 模 m 的階等於 φ(m),則稱 a 為模 m 的一個原根。裡面提到的 φ(m) 是 m 質因數的個數。
給定一個公式 a^t mod b ≡ c,其中 a 是 b 的原根,b 是一個超大的素數,c 是小於 b 大於 0 的正整數。問題是已知 a,t,b 求 c 很容易,但是現在 3^t mod 17≡12,求 t。求解過程非常困難,而且滿足條件的 t 不計其數。這裡用的是 17,如果換成很大的數,那幾乎沒有可能求解出來真正的 t。
RSA 算法#
RSA 是目前使用最廣泛的公鑰密碼體制之一。它是 1977 年由羅納德・李維斯特(Ron Rivest)、阿迪・薩莫爾(Adi Shamir)和倫納德・阿德曼(Leonard Adleman)一起提出的。當時他們三人都在麻省理工學院工作。RSA 就是他們三人姓氏開頭字母拼在一起組成的。
RSA 算法的安全性基於 RSA 問題的困難性,也就是基於大整數因子分解的困難性上。但是 RSA 問題不會比因子分解問題更加困難,也就是說,在沒有解決因子分解問題的情況下可能解決 RSA 問題,因此 RSA 算法並不是完全基於大整數因子分解的困難性上的。
1. RSA 算法描述
1.1 RSA 產生公私鑰對
具體實例講解如何生成密鑰對
1. 隨機選擇兩個不相等的質數 p 和 q。
alice 選擇了 61 和 53。(實際應用中,這兩個質數越大,就越難破解。)
2. 計算 p 和 q 的乘積 n。
n = 61×53 = 3233
n 的長度就是密鑰長度。3233 寫成二進制是 110010100001,一共有 12 位,所以這個密鑰就是 12 位。實際應用中,RSA 密鑰一般是 1024 位,重要場合則為 2048 位。
3. 計算 n 的歐拉函數 φ(n)。稱作 L
根據公式 φ(n) = (p-1)(q-1)
alice 算出 φ(3233) 等於 60×52,即 3120。
4. 隨機選擇一個整數 e,也就是公鑰當中用來加密的那個數字
條件是 1<e < φ(n),且 e 與 φ(n) 互質。
alice 就在 1 到 3120 之間,隨機選擇了 17。(實際應用中,常常選擇 65537。)
5. 計算 e 對於 φ(n) 的模反元素 d。也就是密鑰當中用來解密的那個數字
所謂 "模反元素" 就是指有一個整數 d,可以使得 ed 被 φ(n) 除的餘數為 1。ed ≡ 1 (mod φ(n))
alice 找到了 2753,即 17*2753 mode 3120 = 1
6. 將 n 和 e 封裝成公鑰,n 和 d 封裝成私鑰。
在 alice 的例子中,n=3233,e=17,d=2753,所以公鑰就是 (3233,17),私鑰就是(3233, 2753)。
1.2 RSA 加密
首先對明文進行比特串分組,使得每個分組對應的十進制數小於 n,然後依次對每個分組 m 做一次加密,所有分組的密文構成的序列就是原始消息的加密結果,即 m 滿足 0<=m<n,則加密算法為:
c≡ m^e mod n; c 為密文,且 0<=c<n。
1.3 RSA 解密
對於密文 0<=c<n,解密算法為:
m≡ c^d mod n;
1.4 RSA 簽名驗證
RSA 密碼體制既可以用於加密又可以用於數字簽名。下面介紹 RSA 數字簽名的功能。
已知公鑰(e,n),私鑰 d
1. 對於消息 m 簽名為:sign ≡ m ^d mod n
2. 驗證:對於消息簽名對(m,sign),如果 m ≡ sign ^e mod n,則 sign 是 m 的有效簽名
2. 已知公私鑰,golang 實現 RSA 加解密#
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
)
// 可通過openssl產生
//openssl genrsa -out rsa_private_key.pem 1024
var privateKey = []byte(`
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDfw1/P15GQzGGYvNwVmXIGGxea8Pb2wJcF7ZW7tmFdLSjOItn9
kvUsbQgS5yxx+f2sAv1ocxbPTsFdRc6yUTJdeQolDOkEzNP0B8XKm+Lxy4giwwR5
LJQTANkqe4w/d9u129bRhTu/SUzSUIr65zZ/s6TUGQD6QzKY1Y8xS+FoQQIDAQAB
AoGAbSNg7wHomORm0dWDzvEpwTqjl8nh2tZyksyf1I+PC6BEH8613k04UfPYFUg1
0F2rUaOfr7s6q+BwxaqPtz+NPUotMjeVrEmmYM4rrYkrnd0lRiAxmkQUBlLrCBiF
u+bluDkHXF7+TUfJm4AZAvbtR2wO5DUAOZ244FfJueYyZHECQQD+V5/WrgKkBlYy
XhioQBXff7TLCrmMlUziJcQ295kIn8n1GaKzunJkhreoMbiRe0hpIIgPYb9E57tT
/mP/MoYtAkEA4Ti6XiOXgxzV5gcB+fhJyb8PJCVkgP2wg0OQp2DKPp+5xsmRuUXv
720oExv92jv6X65x631VGjDmfJNb99wq5QJBAMSHUKrBqqizfMdOjh7z5fLc6wY5
M0a91rqoFAWlLErNrXAGbwIRf3LN5fvA76z6ZelViczY6sKDjOxKFVqL38ECQG0S
pxdOT2M9BM45GJjxyPJ+qBuOTGU391Mq1pRpCKlZe4QtPHioyTGAAMd4Z/FX2MKb
3in48c0UX5t3VjPsmY0CQQCc1jmEoB83JmTHYByvDpc8kzsD8+GmiPVrausrjj4p
y2DQpGmUic2zqCxl6qXMpBGtFEhrUbKhOiVOJbRNGvWW
-----END RSA PRIVATE KEY-----
`)
//openssl
//openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
var publicKey = []byte(`
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDfw1/P15GQzGGYvNwVmXIGGxea
8Pb2wJcF7ZW7tmFdLSjOItn9kvUsbQgS5yxx+f2sAv1ocxbPTsFdRc6yUTJdeQol
DOkEzNP0B8XKm+Lxy4giwwR5LJQTANkqe4w/d9u129bRhTu/SUzSUIr65zZ/s6TU
GQD6QzKY1Y8xS+FoQQIDAQAB
-----END PUBLIC KEY-----
`)
// 加密
func RsaEncrypt(origData []byte) ([]byte, error) {
//解碼pem格式的公鑰,得到公鑰的載體block
block, _ := pem.Decode(publicKey)
if block == nil {
return nil, errors.New("public key error")
}
// 解析得到公鑰
pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err // 接口類型斷言
pub := pubInterface.(*rsa.PublicKey)
//加密
return rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
}
// 解密
func RsaDecrypt(ciphertext []byte) ([]byte, error) {
//解碼pem格式的私鑰,得到公鑰的載體block
block, _ := pem.Decode(privateKey)
if block == nil {
return nil, errors.New("private key error!")
}
//解析得到PKCS1格式的私鑰
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
// 解密
return rsa.DecryptPKCS1v15(rand.Reader, priv, ciphertext)
}
func main() {
data, _ := RsaEncrypt([]byte("hello world"))
fmt.Println(base64.StdEncoding.EncodeToString(data))
origData, _ := RsaDecrypt(data)
fmt.Println(string(origData))
}
golang 實現 RSA 生成公私鑰並加解密
package mainimport (
"crypto/rsa"
"crypto/rand"
"fmt"
"crypto/md5"
"encoding/base64"
)
//通過RSA實現加密和解密
//利用RSA的方法生成私鑰對
func main() {
//RSA首先生成的是私鑰,然後根據私鑰生成公鑰
//生成1024位私鑰
pri,_:=rsa.GenerateKey(rand.Reader,2048)
//根據私鑰產生公鑰
pub:=&pri.PublicKey
//定義明文
plaintext:=[]byte("hello china")
//加密成密文,OAEP補碼
ciphertext,_:=rsa.EncryptOAEP(md5.New(),rand.Reader,pub,plaintext,nil)
fmt.Println(base64.StdEncoding.EncodeToString(ciphertext))
//解密
plaintext,_=rsa.DecryptOAEP(md5.New(),rand.Reader,pri,ciphertext,nil)
fmt.Println(string(plaintext))
}
golang 實現 RSA 簽名和驗簽#
package main
import (
"crypto/rsa"
"crypto/rand"
"crypto/md5"
"crypto"
"fmt"
)
//RSA實現簽名和驗簽
func main() {
//生成私鑰
priv,_:=rsa.GenerateKey(rand.Reader,1024)
//產生公鑰
pub:=&priv.PublicKey
//設置明文
plaintext:=[]byte("hello world")
//給明文做哈希散列
h:=md5.New()
h.Write(plaintext)
hashed:=h.Sum(nil)
//簽名
opts:=&rsa.PSSOptions{SaltLength:rsa.PSSSaltLengthAuto,Hash:crypto.MD5}
sign,_:=rsa.SignPSS(rand.Reader,priv,